Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

784223
1016
122600


[리눅스커널][디버깅] 커널 디버깅이란 무엇인가? 3. Linux Kernel Debugging

디버깅은 '디버그'와 'ing' 합성어로 버그를 잡는 과정을 뜻합니다. 
누군가 '디버깅을 하고 있다.' 라고 말하면 '버그를 수정하고 있다.'고 볼 수 있습니다.

이번 절에서 디버깅 방법을 소개하기 전 디버깅이 중요한 이유를 다음 관점으로 알아보겠습니다.
문제 해결 능력
커널 코드 학습 능력

필자는 디버깅을 단지 버그를 잡는 과정으로만 보지 않습니다. 리눅스 커널과 드라이버가 정상 동작할 때 자료구조와 함수 호출까지 파악하는 과정을 디버깅이라고 생각합니다. 그 이유를 더 알아볼까요?

디버깅은 문제 해결을 키우는 지름길

신입 개발자들은 실전 리눅스 개발에서 어떤 업무를 하는지 궁금해합니다. 임베디드 리눅스는 ‘클라우드 서버’, ‘휴대폰’, ‘자동차’ 까지 다양한 분야에서 쓰고 있습니다. 따라서 실전 개발 업무가 어떻다고 설명하긴 어렵습니다. 그런데 임베디드 리눅스 프로젝트 개발 중 다양한 문제를 겪습니다. 임베디드 리눅스는 활용 분야가 다양하지만 대표적인 문제를 추리면 다음과 같습니다.
부팅 도중 커널 크래시 발생
인터럽트 핸들러를 설정했는데 인터럽트 핸들러가 호출되지 않음
시스템 응답 속도가 매우 느려짐
파일 복사가 안됨

임베디드 리눅스 개발자는 위와 같은 문제를 겪습니다. 실전 개발자들은 이런 문제들을 해결해야 합니다. 이것이 실무 개발 능력입니다. 문제 해결 능력은 실무 업무의 핵심입니다. 리눅스 드라이버나 핵심 모듈 개발 업체에서 문제 해결 능력을 겸비한 개발자를 애타게 찾고 있습니다.

그러면 문제 해결 능력은 어떻게 키울까요?
먼저 디버깅 능력을 갖춰야 합니다. 문제 발생 원인을 정확히 파악해야 해결 방안을 찾을 수 있기 때문입니다. 디버깅 능력은 문제 해결 능력 그 자체 이므로 평소에 디버깅을 꾸준히 해서 리눅스 커널을 자주 익혀야 합니다.

필자는 디버깅이 문제 해결에 필요한 능력이라고 언급했습니다. 디버깅에 대한 필자의 생각을 조금 더 이야기하겠습니다. 기존 작은 규모 소프트웨어 프로젝트를 개발할 때는 버그와 관련된 개발자가 디버깅을 했습니다. 바로 자신이 작성한 코드의 논리적 문제점을 분석했습니다. 즉, 프로젝트 개발 도중 해당 코드를 작성한 엔지니어가 버그를 잡았습니다.

하지만 오픈 소스 시대인 지금은 예전 상황과 다릅니다. 자신이 작성한 코드보다 다른 개발자가 작성한 코드를 더 많이 분석합니다. 가장 많이 알려진 오픈소스 프로젝트인 리눅스 커널도 예외는 아닙니다. 우리가 분석하는 리눅스 커널은 다른 개발자가 작성한 코드입니다. 

오픈 소스를 개발할 때 디버깅을 단순히 버그를 잡는 과정에서 그치지 말고 한 발짝 더 나가야 합니다. 세부 리눅스 커널 서브 시스템이 정상 동작할 때 다음 내용을 파악할 필요가 있습니다.
분석하는 코드가 어떤 콜스택으로 동작하는지 확인
함수가 실행할 때 변경되는 자료 구조
함수가 실행되는 빈도와 실행 시각
실행 중인 코드를 어떤 프로세스가 실행하는지 확인

위와 같은 내용을 잘 알아야 하는 이유는 무엇일까요?
정상 동작 시 함수 호출 흐름과 자료 구조를 알고 있어야 오류나 버그가 발생했을 때 무엇이 문제인지 식별할 수 있기 때문입니다.

