Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

27236
1488
262023


'디버깅을 통해 배우는 리눅스 커널의 구조와 원리'- 2021년 대한민국학술원 우수학술도서(자연과학 분야) 선정 Question_Announcement

'디버깅을 통해 배우는 리눅스 커널의 구조와 원리' 책이 2021년 대한민국학술원 우수학술도서(자연 과학 분야)에 선정됐습니다. 자연 과학 분야의 우수 도서로 선정된 책 목록을 보면 책의 상업성을 떠나 학술적으로 가치가 있는 책으로 보이는데, 이런 목록 중에 제가 쓴 책이 있어 참 기쁩니다.

<출처: nas.go.kr> 대학민국 학술원

리눅스 커널은 IT 분야로 볼 수도 있지만, 소프트웨어의 근간을 이루는 운영체제의 핵심 기반 기술이라 자연 과학 학술 분야로 선정된 듯 합니다. 

이 책을 많이 읽어주신 개발자님들과 취준생 분들에게 감사의 말씀을 드리고 싶고요. 앞으로 개발자님들과 취준생 분들에게 유익한 콘텐츠(리눅스 커널, Arm 프로세스)를 블로그와 유튜브에 꾸준히 올리겠습니다. 감사합니다.

[Arm프로세서] AAPCS: Armv8: 스택과 관련된 명령어 - stp AAPCS(Calling Procedure Arm)

SP 레지스터가 변경될 때 실행되는 명령어는 stp와 ltp입니다. 먼저 명령어의 정의를 소개하고, 예제 코드를 분석하면서 명령어의 동작 원리에 대해 알아봅시다.

Armv7 아키텍처에서 SP 레지스터가 변경되면서 프로세스의 스택 공간에 레지스터를 푸시하는 명령어는 'push'입니다. Armv8 아키텍처에서는 push 대신에 stp 명령어를 사용해 push 명령어와 같은 동작을 수행합니다.

Arm 스팩 문서에서 stp 명령어 알아보기

먼저 Arm 스팩 문서를 보면서 stp 명령어에 대해 알아봅시다.

C6.2.273 STP

Store Pair of Registers calculates an address from a base register value and an immediate offset, and stores two 32-bit words or two 64-bit doublewords to the calculated address, from two registers. 
[출처] DDI0487Fc_armv8_arm.pdf

스팩 문서에서 설명된 stp 명령어의 동작은 다음과 같이 요약할 수 있습니다.

   1. 베이스 레지스터가 가리키는 값을 기준으로 지정된 레지스터를 저장함
   2. 베이스 레지스터의 값에 오프셋이 적용될 수 있음

stp 명령어에 대한 설명에는 SP 레지스터에 대한 내용이 없어서, "stp 명령어가 AAPCS와 어떤 연관이 있는가"라는 의문이 듭니다. 

stp 명령어의 세부 문법 알아보기

그런데 stp 명령어는 SP 레지스터와 함께 사용되며, 표기법은 아래 그림과 같습니다.

 
그림 7.4 stp 명령어의 포멧1

위 그림에서 명시된 STP 명령어의 표기법을 설명하기 전에, 표기법에서 보이는 각 구문에 대해 설명하겠습니다.

왼쪽에 보이는 <Xt1>, <Xt2>는 저장하려는 레지스터를 의미합니다. 오른쪽 부분에 있는 <SP>는 SP 레지스터를 의미하고 <imm>은 오프셋을 뜻합니다.

위 명령어에서 눈여겨 볼 부분은 STP 명령어의 가장 오른쪽 부분에 있는 !기호입니다. 이 명령어는 SP 레지스터를 <imm> 만큼 업데이트한 후, 업데이트된 스택 주소에 Xt1과 Xt2 레지스터를 푸시하는 동작입니다.  
이번에는 STP 명령어의 동작을 나타낸 다른 그림을 보겠습니다.

 
그림 7.5 stp 명령어의 포멧2

위 명령어는 SP 레지스터의 값에서 #<imm> 오프셋을 적용한 주소에 Xt1과 Xt2 레지스터를 저장하는 명령어입니다. 

stp 명령어에서 유심히 봐야할 부분이 명령어의 가장 오른쪽 부분입니다. 그림 7.4에서 소개된 명령어와는 달리, STP 명령어의 가장 오른쪽 부분에 !이 없으므로 SP 레지스터의 값이 업데이트되지 않습니다.

stp 명령어를 예제 어셈블리 명령어로 함께 분석하기

stp 명령어의 포멧을 알아봤으니 다음과 같은 예제 코드를 보면서 stp 명령어가 어떻게 
사용되는지 알아봅시다.

