Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[Linux][Kernel] 슬랩 메모리: kmalloc 소개 [Linux][Kernel] MM


리눅스 커널에서 메모리란 소리만 나와도 공포에 질리는 분들이 있습니다. 예전에 저도 그랬죠. 스타크래프트에서 테란 조이기를 당할 때와 비슷한 느낌이었죠. 정말 갑갑했죠. 메모리풀, vmalloc, 슬랩 메모리, 하이 메모리 등등 용어를 듣다 보면 뇌정지가 올 때가 있습니다. 그런데 나중에 깨닫게 된 사실은 리눅스 커널에서 메모리를 제대로 이해하는 개발자가 매우 적다는 것이었습니다. 그래서 조금 위안을 가졌죠.

리눅스 커널 메모리 시스템을 이해하려고 리눅스 커널 코드를 열어 보는 분들이 있습니다. 이것은 정말 옵져버 없이 럴커 밭에 달려드는 질럿과 같습니다. 절대 이렇게 분석하면 포기할 수 밖에 없습니다. 그 이유는 리눅스 커널 메모리 관련 코드가 정말 어렵거든요. 그럼 어떻게 리눅스 커널 메모리 시스템을 분석해야 할까요? 제 생각에 직접 눈에 보이는 현상부터 부딛쳐 보는게 좋다고 생각합니다. 무슨 개소리냐구요? 메모리를 할당하면 실제 메모리 덤프를 떠서 실제 어떤 값인지 확인하는게 낫다는 소리죠.

그럼 리눅스 커널에서 메모리 할당하면 kmalloc이 떠 오르죠. 우선 이 함수에 대해서 좀 알아볼게요.

리눅스 커널 많은 코드에서 kmalloc 이란 함수를 써서 메모리를 할당합니다. 그럼 이 함수를 쓰면 실제 커널은 어떻게 메모리를 관리할까요? 이를 알기 위해 아주 간단한 패치 코드를 우선 만들어서 확인해볼게요.
1 u32 *austin_debug_data;
3 static int kernel_bsp_debug_stat_set(void *data, u64 val)
4 {
5      austin_debug_data = kmalloc(1024, GFP_KERNEL);
6      memset(austin_debug_data, 0x78, 1024);
8      BUG();
9 }

다섯 번 째 줄 코드를 보면 1024와 GFP_KERNEL 옵션으로 kmalloc을 호출합니다. 1024는 쓰고 싶은 메모리 크기고 GFP_KERNEL은 현재 커널 공간에서 메모리를 할당한다는 의미입니다. 인터럽트 컨택스트에서 kmalloc으로 메모리를 할당 하려면 GFP_ATOMIC을 쓰는 게 좋다는 것을 잊지 말아주세요.

여섯 번 째 줄 코드는 할당 받은 메모리에 0x78값을 메모리 복사합니다. memset이란 API는 C언어에서도 많이 봤던 라이브러리 함수죠.

8번째 줄에서는 강제로 커널 크래시를 유발합니다. 커널 공간에서 어떻게 메모리를 할당하는지 보고 싶어서요.

위 코드를 실행시킨 다음 커널 크래시를 유발 시켰습니다. 코어 덤프를 받기 위해서죠.
Trace32로 코어 덤프를 로딩했더니 austin_debug_data 이란 포인터 변수는 0xC4EBDF00 메모리 공간을 가르키고 있습니다. 다른 말로 kmalloc으로 할당한 메모리 주소가 0xC4EBDF00 이란 말이죠.
(static u32 *) austin_debug_data = 0xC4EBDF00

