Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

81112
549
416221


[리눅스커널][디버깅] dump_stack() 함수 3. 커널 디버깅과 코드 학습

printk 외에도 커널에서는 커널 로그를 통해 커널 동작을 보여주는 기능을 제공합니다. 즉, 커널에서 지원하는 dump_stack() 함수를 호출하면 콜스택을 커널 로그로 볼 수 있습니다.

dump_stack() 함수의 사용법은 간단합니다. 커널 로그로 콜스택을 보고 싶은 코드에 dump_stack() 함수를 추가하기만 하면 됩니다. 

dump_stack() 함수를 호출하려면 코드의 윗부분에 다음과 같이 "linux/kernel.h" 헤더 파일을 추가해야 합니다. 

#include <linux/kernel.h>

이어서 dump_stack() 함수의 선언부를 봅시다.

asmlinkage __visible void dump_stack(void);

인자와 반환값 타입이 모두 void입니다. 커널 소스코드의 어디든 dump_stack() 함수만 추가하면 됩니다.

dump_stack() 함수로 커널 로그에서 콜스택 확인하기

이번에는 앞에서 설명한 dump_stack() 함수를 써서 커널 로그로 콜스택을 볼 수 있는 방법을 알아보겠습니다. 다음 패치 코드를 봅시다.

diff --git a/kernel/fork.c b/kernel/fork.c
index 6a219fea4926..cdc5d9ff0900
--- a/kernel/fork.c
+++ b/kernel/fork.c
1 @@ -2017,6 +2017,8 @@ struct task_struct *fork_idle(int cpu)
2   * It copies the process, and if successful kick-starts
3   * it and waits for it to finish using the VM if required.
4   */
5 + 
6 +static int debug_kernel_thread = 1; 
7 long _do_fork(unsigned long clone_flags,
8       unsigned long stack_start,
9       unsigned long stack_size,
10 @@ -2049,6 +2051,11 @@ long _do_fork(unsigned long clone_flags,
11 p = copy_process(clone_flags, stack_start, stack_size,
12 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
13 add_latent_entropy();
14 +
15 + if (debug_kernel_thread) {
16 + printk("[+][%s] process n", current->comm);
17 + dump_stack();
18 + }

먼저 패치 코드를 입력하는 방법을 소개하겠습니다. 6 번째 줄의 코드와 같이 _do_fork() 함수 바로 위에 debug_kernel_thread 전역 변수를 선언하고 _do_fork() 함수에서 add_latent_entropy(); 다음에 보이는 15~18 번째 줄 코드를 입력하면 됩니다. 

이어서 패치 코드의 의미를 알아보자면 프로세스를 생성할 때 _do_fork() 함수가 호출되는데, 이때 함수 콜스택을 확인하기 위한 코드입니다.

_do_fork() 함수는 프로세스를 생성할 때만 호출됩니다. 자주 호출되는 함수는 아니니 안심하고 패치 코드를 입력합시다.

위와 같은 패치 코드를 입력한 다음 커널 이미지를 빌드하고 시스템에 설치합시다. 부팅 후 커널 로그에서 다음과 같은 메시지를 볼 수 있습니다.

1 [    3.819188] CPU: 1 PID: 149 Comm: systemd-udevd Not tainted 4.19.30-v7+ #3
2 [    3.819196] Hardware name: BCM2835
3 [    3.819237] [<8010ffc0>] (unwind_backtrace) from [<8010c1fc>] (show_stack+0x20/0x24)
4 [    3.819257] [<8010c1fc>] (show_stack) from [<80789dfc>] (dump_stack+0xc8/0x10c)
5 [    3.819282] [<80789dfc>] (dump_stack) from [<8011c8ec>] (_do_fork+0xdc/0x43c)
6 [    3.819301] [<8011c8ec>] (_do_fork) from [<8011cd68>] (sys_clone+0x30/0x38)
7 [    3.819320] [<8011cd68>] (sys_clone) from [<80108180>] (__sys_trace_return+0x0/0x10)

그럼 커널 로그를 분석해볼까요? 1번째 줄을 봅시다.

1 [    3.819188] CPU: 1 PID: 149 Comm: systemd-udevd Not tainted 4.19.30-v7+ #3

콜스택을 실행한 프로세스 정보입니다. PID가 149인 systemd-udevd 프로세스가 CPU1에서 실행 중입니다.

02~07번째 줄은 콜스택입니다. 함수 호출은 07번째 줄에서 03번째 줄 방향으로 이뤄집니다. 
 
_do_fork() 함수 위에 보이는 함수들은 dump_stack() 함수에서 콜스택을 출력할 때마다 보이는 함수로서 무시해도 되는 함수 호출 정보입니다.

6번째 줄의 메시지는 sys_clone() 함수에서 _do_fork() 함수를 호출한다고 알려줍니다. 

6 [    3.819301] [<8011c8ec>] (_do_fork) from [<8011cd68>] (sys_clone+0x30/0x38)

dump_stack() 함수를 사용할 때의 주의사항
1초에 100번 이상 호출되는 함수에서 dump_stack() 함수 호출을 추가하면 시스템 응답 속도가 매우 느려질 수 있습니다. 따라서 dump_stack() 함수를 호출해서 콜스택을 보려는 코드가 자주 호출되는지 반드시 점검할 필요가 있습니다.

그런데 dump_stack() 함수 내부에서는 printk보다 훨씬 많은 일을 합니다. dump_stack() 함수를 실행하면 내부에서 현재 실행 중인 프로세스 스택 주소를 읽어서 스택에 푸시된 프레임 포인터(Frame Pointer) 레지스터를 읽습니다. ARM 함수 호출 규약에 따라 프레임 포인터 레지스터를 읽어서 함수 호출 내역을 추적하는 동작을 반복합니다. 따라서 커널 입장에서 printk보다 dump_stack() 함수를 호출할 때 더 많은 일을 합니다.

이처럼 dump_stack() 함수는 다음과 같은 불편함이 있습니다.
함수 실행 빈도를 몰라 dump_stack() 함수를 쓰려고 하니 조금은 두렵기도 하다. 
또한 커널 코드에 dump_stack() 함수를 추가하고 커널 빌드를 해야 한다.

커널에서는 이 같은 불편함을 해소할 수 있는 강력한 디버깅 기능을 지원합니다. 그 주인공인 ftrace에 대해 이어지는 절에서 살펴보겠습니다.

* 유튜브 강의 동영상도 있으니 같이 들으시면 좋습니다.




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

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


커널 디버깅과 코드 학습
# Reference: For more information on 'Linux Kernel';

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

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





핑백

덧글

  • 따뜻한 순록 2021/06/08 04:11 # 답글

    Thanks for the very good article.

    I have a question

    Is dump_stack() ==> show_stack(current, NULL) ==> same as current->stack??

    If no, how to dump current->stack?

    Thanks


  • AustinKim 2021/06/08 20:14 #

    When you want to see callstack in the kernel log,
    either dump_stack() or show_stack(current, NULL) can be called.

    The 'current->stack' is not used, as a way to traverse stack dump.
  • 따뜻한 순록 2021/06/08 04:43 # 답글

    Dumped stacks using dump_stack() and show_stack(current, NULL)
    - Result: Both the dumps are identical.

    1) Need to know if these stack dump functions internally use [current->stack] content.
    2) How to dump content of [current->stack] without using predefined dump functions.

    Thanks
  • AustinKim 2021/06/08 20:23 #

    If you look into unwind_backtrace()[arm] or dump_backtrace()[arm64],
    you will find out how it works.

    https://elixir.bootlin.com/linux/v5.13-rc5/source/arch/arm/kernel/unwind.c#L459
    void unwind_backtrace(struct pt_regs *regs, struct task_struct *tsk,
    const char *loglvl)

    https://elixir.bootlin.com/linux/v5.13-rc5/source/arch/arm64/kernel/stacktrace.c#L161
    void dump_backtrace(struct pt_regs *regs, struct task_struct *tsk,
    const char *loglvl)

    In both cases, 'current->stack' is not used, as a way to traverse stack dump.

    BR,
    Austin Kim
  • kyle 2021/12/14 21:47 # 삭제 답글

    안녕하세요. 블로그 잘 보고있습니다.

    덤프스택은 해당지점의 콜스택을 볼수있다고 이해하였는데.
    해당 콜스택으로 어떠한 통찰을 얻을 수 있을까요? 예를들어 현재 지점까지의 변수 변천사? 같은 것도 볼수있다고 생각드는데.
    이외에 해당 콜스택으로 어떠어떠한 것들을 유추할 수 있는지 궁금합니다.

    감사합니다.
  • AustinKim 2021/12/15 09:42 #

    많은 생각을 하게 하는 훌륭한 질문을 주셔서 감사합니다.

    콜 스택으로는 각 함수의 스택 프레임별로 사용되는 지역 변수의 값을 우선 확인할 수 있는데요.
    이 밖에 유추할 수 있는 정보는 다음과 같아요.

    1. 컨텍스트

    콜 스택을 보면 현재 인터럽트 컨텍스트인지 그냥 프로세스 컨텍스트인지 알 수 있어요.

    다음 ftrace를 보면;

    Xorg-564 [001] d.h1 2177.446811: <stack trace>
    => __traceiter_kmem_cache_alloc+0x64/0x88
    => kmem_cache_alloc+0x334/0x400
    => __sigqueue_alloc+0xd4/0x1a8
    => __send_signal+0x308/0x3e8
    => send_signal+0x70/0x130
    => do_send_sig_info+0x68/0xb0
    => group_send_sig_info+0x88/0xa8
    => kill_pid_info+0x54/0xa0
    => it_real_fn+0x30/0x108
    => __hrtimer_run_queues+0x26c/0x470
    => hrtimer_interrupt+0xfc/0x258
    => arch_timer_handler_phys+0x38/0x48
    => handle_percpu_devid_irq+0xa8/0x2a0
    => generic_handle_irq+0x38/0x50
    => __handle_domain_irq+0x9c/0x110
    => gic_handle_irq+0xb0/0xf0
    => el0_irq_naked+0x50/0x58

    유저 프로세스가 실행 중에 인터럽트가 유발된 다음에 위와 같은 함수가 호출됐다고 알 수 있고요.

    아래 콜 스택은;

    => __traceiter_kmem_cache_free+0x48/0x68
    => kmem_cache_free+0x430/0x538
    => file_free_rcu+0x48/0x88
    => rcu_core+0x264/0x860
    => rcu_core_si+0x18/0x28
    => __do_softirq+0x1a8/0x510
    => irq_exit+0xe8/0x108
    => __handle_domain_irq+0xa0/0x110
    => gic_handle_irq+0xb0/0xf0
    => el1_irq+0xc8/0x180
    => arch_cpu_idle+0x18/0x28
    => default_idle_call+0x58/0x1d4
    => do_idle+0x25c/0x270
    => cpu_startup_entry+0x30/0x70
    => secondary_start_kernel+0x170/0x180

    커널에서 arch_cpu_idle() 함수가 실행 중에 인터럽트가 유발된 다음에,
    soft irq가 실행 중이다란 정보를 확인할 수 있습니다.

    마지막으로 아래 콜 스택은;

    sshd-1012 [001] .... 2164.836778: <stack trace>
    => __traceiter_kmem_cache_alloc+0x64/0x88
    => kmem_cache_alloc+0x334/0x400
    => __alloc_skb+0x5c/0x190
    => sk_stream_alloc_skb+0xc0/0x250
    => tcp_sendmsg_locked+0x3a4/0xc48
    => tcp_sendmsg+0x40/0x68
    => inet_sendmsg+0x4c/0x78
    => sock_sendmsg+0x54/0x60
    => sock_write_iter+0x98/0xf0
    => new_sync_write+0x17c/0x190
    => vfs_write+0x254/0x368
    => ksys_write+0xe0/0xf8
    => __arm64_sys_write+0x24/0x30
    => el0_svc_common.constprop.2+0x9c/0x1a0
    => do_el0_svc+0x2c/0x98
    => el0_svc+0x20/0x30
    => el0_sync_handler+0x90/0xb8
    => el0_sync+0x160/0x180

    시스템 콜이 유발돼 소켓에 데이터를 쓰는 동작이라는 사실을 알 수 있습니다.

    2. 익셉션

    현재 크래시가 유발됐는지, 인터럽트가 유발된 상태인지,
    그냥 정상 동작인지도 확인할 수가 있습니다.

    감사합니다.
댓글 입력 영역