여러분이 임베디드 리눅스 개발을 하다가 문제가 생기면 대부분 커널 로그를 봅니다. 그런데
디바이스 드라이버 코드에 오류가 있으면 우리가 작성한 드라이버 코드에서 오류 메시지를 출력하지 않습니다. 대부분 디바이스 드라이버가 호출한 커널 함수 내부에서 에러 메시지를 출력합니다. 디바이스 드라이버 코드는 대부분 커널이 지원하는 함수를 호출해 구현하기 때문입니다. 디바이스 드라이버 코드에 오류가 있으면 커널 내부 함수에서 오류 메시지를 출력하는 경우가 많습니다.

한 가지 예를 들겠습니다.
[https://www.unix.com/programming/148285-what-unbalanced-irq.html]
01 WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()
02 Unbalanced enable for IRQ 4
03 Modules linked in: svsknfdrvr [last unloaded: osal_linux]
04 Pid: 634, comm: ash Tainted: G W 2.6.28 #1
05 Call Trace:
06 [<c011a7f9>] warn_slowpath+0x76/0x8d
07 [<c012fac8>] profile_tick+0x2d/0x57
08 [<c011ed72>] irq_exit+0x32/0x34
09 [<c010f22c>] smp_apic_timer_interrupt+0x41/0x71
10 [<c01039ec>] apic_timer_interrupt+0x28/0x30
11 [<c011b2b4>] vprintk+0x1d3/0x300
12 [<c013a2af>] __setup_irq+0x11c/0x1f2
13 [<c013a177>] __enable_irq+0x3b/0x57
14 [<c013a506>] enable_irq+0x37/0x54
15 [<c68c9156>] svsknfdrvr_open+0x5e/0x65 [svsknfdrvr]
16 [<c016440a>] chrdev_open+0xce/0x1a4
17 [<c016433c>] chrdev_open+0x0/0x1a4
18 [<c01602f7>] __dentry_open+0xcc/0x23a
19 [<c016049a>] nameidata_to_filp+0x35/0x3f
20 [<c016b3c5>] do_filp_open+0x16f/0x6ef
21 [<c0278fd5>] tty_write+0x1a2/0x1c9
22 [<c0160128>] do_sys_open+0x42/0xcb

신입 개발자 입장에선 굉장히 어려운 커널 로그를 예시로 들었습니다. 하지만 위와 같은 커널 로그는 실전 개발에서 언제든 만날 수 있습니다. 

먼저 첫 번째 에러 로그를 보겠습니다.
01 WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()

__enable_irq() 함수에서 뭔가 오류 조건을 검출한 듯 합니다. 에러 메세지를 유심히 보면서 어느 코드에서 오류를 유발했는지 분석해볼까요?

먼저 콜스택을 어느 코드에서 출력했는지 살펴봅니다. 위 로그는 아래 __enable_irq() 함수 6번째 줄 코드에서 출력합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/irq/manage.c]
1 void __enable_irq(struct irq_desc *desc)
2 {
3 switch (desc->depth) {
4 case 0:
5 err_out:
6 WARN(1, KERN_WARNING "Unbalanced enable for IRQ %dn",
7      irq_desc_get_irq(desc));
 
인터럽트 디스크립터 자료구조인 struct irq_desc 구조체 depth 필드는 인터럽트를 enable하면 0, disable 할 때 1을 설정합니다. 만약 4 번째 인터럽트를 활성화하면 4 번째 인터럽트의 인터럽트 디스크립터 depth 필드를 0으로 설정합니다. 그런데 6번째 줄 코드는 인터럽트를 2번 enable 했을 때 실행합니다. 이미 인터럽트를 enable했으면 depth 필드가 0인데 다시 enable했으니 콜스택을 출력합니다.


WARN() 매크로 함수는 콜스택을 출력합니다.


이번에는 어떤 인터럽트를 연속으로 활성화했는지 파악해볼까요?
01 WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()
02 Unbalanced enable for IRQ 4

02 번째 줄 로그를 보면 “IRQ 4” 메시지가 보입니다. 정리하면 4번 인터럽트를 두 번 enable했으므로 해당 오류 메시지를 출력했습니다. 여기서 desc는 인터럽트에 대한 속성을 저장하는 인터럽트 디스크립터 구조체입니다.

위와 같은 에러 메시지는 인터럽트를 활성화하는 enable_irq() 함수 내부에 논리적 오류가 있어 출력하지 않습니다.  enable_irq() 함수를 호출한 드라이버 코드에 무엇인가 오류가 있을 가능성이 높습니다. 드라이버 코드 레벨에서 4번 인터럽트를 어떻게 2번 연속 활성화하는지 점검해야 합니다. 

그런데 에러 메시지는 커널 내부 함수인 __enable_irq() 함수에서 출력했습니다. 만약 정상 동작할 때 struct irq_desc 구조체 depth 필드를 알고 있으면 이제까지 분석한 에러 메시지의 의미를 바로 알 수 있습니다.

그러면 위와 같은 문제는 어떻게 해결할까요? 물론 관련 드라이버 코드를 상세히 분석해 힌트를 얻을 수 있습니다. 
[https://www.unix.com/programming/148285-what-unbalanced-irq.html]
01 WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()
02 Unbalanced enable for IRQ 4
03 Modules linked in: svsknfdrvr [last unloaded: osal_linux]

커널 에러 메시지를 보면 svsknfdrvr 드라이버 모듈이 보입니다. 먼저 svsknfdrvr 드라이버 코드 분석한 후 논리적인 오류가 있는지 점검해야 합니다. 

만약 필자에게 위와 같은 문제가 할당되면 어떻게 문제를 좁힐까요? svsknfdrvr 드라이버 코드를 분석하면 좋겠지만 드라이버 개발 업체가 드라이버 코드를 공유하지 않고 모듈(ko) 형태로 배포할 수 도 있습니다. 가끔은 자신이 작성한 드라이버는 문제가 없다고 주장할 수 있습니다.

대신 다음과 같은 패치 코드를 입력해 문제 현상을 재현해 커널 로그를 받아보면 됩니다. 커널 로그가 누가 4번 인터럽트를 2번 활성화했는지 말해 줄 겁니다. (물론 패치 코드를 입력한 다음 커널 빌드해 커널 이미지를 설치한 후 다시 재현을 시켜야 합니다.)
diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
--- a/kernel/irq/manage.c
+++ b/kernel/irq/manage.c
@@ -388,6 +388,8 @@ setup_affinity(unsigned int irq, struct irq_desc *desc, struct cpumask *mask)

1 void __disable_irq(struct irq_desc *desc, unsigned int irq)
2 {
3 +       if ( irq == 4 )
4 +               WARN(1, KERN_WARNING " irq 4 is disbled %d, desc->depth %d n", irq, desc->depth);
5        if (!desc->depth++)
6                irq_disable(desc);
7 }
@@ -442,6 +444,9 @@ EXPORT_SYMBOL(disable_irq);
8
9 void __enable_irq(struct irq_desc *desc, unsigned int irq)
10 {
11 +       if ( irq == 4 )
12 +               WARN(1, KERN_WARNING " irq 4 is enabled %d, desc->depthn", irq, desc->depth);
13 +
14        switch (desc->depth) {
15        case 0:
16  err_out:

위와 같은 코드를 디버깅 패치라고 부릅니다. 문제의 원인을 좁힐 수 있는 단서를 찾을 수 있습니다. 

패치 코드를 설명하겠습니다. __disable_irq() 함수와 __enable_irq() 함수 2 번째 인자는 irq입니다. 인터럽트 번호입니다. 인터럽트 번호가 4일 때 콜스택을 호출하는 코드입니다.

위 패치 코드를 작성해 커널을 빌드하면 4 번 인터럽트를 활성화 혹은 비활성화할 때 콜스택을 커널 로그로 출력합니다. 어떤 코드에서 4 번 인터럽트를 하는지 알 수 있습니다.


필자는 리눅스 커널 고수 중 디버깅에 잘 못하는 개발자는 한 번도 만나본 적이 없습니다. 디버깅 능력은 문제 해결 능력 그 자체 이므로 평소에 디버깅을 꾸준히 해서 리눅스 커널을 익힙시다.

디버깅과 코드 학습 능력

디버깅과 함께 리눅스 커널 코드를 분석하면 다음과 같이 많은 정보를 얻을 수 있습니다. 
분석하는 코드가 어떤 콜스택으로 동작하는지 확인
함수가 실행할 때 변경되는 자료 구조
함수가 실행되는 빈도와 실행 시각
실행 중인 코드를 어떤 프로세스가 실행하는지 확인

대부분 리눅스 커널을 공부할 때 커널 소스 코드를 열어 봅니다. 물론 소스 코드를 보는 것은 매우 중요합니다. 코드 이해 능력은 리눅스 개발자의 기본 체력입니다. 하지만 소스 코드 분석으로 시스템 동작 일부분 만 보게됩니다. 조금 더 넓은 시야로 함수 실행 흐름과 실행 빈도를 볼 필요가 있습니다.

커널 소스 코드만 분석한 분이 있다고 가정하겠습니다. 또 다른 분은 커널 디버깅과 함께 커널 코드를 같이 분석했습니다. 둘 중에 누가 더 빨리 커널을 익힐 수 있을까요? 필자는 디버깅과 함께 커널 소스를 분석하면 소스만 분석할 때 보다 5배 정도 학습 효과가 있다고 생각합니다. 디버깅을 하면 5배 정도 많은 정보를 얻기 때문입니다. 그만큼 커널 디버깅이 중요합니다. 

이번 절에서는 디버깅의 중요성과 코드 학습에 대해서 살펴봤습니다. 다음 절에서 커널 디버깅 툴 방법을 소개합니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려 드리겠습니다!"

#커널 디버깅
디버깅이란
ftrace
임베디드 디버거의 전설 Trace32
편하게 커널 코드를 수정할 수 있는 디버깅용 Debugfs 드라이버 코드 소개

핑백

덧글

  • 학생 2019/11/15 19:01 # 삭제 답글

    안녕하세요~ 디버깅 프로세스에 대해 공부하고 싶은데 (디버깅 방법이 아니라 디버거가 동작하는 원리가 궁금해요. 0.디버깅하려는 프로세스를 실행하고 1.디버깅하려는 프로세스에 디버거가 부착되고 3.??? 4??? 5.사용자한테 뭘보여주고...

    추천하는 책이나 블로그나 웹이나 기사나 뭐 아무거나 좋은게 있을까요?
  • AustinKim 2019/11/18 10:35 #

    열심히 공부하시는 모습이 참 좋습니다. 그런데 디버거라면 gdb를 말씀하시는 건가요?
    리눅스에서 지원하는 디버거의 종류는 여러 가지거든요. 조금 더 구체적으로 질문을 해주시면 주신 질문에 따라 답을 드릴 수 있을 것 같습니다.
  • 스붕이 2019/11/18 13:09 # 삭제 답글

    아마 디버거 기본 동작 원리에 대해 흥미를 갖으신 듯 싶습니다 학창 시절에 저도 관심이 있었는데요 정확히는 모르지만 글을 남겨봅니다
    일반적으로 사용하는 SW 방식은 브레이크 포인트를 설정한 소스 라인과 일치하는 코드영역 메모리 번지에 기존에 있던 기계어 대신에 exception 코드를 바꿔두고 그 번지가 실행되면
    exception 발생해서 디버거로 넘어가게끔 해서 디버거가 디버깅 변수 출력이나 콜스택 출력 등 처리한 후 원래 기계어로 돌려놓고 그 번지로 복귀해서 다시 실행하는 걸로 알고 있고,
    H/W 방식(JTAG 디버거)은 CPU 내 디버거 로직에 브레이크 포인트 걸린 주소가 저장되고 해당 번지 실행될 때 exception 발생해서 다시 디버거가 그걸 처리할 듯 싶습니다
    아마 AustinKim님이 정확한 내용으로 답변 달아 주실 겁니다
댓글 입력 영역