자 그럼 이제 0xC4EBDF00 메모리 덤프를 확인해볼까요? 참고로 아래 메모리 덤프는 모두 16진수 기준으로 표현합니다.
_____address|_data________|value_____________|symbol
1 NSD:C4EBDF00|_78_78_78_78__0x78787878
2 NSD:C4EBDF04| 78 78 78 78  0x78787878
3 NSD:C4EBDF08| 78 78 78 78  0x78787878
4 NSD:C4EBDF0C| 78 78 78 78  0x78787878
5 NSD:C4EBDF10| 78 78 78 78  0x78787878
6 NSD:C4EBDF14| 78 78 78 78  0x78787878
7 NSD:C4EBDF18| 78 78 78 78  0x78787878
8 NSD:C4EBDF1C| 78 78 78 78  0x78787878
9 NSD:C4EBDF20| 78 78 78 78  0x78787878
10 NSD:C4EBDF24| 78 78 78 78  0x78787878
11 NSD:C4EBDF28| 78 78 78 78  0x78787878
12 NSD:C4EBDF2C| 78 78 78 78  0x78787878
//..
13 NSD:C4EBE2F8| 78 78 78 78  0x78787878
14 NSD:C4EBE2FC| 78 78 78 78  0x78787878
15 NSD:C4EBE300| CC CC CC CC  0xCCCCCCCC
16 NSD:C4EBE304| C0 D0 EB C4  0xC4EBD0C0
17 NSD:C4EBE308| 74 68 43 C0  0xC0436874         \\vmlinux\kernel_bsp_debug_stat_set+0xFC
18 NSD:C4EBE30C| F0 EB 14 C0  0xC014EBF0         \\vmlinux\slub\kmem_cache_alloc_trace+0xB8
19 NSD:C4EBE310| 74 68 43 C0  0xC0436874         \\vmlinux\kernel_bsp_debug_stat_set+0xFC
20 NSD:C4EBE314| 10 EF 17 C0  0xC017EF10         \\vmlinux\libfs\simple_attr_write+0xD4
21 NSD:C4EBE318| 60 A7 15 C0  0xC015A760         \\vmlinux\fs/read_write\vfs_write+0xC8
22 NSD:C4EBE31C| EC AB 15 C0  0xC015ABEC         \\vmlinux\fs/read_write\sys_write+0x4C
23 NSD:C4EBE320| 00 F3 00 C0  0xC000F300         \\vmlinux\Global\ret_fast_syscall
24 NSD:C4EBE324| 00 00 00 00  0x0
25 NSD:C4EBE328| 00 00 00 00  0x0
26 NSD:C4EBE32C| 00 00 00 00  0x0
27 NSD:C4EBE330| 00 00 00 00  0x0
28 NSD:C4EBE334| 00 00 00 00  0x0
29 NSD:C4EBE338| 00 00 00 00  0x0
30 NSD:C4EBE33C| 00 00 00 00  0x0
31 NSD:C4EBE340| 00 00 00 00  0x0
32 NSD:C4EBE344| 00 00 00 00  0x0
33 NSD:C4EBE348| 00 00 00 00  0x0
34 NSD:C4EBE34C| 05 00 00 00  0x5                
35 NSD:C4EBE350| 54 18 00 00  0x1854
36 NSD:C4EBE354| DE 7F 00 00  0x7FDE
37 NSD:C4EBE358| 38 3D 08 C0  0xC0083D38         \\vmlinux\printk\do_syslog\__out+0x1C4
38 NSD:C4EBE35C| 40 FA 14 C0  0xC014FA40         \\vmlinux\slub\kfree+0x238
39 NSD:C4EBE360| 38 3D 08 C0  0xC0083D38         \\vmlinux\printk\do_syslog\__out+0x1C4
40 NSD:C4EBE364| 30 3F 08 C0  0xC0083F30         \\vmlinux\printk\sys_syslog+0x1C
41 NSD:C4EBE368| 00 F3 00 C0  0xC000F300         \\vmlinux\Global\ret_fast_syscall
42 NSD:C4EBE36C| 00 00 00 00  0x0
43 NSD:C4EBE370| 00 00 00 00  0x0
44 NSD:C4EBE374| 00 00 00 00  0x0
45 NSD:C4EBE378| 00 00 00 00  0x0
24 NSD:C4EBE37C| 00 00 00 00  0x0
24 NSD:C4EBE380| 00 00 00 00  0x0
24 NSD:C4EBE384| 00 00 00 00  0x0
24 NSD:C4EBE388| 00 00 00 00  0x0
24 NSD:C4EBE38C| 00 00 00 00  0x0
24 NSD:C4EBE390| 00 00 00 00  0x0
24 NSD:C4EBE394| 00 00 00 00  0x0
24 NSD:C4EBE398| 00 00 00 00  0x0
24 NSD:C4EBE39C| 05 00 00 00  0x5                 
24 NSD:C4EBE3A0| 5D 04 00 00  0x45D               
24 NSD:C4EBE3A4| 15 7F 00 00  0x7F15
24 NSD:C4EBE3A8| 5A 5A 5A 5A  0x5A5A5A5A
24 NSD:C4EBE3AC| 5A 5A 5A 5A  0x5A5A5A5A

그럼 차근 차근 메모리 덤프를 분석해볼까요? 이 코드는 1024만큼 메모리를 할당했죠. 1024는 16진수로는 0x400인데요. C4EBDF00에서 0x400만큼 더한 C4EBE2FC 메모리 공간을 씁니다.
1 NSD:C4EBDF00|_78_78_78_78__0x78787878
2 NSD:C4EBDF04| 78 78 78 78  0x78787878
3 NSD:C4EBDF08| 78 78 78 78  0x78787878
4 NSD:C4EBDF0C| 78 78 78 78  0x78787878
5 NSD:C4EBDF10| 78 78 78 78  0x78787878
6 NSD:C4EBDF14| 78 78 78 78  0x78787878
7 NSD:C4EBDF18| 78 78 78 78  0x78787878
8 NSD:C4EBDF1C| 78 78 78 78  0x78787878
9 NSD:C4EBDF20| 78 78 78 78  0x78787878
10 NSD:C4EBDF24| 78 78 78 78  0x78787878
11 NSD:C4EBDF28| 78 78 78 78  0x78787878
12 NSD:C4EBDF2C| 78 78 78 78  0x78787878
//..
13 NSD:C4EBE2F8| 78 78 78 78  0x78787878
14 NSD:C4EBE2FC| 78 78 78 78  0x78787878
15 NSD:C4EBE300| CC CC CC CC  0xCCCCCCCC
16 NSD:C4EBE304| C0 D0 EB C4  0xC4EBD0C0

