Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 프로세스: current_thread_info() 매크로 함수 분석 4. Process Management

이전 절에서 프로세스 스택 최상단 주소에 struct thread_info 구조체에 프로세스 실행을 저장한다고 분석했습니다.

current_thread_info() 매크로 함수는 프로세스가 어떤 함수를 실행하더라도 struct thread_info 구조체가 있는 스택 최상단 주소를 전달합니다.

current_thread_info() 매크로 함수 구현부를 분석하기 전에 매크로 함수가 어느 코드에서 쓰는 지 알아봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
1  static __always_inline int preempt_count(void)
2  {
return READ_ONCE(current_thread_info()->preempt_count);
4  }
5
6  static __always_inline volatile int *preempt_count_ptr(void)
7  {
8 return &current_thread_info()->preempt_count;
9  }
10
11 static __always_inline void preempt_count_set(int pc)
12 {
13 *preempt_count_ptr() = pc;
14 }

1번째 줄 코드에서 preempt_count() 함수는 current_thread_info() 함수를 써서 struct thread_info 구조체 멤버인 preempt_count 에 접근합니다.

6번째 줄 코드에 있는 preempt_count_ptr() 함수는 struct thread_info 구조체 멤버 preempt_count 주소를 반환합니다.

11~13번째 줄 코드는 preempt_count_ptr() 함수를 써서 struct thread_info 구조체 멤버 preempt_count에 pc란 인자를 저장합니다.

이번에는 다른 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/smp.h]
#define raw_smp_processor_id() (current_thread_info()->cpu)

struct thread_info 구조체 멤버인 cpu에 접근하는 매크로 함수입니다.
프로세스가 현재 실행 중이거나 이전에 실행했던 CPU 번호를 저장합니다.

위에서 다룬 리눅스 커널 전반에 아주 많이 쓰는 함수니 눈에 잘 익히도록 합시다.

이제 current_thread_info() 매크로 함수 코드를 분석하겠습니다.

[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
1 register unsigned long current_stack_pointer asm ("sp");
2
3 static inline struct thread_info *current_thread_info(void)
4 {
5 return (struct thread_info *)
6 (current_stack_pointer & ~(THREAD_SIZE - 1));
7 }

참고로, THREAD_SIZE 이란 0x2000 값으로 ARM 아키텍처에서 실행하는 프로세스 스택 사이즈를 의미합니다.
#define THREAD_SIZE 0x2000

ARM64 비트 아키텍처에서는 커널 프로세스 스택 사이즈가 0x4000 바이트입니다.
이 책에서는 라즈베리파이가 탑재한 ARM32 아키텍처를 기준으로 코드를 분석합니다.

current_thread_info() 매크로 함수에서 current_stack_pointer 코드의 의미를 먼저 확인합시다.

이 코드는 현재 구동 중인 프로세스의 스택 주소를 current_stack_pointer 변수로 가져오는 명령어입니다.
1 register unsigned long current_stack_pointer asm ("sp");

asm ("sp")  어셈블리 명령어는 현재 실행 중인 프로세스 스택 주소를 알려줍니다. 

우리가 보는 모든 함수는 프로세스 스택 공간에서 실행합니다. 위 명령어와 조합해서 1번 코드와 같이 선언하면 current_stack_pointer 전역 변수에 스택 주소를 저장합니다.

분석한 내용을 적용해서 6~7번째 줄 코드를 다음과 같이 변경해봅시다.
(current_stack_pointer & ~((0x2000) - 1));
(current_stack_pointer & ~(0x1fff));

THREAD_SIZE가 0x2000이고 0x2000 에서 1을 빼면 0x1fff입니다.

위 코드에서 ~란 비트 연산자는 비트를 모두 역전시킵니다. 
0x1fff를 이진수로 바꿔서 ~연산을 수행하면 다음과 같은 결괏값을 얻을 수 있습니다.
0001|1111|1111|1111
~연산
-------------------
1110|0000|0000|0000

1110|0000|0000|0000 이진수를 16진수로 바꾸면 0xE000이 되는 것입니다.

현재 스택 주소에서 0xE000란 값을 AND 연산하는 코드입니다. 
(current_stack_pointer & ~((0x2000) - 1));
(current_stack_pointer & ~(0x1fff));
(current_stack_pointer & (0xe000));

그런데 현재 스택 주소에서 0xE000을 AND 연산하는 게 어떤 의미가 있을까요?
아래 볼드체로된 0--12비트는 모두 0이니 다른 주소와 AND 연산을 하면 0이 됩니다.
1110|0000|0000|0000(0xE000)

0xD000C248 & (0000|0000|0000) 연산을 하면 0xD000C000이 됩니다. 16진수 3자리 값을 모두 0으로 변환시킵니다. 
1100|0000|0000|0000|1011|0010|0100|1000 (0xD000C248)
                              0000|0000|0000
AND ------------------------------------------
1100|0000|0000|0000|1011|0000|0000|0000 (0xD000C000)


이번에 0xE000 값에서 0xE만 빼서 이진수로 어떤 동작을 하는지 확인할 차례입니다.
이제 0xE와 AND 비트 연산을 하는 의미를 알아보려는 것입니다.
1110|0000|0000|0000

1110비트 연산을 수행하면 현재 값이 짝수면 그 값을 그대로 유지하고 홀수면 -1만큼 뺍니다.
아래 연산 결과를 참고하시면 이해가 빠를 겁니다.
 
이제 정리를 합시다. 각각 스택 주소를 다음과 같다고 했을 때 0xE000와 AND 연산을 하면 결과값은 다음과 같습니다.
스택주소 
0xD0002248 & 0xE000 = 0xD0002000
0xD0003248 & 0xE000 = 0xD0002000
0xD0004248 & 0xE000 = 0xD0004000
0xD0005248 & 0xE000 = 0xD0004000

현재 스택 주소에서 위 규칙으로 비트를 연산하는 이유는 뭘까요? ARM32 비트 아키텍처에서 0x2000바이트 크기만큼 스택을 지정합니다. 커널이 프로세스 스택 주소를 할당할 때 0x2000바이트 기준 정렬을 맞춰서 할당하기 때문입니다. 프로세스 최상단 주소는 짝수 바이트입니다. 그래서 위와 같은 비트 연산으로 스택 최상단 주소를 계산한 것입니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

#Reference(프로세스 관리)
프로세스 디버깅
   glibc fork 함수 gdb 디버깅


 

핑백

덧글

댓글 입력 영역