01 <__fork>:
02   stp x29, x30, [sp, #-0x20]!
03   stp x28, x27, [sp, #+0x10]

02번째 줄이 실행되기 전에 SP 레지스터는 0x20000이라고 가정하겠습니다.

아래 그림을 보면서 02번째 줄이 실행될 때 내부 동작 방식을 알아봅시다.

 

그림 7.6 stp 명령어를 실행할 때 업데이트되는 SP 레지스터  

아래 공식에 따라 SP 레지스터의 값이 업데이트되면서 스택 공간에 x29와 x30의 값을 저장합니다.

    "SP = 0x1ffe0 = 0x20000 - 0x20"

또한 0x1ffe0와 0x1ffe0+0x8 주소에 x29와 x30의 값을 저장합니다. 

02번째 어셈블리 명령어가 실행되면 SP 레지스터를 업데이트하면서 프로세스의 스택 공간에 지정된 레지스터를 푸시하는 동작을 수행합니다. 

'stp x29, x30, [sp, #-0x20]!' 명령어를 Armv7 아키텍처에서 사용된 명령어를 활용해 어떻게 표기할 수 있을까요? 유사하게 다음과 같이 표기할 수 있습니다.

01 sub sp, sp, 0x10
02 push {x29, x30}

01번째 줄은 sp 레지스터의 값을 0x10만큼 빼서 업데이트합니다. 02번째 줄은 x29와 x30 레지스터를 sp 레지스터에 푸시하면서, sp 레지스터는 0x10(x29, x30 크기)만큼 업데이트됩니다. 

이번에는 03번째 줄의 명령어를 분석하겠습니다.

01 <__fork>:
02   stp x29, x30, [sp, #-0x20]!
03   stp x28, x27, [sp, #+0x10]

03번째 줄이 실행되면서, 다음 그림과 같이 스택 공간에 데이터가 업데이트됩니다.

 

그림 7.7 stp 명령어를 실행해 스택에 레지스터를 푸시

먼저 x27과 x28는 푸시할 레지스터의 값을 의미합니다. 만약 x27 레지스터의 값이 0x27, x28 레지스터의 값이 0x28이면 0x27, 0x28이 프로세스의 스택 공간에 저장됩니다.

이어서 '[sp, #0x10]' 구문의 의미를 알아봅시다. 이 구문은 sp 레지스터의 값을 기준으로 +0x10 오프셋 주소에 지정된 레지스터(x27, x28)를 저장하는 동작으로 해석할 수 있습니다. 그림과 같이 sp 레지스터에서 +0x10만큼 떨어진 주소에 x27, x28 레지스터의 값이 저장됩니다.

이 명령어가 실행될 때는 SP 레지스터가 업데이트되지는 않습니다. stp 명령어의 가장 오른쪽 부분에 !가 없기 때문입니다.
[중요]
6장에서 Armv7 아키텍처 기반의 Arm 코어는 스타트업 코드가 실행될 때 SP 레지스터의 값을 처음 알게 된다고 말씀드렸습니다. 그렇다면 Armv8 아키텍처 기반의 Arm 코어는 언제 SP 레지스터의 값을 알게 될까요? 스타트업 코드가 실행될 때 업데이트됩니다. 

이번에도 실전 프로젝트에서 가장 많이 사용되는 부트로더인 LK(Little Kernel)의 스타트업 코드를 보면서 이 내용을 확인해 봅시다.

https://github.com/littlekernel/lk/blob/master/arch/arm64/start.S
...
01    /* Set up the stack */
02    ldr     tmp, =__stack_end
03    mov     tmp2, #ARCH_DEFAULT_STACK_SIZE
04    mul     tmp2, tmp2, cpuid
05    sub     sp, tmp, tmp2

02~04번째 줄은 스택 사이즈를 계산하는 명령어이고, 05번째 줄에서 SP 레지스터에 tmps를 저장해 SP 레지스터를 설정합니다. 이후 Arm 코어는 계속 SP 레지스터의 값을 계속 알게 됩니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[Arm프로세서] AAPCS: Armv8: 서브 루틴(함수)로 분기될 때 실행되는 어셈블리 명령어 AAPCS(Calling Procedure Arm)

arm 아키텍처의 세부 동작을 제대로 파악하려면, 해당 Arm 아키텍처에서 지원하는 어셈블리 명령어를 배워야 합니다. 이는 Armv8 아키텍처의 AAPCS64에도 마찬가지로 적용될 수 있습니다.

이번 절에서는 Armv8 아키텍처의 AAPCS64와 연관된 명령어를 소개합니다.

AAPCS와 연관된 명령어의 목록은 다음과 같습니다.


표 7.4 Armv8 아키텍처에서 사용되는 AAPCS와 관련된 명령어

위 표에서 보이는 각 명령어를 소개하고 예제 코드를 분석하면서, 명령어의 동작 원리를 배워봅시다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv8: 함수를 호출하기 위한 디자인 AAPCS(Calling Procedure Arm)

이번 절에서는 AAPCS64 기준으로, 실제 함수를 호출하는 예시 코드와 함께, 함수를 호출하면 SP와 X30 레지스터가 어떻게 바뀌는지 알아보겠습니다.

함수가 호출될 때의 세부 동작 원리 파악하기

여러분이 다음과 같은 함수를 작성했다고 가정하겠습니다.

01 int add_func(int x, int y)
02 {
03    int result = x + y;
04    printf("x:%d, y:%d \n", x, y);
05   
06    return result;
07 }

[정보]
위에서 든 예시 코드는 6.2 절에서 봤던 코드와 같은데, Armv8 아키텍처 기반의 Arm 코어에서 이 코드를 실행하면 같은 기능을 수행합니다. 

Armv7 아키텍처 다룬 함수 디자인 규칙은 Armv8 아키텍처에 거의 그대로 적용됩니다. 공통으로 적용될 수 있는 원칙은 다음과 같습니다.

   * 먼저 add_func() 함수가 실행된 후 복귀할 주소를 어딘가에 저장해 놓는다.
   * add_func() 함수의 실행을 마무리한 후 복귀한다.

위와 같이 add_func() 함수가 호출되려면, 다음과 같은 요구사항을 만족해야 합니다. 

    “함수를 호출하면 함수를 호출한 후 복귀할 주소를 링크 레지스터인
      X30 레지스터에 업데이트한다.” 

'bl [함수 주소]' 명령어가 실행되면 지정된 주소로 분기하는데, 이 때 Arm 코어는 서브 루틴을 호출한 후 복귀할 주소를 링크 레지스터인 X30 레지스터에 업데이트합니다. 그래서 서브 루틴이 호출된 후 가장 먼저 실행되는 명령어는 다음과 같습니다.

    "X30 레지스터(복귀할 주소가 저장됨)의 값을 프로세스의 스택 공간에 푸시(저장)"

서브 루틴은 자신의 기능을 마무리한 후 스택에 저장된 X30 레지스터의 값을 다시 X30 레지스터에 로딩합니다. 이어서 ret 명령어를 실행하면, X30 레지스터의 값을 PC에 넣어주게 되는데 이를 통해 서브 루틴을 호출한 주소로 복귀합니다. 

이어서 다음 예제 코드를 보면서 X30 레지스터의 동작 원리를 알아봅시다.

01  0x1004 some_routine:  stp x29, x30, [sp, #-0x10]! 
02  0x1008                 ; // code
03  0x100c                 bl 0x2000 <sub_routine>
04  0x1010                 ; // code
05  0x1014                 ldp x29, x30, [sp], #0x10
06  0x1018                 ret

[정보]
01번째 줄은 some_routine은 호출되는 함수나 레이블을 나타내는 심벌 이름을 나타냅니다.

01번째 줄이 실행되면 x29, x30 레지스터의 값을 프로세스의 스택에 백업(푸시)합니다. 

이어서 03번째 줄을 보겠습니다. 
'bl 0x2000 <sub_routine>' 명령어를 실행하면 sub_routine 심벌이 위치한 0x2000 주소로 분기합니다. 그런데 03번째 줄의 주소는 0x100c이고 04번째 줄의 주소는 0x1010입니다. 03번째 줄을 실행해 'sub_routine'이란 심벌의 서브 루틴으로 분기한 다음에 어느 주소로 복귀해야 할까요? 당연히 04번째 줄인 0x1010 주소입니다.

그래서 03번째 줄 명령어를 실행하면 Arm 코어는 하드웨어적으로 다음과 같은 동작을 수행합니다.

   "X30 레지스터에 복귀할 주소인 0x1010를 업데이트해준다."

02~04번째 줄은 어느 함수에서도 실행될 수 있는 일반적인 루틴을 표기한 것입니다.

05번째 줄은 01번째 줄에서 프로세스의 스택에 백업한 x29와 x30 레지스터의 값을 다시 x29, x30 레지스터에 로딩하는 동작입니다.

'bl [주소]' 패턴의 명령어를 실행하면 Arm 프로세서는 하드웨어적으로 서브 루틴을 호출한 다음에 복귀할 주소를 X30 레지스터에 업데이트해주므로, 함수가 독립적인 하나의 기능으로 실행하게 됩니다. 

함수에 전달되는 인자는 어떻게 처리될까

Armv7 아키텍처에서 함수에 전달되는 인자는 R0 ~ R3 레지스터에 저장됩니다. 그렇다면 Armv8 아키텍처에서 함수에 전달되는 인자는 어느 레지스터에 저장될까요? 바로 X0-X7 레지스터에 저장됩니다. Armv7 아키텍처에 비해 더 많은 갯수의 인자가 레지스터에 저장됩니다. 

다음 그림을 보면서 Armv8 아키텍처에서는 함수에 전달되는 인자를 처리하는 방식을 알아봅시다.

 

그림 7.2 함수가 호출될 때 사용되는 X0-X7와 X0 레지스터

그림 7.2 왼쪽 부분을 보면 a, b, c, d 인자를 적용해 sub_rountine() 함수를 호출합니다. 각각 인자는 다음과 같은 레지스터에 저장됩니다.

   * X0: a 
   * X1: b 
   * X2: c
   * X3: d

Armv7 아키텍처에서는 4개의 함수 인자까지만 R0-R3 레지스터에 저장됩니다. 하지만, Armv8 아키텍처에서는 8개의 함수에 전달되는 인자까지 X0-X7 레지스터에 저장됩니다.

여기서 X0~X7은 X0부터 X7까지 레지스터이니 레지스터의 갯수가 8개입니다.  이 내용을 읽으면 "함수에 전달되는 인자는 언제나 X0-X7 레지스터에 저장된다"라고 간주할 수 있는데, 다음 표를 보면 함수에 전달되는 인자가 늘어남에 따라, 어떤 방식으로 처리되는지 알 수 있습니다.


표 7.2 함수에 전달되는 인자의 개수별 사용되는 레지스터

위 표를 보면 함수에 전달되는 인자의 갯수에 따라 사용되는 레지스터가 다르다는 사실을 알 수 있습니다.
 
함수에 전달되는 인자의 갯수가 8개 이하면, X0-X7 레지스터에 인자의 값이 저장됩니다. 만약 인자의 갯수가 9개 이상으로 늘어나면, 8개까지의 인자까지는 X0-X7 레지스터에 저장되고, 9번째 인자부터는 프로세스의 스택 공간에 저장됩니다.

이어서 그림의 오른쪽에 보이는 sub_rountine() 함수의 가장 아랫 부분을 봅시다. 'return result;' 구문이 확인되는데, 이는 함수를 종료하면서 result 값을 반환하는 동작입니다. 이 때 result의 값은 어느 레지스터에 저장될까요? X0 레지스터에 result값이 저장돼 sub_rountine() 함수를 호출할 코드의 res 이란 지역 변수에 전달됩니다.

[주의]
이처럼 Arm 아키텍처에 따라 서브 루틴을 호출할 때 세부 동작이 약간 다릅니다.

예제 코드 분석: 함수에 전달되는 인자의 처리 방식

이번에는 다음 예제 코드를 보면서 함수에 전달되는 인자가 어떻게 처리되는지 알아봅시다. 

01 int add_func(int x, int y)
02 {
03    int result = x + y;
04    printf("x:%d, y:%d \n", x, y);
05   
06    return result;
07 }

add_func() 함수는 2개의 인자를 받아서, 각 인자를 더한 결과를 반환합니다. 

그런데 add_func() 함수의 인자인 x와 y는, X0와 X1 레지스터에 실려서 전달됩니다. 또한 add_func() 함수는 'return result;' 구문과 함께 result를 반환합니다. 여기서 함수가 반환하는 값인 result는 X0 레지스터에 저장됩니다.

이번에는 다음 그림을 보면서 함수에 전달되는 인자와, 함수가 반환하는 값이 어느 레지스터에 전달되는지 정리해 봅시다.

 
그림 7.3 add_func() 함수가 호출될 때 사용되는 X0-X1와 X0 레지스터

그림 7.3를 보면 main() 함수에서 add_func() 함수를 호출하는 동작을 알 수 있습니다.

그림의 왼쪽 부분을 보면 a와 b라는 인자와 함께 add_func() 함수를 호출합니다. 함수에 전달되는 인자는 X0와 X1 레지스터에 저장됩니다. 이는 Armv7 아키텍처의 동작 방식(R0-R1 레지스터에 저장)과 거의 유사합니다.
 
이번에 그림에서 add_func() 함수의 가장 아랫 부분을 봅시다. 'return result;' 구문인데, 이 구문이 실행되면 함수의 실행을 종료하면서 result의 값을 X0 레지스터에 저장합니다. X0 레지스터에 저장된 값이 result은 add_func() 함수를 호출한 코드의 res 지역 변수에 전달됩니다.

여기까지 함수가 호출될 때의 다자인에 대해 알아봤습니다. Armv8 아키텍처에서 서브 루틴(함수)를 호출하기 위한 세세한 처리 방식은 Armv7 아키텍처와 거의 유사합니다. 유사점과 차이점은 다음 표에서 확인할 수 있습니다.


표 7.3 Armv8와 Armv7 아키텍처 별 AAPCS를 처리하는 방식

이처럼 CPU 아키텍처에 따라 '함수 호출'을 가능하게 하는, 세부 구현 방식에 차이가 있습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv8: SP_ELn와 X30 레지스터란 AAPCS(Calling Procedure Arm)

Armv8 아키텍처에서 정의된 레지스터 중 SP_ELn와 X30 레지스터는 AAPCS와 연관된 핵심 레지스터입니다. 먼저 전체 레지스터 목록 중에서 SP_ELn와 X30 레지스터를 보겠습니다.

전체 레지스터 목록 중 SP_ELn와 X30 레지스터

다음 그림을 보면서 Armv8 아키텍처에서 정의된 레지스터 중 AAPCS와 연관된 레지스터를 알아봅시다.

 

그림 7.1 Armv8 아키텍처의 레지스터 목록 중 AAPCS와 연관된 레지스터
(출처: ARMv8-A-Programmer-Guide.pdf)

위 그림은 Armv8 아키텍처에서 정의된 레지스터 목록입니다. 그림에서 빗금으로 표기된 박스를 보겠습니다.

SP_EL1는 익셉션 레벨1에서 실행되는 SP 레지스터, SP_EL0은 익셉션 레벨0에서 실행되는 SP 레지스터를 의미합니다. 또한 하이퍼바이저가 실행되는 EL2에서 실행되는 SP 레지스터는 SP_EL2로 표기합니다.

[정보]
Armv8 아키텍처에서는 SP 레지스터를 SP_ELn으로도 표기합니다.
일반적으로 다음과 같은 명령어를 실행하면 이를 어떻게 해석해야 할까요?

sub sp, sp, #4

Armv8 아키텍처의 어셈블리 명령어에서 sp와 같이 표기됐으면 해당 어셈블리 명령어를 실행하는 익셉션 레벨을 기준으로 해석하면 됩니다.

만약 위 명령어를 실행하는 익셉션 레벨이 EL1이면 SP_EL1을 의미하고, EL2이면 SP_EL2를 의미합니다.

예를 들어 운영체제 커널은 EL1에서 실행되므로, 커널 공간에 있는 프로세스의 SP 레지스터는 SP_EL1에서 확인할 수 있습니다. 유저 애플리케이션은 EL0에서 실행되므로, 유저 프로세스의 SP 레지스터는 SP_EL0에서 확인할 수 있습니다. 


다음으로 X30 레지스터는 링크 레지스터를 뜻하며, 서브 루틴을 호출한 다음에 복귀한 주소를 저장하는 역할을 수행합니다. Armv7 아키텍처의 링크 레지스터인 R14와 같은 기능입니다.

SP_ELn 아랫 부분에 표기된 레지스터를 보겠습니다.
프로세서의 상태를 저장하는 SPSR(Saved Processor State Register) 레지스터와 ELR(Exception Link Register) 레지스터가 있습니다. 이 레지스터도 익셉션 레벨 별로 정의돼 있습니다. 이 레지스터는 AAPCS와는 직접적인 연관은 없지만 익셉션이 유발돼 익셉션 레벨이 변경될 때 이전 상태로 복귀하기 위한 용도로 쓰입니다. 

Arm 스팩 문서에서 본 SP 레지스터

이번에는 Arm 스팩 문서를 보면서 SP와 X30 레지스터에 대해 조금 더 알아봅시다. 먼저 SP 레지스터를 살펴봅시다.  

4.1.2 Stack pointer

In the ARMv8 architecture, the choice of stack pointer to use is separated to some extent from the Exception level. By default, taking an exception selects the stack pointer for the targetException level, SP_ELn. 

For example, taking an exception to EL1 selects SP_EL1. Each Exception level has its own stack pointer, SP_EL0, SP_EL1, SP_EL2, and SP_EL3.
[출처: Programmer’s Guide for ARMv8-A]

위 스팩 내용에서 중요한 부분은 다음과 같이 정리할 수 있습니다.

   * 익셉션 레벨 별로 스택이 존재하는데, 이 스택 주소를 저장하는 SP_ELn 레지스터가 있다.
   * 예를 들어 SP_EL0, SP_EL1, SP_EL2, and SP_EL3와 같은 스택 포인터 레지스터가 존재한다.

여기서 말하는 스택은 프로세스가 사용하는 스택을 의미합니다. Armv8에서 정의된 SP_ELn 레지스터도 프로세스의 스택 구간 내의 주소 구간 내에서 업데이트됩니다. 

Arm 스팩 문서에서 본 링크 레지스터: X30

이어서 X30 레지스터를 스팩 문서 분석을 통해 알아봅시다.

6.4 Flow control
Calls to subroutines, where it is necessary for the return address to be stored in the link register (X30), use the BL instruction. This does not have a conditional version. BL behaves as a B instruction with the additional effect of storing the return address, which is the address of the instruction after the BL, in register X30.
[출처: Programmer’s Guide for ARMv8-A]

X30 레지스터를 명쾌하게 설명한 문장인데요. 요약하면 다음과 같습니다.

   * X30은 BL 명령어와 함께 주소나 함수로 분기했을 때 서브 루틴에서 복귀할 주소를 저장하는 레지스터이다. 

Armv7 아키텍처에서는 R14가 각 Arm 모드 별로 존재했습니다. R14는 서브 루틴(함수 호출)을 호출할 때 복귀할 주소뿐만 아니라, 익셉션이 유발된 후 복귀할 주소를 저장했습니다.

그런데 Armv8 아키텍처에서는 익셉션이 발생한 후 복귀할 주소를 저장하는 익셉션 링크 레지스터(ELR: Exception Link Register)를 익셉션 레벨 별로 따로 정의합니다. 그래서 Armv8 아키텍처에서 X30는 익셉션 레벨 별로 존재하지 않습니다.

이처럼 SP와 X30 레지스터는 Armv8 아키텍처에서 정의된 레지스터와는 다른 레지스터와는 달리 서브 루틴을 호출하기 위한 사용됩니다.  

이번 절에 소개한 SP_ELn와 X30 레지스터를 표로 정리해 보겠습니다.


표 7.1 SP_ELn와 X30 레지스터의 특징

어셈블리 명령어 중에서 SP_ELn와 X30레지스터를 보면, 위 표의 내용이 머리에 자동으로 떠올랐으면 좋겠습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[리눅스커널] Arm64: Preemption 3가지 함수 실행 흐름 (v5.4) 10. 프로세스 스케줄링

1. 커널 코드 실행 도중(인터럽트 핸들러를 처리한 시점): Preemption 

el1_irq 레이블에서 arm64_preempt_schedule_irq 함수를 호출함 

https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/entry.S
el1_irq:
...
irq_handler

#ifdef CONFIG_PREEMPT
ldr x24, [tsk, #TSK_TI_PREEMPT] // get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
/*
 * DA_F were cleared at start of handling. If anything is set in DAIF,
 * we come back from an NMI, so skip preemption
 */
mrs x0, daif
orr x24, x24, x0
alternative_else_nop_endif
cbnz x24, 1f // preempt count != 0 || NMI return path
bl arm64_preempt_schedule_irq // irq en/disable is done inside
1:
#endif

2. 유저 프로세스 실행 도중(인터럽트 핸들러를 처리하고 난 시점): Preemption 

2.1 el0_irq -> ret_to_user
  : el0_irq 레이블에서 ret_to_user 레이블로 브랜치

https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/entry.S
el0_irq:
kernel_entry 0
...
irq_handler

#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
b ret_to_user
ENDPROC(el0_irq)

2.2 ret_to_user -> work_pending
   : flags 정보를 보고 work_pending로 브랜치 
   
https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/entry.S   
ret_to_user:
disable_daif
gic_prio_kentry_setup tmp=x3
ldr x1, [tsk, #TSK_TI_FLAGS]
and x2, x1, #_TIF_WORK_MASK
cbnz x2, work_pending

2.3 work_pending -> do_notify_resume
   : flags 정보를 보고 work_pending로 브랜치 

https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/entry.S   
work_pending:
mov x0, sp // 'regs'
bl do_notify_resume

2.4 do_notify_resume -> schedule
   : thread_flags에 _TIF_NEED_RESCHED가 포함돼 있으면 schedule() 함수 호출 

https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/signal.c
asmlinkage void do_notify_resume(struct pt_regs *regs,
 unsigned long thread_flags)
{
/*
 * The assembly code enters us with IRQs off, but it hasn't
 * informed the tracing code of that for efficiency reasons.
 * Update the trace code with the current status.
 */
trace_hardirqs_off();

do {
/* Check valid user FS if needed */
addr_limit_user_check();

if (thread_flags & _TIF_NEED_RESCHED) {
/* Unmask Debug and SError for the next task */
local_daif_restore(DAIF_PROCCTX_NOIRQ);

schedule();

2.5 함수 실행 흐름 정리 

el0_irq
   ret_to_user
      work_pending
         do_notify_resume
schedule
2.6 관련 패치 

arm64: factor work_pending state machine to C

Currently ret_fast_syscall, work_pending, and ret_to_user form an ad-hoc
state machine that can be difficult to reason about due to duplicated
code and a large number of branch targets.

This patch factors the common logic out into the existing
do_notify_resume function, converting the code to C in the process,
making the code more legible.

This patch tries to closely mirror the existing behaviour while using
the usual C control flow primitives. As local_irq_{disable,enable} may
be instrumented, we balance exception entry (where we will almost most
likely enable IRQs) with a call to trace_hardirqs_on just before the
return to userspace.

diff --git a/arch/arm64/kernel/entry.S b/arch/arm64/kernel/entry.S
index 441420c..6a64182 100644
--- a/arch/arm64/kernel/entry.S
+++ b/arch/arm64/kernel/entry.S
@@ -707,18 +707,13 @@ ret_fast_syscall_trace:
  * Ok, we need to do extra processing, enter the slow path.
  */
 work_pending:
-       tbnz    x1, #TIF_NEED_RESCHED, work_resched
-       /* TIF_SIGPENDING, TIF_NOTIFY_RESUME or TIF_FOREIGN_FPSTATE case */
        mov     x0, sp                          // 'regs'
-       enable_irq                              // enable interrupts for do_notify_resume()
        bl      do_notify_resume
-       b       ret_to_user
-work_resched:
 #ifdef CONFIG_TRACE_IRQFLAGS
-       bl      trace_hardirqs_off              // the IRQs are off here, inform the tracing code
+       bl      trace_hardirqs_on               // enabled while in userspace
 #endif
-       bl      schedule
-
+       ldr     x1, [tsk, #TI_FLAGS]            // re-check for single-step
+       b       finish_ret_to_user

3. 유저 프로세스 실행 도중(시스템 콜을 처리하고 복귀하기 직전): Preemption 

3.1 el0_svc-> ret_to_user
   : el0_svc에서 el0_svc_handler() 함수를 호출한 다음에 ret_to_user 레이블 브랜치 

https://elixir.bootlin.com/linux/v5.4.130/source/arch/arm64/kernel/entry.S      
el0_svc:
gic_prio_kentry_setup tmp=x1
mov x0, sp
bl el0_svc_handler
b ret_to_user
ENDPROC(el0_svc)

3.2 ret_to_user 레이블 부터의 루틴은 2.1~2.5와 동일(생략)

3.3 함수 실행 흐름 정리

el0_sync // EL0 Synchronization 익셉션(EL0에서 svc 명령어 실행)
   el0_svc  // el0_svc_handler() 함수를 호출해 시스템 콜 핸들러 호출
      ret_to_user
         work_pending
            do_notify_resume
       schedule   


[공유] YES24 이북(Ebook) 출간: 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 Question_Announcement

'디버깅을 통해 배우는 리눅스 커널의 구조와 원리' 책이 출간될 이후에 수 많은 개발자 분들이 '언제 이북(Ebook)이 출간되는지 문의를 주셨는데요. 특히 해외에 진출하신 분(개발자, 유학생)이 이북(Ebook) 출간을 기다리셨던 것 같은데요. 

드디어 2021년 06/07(월)에 출간됐습니다.



그 동안 '디버깅을 통해 배우는 리눅스 커널의 구조와 원리' 책을 많이 읽어주셔서 감사드리고,
개발자님들과 취준생 분들에게 많은 도움을 줄 수 있는 실용적인 콘텐츠를 블로그와 유튜브에 꾸준히 올리겠습니다. 감사합니다.

'디버깅을 통해 배우는 리눅스 커널의 구조와 원리' 저자
리눅스 커널 개발자, 김동현 올림

[Qualcomm] Security: product-security/bulletins Linux Security

Qualcomm에서 관리하는 security 패치 사이트는 다음과 같습니다.

qualcomm.com/company/product-security/bulletins


패치 릴리즈 예시 1.

아래 경로에 접근하면, 


패치 세트의 링크가 걸려 있습니다.



패치 릴리즈 예시 2.

www.qualcomm.com/company/product-security/bulletins/january-2020-bulletin#_cve-2019-10581

source.codeaurora.org/quic/la/kernel/msm-4.9/commit/?id=8d1b367cf622f63e75d9ed87a901d9865459309f

diff --git a/drivers/char/adsprpc.c b/drivers/char/adsprpc.c
index 2d05742..76f13e3 100644
--- a/drivers/char/adsprpc.c
+++ b/drivers/char/adsprpc.c
@@ -620,8 +620,13 @@ static int fastrpc_mmap_find(struct fastrpc_file *fl, int fd,
  if (va >= map->va &&
  va + len <= map->va + map->len &&
  map->fd == fd) {
- if (refs)
+ if (refs) {
+ if (map->refs + 1 == INT_MAX) {
+ spin_unlock(&me->hlock);
+ return -ETOOMANYREFS;
+ }
  map->refs++;
+ }
  match = map;
  break;
  }
@@ -632,8 +637,11 @@ static int fastrpc_mmap_find(struct fastrpc_file *fl, int fd,
  if (va >= map->va &&
  va + len <= map->va + map->len &&
  map->fd == fd) {
- if (refs)
+ if (refs) {
+ if (map->refs + 1 == INT_MAX)
+ return -ETOOMANYREFS;
  map->refs++;
+ }
  match = map;
  break;
  }

유익한 사이트이니 자주 가서 코드를 봐야 겠네요.

[관련 코드]

'map->refs + 1 == INT_MAX' 구문에서 refs는 int 형이라는 점을 명심할 필요가 있네요.

struct fastrpc_mmap {
struct hlist_node hn;
struct fastrpc_file *fl;
struct fastrpc_apps *apps;
int fd;
uint32_t flags;
struct dma_buf *buf;
struct sg_table *table;
struct dma_buf_attachment *attach;
struct ion_handle *handle;
uint64_t phys;
size_t size;
uintptr_t va;
size_t len;
int refs; //<<--

static int fastrpc_mmap_find(struct fastrpc_file *fl, int fd,
uintptr_t va, size_t len, int mflags, int refs,
struct fastrpc_mmap **ppmap)
{
struct fastrpc_apps *me = &gfa;
struct fastrpc_mmap *match = NULL, *map = NULL;  //<<--
struct hlist_node *n;

if ((va + len) < va)
return -EOVERFLOW;
if (mflags == ADSP_MMAP_HEAP_ADDR ||
mflags == ADSP_MMAP_REMOTE_HEAP_ADDR) {
spin_lock(&me->hlock);
hlist_for_each_entry_safe(map, n, &me->maps, hn) {
if (va >= map->va &&
va + len <= map->va + map->len &&
map->fd == fd) {
if (refs) {
if (map->refs + 1 == INT_MAX) { //<<--
spin_unlock(&me->hlock);
return -ETOOMANYREFS;
}
map->refs++; //<<--
match = map;
break;
}
}
spin_unlock(&me->hlock);

[Arm프로세서] AAPCS: Armv8 아키텍처에서 AAPCS 관련 레지스터 AAPCS(Calling Procedure Arm)

Armv8 아키텍처의 AAPCS를 구성하는 주요 개념은 Armv7 아키텍처와 거의 유사합니다. 6장에서 다룬 내용을 요약하면 다음과 같습니다.

   * 서브 루틴을 호출하면 프로세스의 스택 공간에 레지스터를 푸시한다.
   * 'bl [주소]' 명령어를 실행해 서브 루틴으로 분기하면 Arm 프로세서는 
     링크 레지스터인 R14에 복귀할 주소를 업데이트한다.
   * 서브 루틴을 호출할 때 전달되는 인자는 R0-R3 레지스터에 저장된다.   
   * 함수의 리턴값은 R0 레지스터에 저장된다.

위에서 설명한 내용을 Armv8 아키텍처 관점으로 다음과 같이 바꿔서 말할 수 있습니다.

   * 서브 루틴을 호출하면 프로세스의 스택 공간에 레지스터를 푸시한다.
   * 'bl [주소]' 명령어를 실행해 서브 루틴으로 분기하면 Arm 프로세서는 링크 레지스터인 X30에 
      복귀할 주소를 업데이트한다.  
   * 서브 루틴을 호출할 때 전달되는 인자는 X0-X7 레지스터에 저장된다.
   * 함수의 리턴값은 X0 레지스터에 저장된다.

위 내용을 보면 알 수 있듯이, Armv7 아키텍처에서 다룬 AACPS의 개념은 Armv8 아키텍처에 거의 그대로 적용됩니다. 하지만 서브 루틴을 호출할 때 사용되는 레지스터와 어셈블리 명령어가 다를 뿐입니다. 

이번 절에서는 Armv8 아키텍처에서 정의된 레지스터 목록 중에서 AAPCS와 관련된 레지스터를 소개합니다. Armv7 아키텍처에서 소개한 AAPCS와 유사한 개념이 많으니 차이점 위주로 설명하겠습니다.

[정보]

이번 장에서 소개하는 Armv8 아키텍처의 AAPCS는 64비트를 기준으로 설명하는데, 이를 AAPCS64로 표기합니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv7: 매우 자주 호출되는 함수는 inline 키워드로 선언 AAPCS(Calling Procedure Arm)

이번 장에서 서브 루틴을 호출하면 다음과 같은 동작을 수행한다고 배웠습니다. 

   * R0~R3 레지스터에 함수의 인자를 저장
   * 서브 루틴은 push와 pop 명령어를 사용해 링크 레지스터를 백업
   * 서브 루틴에서 반환하는 값을 R0 레지스터에 저장 

이번 장에서 소개된 add_func() 함수를 호출하는 과정에서 실행되는 어셈블리 명령어는 요약하면 다음과 같습니다.

01   10498:       e51b100c        ldr     r1, [fp, #-12]
02   1049c:       e51b0008        ldr     r0, [fp, #-8]
03   104a0:       ebffffe3        bl      10434 <add_func>
...
04 00010434 <add_func>:
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07   1043c:       e24dd010        sub     sp, sp, #16
...
08   1046c:       e1a00003        mov     r0, r3
09   10470:       e24bd004        sub     sp, fp, #4
10   10474:       e8bd8800        pop     {fp, pc}

01~10번째 줄까지, 10개 정도의 어셈블리 명령어가 실행됩니다.

그런데 다음과 같은 상황에서 함수 호출 시 실행되는 어셈블리 명령어는 오버헤드가 될 수 있습니다.

   * 함수가 실행하는 코드는 1~2줄 밖에 안되는 간단한 코드
   * 매우 자주 호출되는 함수

이 경우 inline 키워드로 함수를 선언하면, 컴파일러는 함수의 코드를 함수를 호출한 부분에 복사합니다. 결국 함수 호출 시 실행되는 명령어를 줄일 수 있습니다.

AAPCS는 최적화 이외에도 실전 소프트웨어 개발에 활용될 수 있는 내용이 많습니다. 이는 Armv7와 Armv8 아키텍처에 공통으로 적용될 수 있는 내용이라, 8장에서 소개합니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv7: 함수 반환형은 워드 단위로 지정 AAPCS(Calling Procedure Arm)

함수를 반환하는 자료형은 워드 단위로 제한하는 것이 좋습니다. 워드형인 경우 반환값은 R0 레지스터에 저장하나, 데이터가 워드 범위를 벗어난 경우 R0와 R1레지스터에 반환값을 나눠서 처리하기 때문입니다.

[정보]
위에서 언급한 워드형이 무엇인지 알아봅시다. 워드(word)는 어셈블리 명령어로 연산 결과를 레지스터에 저장할 수 있는 데이터 단위입니다. 그렇다면 함수가 반환하는 타입이 무엇인지 파악하려면, 함수 선언부의 가장 앞 부분을 보면 됩니다. 예를 들어 이번 소절에서 소개한 add_func() 함수는 int 형의 인자를 반환합니다.

int add_func(int a, int b, int c, int d, int e);

대부분 함수가 반환하는 자료형은 워드 타입으로, int, unsinged int 혹은 포인터 형입니다. 
32비트 기반의 아키텍처에서 워드는 4바이트인데, 8바이트 포멧으로 함수가 반환하는 타입을 선언하면 워드 데이터 범위를 넘어서게 됩니다.

이번에도 예제 코드를 보면서 함수의 반환형을 워드 단위로 지정해야 하는 이유에 대해 더 알아봅시다.

unsigned long long add_func(int x, int y)
{
unsigned long long result = x + y;
printf("x:%d, y:%d \n", x, y);
return result;
}

위 함수를 보면 함수의 반환형이 unsigned long long입니다. 즉 8바이트(64비트)로 result를 반환하는데, 이 때 R0와 R1 레지스터에 걸쳐서 반환값이 저장됩니다. 여기서 "C 코드에서 이런 내용을 어떻게 확인할 수 있을까"라는 의문이 생깁니다. add_func() 함수를 어셈블리 명령어로 보면, 이런 의문을 해소할 수 있습니다. 

다음은 add_func() 함수의 반환형을 int 로 지정한 명령어[before]와 unsigned long long으로 지정한 명령어[after]입니다.

[before]: 함수 반환형(int)
01   104a0: e1a00003  mov r0, r3
02   104a4: e24bd004  sub sp, fp, #4
03   104a8: e8bd8800  pop {fp, pc}

[after]: 함수 반환형(unsigned long long)
04   10474: e1a00002  mov r0, r2
05   10478: e1a01003  mov r1, r3
06   1047c: e24bd004  sub sp, fp, #4
07   10480: e8bd8800  pop {fp, pc}

함수의 반환형을 워드 단위로 지정한 경우, 01번째 줄과 같이 R0 레지스터에 반환값을 저장합니다. 그런데 함수의 반환형이 워드 단위를 넘어서면 04~05번째 줄과 같이 R0와 R1 레지스터에 반환값을 저장합니다.

한 개의 명령어로 반환값을 R0 레지스터에 저장할 수 있는데, 명령어의 갯수가 2개로 늘었습니다. 이에 비례해서 add_func() 함수를 호출하는 구문도 어셈블리 명령어의 갯수가 늘어나게 됩니다.

정리하면, 되도록 함수의 반환형은 워드 단위로 지정하는게 바람직합니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[Arm프로세서] AAPCS: Armv7: 함수 인자의 갯수는 4개 이하로 제한 AAPCS(Calling Procedure Arm)

함수에 전달되는 인자는 4개 이하로 제한하는 것이 좋습니다. 함수의 인자의 갯수가 5개를 넘어가면, 프로세스의 스택 공간에 인자를 저장해 전달하기 때문입니다.

이번에는 함수에 5개의 인자를 전달하는 예제 코드를 분석하면서, 이 내용에 대해 조금 더 짚어봅시다. 

01 int add_func(int a, int b, int c, int d, int e)
02 {
03 int result = a + b + c + d + e;
04 printf("a:%d, b:%d, c:%d, d:%d, e:%d \n", 
05 a, b, c, d, e);
06
07 return result;
08 }
09
10 int main(void) 
11 {
12 int a = 1, b = 2, c = 3, d = 4, e = 5; 
13
14 int res = add_func(a, b, c, d, e);
15 printf("add result = %d \n", res);
16 
17 return 0;
18}

14번째 줄을 보면 add_func() 함수에 전달되는 인자의 갯수가 5개입니다. 이 경우 함수에 전달되는 인자는 어느 레지스터에 저장될까요? 위 코드를 컴파일하면 생성되는 어셈블리 코드에서 그 답을 찾을 수 있습니다. 다음 어셈블리 명령어를 보면서 이 내용을 더 자세히 알아봅시다.

01 000104b0 <main>:
02 
03 int main(void) 
04 {
05   104b0: e92d4800  push {fp, lr}
06   104b4: e28db004  add fp, sp, #4
07   104b8: e24dd020  sub sp, sp, #32
08 int a = 1, b = 2, c = 3, d = 4, e = 5; 
...
09 int res = add_func(a, b, c, d, e);
10   104e4: e51b3018  ldr r3, [fp, #-24] ; 0xffffffe8
11   104e8: e58d3000  str r3, [sp]
12   104ec: e51b3014  ldr r3, [fp, #-20] ; 0xffffffec
13   104f0: e51b2010  ldr r2, [fp, #-16]
14   104f4: e51b100c  ldr r1, [fp, #-12]
15   104f8: e51b0008  ldr r0, [fp, #-8]
16   104fc: ebffffcc  bl 10434 <add_func>

위 어셈블리 명령어에서 눈여겨 볼 부분은 다음과 같이 11~15번째 줄인데, add_func() 함수의 주소로 분기하기 전에 인자를 저장하는 루틴입니다.

11   104e8: e58d3000  str r3, [sp]
12   104ec: e51b3014  ldr r3, [fp, #-20] ; 0xffffffec
13   104f0: e51b2010  ldr r2, [fp, #-16]
14   104f4: e51b100c  ldr r1, [fp, #-12]
15   104f8: e51b0008  ldr r0, [fp, #-8]

12~15번째 줄을 쉽게 설명하면, R0, R1, R2, R3 레지스터에 함수에 전달되는 인자를 저장하는 동작입니다. 12~15번째 줄은 비슷한 패턴의 명령어인데, 이 중 한 가지 명령어만 알아봅시다. 15번째 줄에 있는 명령어는 "fp 레지스터의 값을 기준으로 -8바이트에 위치한 데이터를 r0 레지스터에 로딩하는 동작입니다.

이어서 11번째 줄을 보겠습니다.

11   104e8: e58d3000  str r3, [sp]
 
r3 레지스터의 값을 현재 스택 공간에 저장하는 동작입니다. str는 store 명령어인데, sp 레지스터가 가리키는 주소에 r3 레지스터의 값을 저장하게 됩니다. 

이 동작으로 함수에 전달되는 인자의 갯수가 5개이면, 4개의 인자까지는 R0, R1, R2, R3 레지스터까지 인자를 저장하고, 5번째 인자는 스택 공간에 저장한다라는 사실을 알 수 있습니다.

이를 일반화해서 "함수에 전달되는 인자의 갯수가 4개를 넘어서면, 4개까지는 R0~R3 레지스터에 저장하고, 5개 이후는   프로세스의 스택 공간에 인자를 저장한다"고 할 수 있습니다.
구조 설계 관점으로 함수는 한 가지 기능을 수행하도록 작성하는 것도 중요하지만, 함수에 전달되는 인자의 갯수가 5개 이상인지 확인할 필요가 있습니다. C 코드로 함수의 인자의 갯수가 늘어나는게 별 게 아닌 것 같지만, Armv7 아키텍처 관점으로 보면 어셈블리 명령어의 양이 늘어나 복잡도가 커집니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[라즈베리파이] 라즈비안 커널 빌드에 대한 유용한 정보 2. 라즈베리 파이 설정

[라즈베리파이] 라즈비안 커널 빌드에 대한 유용한 정보

https://wiki.gentoo.org/wiki/Raspberry_Pi/Kernel_Compilation


[라즈베리파이] 64비트 라즈비안 이미지 위치 2. 라즈베리 파이 설정

'2021-05-28'에 릴리즈한 이미지 위치

downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2021-05-28/

기존에 릴리즈한 라즈비안 이미지 위치

downloads.raspberrypi.org/raspios_arm64/images/

[디버깅] Crash-Utility(디버깅 패치) [Debugging] Tips


크래시 유틸리티를 디버깅할 수 있는 유용한 디버깅 패치다.
100% 내가 만든 것이다.

diff --git a/arm64.c b/arm64.c
--- a/arm64.c
+++ b/arm64.c
@@ -361,6 +361,19 @@ arm64_init(int when)
        /* use machdep parameters */
        arm64_calc_phys_offset();
     
+       error(INFO, "[+][%s][%d] at %s\n", __func__, __LINE__, __FILE__);
+       error(INFO, "kimage_voffset: %lx phys_offset: %lx \n",
+                               machdep->machspec->kimage_voffset, machdep->machspec->phys_offset);
+
+        error(INFO, "CONFIG_ARM64_VA_BITS: %ld\n", ms->CONFIG_ARM64_VA_BITS);
+        error(INFO,  "     VA_BITS_ACTUAL: %ld\n", ms->VA_BITS_ACTUAL);
+        error(INFO, "(calculated) VA_BITS: %ld\n", ms->VA_BITS);
+        error(INFO, " PAGE_OFFSET: %lx\n", ARM64_PAGE_OFFSET_ACTUAL);
+        error(INFO, "    VA_START: %lx\n", ms->VA_START);
+        error(INFO, "     modules: %lx - %lx\n", ms->modules_vaddr, ms->modules_end);
+        error(INFO, "     vmalloc: %lx - %lx\n", ms->vmalloc_start_addr, ms->vmalloc_end);
+        error(INFO, "kernel image: %lx - %lx\n", ms->kimage_text, ms->kimage_end);
+        error(INFO, "     vmemmap: %lx - %lx\n\n", ms->vmemmap_vaddr, ms->vmemmap_end);
        if (CRASHDEBUG(1)) {
            if (machdep->flags & NEW_VMEMMAP)
                fprintf(fp, "kimage_voffset: %lx\n",
@@ -816,6 +829,11 @@ arm64_parse_cmdline_args(void)
                    "setting max_physmem_bits to: %ld\n\n",
                    machdep->max_physmem_bits);
                continue;
+           } else if (arm64_parse_machdep_arg_l(arglist[i], "kaddr_offset",
+               &machdep->machspec->kimage_addr_offset)) {
+               error(WARNING, "setting kimage_addr_offset to: 0x%lx\n\n",
+                                   machdep->machspec->kimage_addr_offset);
+               continue;
            }
  
             error(WARNING, "ignoring --machdep option: %s\n",
@@ -1011,6 +1029,12 @@ arm64_kdump_phys_base(ulong *phys_offset)
    if ((machdep->flags & NEW_VMEMMAP) &&
        machdep->machspec->kimage_voffset &&
        (sp = kernel_symbol_search("memstart_addr"))) {
+
+       physaddr_t kimage_offset_addr = machdep->machspec->kimage_addr_offset;
+
+       if (kimage_offset_addr) {
+           machdep->machspec->kimage_voffset -= kimage_offset_addr;
+       }
        paddr = sp->value - machdep->machspec->kimage_voffset;
        if (READMEM(-1, phys_offset, sizeof(*phys_offset),
            sp->value, paddr) > 0) 


[리눅스커널] 리눅스 커널을 컨트리뷰션하는 6가지 방법 Linux Kernel Contribution

From: 

'Qu Wenruo' 개발자께서 리눅스 커널을 컨트리뷰션하는 방법을
lkml에서 공유했습니다.

...

- Better kernel CI/Zero-day testing
   One of the better example is Intel LKP, it caught quite some bugs.
   (although sometimes not that reproducible)
   If you guys could have such facility running for not only upstream
   kernels but also maintainers' trees, I have no doubt it will be well
   received.

 * 현재 갖고 있는 샘플에 스트레스 테스트를 돌려보는 것도 좋은 방법입니다.
   이 과정에서 버그가 확인될 수 있죠.

- BUG_ON() removal
   I'm pretty sure there are quite some code using BUG_ON() to handle
   errors, especially for -ENOMEM (just handled a dozen in btrfs).
   Can't imagine a maintainer don't like this (of course with proper
   commit message)
 
  * CONFIG_BUG 컨피그를 제거하고 테스트를 진행하면 좋겠네요.

- Error injection tests
   Especially when combined with above BUG_ON() removal.
   Any everyone can also learn some tricks from BCC community.

  * 리눅스 커널에서는 강제로 에러를 추가해 테스트를 돌릴 수 있습니다. 
    BCC community의 정체에 대해서는 조금 더 리서치를 해봐야 겠네요.

- Better code refactor for super long parameter lists/super deep loops
   I'm definitely not referring functions like submit_extent_page().
   Although if anyone has better way to refactor such functions, it would
   definitely be a good move.

  * 함수에 전달되는 파라미터의 갯수가 아주 많은 경우, 이를 리펙토링하는 
    코드를 적용할 수 있겠네요.
   
   submit_extent_page() 함수의 정체를 확인해 봤더니, 인자의 갯수가 12개네요.

   https://elixir.bootlin.com/linux/v5.13/source/fs/btrfs/extent_io.c#L3204
   static int submit_extent_page(unsigned int opf,
      struct writeback_control *wbc,
      struct page *page, u64 disk_bytenr,
      size_t size, unsigned long pg_offset,
      struct bio **bio_ret,
      bio_end_io_t end_io_func,
      int mirror_num,
      unsigned long prev_bio_flags,
      unsigned long bio_flags,
      bool force_bio_submit)
   {
int ret = 0;
struct bio *bio; 


- More comprehensive metadata check for various filesystems
   Not sure about other fses, as they have much less metadata usage.
   But we have almost member by member check for all metadata, it may
   be an interesting idea to enhance other filesystems too.

   * 다양한 파일 시스템의 메타 데이터를 체크하는 패치도 반영되면 좋겠네요.
 
- More upstream phone/tablet support
   Especially for guys even running upstream kernel on RPI CM4 like me,
   more ARM devices with upstream kernel support will just be more
   happiness.

   * 다양한 디바이스를 지원할 수 있는 피쳐가 반영되면 좋겠네요.

   Not to mention this also means super long time support, way longer
   than the lifespan of those devices.

   It's super sad to see just less than a dozen phones/tablets got
   upstream kernel support.
   Even more frustrating that those mainlined devices are already pretty
   old and slow for today's standard.

   If you guys can change the trend, it would be wonderful.

- More testing on extra page size
   Well, this is more or less related to my personal work, testing
   btrfs subpage support on 64K page sized Aarch64 platforms.

  * 페이지 사이즈를 키우는 설정을 한 다음에 테스트를 진행해도 좋겠네요.

   Despite of my impure motivation, tests on new page size would
   definitely help everyone.

   (Looking at some M1 chips which doesn't even  support 64K page size
    at all)

정리

정말 정말 훌륭한 정보인 것 같습니다.
'Qu Wenruo' 님 감사합니다.

[Arm프로세서] AAPCS: Armv7: AAPCS와 C 코드 최적화 AAPCS(Calling Procedure Arm)

이제까지 다룬 Armv7 아키텍처에서 정의된 AAPCS를 배우고 나면 자연스럽게 다음과 같은 의문이 생길 가능성이 높습니다.

    "AAPCS와 관련된 내용을 실무에 어떻게 활용할 수 있을까?" 

이번 장의 앞 부분에서 강조했지만, Armv7 아키텍처를 구성하는 다른 내용보다도 AAPCS는 실무에 활용될 내용이 많습니다. 특히 AAPCS는 최적화를 이야기할 때 항상 논의됩니다.

사실 개발자들이 주어진 스팩이나 시나리오에 맞게 기능을 구현한 후, 어느 정도 시점이 지나면 최적화 작업을 시작하는 경우가 많습니다. 작성된 코드를 최적화해야, 맡은 기능이 제대로 동작하는 경우가 있습니다. 가끔 소리가 끊기거나 화면이 부드럽게 출력되지 않는 상황입니다.

임베디드 시스템 개발자 관점으로는 성능을 최적화하면 전력 소모와 클럭 속도를 줄일 수 있습니다. 특히 적은 메모리와 클럭을 기반으로 구동되는 소형 임베디드 디바이스에서는 최적화가 더 요구됩니다.

사실 최적화 작업은 어찌 보면 쉽지 않은 고된 과제입니다. 아무리 코드를 찾아봐도 개선 포인트가 보이지 않습니다. 하지만 작성된 코드를 바로 개선할 수 있는 아이디어를 손 쉽게 AAPCS에서 확인할 수 있습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv7: 함수를 호출할 때 쓰이는 R0 ~ R3 레지스터와 명령어 분석 AAPCS(Calling Procedure Arm)

SP 레지스터, LR 레지스터(R14)와 더불어 눈여겨봐야 할 레지스터가 R0 ~ R3 레지스터입니다.
함수를 호출할 때 전달되는 인자는 R0 ~ R3 레지스터에 저장되고, 함수가 반환하는 값은 R0 레지스터로 저장되기 때문입니다.

R0 ~ R3 레지스터의 역할: 함수에 전달된 인자 저장

다음 예제 코드를 보면서 함수에 인자가 전달될 때의 세부 동작을 알아봅시다.

01 0001047c <main>:
02
03 int main(void)
04 {
05   1047c:       e92d4800        push    {fp, lr}
06   10480:       e28db004        add     fp, sp, #4
07   10484:       e24dd010        sub     sp, sp, #16
08        int x = 1;
09   10488:       e3a03001        mov     r3, #1
10   1048c:       e50b3008        str     r3, [fp, #-8]
11        int y = 2;
12   10490:       e3a03002        mov     r3, #2
13   10494:       e50b300c        str     r3, [fp, #-12]
14
15        int res = add_func(x, y);
16   10498:       e51b100c        ldr     r1, [fp, #-12]
17   1049c:       e51b0008        ldr     r0, [fp, #-8]
18   104a0:       ebffffe3        bl      10434 <add_func>
19   104a4:       e50b0010        str     r0, [fp, #-16]

위 코드의 핵심 루틴은 다음 15~18번째 줄입니다.

15        int res = add_func(x, y);
16   10498:       e51b100c        ldr     r1, [fp, #-12]
17   1049c:       e51b0008        ldr     r0, [fp, #-8]
18   104a0:       ebffffe3        bl      10434 <add_func>

먼저 16~17번째 줄을 분석하겠습니다. fp 레지스터가 가리키고 있는, 메모리 주소에 있는 데이터를 r1, r0 레지스터에 로딩하는 동작입니다.

이어서 18번째 줄은 add_func() 함수의 주소인 0x10434로 분기하는 동작입니다.

16~18번째 줄의 어셈블리 명령어에 대응하는 C 코드는 15번째 줄인데, x와 y라는 인자를 전달하면서 add_func() 함수를 호출합니다. 이 동작을 Arm 아키텍처 관점으로 이 함수에 전달되는 인자는 R0와 R1 레지스터에 저장된다고 분석할 수 있습니다.   

   * R0 레지스터: x
   * R1 레지스터: y

위 예시에서는 2개의 인자를 함수에 전달합니다. 그렇다면 함수에 전달되는 인자의 갯수가 3이면 어떻게 동작을 할까요? R0, R1, R2 레지스터에 함수의 인자가 저장됩니다. 

일반적으로 R0 ~ R3 레지스터에 함수에 전달되는 인자가 저장되는데, 함수의 인자가 4개인 경우 R0 ~ R3 레지스터에 인자가 저장됩니다. 만약 함수에 전달되는 인자의 갯수가 5개 이상이면 프로세스의 스택 공간에 인자를 저장하게 됩니다.

예제 코드 분석: R0 ~ R3 레지스터에 인자 저장

이번에는 add_func() 함수의 앞 부분 어셈블리 코드를 보면서, R0와 R1 레지스터에 저장된 인자가 어떻게 처리되는지 확인해 봅시다. 

01 00010434 <add_func>:
02
03 int add_func(int x, int y)
04 {
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07   1043c:       e24dd010        sub     sp, sp, #16
08   10440:       e50b0010        str     r0, [fp, #-16]
09   10444:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec
10        int result = x + y;

위 어셈블리 명령어에서 눈여겨 볼 부분은 08~10번째 줄입니다.

08   10440:       e50b0010        str     r0, [fp, #-16]
09   10444:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec

먼저 08번째 줄을 분석하겠습니다.

'str r0, [fp, #-16]' 명령어는 fp 레지스터가 가리키는 주소에서 16만큼을 뺀 주소에 r0 레지스터의 값을 저장하는 동작입니다. 먼저 09번째 줄도 비슷한 명령어인데, fp 레지스터가 가리키는 주소에서 20만큼을 뺀 주소에 r1 레지스터의 값을 저장하는 동작입니다.

이를 다른 관점으로 해석하면, 함수에 전달된 x와 y인자를 스택 메모리 공간에 저장하는 동작이라고 볼 수 있습니다.

이번에는 add_func() 함수의 주소로 분기하는 부분과 add_func() 함수의 앞 부분의 어셈블리 코드를 합쳐서 같이 보겠습니다.

01   10498:       e51b100c        ldr     r1, [fp, #-12]
02   1049c:       e51b0008        ldr     r0, [fp, #-8]
03   104a0:       ebffffe3        bl      10434 <add_func>
...
04 00010434 <add_func>:
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07   1043c:       e24dd010        sub     sp, sp, #16
08   10440:       e50b0010        str     r0, [fp, #-16]
09   10444:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec

위 예제 코드에서 03번째 줄은 R0와 R1 레지스터에 인자를 저장해 add_func() 함수의 주소로 분기하는 동작입니다. 이어서 add_func() 함수의 어셈블리 코드를 보면, 08~09번째 줄에서 함수에 전달된 인자를 담고 있는 r0, r1 레지스터를 저장합니다.

[정보]
어셈블리 명령어를 분석할 때, 명령어의 각각 의미를 문법에 치중하는 것보다, 이처럼 Arm 아키텍처의 동작 원리는 이해하면 배운 내용을 더 오랫동안 기억할 수 있습니다.

R0 레지스터의 역할: 함수의 반환값 저장

함수가 자신의 기능을 수행한 다음에 반환하는 결과값은 R0 레지스터에 저장됩니다.

이어서 add_func() 함수의 'return 구문'에 매핑되는 어셈블리 코드를 보면서, 이 내용에 대해 더 배워봅시다.

01 00010434 <add_func>:
02
03 int add_func(int x, int y)
04{
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07...
08
09   10464:       ebffff9f        bl      102e8 <printf@plt>
10
11        return result;
12   10468:       e51b3008        ldr     r3, [fp, #-8]
13}
14   1046c:       e1a00003        mov     r0, r3
15   10470:       e24bd004        sub     sp, fp, #4
16   10474:       e8bd8800        pop     {fp, pc}
17   10478:       00010554        .word   0x00010554

11번째 줄을 보면 add_func() 함수는 'return result' 구문과 함께 실행 결과를 반환하고, 실행을 마무리합니다. 'return result' 구문에 대응되는 어셈블리 코드는 다음 12~16번째 줄입니다.

11        return result;
12   10468:       e51b3008        ldr     r3, [fp, #-8]
13}
14   1046c:       e1a00003        mov     r0, r3
15   10470:       e24bd004        sub     sp, fp, #4
16   10474:       e8bd8800        pop     {fp, pc}

보다시피, 14번째 줄에서 R0 레지스터의 값은 R0 레지스터에 이동됩니다. 이는 result를 r0 레지스터에 이동시키는 동작으로 해석될 수 있습니다. 이어서 16번째 줄을 실행되면 스택에 푸시된 fp, lr 레지스터의 값을 fp와 pc 레지스터에 로딩해 add_func() 함수를 호출한 다음에 복귀할 주소로 분기합니다.

이번에는 add_func() 함수가 실행을 끝낸 후 복귀해 실행되는 코드를 보겠습니다.

15        int res = add_func(x, y);
16   10498:       e51b100c        ldr     r1, [fp, #-12]
17   1049c:       e51b0008        ldr     r0, [fp, #-8]
18   104a0:       ebffffe3        bl      10434 <add_func>
19   104a4:       e50b0010        str     r0, [fp, #-16]

add_func() 함수의 마지막에 위치한 'pop {fp, pc}' 명령어를 실행하면, 19번째 줄과 같이 0x104a4 주소에 있는 'str r0, [fp, #-16]' 명령어가 보입니다. 이 명령어에 보이는 R0 레지스터에는 add_func() 함수가 반환하는 result 값이 저장돼 있습니다.

add_func() 함수가 반환하는 값을 res라는 변수가 저장하는데, 이에 해당하는 어셈블리 명령어가 'str r0, [fp, #-16]'입니다.

여기서 기억해야 할 중요한 포인트는 다음과 같습니다.

   “함수가 return 구문과 함께 반환하는 값은 r0 레지스터에 저장된다.”

이번에는 add_func() 함수의 마지막 부분과 add_func() 함수의 반환값을 읽는 어셈블리 코드를 함께 보겠습니다.

01   10498:       e51b100c        ldr     r1, [fp, #-12]
02   1049c:       e51b0008        ldr     r0, [fp, #-8]
03   104a0:       ebffffe3        bl      10434 <add_func>
...
04 00010434 <add_func>:
05   10434:       e92d4800        push    {fp, lr}
...
06 // <add_func> 함수의 마지막 어셈블리 코드
07   1046c:       e1a00003        mov     r0, r3
08   10470:       e24bd004        sub     sp, fp, #4
09   10474:       e8bd8800        pop     {fp, pc}
10   10478:       00010554        .word   0x00010554
...
11   104a4:       e50b0010        str     r0, [fp, #-16] 

위 코드의 핵심 명령어는 07번째 줄과 11번째 줄인데, "add_func() 함수가 반환하는 값은 r0 레지스터에 실려 온다"라는 내용을 그리며 분석하면 더 쉽게 이해할 수 있습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv7: LR(R14) 링크 레지스터와 어셈블리 명령어 분석 AAPCS(Calling Procedure Arm)

SP 레지스터보다 링크 레지스터인 LR(R14)는 '함수 호출'에서 가장 중요한 역할을 수행합니다. 함수를 호출한 다음에 복귀할 주소를 LR(R14) 레지스터가 저장하기 때문입니다.

우리가 함수를 작성하면 어디선가 함수를 호출할 것이라 예상합니다. 그런데 함수를 호출한 다음에, 호출된 함수의 실행이 종료되면, 함수를 호출한 바로 아랫 부분에 있는 코드가 실행됩니다. 

LR 레지스터와 관련된 예제 코드 분석

이번에는 아래와 같이 main() 함수의 코드(C 코드와 어셈블리 코드로 같이 변환)을 보면서 이 내용에 대해 조금 더 알아봅시다.

01 0001047c <main>:
02
03 int main(void)
04 {
05   1047c:       e92d4800        push    {fp, lr}
06   10480:       e28db004        add     fp, sp, #4
07   10484:       e24dd010        sub     sp, sp, #16
08        int x = 1;
09   10488:       e3a03001        mov     r3, #1
10   1048c:       e50b3008        str     r3, [fp, #-8]
11        int y = 2;
12   10490:       e3a03002        mov     r3, #2
13   10494:       e50b300c        str     r3, [fp, #-12]
14
15        int res = add_func(x, y);
16   10498:       e51b100c        ldr     r1, [fp, #-12]
17   1049c:       e51b0008        ldr     r0, [fp, #-8]
18   104a0:       ebffffe3        bl      10434 <add_func>
19   104a4:       e50b0010        str     r0, [fp, #-16]

위 코드의 핵심은 아래와 같이 15~19번째 줄 루틴입니다.

15        int res = add_func(x, y);
16   10498:       e51b100c        ldr     r1, [fp, #-12]
17   1049c:       e51b0008        ldr     r0, [fp, #-8]
18   104a0:       ebffffe3        bl      10434 <add_func>
19   104a4:       e50b0010        str     r0, [fp, #-16]

C 코드 기준으로 15번째 줄은 add_func() 함수를 호출하는 코드인데, 어셈블리 코드 기준으로는 18번째 줄에서 add_func() 함수의 주소로 분기합니다.

[정보]
위 코드는 이해를 돕기 위해 C 코드와 어셈블리 명령어를 같이 표기한 것입니다.


그렇다면 18번째 줄에서 add_func() 함수의 주소로 분기한 다음에, add_func() 함수가 실행을 마치면 어느 주소에 있는 코드가 실행될까요? 

다음 코드와 같이 19번째 줄입니다.

18   104a0:       ebffffe3        bl      10434 <add_func>
19   104a4:       e50b0010        str     r0, [fp, #-16]

이를 당연한 동작이라고 여기지만, 조금만 깊게 생각하면 누군가의 도움을 받아야만 가능하다는 사실을 알게 됩니다. 즉, add_func() 함수를 호출(함수의 주소로 분기)한 다음에 복귀할 주소를 누군가가 알고 있어야 이런 동작이 가능한 것입니다. 그런데 함수를 호출한 다음에 복귀할 주소를 저장하는 역할을 LR(R14) 레지스터가 수행합니다.

서브 루틴(함수)이 호출될 때 LR(R14) 레지스터의 동작 원리

이를 조금 더 일반화해서 다음과 같이 설명할 수 있습니다.  

   “어떤 코드에서 함수를 호출하면, Arm 코어는 함수가 실행을 끝낸 후 복귀할 주소를 
    LR 레지스터에 업데이트한다.”
 
이해를 돕기 위해 함수가 호출되면 Arm 아키텍처 관점으로 어떤 동작을 하는지를, 세부 단계 별로 나누면 다음과 같습니다. 

   1. 'bl [함수이름]' 포멧의 명령어를 실행한다.
   2. Arm 코어는 함수 실행을 마무리한 후 복귀할 주소를 LR 레지스터에 업데이트한다.
   3. 함수의 앞 부분에 LR 레지스터를 프로세스의 스택 공간에 푸시한다.
   4. 함수는 기능을 수행한다.
   5. 함수가 실행을 끝내면, 스택 공간에 푸시된 R13 레지스터를 프로그램 카운터에 넣어준다.
   6. 함수를 호출한 다음 주소로 복귀한다.

위 단계에서 가장 중요한 동작은 “'bl [함수이름]' 포멧의 명령어를 실행하면 Arm 코어는 하드웨어적으로 LR 레지스터에 함수 실행을 마무리한 후 복귀할 주소를 업데이트해준다”라고 말할 수 있습니다.  

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



리눅스 디바이스 드라이버와 리눅스 커널은 정말 배울 필요가 없을까? 임베디드 에세이

이번에는 임베디드 혹은 시스템 리눅스 개발과 관련된 이야기를 해보려고 해요. 

저는 개발자 뿐만 아니라 취준생 분들과 교류를 하면서 여러 가지 정보를 공유받고 공유하기도 하는데요. 가끔 황당할 때가 있어요. 어! 어떻게 이런 생각을 할 수가 있지? 이런 정보를 어떻게 들었지? 그럼 어떤 황당한 소리를 들었냐고요? 이제부터 이야기를 해볼께요.

'리눅스 디바이스 드라이버'는 배울 필요가 없다

가장 먼저 들었던 소리는 '리눅스 디바이스 드라이버'는 배울 필요가 없다는 의견을 주신 분들이 있었어요. 앱 개발자 분들은 이런 말을 할 수도 있을 것 같아요. 그런데 임베디드나 리눅스 시스템 분야 개발을 지망하시는 분들 입에서 이런 말이 나오더라고요. 그래서 제가 물어봤죠. 디바이스 드라이버를 배우지 않아도 되는 이유가 뭐에요? 그런데 뭐라고 대답하는 줄 아시나요? SoC 칩 업체에서 안정된 리눅스 드라이버를 제공하기 때문이래요. 이 이야기를 듣고 저도 모르게 나오는 웃음을 참기 어려웠어요. 이게 뭔 소리지?

다시 한번 강조하지만 이런 말은 한 사람이 임베디드 리눅스 개발자 지망생이었어요. 그래서 이 친구에게 다시 한번 물어봤어요. 이게 니 생각이냐? 그랬더니 자신이 다녔던 IT 학원 강사가 이런 말을 했다는 거였어요. 취준생들이 IT 강사들이 하는 말을 그대로 믿는다는 게 좀 안타까웠어요. 

그래서 제가 그 친구에게 임베디드 리눅스 개발자가 리눅스 디바이스 드라이버를 꼭 배워야 하는 이유를 설명해 줬어요. 그 이야기를 여기서 하면요.

먼저 제품을 구성할 때 부품이 바뀔 수가 있어요. 예를 들어 A란 센서 부품에서 B라는 센서 부품으로 변경됐어요. 그럼 B라는 센서의 드라이버를 제품에 맞게 수정해야 해요. 

이 때 SoC 칩 업체 개발자에게 B란 센서의 드라이버를 제작해 달라고 하면, 소스를 줄까요? 이런 요청을 하면 아예 들은 척도 하지 않을껄요?

두 번째로는 SoC 칩 업체에서 전달하는 드라이버 자체도 100% 안정성이 보장된다고는 볼도 수 없고요. 이건 좀 민간한 이야기기도 한데요. 생각지도 못한 버그가 숨어 있을 수 있어요. 그런데 제품을 개발하는 도중에 버그가 나오면 SoC 칩 업체 개발자가 그 버그를 잡아 줄까요? 물론 개발하려는 SoC 칩에 대한 기술 지원 비용을 지불한다면 가능하겠죠.

그런데 생각보다 SoC 칩에 대한 기술 지원 비용을 내지 않고 개발하는 프로젝트가 은근히 많아요. 이런 상황에서는 제품 개발자가 SoC 칩 업체에서 전달된 드라이버 코드를 수정해야 되죠.

또한 디바이스 드라이버를 모르면 부팅 시간이라던가 디바이스의 실행 시간을 최적화하는 작업을 진행할 수 없어요. 리눅스 시스템 프로그래밍으로 이런 작업을 할 수 있다고 주장하시는 분도 있는데요. 사실 시스템 프로그래밍은 직접 하드웨어를 콘트롤 할 수 없으므로 한계가 있어요.

스케줄러 코드를 수정할 필요가 없으니 리눅스 커널은 배울 필요가 없다

이어서 제가 들었던 황당한 이야기를 해볼께요. 리눅스 시스템 개발 업계에서 이런 소리를 하시는 분들이 있는데요. 스케줄러 코드를 수정할 필요가 없으니 리눅스 커널은 배울 필요가 없다는 소리를 해요. 작년까지만 해도 2000년대 초반에 임베디드 개발을 시작했다가 리눅스에 적응하지 못했던 퇴물 개발자들로부터 이런 소리를 들었는데요. 요즘에는 주니어 개발자들의 입에서 이런 말을 들을 때 '약간 어이가 없다'는 생각이 좀 들었어요. 참 꼰대는 나이를 초월해서 존재한다는 생각도 드는데요. 

'리눅스 커널의 스케줄러 소스 코드를 수정할 필요가 없다' 맞는 소리죠. 다른 해야 할 일이 얼마나 많은데요. 이런 말을 하면서 차라리 리눅스 커널에서 실전 개발에 도움이 될만한 내용을 잘 배우자라고 주장하면 수긍할꺼에요. 저도 비슷한 생각이거든요. 리눅스 커널의 내용은 너무 방대해서 먼저 배우면 좋은 내용부터 배우자란 말에 동의해요. 그런데 리눅스 커널은 배울 필요가 없다라는 주장에는 전혀 동의할 수가 없어요. 

리눅스가 어떻게 돌아가는지 시스템 개발자 수준으로 알려면 리눅스 커널의 기본 동작 원리는 알아야 해요. 이걸 모르고 알고는 엄청난 차이에요. 또한 리눅스 드라이버나 리눅스 시스템 프로그램으로 하드웨어를 제어해도, 이런 프로그램의 동작 원리를 깊게 파고 들어가면 만나는게 리눅스 커널이거든요. 그리고 리눅스 디바이스 드라이버는 리눅스 커널에서 제공하는 API로 구성돼 있어요. 그러니 리눅스 커널을 모르면 리눅스 디바이스 드라이버 자체를 제대로 개발할 수가 없죠.

그렇다면 리눅스 디바이스 드라이버를 배울 필요가 없다. 리눅스 커널을 배울 필요가 없다는 소리를 하는 이유는 뭘까요?

이런 소리를 IT 강사들이 한다면, 리눅스 커널 드라이버가 리눅스 커널을 제대로 가르칠 능력이 안되기 때문일 꺼에요. 학생들이 리눅스 커널에 대해 질문을 하면 '그거 SoC 칩 업체에서 전달하는 거니 잘 몰라도 돼'라고 대답하는 거죠.

실제 현업에서는 리눅스 디바이스 드라이버를 배울 필요가 없다는 소리보다 리눅스 커널을 제대로 배울 필요가 없다는 소리를 더 자주 듣는데요. 개발자보다 관리자 즉 매니저들이 이런 말을 더 자주 할 가능성이 높아요. 저도 그랬고요.  이렇게 말하는 가장 큰 이유는 조금은 극단적인데요. 개발자를 오로지 비용 관점으로 관리하기 때문이에요. 회사에서 100원을 투자하면 120원만큼 개발자가 일을 하기 원하는 경우가 있거든요. 그런데 개발자가 리눅스 커널을 배우는데 시간을 투자하면 회사 입장에서는 손해라고 생각하는 거에요. 왜냐면 리눅스 커널은 며칠 배운다고 바로 티가 나지 않거든요. 뭔가 개발자의 기초 체력을 키운다란 느낌이에요.  

이렇게 제가 대본까지 써가면서 이런 콘텐츠를 만든 이유는, 취준생들이 왜곡된 정보를 듣고 잘못된 방향으로 커리어를 선택할 수 있이 때문이에요.

이렇게 제가 이런 글을 쓴 이유는, 취준생들이 왜곡된 정보를 듣고 잘못된 방향으로 커리어를 선택할 수 있기 때문이에요. 이런 왜곡된 정보를 참고해서 리눅스 디바이스 드라이버나 리눅스 커널에 대해 아무런 공부를 하지 않은 체, 면접을 볼 수도 있거든요. 운이 좋게 면접에 통과 하면 좋겠지만 면접에서 리눅스 디바이스 드라이버나 커널에 대해 조금이라도 배운 경쟁자가 있다면 바로 탈락하겠죠.

SW 업계에서 배울 필요가 없는 지식은 없고 우선순위만 있을 뿐이다.

또한 IT 개발자가 배울 필요가 없는 지식은 없는 것 같아요. 리눅스 시스템 개발자라도 해도 파이썬으로 애플리케이션을 코딩하는 것 배울 필요가 없는 건 아니에요. 시간이 있으면 배우면 좋지만, 우선 순위라는 게 있잖아요. 뭐 arm 프로세서나 하드웨어와 같이 더 중요하게 익혀야 하는 테마가 있잖아요.





[리눅스커널] lkml: 메인테이너의 시간을 뺏지 마세요! Linux Kernel Contribution

흥미로운 메시지가 lkml에서 보여 소개합니다. 
출처와 메시지는 다음과 같은데요. 기술적인 내용은 별로 없으니 심플하게 읽어도 좋겠네요.

From: 

> Hi Leizhen, and guys in the mail list,
> Recently I find one patch removing a debug OOM error message from btrfs 
> selftest.
> It's nothing special, some small cleanup work from some kernel newbie.
> But the mail address makes me cautious, "@huawei.com".
> The last time we got some similar patches from the same company, doing 
> something harmless "cleanup". But those "fixes" are also useless.

화웨이에 소속된 개발자들이 크린업 패치를 많이 제안한 것 같네요.

> It's OK for first-time/student developers to submit such patches, and I 
> really hope such patches would make them become a long term contributor.
> In fact, I started my kernel contribution exactly by doing such "cleanups".

처음 컨트리뷰션을 할 때 보통 cleanup 패치로 시작하긴 하죠.

> But what you guys are doing is really KPI grabbing, I have already see 
> several maintainers arguing with you on such "cleanups", and you're 
> always defending yourself to try to get those patches merged.

음, 크린업 패치를 밀어 넣으면서 메인테이너랑 논쟁을 벌이나 보네요. 세상에...
보통 메인테이너가 '크린업 패치를 보고 부정적인 의견'을 보이면 바로 물러 서는데요.

[리눅스커널] SELinux, SMACK: 커널 컨피그 Linux Security


SELinux

SELinux를 사용하기 위해서는 다음과 같은 커널 컨피그가 설정돼 있어야 합니다.

* CONFIG_AUDIT=y
* CONFIG_NF_CONNTRACK_SECMARK=y
* CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
* CONFIG_NETFILTER_XT_TARGET_SECMARK=y
* CONFIG_IP_NF_SECURITY=y
* CONFIG_EXT4_FS_SECURITY=y
* CONFIG_SECURITY=y
* CONFIG_SECURITY_NETWORK=y
* CONFIG_LSM_MMAP_MIN_ADDR=4096
* CONFIG_SECURITY_SELINUX=y

만약 linux-next 브랜치에서 가져온 리눅스 커널 개발 용 소스에서는 아래 패치를 적용하면
SELinux 관련 컨피그를 모두 킬 수 있습니다.

diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig
index 662b003..be9a2e7 100644
--- a/arch/arm64/configs/defconfig
+++ b/arch/arm64/configs/defconfig
@@ -1186,6 +1186,15 @@ CONFIG_9P_FS=y
 CONFIG_NLS_CODEPAGE_437=y
 CONFIG_NLS_ISO8859_1=y
 CONFIG_SECURITY=y
+CONFIG_NF_CONNTRACK_SECMARK=y
+CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
+CONFIG_NETFILTER_XT_TARGET_SECMARK=y
+CONFIG_IP_NF_SECURITY=y
+CONFIG_EXT4_FS_SECURITY=y
+CONFIG_SECURITY=y
+CONFIG_SECURITY_NETWORK=y
+CONFIG_LSM_MMAP_MIN_ADDR=4096
+CONFIG_SECURITY_SELINUX=y
 CONFIG_CRYPTO_ECHAINIV=y
 CONFIG_CRYPTO_ANSI_CPRNG=y
 CONFIG_CRYPTO_USER_API_RNG=m

대중적으로 많이 활용되는 라즈비안 커널에서는 아래 컨피그를 적용해야 합니다.

diff --git a/arch/arm/configs/bcm2709_defconfig b/arch/arm/configs/bcm2709_defconfig
index f4da602..76bc83d 100644
--- a/arch/arm/configs/bcm2709_defconfig
+++ b/arch/arm/configs/bcm2709_defconfig
@@ -1451,6 +1451,14 @@ CONFIG_NLS_KOI8_R=m
 CONFIG_NLS_KOI8_U=m
 CONFIG_DLM=m
 CONFIG_SECURITY=y
+CONFIG_NF_CONNTRACK_SECMARK=y
+CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
+CONFIG_NETFILTER_XT_TARGET_SECMARK=y
+CONFIG_IP_NF_SECURITY=y
+CONFIG_EXT4_FS_SECURITY=y
+CONFIG_SECURITY_NETWORK=y
+CONFIG_LSM_MMAP_MIN_ADDR=4096
+CONFIG_SECURITY_SELINUX=y
 CONFIG_SECURITY_APPARMOR=y
 CONFIG_LSM=""
 CONFIG_CRYPTO_USER=m

위와 같이 설정하면 디폴트 LSM으로 SELINUX가 설정되며, .config에서 아래 내용을 확인할 수 있습니다.

CONFIG_DEFAULT_SECURITY_SELINUX=y
# CONFIG_DEFAULT_SECURITY_DAC is not set
CONFIG_LSM="landlock,lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf"

SMACK 설정

SELinux와 더불어 가장 널리 사용되는 LSM인 SMACK을 사용하려면,
아래와 같은 컨피그가 설정돼 있어야 합니다.

* CONFIG_AUDIT=y
* CONFIG_NF_CONNTRACK_SECMARK=y
* CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
* CONFIG_NETFILTER_XT_TARGET_SECMARK=y
* CONFIG_IP_NF_SECURITY=y
* CONFIG_EXT4_FS_SECURITY=y
* CONFIG_SECURITY=y
* CONFIG_SECURITY_NETWORK=y
* CONFIG_LSM_MMAP_MIN_ADDR=4096
* CONFIG_SECURITY_SMACK=y
* CONFIG_SECURITY_SMACK_BRINGUP=y
* CONFIG_DEFAULT_SECURITY_SMACK=y
* CONFIG_DEFAULT_SECURITY="smack"

만약 linux-next 브랜치에서 가져온 리눅스 커널 개발 용 소스에서는 아래 패치를 적용하면
SMACK 관련 컨피그를 모두 킬 수 있습니다.

diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig
index 662b003..13abbd84 100644
--- a/arch/arm64/configs/defconfig
+++ b/arch/arm64/configs/defconfig
@@ -1186,6 +1186,18 @@ CONFIG_9P_FS=y
 CONFIG_NLS_CODEPAGE_437=y
 CONFIG_NLS_ISO8859_1=y
 CONFIG_SECURITY=y
+CONFIG_NF_CONNTRACK_SECMARK=y
+CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
+CONFIG_NETFILTER_XT_TARGET_SECMARK=y
+CONFIG_IP_NF_SECURITY=y
+CONFIG_EXT4_FS_SECURITY=y
+CONFIG_SECURITY=y
+CONFIG_SECURITY_NETWORK=y
+CONFIG_LSM_MMAP_MIN_ADDR=4096
+CONFIG_SECURITY_SMACK=y
+CONFIG_SECURITY_SMACK_BRINGUP=y
+CONFIG_DEFAULT_SECURITY_SMACK=y
+CONFIG_DEFAULT_SECURITY="smack"
 CONFIG_CRYPTO_ECHAINIV=y
 CONFIG_CRYPTO_ANSI_CPRNG=y
 CONFIG_CRYPTO_USER_API_RNG=m

다음은 라즈비안에서 설정한 컨피그입니다.

diff --git a/arch/arm/configs/bcm2709_defconfig b/arch/arm/configs/bcm2709_defconfig
index f4da602..904c801 100644
--- a/arch/arm/configs/bcm2709_defconfig
+++ b/arch/arm/configs/bcm2709_defconfig
@@ -1451,6 +1451,17 @@ CONFIG_NLS_KOI8_R=m
 CONFIG_NLS_KOI8_U=m
 CONFIG_DLM=m
 CONFIG_SECURITY=y
+CONFIG_NF_CONNTRACK_SECMARK=y
+CONFIG_NETFILTER_XT_TARGET_CONNSECMARK=y
+CONFIG_NETFILTER_XT_TARGET_SECMARK=y
+CONFIG_IP_NF_SECURITY=y
+CONFIG_EXT4_FS_SECURITY=y
+CONFIG_SECURITY_NETWORK=y
+CONFIG_LSM_MMAP_MIN_ADDR=4096
+CONFIG_SECURITY_SMACK=y
+CONFIG_SECURITY_SMACK_BRINGUP=y
+CONFIG_DEFAULT_SECURITY_SMACK=y
+CONFIG_DEFAULT_SECURITY="smack"
 CONFIG_SECURITY_APPARMOR=y
 CONFIG_LSM=""
 CONFIG_CRYPTO_USER=m

위와 같이 설정하면 디폴트 LSM으로 SMACK가 설정되며, .config에서 아래 내용을 확인할 수 있습니다.

CONFIG_DEFAULT_SECURITY_SMACK=y
# CONFIG_DEFAULT_SECURITY_DAC is not set
CONFIG_LSM="landlock,lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,bpf"



[Arm프로세서] AAPCS: Armv7: SP(스택 포인터) 레지스터의 세부 동작 AAPCS(Calling Procedure Arm)

SP는 스택 포인터(Stack Pointer) 레지스터라고 하며 약어로 R13으로 표기합니다. 일반적으로 여러분이 작성한 코드는 프로세스의 스택 공간에서 동작하므로, 어떤 코드가 실행하던 스택 주소의 위치를 알 수 있습니다. 스택 주소의 위치를 추적하는 레지스터가 SP 혹은 R13입니다. 

SP 레지스터와 프로세스의 스택과의 관계

그렇다면 SP 레지스터는 어떤 값을 저장할까요? SP(R13) 레지스터는 프로세스 스택 포인터의 주소를 저장합니다. 그러면 여기서 말하는, 프로세스의 스택 포인터 주소는 무엇일까요? 바로 프로세스에게 할당된 스택 주소의 위치를 나타냅니다. 

프로세스는 처음 생성될 때 운영체제로부터 스택 공간을 할당받고, 자신에게 할당된 스택 공간 내에서 함수를 호출합니다. 달리 말하면 SP 레지스터는 프로세스가 할당된 스택 메모리 주소의 범위 내에서 바뀝니다.

다음 그림을 보면서 SP 레지스터의 범위를 알아봅시다.

 

그림 6.10 프로세스의 스택 주소와 SP 레지스터의 관계

위 그림은 0xC000_0000~0xC000_2000 구간인 프로세스의 스택 공간에서 main() 함수에서 __printf() 함수까지 호출되는 흐름을 나타냅니다. 가장 마지막에 실행된 함수가 __printf() 함수이므로, SP 레지스터는 __printf() 함수가 호출된 프로세스의 스택 주소를 가리키고 있습니다.

반복해 설명드리지만, 프로세스는 자신의 스택 공간에서만 실행하므로, 현재 실행 중인 코드의 스택 주소를 저장하는 SP 레지스터는 0xC000_0000 ~ 0xC000_2000 구간의 값을 저장합니다. 

이처럼 부처님 손바닥에 있는 손오공과 같이, SP 레지스터는 프로세스의 스택 메모리 주소 공간 범위 내에서 업데이트됩니다. 만약 SP 레지스터가 프로세스의 스택 주소 공간을 넘게 되면, 이를 스택 오버플로우라고 하며, 대부분의 경우 크래시가 발생해 시스템은 오동작합니다. 

소프트웨어 개발자는 스택 오버플로우가 발생하지 않게 주의해 코드를 입력해야 합니다. 스택 오버플로우가 발생하면 치명적인 오류를 유발하므로, 만약 예기치 않게 스택 오버플로우가 유발되면 반드시 해결해야 합니다.
push 명령어를 실행하면 SP 레지스터와 프로세스의 스택은 어떻게 바뀔까?

'push' 명령어를 분석할 때는 레지스터와 스택 메모리 공간과 함께 조금 입체적으로 시야를 넓히면 이해가 빠릅니다. 'push' 명령어가 실행되면 푸시하려는 레지스터의 갯수에 따라 SP 레지스터의 값이 업데이트되면서, 프로세스의 스택 공간에 지정된 레지스터의 값이 저장되기 때문입니다.

이점을 유념하면서 다음 어셈블리 코드를 분석해 봅시다.

01 00010434 <add_func>:
02
03 int add_func(int x, int y)
04 {
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07   1043c:       e24dd010        sub     sp, sp, #16
08   10440:       e50b0010        str     r0, [fp, #-16]
09   10444:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec
10        int result = x + y;
11   10448:       e51b2010        ldr     r2, [fp, #-16]
12   1044c:       e51b3014        ldr     r3, [fp, #-20]  ; 0xffffffec
13   10450:       e0823003        add     r3, r2, r3
14   10454:       e50b3008        str     r3, [fp, #-8]
15        printf("x:%d, y:%d \n", x, y);
16   10458:       e51b2014        ldr     r2, [fp, #-20]  ; 0xffffffec
17   1045c:       e51b1010        ldr     r1, [fp, #-16]
18   10460:       e59f0010        ldr     r0, [pc, #16]   ; 10478 <add_func+0x44>
19   10464:       ebffff9f        bl      102e8 <printf@plt>
20
21        return result;
22   10468:       e51b3008        ldr     r3, [fp, #-8]
23 }
24   1046c:       e1a00003        mov     r0, r3
25   10470:       e24bd004        sub     sp, fp, #4
26   10474:       e8bd8800        pop     {fp, pc}
27   10478:       00010554        .word   0x00010554

add_func() 함수의 주소로 분기하면 가장 먼저 05번째 줄이 실행됩니다.

05번째 줄을 보면 'push {fp, lr}' 명령어가 보이는데, 이는 fp(r11)과 lr(R14) 레지스터를 스택에 푸시하는 동작입니다. 이 명령어를 실행하면 두 가지 동작을 동시에 수행합니다.

   * fp(r11)과 lr(R14) 레지스터의 값이 스택에 푸시된다.
   * SP 레지스터가 0x08 만큼 감소된다.

이번에는 다음 그림을 보면서, 'push {fp, lr}' 명령어를 실행하면 업데이트되는 SP 레지스터의 값과 스택 주소와 데이터를 확인해 봅시다. 

 
그림 6.11 스택 주소와 스택 공간에 있는 데이터

'push {fp, lr}' 명령어를 실행하기 직전에, Arm 코어는 프로세스의 스택 주소를 SP 레지스터에 저장하고 있습니다. 이는 어떤 코드가 실행돼도 Arm 코어는 프로세스의 스택 주소를 알고 있다고 볼 수 있습니다.

이해를 돕기 위해 한 가지 예를 들겠습니다.
'push {fp, lr}' 명령어를 실행하기 전에, SP 레지스터의 값이 0xbefff5f8이라고 가정합시다. 이 때 'push {fp, lr}' 명령어를 실행하면 SP 레지스터의 값은 0xbefff5f0로 변경됩니다. push 명령어를 실행하면 지정된 레지스터를 프로세스의 스택 공간에 푸시하면서 SP 레지스터는 업데이트됩니다. 

이처럼 SP 레지스터는 프로세스의 스택 메모리 공간을 같이 떠올리면서 분석해야, 세부 동작 원리를 정확히 이해할 수 있습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[Arm프로세서] AAPCS: Armv7: AAPCS와 관련된 레지스터와 어셈블리 명령어 분석 AAPCS(Calling Procedure Arm)

6.1절에서 Arm 스팩 문서 분석으로 AAPCS와 관련된 레지스터를 소개했고, 6.2절에서는 AAPCS와 관련된 어셈블리 명령어에 대해 살펴봤습니다. 이번 절에서는 AAPCS와 관련된 레지스터가 어떻게 사용되는지, 어셈블리 명령어와 함께 분석하면서 자세히 알아보겠습니다.

먼저 AAPCS와 연관된 레지스터의 목록을 알아볼까요?  

표 6.4 AAPCS와 연관된 레지스터

먼저 SP 레지스터에 대해 소개하고, SP 레지스터가 어떻게 바뀌는지 어셈블리 명령어 분석으로 알아보겠습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[Arm프로세서] AAPCS: Armv7: 브랜치 명령어(bl) AAPCS(Calling Procedure Arm)

C 프로그래밍으로 함수를 호출하는 구문을 어셈블리 코드로 확인하면 'bl [심벌 주소]'와 같은 명령어가 보입니다. 이번 절에서는 bl 명령어의 정의를 소개하고, 예제 코드를 분석하면서 bl 명령어의 사용법을 살펴보겠습니다.

bl 명령어란

이번에도 Arm 스팩 문서에서 bl 명령어를 설명하는 부분을 소개합니다.

5.4 Branches

Calls to subroutines, where it is necessary for the return address to be stored in the link register, use the BL instruction.
(출처: DEN0013D_cortex_a_series_PG.pdf)

위 내용은 "BL 명령어를 사용하면 서브 루틴을 호출하는데, 서브 루틴을 호출한 다음에 복귀할 주소가 LR(R14) 레지스터에 저장된다"라고 요약할 수 있습니다.

이어서 명령어의 표기법을 보겠습니다.

BL label

표기법의 왼쪽에 label이 있는데 이는 분기할 주소를 의미합니다. 'BL label' 는 지정된 주소로 분기하는 동작을 수행합니다.

예제 코드 분석으로 bl 명령어의 사용법 알아보기

이번에는 다음과 같은 간단한 예제 코드를 보면서 BL 명령어의 용법을 알아 봅시다.

01 0x1047c      <main>:
02                             ....
03 0x104a0                 bl 104c4 <add_func>
04 0x104a4                 str     r0, [fp, #-16]
...
05 0x104c4 <add_func>:
06 0x104c8                 add r0, r1, r2 
07 0x104cc                 BL lr 

먼저 3번째 줄을 보겠습니다.

03 0x104a0                 bl 104c4 <add_func>

'bl 104c4' 명령어는 104c4 주소로 분기하는 동작을 수행하는데, 104c4는 add_func() 함수의 시작 주소입니다. 이를 쉽게 설명하면 add_func() 함수가 호출되는 동작이라고 볼 수 있습니다.

여기서 기억할 점은 3번째 줄을 실행하면 복귀할 주소가 LR(R14) 레지스터에 업데이트된다는 사실입니다. 예제 코드 기준으로는 03번째 줄 다음에 있는 04번째 줄의 주소인 0x104a4가 LR 레지스터에 저장됩니다.

정리하면 'bl 104c4' 명령어가 실행되면 동시에 다음과 같은 동작을 수행합니다.

   * add_func() 함수의 시작 주소인 0x104c4 주소로 분기
   * LR 레지스터는 서브 루틴 호출 후 복귀할 주소인 0x104a4로 업데이트됨

[정보]
이 밖에도 Arm 아키텍처에서는 다음과 같은 브랜치 명령어를 지원합니다.

B label

그런데 B 명령어를 실행하면 label로 지정된 주소로 분기하는데, label 주소에 해당되는 서브 루틴으로 분기한 다음에 복귀할 LR 레지스터가 업데이트되지 않습니다. 따라서 B 명령어를 사용해 특정 주소나 심벌을 브랜치하는 동작을 직접 어셈블리 명령어를 입력해 구현할 경우에는 복귀할 주소를 지정해 줘야 합니다.


이번 절에서는 AAPCS와 관련된 어셈블리 명령어를 소개했습니다.

다음 절에서는 AAPCS와 연관된 레지스터를 소개하고 이번 절에 소개한 어셈블리 명령어가 실행되면 관련 레지스터가 어떻게 바뀌는지 살펴보겠습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자




[리눅스커널] Brute LSM: 훅 함수 소개 Linux Security

이번에 Brute라는 새로운 LSM(Linux security module)이 소개됐는데요.
아직 리눅스 커널의 메인 트리에 업스트림되지 않았지만, 조만간에 업스트림될 것으로 보입니다.

* 관련 링크

훅(함수) 정의

LSM를 분석할 때는 2가지를 포인트로 코드를 보면 해석이 빠른데요.
이는 다음과 같습니다.

   * 어디에 훅 함수가 추가됐는가? 
   * LSM에서 구현된 훅 함수에서 해커의 공격을 어떻게 감지하는가?

먼저 brute lsm 모듈에서 추가된 훅 함수의 선언부를 보겠습니다.

security/brute/brute.c
static struct security_hook_list brute_hooks[] __lsm_ro_after_init = {
    LSM_HOOK_INIT(task_fatal_signal, brute_task_fatal_signal),
    LSM_HOOK_INIT(bprm_creds_from_file, brute_task_execve),
    LSM_HOOK_INIT(task_fix_setuid, brute_task_change_priv),
    LSM_HOOK_INIT(task_fix_setgid, brute_task_change_priv),
#ifdef CONFIG_SECURITY_NETWORK
    LSM_HOOK_INIT(socket_accept, brute_socket_accept),
#endif
};

선언부와 같이 5개의 훅 함수가 추가됐습니다. 

brute_task_fatal_signal() 함수 분석

먼저 task_fatal_signal 훅 함수가 호출되는 코드를 보겠습니다.
패치는 다음과 같습니다.

diff --git a/kernel/signal.c b/kernel/signal.c
index f7c6ffcbd044..4380763b3d8d 100644
--- a/kernel/signal.c
+++ b/kernel/signal.c
@@ -2804,6 +2804,7 @@ bool get_signal(struct ksignal *ksig)
                /*
                 * Anything else is fatal, maybe with a core dump.
                 */
+               security_task_fatal_signal(&ksig->info);
                current->flags |= PF_SIGNALED;

                if (sig_kernel_coredump(signr)) {

패치 코드를 보면 어느 루틴에 'security_task_fatal_signal(&ksig->info);' 구문이 추가됐는지 확인하기 어려우니
전체 소스를 보겠습니다.
 
https://elixir.bootlin.com/linux/v5.13-rc5/source/kernel/signal.c 
bool get_signal(struct ksignal *ksig)
{
struct sighand_struct *sighand = current->sighand;
struct signal_struct *signal = current->signal;
int signr;
...
/* Has this task already been marked for death? */
if (signal_group_exit(signal)) {
ksig->info.si_signo = signr = SIGKILL;
sigdelset(&current->pending.signal, SIGKILL);
trace_signal_deliver(SIGKILL, SEND_SIG_NOINFO,
&sighand->action[SIGKILL - 1]);
recalc_sigpending();
goto fatal;
}
...
fatal:
spin_unlock_irq(&sighand->siglock);
if (unlikely(cgroup_task_frozen(current)))
cgroup_leave_frozen(true);

/*
* Anything else is fatal, maybe with a core dump.
*/
current->flags |= PF_SIGNALED;
+                         security_task_fatal_signal(&ksig->info);

if (sig_kernel_coredump(signr)) {
if (print_fatal_signals)
print_fatal_signal(ksig->info.si_signo);
proc_coredump_connector(current);
do_coredump(&ksig->info);
}

/*
* Death signals, no core dump.
*/
do_group_exit(ksig->info.si_signo);
/* NOTREACHED */
}
spin_unlock_irq(&sighand->siglock);
out:
ksig->sig = signr;

if (!(ksig->ka.sa.sa_flags & SA_EXPOSE_TAGBITS))
hide_si_addr_tag_bits(ksig);

return ksig->sig > 0;
}

위 코드를 보면 알 수 있듯이, 시그널 그룹에 속한 프로세스가 종료될 때 fatal 레이블로 이동을 하는데, 이 때 훅 함수가 호출됩니다.

LSM 모듈의 세세한 함수를 구현하는 것보다 어느 커널 함수에 훅을 추가하는지 더 많은 고민을 했을텐데요.
왜냐면 프로세스가 종료되는 정확한 시점에 훅 함수를 추가해야 공격을 제대로 방지할 수 있거든요.

brute_bprm_creds_from_file() 훅 함수 호출

brute_bprm_creds_from_file 훅 함수는 기존의 루틴을 그대로 활용합니다.
다음은 bprm_creds_from_file() 함수에서 security_bprm_creds_from_file() 함수를 호출하는 코드입니다.

https://elixir.bootlin.com/linux/v5.13-rc4/source/fs/exec.c
static int bprm_creds_from_file(struct linux_binprm *bprm)
{
/* Compute creds based on which file? */
struct file *file = bprm->execfd_creds ? bprm->executable : bprm->file;

bprm_fill_uid(bprm, file);
return security_bprm_creds_from_file(bprm, file);
}

bprm_creds_from_file() 함수는 새로운 실행 파일(리눅스 유틸리티)이 실행되기 직전에,
creds를 체크하는 루틴입니다.

참고로, bprm_creds_from_file() 함수는 begin_new_exec() 함수에서 호출되는데, ftrace 메시지로 콜 스택을 확인해봐야 겠습니다.

/* TODO: call stack 추가 /*

brute_task_fix_setuid() 훅 함수

brute_task_fix_setuid() 함수도 기존의 LSM 훅을 활용하는데,
__sys_setreuid(), __sys_setuid(), __sys_setfsuid(), __sys_setfsuid() 함수에서 호출됩니다.

https://elixir.bootlin.com/linux/v5.13-rc5/source/kernel/sys.c
long __sys_setreuid(uid_t ruid, uid_t euid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setuid(new, old, LSM_SETID_RE);
if (retval < 0)
goto error;

long __sys_setuid(uid_t uid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setuid(new, old, LSM_SETID_ID);
if (retval < 0)
goto error;

long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setuid(new, old, LSM_SETID_RES);
if (retval < 0)
goto error;

long __sys_setfsuid(uid_t uid)
{
const struct cred *old;
struct cred *new;
...
if (uid_eq(kuid, old->uid)  || uid_eq(kuid, old->euid)  ||
    uid_eq(kuid, old->suid) || uid_eq(kuid, old->fsuid) ||
    ns_capable_setid(old->user_ns, CAP_SETUID)) {
if (!uid_eq(kuid, old->fsuid)) {
new->fsuid = kuid;
if (security_task_fix_setuid(new, old, LSM_SETID_FS) == 0)
goto change_okay;
}
}

/* TODO: ftrace로 콜 스택 확인  */

brute_task_fix_setgid() 함수의 훅 포인트는 다음과 같습니다.
__sys_setregid(), __sys_setuid(), __sys_setresuid() 그리고 __sys_setfsuid() 함수에서 호출됩니다.

long __sys_setregid(gid_t rgid, gid_t egid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setgid(new, old, LSM_SETID_RE);
if (retval < 0)
goto error;

long __sys_setuid(uid_t uid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setuid(new, old, LSM_SETID_ID);
if (retval < 0)
goto error;

long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
...
retval = security_task_fix_setuid(new, old, LSM_SETID_RES);
if (retval < 0)
goto error;

long __sys_setfsuid(uid_t uid)
{
const struct cred *old;
struct cred *new;
...
if (uid_eq(kuid, old->uid)  || uid_eq(kuid, old->euid)  ||
    uid_eq(kuid, old->suid) || uid_eq(kuid, old->fsuid) ||
    ns_capable_setid(old->user_ns, CAP_SETUID)) {
if (!uid_eq(kuid, old->fsuid)) {
new->fsuid = kuid;
if (security_task_fix_setuid(new, old, LSM_SETID_FS) == 0)
goto change_okay;
}
}

[Arm프로세서] AAPCS: Armv7: pop 명령어 AAPCS(Calling Procedure Arm)

push 명령어를 실행한 후 함수가 종료되기 직전에 반드시 실행되는 명령어가 pop입니다.

Arm 스팩 문서에서 pop 명령어 분석하기

이어서 Arm 스팩 문서에서 POP 명령어를 어떻게 설명하는지 알아 봅시다. 

A8.8.132 POP (ARM)
Pop Multiple Registers loads multiple registers from the stack, loading from consecutive memory locations starting at the address in SP, and updates SP to point just above the loaded data.

스팩 문서의 내용은 다음과 같이 알기 쉽게 해석할 수 있습니다.

   1. 프로세스의 스택에 있는 데이터를 지정된 레지스터로 빼냄 
   2. SP 레지스터의 값이 가장 마지막에 빼낸 데이터의 주소로 업데이트됨

pop 명령어의 표기법

이어서 명령어의 표기법을 보겠습니다.

POP {list_of_low_register{, lr})

pop 명령어의 왼쪽 부분에 레지스터의 이름을 지정하면, 스택에 저장된 데이터를 지정된 레지스터에 넣어주는 동작을 수행합니다. pop 명령어는 push와 마찬가지로 명령어에 SP 레지스터가 없다는 점을 기억합시다. Arm 코어가 SP 레지스터의 값을 항상 알고 있기 때문입니다.

다음 그림을 보면서 pop 명령어의 동작 원리를 알아봅시다.

 

그림 6.8 pop 명령어를 실행할 때의 세부 동작

먼저 위 그림의 왼쪽 부분에 SP는 pop 명령어를 수행할 때 변경되는 SP 레지스터를 나타냅니다. pop 명령어를 실행하기 전에 SP 레지스터는 0x1008이였는데, pop 명령어를 실행하니 0x1000이 된 것입니다.

이어서 그림의 가운데 부분에 있는 화살표는 '레지스터1', '레지스터2'가 레지스터에 저장되는 동작을 나타냅니다.  스택에 푸시된 데이터를 레지스터에 로딩하게 됩니다, 

[정보]
사실 위 그림은 'pop {fp, pc}' 명령어를 실행할 때의 동작입니다.

pop 명령어는 3가지 동작을 한 번에 수행하는 용도로 설계됐습니다. 따라서 그림과 같이 pop 명령어를 보면 머릿 속으로 3가지를 같이 떠올리면서 분석해야 정확한 동작 원리를 파악할 수 있습니다.

   * 스택 주소와 데이터
   * pop 명령어를 실행하기 전 후의 SP 레지스터의 값
   * pop 명령어를 실행한 후 변경되는 레지스터

pop 명령어는 다른 어셈블리 명령어보다 어려우니 예제 코드를 분석하면서 그 용법을 익힐 필요가 있습니다.

pop 명령어를 예시 코드와 함께 분석해보기

이번에는 예제 코드를 보면서 pop 명령어의 사용법을 살펴보겠습니다.

01 00010434 <add_func>:
02   10434:       e92d4800        push    {fp, lr}
03   10438:       e28db004        add     fp, sp, #4
04   1043c:       e24dd010        sub     sp, sp, #16
...
05   10474:       e8bd8800        pop     {fp, pc}

05번째 줄을 보면 'pop {fp, pc}' 명령어가 보이는데, pop 명령어의 정의와 같이 pop 명령어의 오른쪽에 팝하려는 레지스터의 목록이 보입니다.

이번에는 다음 그림을 보면서 'pop {fp, pc}' 명령어를 실행하면 스택에 어떤 변화가 있는 지 알아봅시다.

 

그림 6.9 pop 명령어를 실행할 때의 세부 동작

pop 명령어의 핵심은 스택에 푸시된 데이터를 레지스터에 로딩하는 동작입니다. 그림 6.9를 보면 pop 명령어를 실행하기 전에 SP 레지스터는 0xbefff5f0을 담고 있는데, 이 기준으로 스택에 있는 데이터를 다음과 같이 팝합니다. 

   * 0xbefff5f0 주소에 있는 0xbefff604를 fp(R11) 레지스터에 로딩
   * 0xbefff5f4 주소에 있는 0x10488를 PC(프로그램 카운터)에 로딩

위와 같은 동작을 수행하면서, SP 레지스터는 0xbefff5f0에서 0xbefff5f8로 업데이트됩니다.

0x10488 주소는 add_func() 함수이 호출된 후 복귀할 주소인데, 2번째 줄이 실행했을 때 스택의 0xbefff5f4 주소에 저장돼 있었습니다. pop 명령어를 실행할 때 0x10488(복귀할 주소)를 PC(프로그램 카운터)에 넣어줘, add_func() 함수가 호출된 다음의 복귀할 주소로 분기하는 것입니다. 

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



[리눅스커널] 메모리: kcalloc() 함수와 kmalloc_array() 함수의 차이점 14. 메모리 관리

kcalloc() 함수는 어떤 역할을 수행할까요? 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v5.12/source/include/linux/slab.h
static inline void *kcalloc(size_t n, size_t size, gfp_t flags)
{
return kmalloc_array(n, size, flags | __GFP_ZERO);
}

kcalloc() 함수를 보니 자신에게 전달된 인자를 kmalloc_array() 함수에 그대로 전달하는데,
마지막 인자의 속성을 변경합니다.
 
flags에 __GFP_ZERO 플래그와 OR 비트 연산을 한 결과를 마지막 인자로 전달하는 것입니다. kmalloc_array() 함수를 호출해 메모리를 할당할 때 0으로 초기화해 달라는 의미죠.

이 점만 빼면 kcalloc() 함수는 kmalloc_array() 함수와 같은 역할을 수행합니다.

[Summary] Linux Security Summit Europe 2020 Linux Security

Deep Analysis of Exploitable Linux Kernel Vulnerabilities 2017-2019 
- Tong Lin & Luhai Chen, Intel

To improve security, a series of hardening features (such as SMEP/PXN, SMAP/PAN, KASLR, CFI, etc.) were added to Linux kernel. Indeed, these mitigations have reduced the impact of vulnerabilities and made some exploits invalid. However, at the same time, some exploitation techniques which could bypass these existing mitigations are constantly being disclosed.

This talk will first detail the basic Linux kernel privilege escalation techniques, highlighting how these techniques work and how adversaries are using them. Then, some typical exploitable Linux kernel vulnerabilities from 2017 to 2019 will be selected for in-depth analysis. Specifically, the complete exploit chain which includes getting kernel arbitrary R/W and bypassing mitigations will be shown for each case.

https://www.youtube.com/watch?v=MYEAGmP_id4&list=PLbzoR-pLrL6rF8E5yyknJzrVQaHeYszTR

Exploiting Race Conditions Using the Scheduler - Jann Horn, Google

This talk shows how two bugs involving somewhat narrow-looking race windows (https://crbug.com/project-zero/1695 in the Linux kernel, https://crbug.com/project-zero/1741 in Android userspace code) can be stretched wide enough to win the race conditions on a Google Pixel 2 phone, running a Linux 4.4 kernel, by making use of the unprivileged sched_*() syscalls.


Tracing: The Bane of You Security Folks 
- Steven Rostedt, VMware Inc

Tracing has the opposite purpose of security. Security tries to hide secrets, and the less the Linux kernel allows user applications know, the better the security. Tracing on the other hand, tries to give as much information as it can to the user. It should only give this information to those that needs it, but even determining that conflicts many times with the security ideas. This talk will talk a bit about what tracing is trying to show, and to whom it is showing it to. It will discuss tactics that are done that Linux kernel security folks should really be aware of. As tracing tries to be as low overhead when not enabled, it will take on tricks like live text modification, and redirecting calls. Tracing does everything a root kit author loves. And this talk will tell you what you need to know about that.

https://www.youtube.com/watch?v=zUYCXBlUcYI&list=PLbzoR-pLrL6rF8E5yyknJzrVQaHeYszTR&index=6

Kernel Runtime Security Instrumentation 
- KP Singh, Google

Existing Linux Security Modules can only be extended by modifying and rebuilding the kernel, making it difficult to react to new threats. Kernel Runtime Security Instrumentation (KRSI) [1] aims to provide an extensible Linux Security Module (LSM) by allowing userspace programs and system owners to attach eBPF (extended Berkeley Packet Filter) programs to security hooks. This makes the LSM framework extensible without needing to rebuild/re-write and enables a new class of security and auditing software.

The talk discusses the need for such an LSM (with representative use cases) and compares it to some existing alternatives, such as Landlock, a separate custom LSM, kprobes+eBPF etc. The second half of the talk outlines the proposed design and interfaces, and includes a live demo

[리눅스커널] Security: 모듈 제거 후킹 무력화 Linux Security

아래 패치를 반영하면 모듈 제거 후킹을 무력화할 수 있다.

index b5dd92e..6e15c44 100644
--- a/kernel/module.c
+++ b/kernel/module.c
@@ -95,6 +95,14 @@ static void do_free_init(struct work_struct *w);
 static DECLARE_WORK(init_free_wq, do_free_init);
 static LLIST_HEAD(init_free_list);

+#define capable(arg) \
+       _capable(arg)
+
+static bool _capable(int arg)
+{
+       return false;
+}
+
 #ifdef CONFIG_MODULES_TREE_LOOKUP

 /*
@@ -3757,6 +3765,7 @@ static noinline int do_init_module(struct module *mod)

 static int may_init_module(void)
 {
+       may_init_module = 0;
        if (!capable(CAP_SYS_MODULE) || modules_disabled)
                return -EPERM;

브링업이나 개발용으로만 활용되면 좋을 것 같다.

1 2 3 4 5 6 7 8 9 10 다음