그런데 1번째 줄 덤프부터 14번째 줄 덤프까지 0x78로 도배하고 있네요. 음, 왜 그럴까요?
그 이유는 위에서 memset으로 0x78을 메모리 복사를 했기 때문이에요. 
6      memset(austin_debug_data, 0x78, 1024);

이번엔 15번째 줄 덤프입니다. 0xCC란 값이 있군요. 그런데 왜 갑자기 이런 값을 저장할까요?
15 NSD:C4EBE300| CC CC CC CC  0xCCCCCCCC

리눅스 커널 메모리 시스템에선 메모리 속성을 나타내기 위해 여러 핵사 값을 지정했는데요.
0xcc란 지금 메모리를 할당해서 쓰고 있다는 의미입니다.
[include/linux/poison.h]
#define SLUB_RED_ACTIVE 0xcc

그럼 16번째 덤프를 볼 차례입니다.
16 NSD:C4EBE304| C0 D0 EB C4  0xC4EBD0C0

0xC4EBD0C0란 메모리 덤프를 저장하고 있는데요. 이 메모리 주소는 다음에 할당할 메모리 주소를 가르키고 있습니다. 이 내용은 슬랩 메모리에 대해 설명할 때 다룰 예정이니 조금만 기다리세요. 

이제 17번줄부터 메모리 덤프를 볼 시간입니다.
17 NSD:C4EBE308| 74 68 43 C0  0xC0436874         \\vmlinux\kernel_bsp_debug_stat_set+0xFC
18 NSD:C4EBE30C| F0 EB 14 C0  0xC014EBF0         \\vmlinux\slub\kmem_cache_alloc_trace+0xB8
19 NSD:C4EBE310| 74 68 43 C0  0xC0436874         \\vmlinux\kernel_bsp_debug_stat_set+0xFC
20 NSD:C4EBE314| 10 EF 17 C0  0xC017EF10         \\vmlinux\libfs\simple_attr_write+0xD4
21 NSD:C4EBE318| 60 A7 15 C0  0xC015A760         \\vmlinux\fs/read_write\vfs_write+0xC8
22 NSD:C4EBE31C| EC AB 15 C0  0xC015ABEC         \\vmlinux\fs/read_write\sys_write+0x4C
23 NSD:C4EBE320| 00 F3 00 C0  0xC000F300         \\vmlinux\Global\ret_fast_syscall
24 NSD:C4EBE324| 00 00 00 00  0x0
25 NSD:C4EBE328| 00 00 00 00  0x0
26 NSD:C4EBE32C| 00 00 00 00  0x0
27 NSD:C4EBE330| 00 00 00 00  0x0
28 NSD:C4EBE334| 00 00 00 00  0x0
29 NSD:C4EBE338| 00 00 00 00  0x0
30 NSD:C4EBE33C| 00 00 00 00  0x0
31 NSD:C4EBE340| 00 00 00 00  0x0
32 NSD:C4EBE344| 00 00 00 00  0x0
33 NSD:C4EBE348| 00 00 00 00  0x0
34 NSD:C4EBE34C| 05 00 00 00  0x5                
35 NSD:C4EBE350| 54 18 00 00  0x1854
36 NSD:C4EBE354| DE 7F 00 00  0x7FDE

17번째부터 32번째 줄 코드까지 이 메모리를 할당한 콜스택 정보를 담고 있습니다. 이렇게 리눅스 커널 메모리 시스템에서 메모리를 어떻게 할당했는지 친절하게 저장하는 코드가 많습니다. 그 자료 구조 중에 (struct track *)이란 구조체가 있습니다. 이 자료구조는 슬랩 메모리 속성을 표현합니다. 그럼 이 구조체를 같이 볼까요?
[mm/slub.c]
1 #define TRACK_ADDRS_COUNT 16
2 struct track {
3 unsigned long addr; /* Called from address */  //<<-[1]
4 #ifdef CONFIG_STACKTRACE
5 unsigned long addrs[TRACK_ADDRS_COUNT]; /* Called from address */ //<-[2]
6 #endif
7 int cpu; /* Was running on cpu */ //<-[3]
8 int pid; /* Pid context */ //<-[4]
9 unsigned long when; /* When did the operation occur */ //<-[5]
10 };

