Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

22236
1488
262018


[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 : 슬럽 오브젝트 메모리를 할당한(혹은 해제한) 코드의 주소 정보입니다.

다음 코드를 예로 들겠습니다.

01 void sample_func(void) 
02 {
03   struct eni_vcc *eni_vcc = kmalloc(sizeof(struct eni_vcc),GFP_KERNEL);
04
05   if (!eni_vcc)
06      return -ENOMEM;
07
08   proc_vcc(eni_vcc);
09   free(eni_vcc);
10 }

03번째와 09번째 줄의 주소가 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번째 줄 덤프를 보면 0xC4FBE304 메모리 공간에서 (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를 쓸 때 일어나는 일에 대해서 살펴볼 예정입니다.


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2





핑백

덧글

  • ym0914 2020/12/10 13:56 # 삭제 답글

    "17번째 줄 덤프를 보면 0xC4EBE308 메모리 공간에서 (struct track*) 멤버가 위치한다고 했죠. 그럼 이 주소를 (struct track*)으로 캐스팅해서 볼까요? 결과는 다음과 같아요.
    (struct track *) (struct track*)0xC4EBE304 = 0xC4EBE304 -> ("

    0xC4FBE308 이 아닌 0xC4FBE304 를 casting 하는 이유가 있을까요?

    0xC4FBF304 주소에는 0xC4EBD0C0 가 저장되어 있어 "다음에 할당할 메모리 주소" 라고 이해했었습니다.

    16 NSD:C4EBE304| C0 D0 EB C4 0xC4EBD0C0

  • AustinKim 2020/12/10 14:56 #

    다시 꼼꼼히 읽어보니 오타군요. 바로 수정했습니다.
    '0xC4EBE308'주소가 아니라 '0xC4EBE304' 주소가 struct track 구조체의 시작 주소입니다.

    이제 포스팅을 업데이트할 때, 꼼꼼히 리뷰하고 올려야 겠습니다.
    감사드리고, 즐거운 하루 되십시오.
  • 2020/12/10 14:56 # 답글 비공개

    비공개 덧글입니다.
  • ym0914 2020/12/10 15:37 # 삭제 답글

    넵 확인 감사합니다.

    0xC4FBF304 에는 "다음에 할당할 메모리 주소" 가 들어있다고 하셨는데요,
    (struct track*)0xC4FBF304 으로 casting 하게 되면,

    [mm/slub.c]
    1 #define TRACK_ADDRS_COUNT 16
    2 struct track {
    3unsigned long addr;/* Called from address */ //<<-[1]
    4 #ifdef CONFIG_STACKTRACE
    5unsigned long addrs[TRACK_ADDRS_COUNT];/* Called from address */ //<-[2]
    6 #endif
    7int cpu;/* Was running on cpu */ //<-[3]
    8int pid;/* Pid context */ //<-[4]
    9unsigned long when;/* When did the operation occur */ //<-[5]
    10 };

    위 구조체에서 "addr" 변수에 저장되게 됩니다. "addr" 변수는 메모리를 할당한 함수 주소 정보라고 하는데
    "다음에 할당할 메모리 주소' vs "현재 메모리를 할당한 함수 주소" 중 어떤게 맞을지 헷갈리네요..


  • AustinKim 2020/12/10 16:49 #

    "현재 메모리를 할당한 함수 주소"가 맞습니다. 명확하지 않게 설명한 것 같아, 이 포스트의 내용을 수정했습니다.

    관련 내용은 아래 새롭게 올린 포스팅을 참고하세요.
    http://rousalome.egloos.com/10021949

    댓글로 달려다가, 코드 분석의 양이 많아져 아예 새롭게 올렸습니다.
    즐거운 하루 되세요.
  • ym0914 2020/12/10 15:40 # 삭제 답글

    그리고 오타에 대해 너무 스트레스 받으시지 않으셨으면 합니다.
    이미 너무 훌륭한 게시글을 포스팅 해주시고 계신데 사소한 오타는 크게 문제되지 않습니다. (다른분들이 보시다가 발견해주시기도 하구요)
    또한 의미전달은 충분히 되고 있으니 너무 신경쓰시지 마시고 계속 포스팅 해주셨으면 합니다.
    감사합니다!!
  • AustinKim 2020/12/10 16:51 #

    좋게 봐주셔서 감사합니다.
    그런데 질문의 내공으로 보아 실전 개발자이신 것 같은데요. 앞으로 실전 개발자라고 가정하고 댓글에 답을 드리는 게 좋을 것 같단 생각이 듭니다.
  • ym0914 2020/12/10 17:20 # 삭제 답글

    예 개발자 이나 아직 미숙합니다 ㅠㅠ. 블로그 통해서 많이 배우고 있어 항상 감사한 마음 갖고 있습니다. 감사합니다!!
  • AustinKim 2020/12/11 08:41 #

    질문의 내용으로 보아 상당히 내공이 있으신 분 같습니다.
    제 블로그에 올린 글을 좋게 봐주시고 꼼꼼히 읽고 댓글 주셔서 정말 감사합니다.

    앞으로도 꾸준히 개발에 도움이 될만한 유용한 컨텐츠를 올릴 예정이니 잘 참고하셨으면 좋겠습니다.
    즐거운 금요일 되세요. 감사합니다.
댓글 입력 영역