[1] unsigned long addr : 메모리를 할당한 함수 주소 정보입니다.

[2] unsigned long addrs : 메모리를 할당할 때 콜스택를 저장하는 배열입니다. TRACK_ADDRS_COUNT 매크로가 16이니 16개 함수를 콜스택으로 저장하는군요. 

[3] int cpu: 메모리를 할당할 때 구동 중이던 CPU 번호입니다.

[4] int pid: 프로세스의 pid 정보입니다. 어떤 프로세스가 이 메모리를 할당했는지 알고 싶어서 추가한 멤버로 보입니다.

[5] unsigned long when: 메모리를 언제 할당했는지 알려주는 시간입니다. 메모리를 할당할 때 jiffies값을 저장합니다.

그럼 위 정보는 어느 코드에서 설정하냐고 어떤 분이 질문을 할 것 같군요. 다음 set_track 함수에서 설정합니다. 7번째부터 10번째 줄 코드까지 눈여겨 보세요.
1 static void set_track(struct kmem_cache *s, void *object,
2 enum track_item alloc, unsigned long addr)
3 {
4 struct track *p = get_track(s, object, alloc);
5
6 if (addr) {
//...
7 p->addr = addr;
8 p->cpu = smp_processor_id();
9 p->pid = current->pid;
10 p->when = jiffies;
11 } else
12 memset(p, 0, sizeof(struct track));
13 }

그럼 다시 메모리 덤프 분석으로 돌아갈께요.
17 NSD:C4EBE308| 74 68 43 C0  0xC0436874         \\vmlinux\kernel_bsp_debug_stat_set+0xFC

17번째 줄 덤프를 보면 0xC4EBE308 메모리 공간에서 (struct track*) 멤버가 위치한다고 했죠. 그럼 이 주소를 (struct track*)으로 캐스팅해서 볼까요? 결과는 다음과 같아요.
(struct track *) (struct track*)0xC4EBE304 = 0xC4EBE304 -> (
  (long unsigned int) addr = 3303788736 = 0xC4EBD0C0,
  (long unsigned int [16]) addrs = (
    [0] = 3225643124 = 0xC0436874,  // kernel_bsp_debug_stat_set+0xFC
    [1] = 3222596592 = 0xC014EBF0,  // kmem_cache_alloc_trace+0xB8
    [2] = 3225643124 = 0xC0436874,  // kernel_bsp_debug_stat_set+0xFC
    [3] = 3222794000 = 0xC017EF10,  // simple_attr_write+0xD4
    [4] = 3222644576 = 0xC015A760,  // vfs_write+0xC8
    [5] = 3222645740 = 0xC015ABEC, // sys_write+0x4C
    [6] = 3221287680 = 0xC000F300, // ret_fast_syscall
    [7] = 0 = 0x0,
    [8] = 0 = 0x0,
    [9] = 0 = 0x0,
    [10] = 0 = 0x0,
    [11] = 0 = 0x0,
    [12] = 0 = 0x0,
    [13] = 0 = 0x0,
    [14] = 0 = 0x0,
    [15] = 0 = 0x0),
  (int) cpu = 5,  //<<- CPU번호
  (int) pid = 6228,  //<<- pid 번호
  (long unsigned int) when = 21346) //<<- 메모리 할당할 때의 시간  

다음은 커널 크래시가 발생할 때의 프로세스 정보입니다. 아래 정보를 보면 CPU5에서 돌던 “sh” 프로세스의 pid가 6228이군요.
crash> runq -m
 CPU 0: [0 00:10:25.295]  PID: 0      TASK: c19553e8  COMMAND: "swapper/0"
 CPU 1: [0 00:08:46.071]  PID: 0      TASK: ead1c5c0  COMMAND: "swapper/1"
 CPU 2: [0 00:10:21.620]  PID: 0      TASK: ead1b640  COMMAND: "swapper/2"
 CPU 3: [0 00:05:45.008]  PID: 0      TASK: ead1be00  COMMAND: "swapper/3"
 CPU 4: [0 00:10:27.343]  PID: 0      TASK: eaf60000  COMMAND: "swapper/4"
 CPU 5: [0 00:00:00.096]  PID: 6228   TASK: e34a6c80  COMMAND: "sh"
 CPU 6: [0 00:10:27.442]  PID: 0      TASK: eaf607c0  COMMAND: "swapper/6"
 CPU 7: [0 00:10:27.443]  PID: 0      TASK: eaf66c80  COMMAND: "swapper/7"

자 여기까지 kmalloc으로 메모리를 할당하면 실제 메모리 덤프으로 메모리를 어떻게 쓰는지 확인했습니다.
다음에는 kfree를 쓸 때 일어나는 일에 대해서 살펴볼 예정입니다.

핑백

덧글

댓글 입력 영역