노무현 대통령 배너


2006. 8. 10. 10:19

리눅스 디버깅 기술 마스터하기 (한글)

출처: http://www-128.ibm.com/developerworks/kr/library/l-debug/index.html

리눅스 디버깅 기술 마스터하기 (한글)

리눅스 상에서 버그를 해결하는 핵심 전략들

developerWorks
문서 옵션
이 페이지를 이메일로 보내기

');// :badtag -->
이 페이지를 이메일로 보내기

이 페이지를 이메일로 보내기

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.


제안 및 의견
피드백

난이도 : 중급

Steve Best, JFS core team member, IBM

2006 년 6 월 19 일

실행중인 사용자 공간 프로그램을 검사하는 방법은 여러 가지이다. 디버거를 실행하여 프로그램을 직접 관찰하거나, print 문을 추가하거나, 프로그램을 분석하는 툴을 추가할 수 있다. 이 글에서, 리눅스에서 실행되는 프로그램을 디버깅 할 때 사용할 수 있는 방법들을 설명한다. 세그멘테이션 오류, 메모리 오버런 및 누수, 행(hang) 같은 문제들을 디버깅하는 네 가지 시나리오를 검토해 본다. (이 글은 2002년 8월에 IBM developerWorks에 게재되었다.)

이 글에서 네 가지 디버깅 시나리오를 제시한다. 시나리오 1의 경우, 메모리 할당 문제가 있는 두 개의 샘플 프로그램을 사용한다. MEMWATCH와 Yet Another Malloc Debugger (YAMD) 툴을 사용하여 디버깅 한다. 시나리오 2에서는 strace 유틸리티를 사용한다. 시스템 호출과 시그널을 추적하여 프로그램이 어디서 잘못되었는지를 규명한다. 시나리오 3에서는 리눅스 커널의 Oops를 사용하여 세그멘테이션 오류 문제를 해결하고 커널 소스 레벨 디버거(kgdb)를 설정하여 GNU 디버거(gdb)를 사용하여 같은 문제를 해결한다. kgdb 프로그램은 직렬 연결을 통한 리눅스 커널 원격 gdb이다. 시나리오 4는 행을 일으키는 컴포넌트 정보를 디스플레이 한다. 매직 키 시퀀스를 사용한다.

일반적인 디버깅 전략

프로그램에 버그가 포함되어 있다면 대게 코드 어딘가에 있을 것이다. 여러분이 사실이라고 믿는 이러한 상황은 실제로는 잘못되었다. 버그를 찾는다는 것은 무엇인가 잘못되었다는 것을 발견할 때까지 여러분이 사실이라고 믿는 것을 다시 한번 확인하는 과정이다.

다음은 여러분이 사실이라고 믿고 있는 몇 가지 유형이다.

  • 소스 코드의 특정 위치에서, 변수는 특정 값을 가진다.
  • 주어진 위치에서, 구조가 정확히 설정되었다.
  • if-then-else 문에서 if 부분은 실행되었던 경로이다.
  • 서브루틴이 호출될 때 루틴은 매개변수를 정확히 받는다.

버그를 찾는 것에는 이러한 모든 것들을 확인하는 과정이 포함된다. 특정 변수가 특정 값을 갖고 있다고 믿는 다면 서브루틴이 호출될 대 이를 검사해보라. if 구조가 실행된다는 것을 믿는다면 검사해 보라. 여러분이 가정하는 것을 확인해보면 결국 여러분의 신념이 잘못되었다는 것을 발견할 것이다. 결국 여러분은 버그의 위치를 찾게 된다.

디버깅은 피해갈 수 없는 과정이다. 메시지를 스크린에 프린팅 하기, 디버거 사용하기, 프로그램 실행에 대해 생각하기, 문제에 대한 현학적인 고찰하기 같이 디버깅에 대한 많은 방식들이 있다.

버그를 픽스하기 전에 소스의 위치부터 알아야 한다. 예를 들어, 세그멘테이션 오류가 있을 경우, 코드의 어떤 라인에서 seg 오류가 발생했는지를 알아야 한다. 문제의 코드 라인을 찾아내면 그 메소드에서의 변수의 값, 메소드가 호출되었던 방법, 에러가 발생했던 이유 등을 파악하라. 디버거를 사용하면 이 모든 정보를 찾기가 쉬워진다. 디버거를 사용할 수 없다면 다른 툴들도 있다. (제품 환경에서는 디버거를 사용하지 못할 수도 있다. 리눅스 커널에는 디버거가 빌트인 되어있지 않다.)

유용한 메모리 및 커널 디버깅 툴
리눅스에서 디버그 툴을 사용하여 사용자 공간과 커널 문제를 추적하는 방법은 여러 가지이다. 다음 툴과 기술을 사용하여 소스 코드를 디버깅 하라.

사용자 공간 툴:

  • 메모리 툴: MEMWATCH와 YAMD
  • strace
  • GNU debugger (gdb)
  • Magic key sequence

커널 툴:

  • Kernel source level debugger (kgdb)
  • Built-in kernel debugger (kdb)
  • Oops

이 글에서는 코드를 육안으로 검사해서는 찾기 어려운 문제들과, 매우 드문 환경에서 발생하는 문제 유형에 대해 살펴보도록 하겠다. 환경이 결합될 때에만 발생하는 에러가 있고, 프로그램을 전개한 후에만 메모리 버그를 발견할 때가 가끔 있다.




위로


시나리오 1: 메모리 디버깅 툴

리눅스 시스템 상의 표준 프로그래밍 언어로서 C 언어는 동적 메모리 할당에 대한 많은 제어권을 제공한다. 하지만 이러한 자유가 중대한 메모리 관리 문제를 야기시키고 이 문제는 프로그램 충돌 또는 강등으로 이어진다.

메모리 누수(malloc() 메모리가 상응하는 free() 호출로 결코 누출되지 않는다.)와 버퍼 오버런(어레이에 할당되었던 지난 메모리 작성하기)은 공통의 문제들이고 탐지하기 어렵다. 이 섹션에서는 메모리 문제를 탐지하고 고립하는 몇 가지 디버깅 툴에 대해 알아보겠다.




위로


MEMWATCH

Johan Lindh가 작성한 MEMWATCH는 다운로드 가능한 C를 위한 오픈 소스 메모리 에러 탐지 툴이다.(참조자료) 헤더 파일을 코드에 추가하고 gcc 문에 MEMWATCH를 정의하면 프로그램에 메모리 누수와 오염을 탐지할 수 있다. MEMWATCH는 ANSI C를 지원하고, 결과 로그를 제공하고, 중복도 없고 에러도 없는 실행하지 않는 메모리와 오버플로우와 언더플로우를 탐지한다.


Listing 1. 메모리 샘플 (test1.c)
		#include #include #include "memwatch.h"int main(void){  char *ptr1;  char *ptr2;  ptr1 = malloc(512);  ptr2 = malloc(512);  ptr2 = ptr1;  free(ptr2);  free(ptr1);}

Listing 1에 있는 코드는 두 개의 512-바이트 메모리 블록을 할당하고 첫 번째 블록에 대한 포인터가 두 번째 블록에 설정된다. 결과적으로 두 번째 블록의 주소가 소실되고 메모리 누수가 생긴다.

이제 memwatch.c를 컴파일 한다. 다음은 makefile 예제이다.


test1
		gcc -DMEMWATCH -DMW_STDIO test1.c memwatchc -o test1

test1 프로그램을 실행하면 누수 된 메모리 리포트가 만들어 진다. Listing 2는 memwatch.log 아웃풋 파일 예제이다.


Listing 2. test1 memwatch.log 파일
		MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh...double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)...unfreed: <2> test1.c(11), 512 bytes at 0x80519e4{FE FE FE FE FE FE FE FE FE FE FE FE ..............}Memory usage statistics (global):  N)umber of allocations made: 	2  L)argest memory usage : 	1024  T)otal of all alloc() calls: 	1024  U)nfreed bytes totals : 	512

MEMWATCH는 문제가 있는 실제 라인을 제공한다. 이미 제거된 포인터를 제거하면 알려준다. 제거되지 않은 메모리에도 똑 같이 적용된다. 로그의 끝에 있는 섹션은 얼마나 많은 메모리가 누수가 있었는지, 얼마나 많이 사용되었는지, 총 할당량은 어느 정도인지를 디스플레이 한다.




위로


YAMD

Nate Eldredge가 작성한 YAMD 패키지는 C와 C++에서 메모리 할당과 관련 문제들을 찾는다. 이 글을 쓰고 있는 현재 최신 버전의 YAMD는 0.32다. yamd-0.32.tar.gz를 다운로드 하라.(참고자료) make 명령어를 실행하여 프로그램을 구현한 다음 make install 명령어를 사용하여 프로그램을 설치하고 툴을 설정한다.

YAMD를 다운로드 한 후에 test1.c에서 이것을 사용한다. #include memwatch.h를 제거하고 makefile을 다음과 같이 변경한다.


YAMD를 사용한 test1
		gcc -g test1.c -o test1

Listing 3은 test1 상에서의 YAMD의 아웃풋이다.


Listing 3. YAMD를 사용한 test1 결과
		YAMD version 0.32Executable: /usr/src/test/yamd-0.32/test1...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal deallocation of this blockAddress 0x40025e00, size 512...ERROR: Multiple freeing Atfree of pointer already freedAddress 0x40025e00, size 512...WARNING: Memory leakAddress 0x40028e00, size 512WARNING: Total memory leaks:1 unfreed allocations totaling 512 bytes*** Finished at Tue ... 10:07:15 2002Allocated a grand total of 1024 bytes 2 allocationsAverage of 512 bytes per allocationMax bytes allocated at one time: 102424 K alloced internally / 12 K mapped now / 8 K maxVirtual program size is 1416 KEnd.

YAMD는 우리가 이미 메모리를 제거했고 메모리 누수가 있다는 것을 알려준다. 또 다른 샘플 프로그램에서 YAMD를 실행해 보자.


Listing 4. 메모리 코드 (test2.c)
		#include #include int main(void){  char *ptr1;  char *ptr2;  char *chptr;  int i = 1;  ptr1 = malloc(512);  ptr2 = malloc(512);  chptr = (char *)malloc(512);  for (i; i <= 512; i++) {    chptr[i] = 'S';  }	  ptr2 = ptr1;  free(ptr2);  free(ptr1);  free(chptr);}

다음 명령어를 사용하여 YAMD를 시작한다.

./run-yamd /usr/src/test/test2/test2

Listing 5는 샘플 프로그램 test2에서 YAMD를 사용한 결과를 보여준다. YAMD는 for 루프에서 우리가 out of bound 조건을 갖고 있다는 것을 말해준다.


Listing 5. YAMD를 사용한 test2 결과
		Running /usr/src/test/test2/test2Temp output to /tmp/yamd-out.1243*********./run-yamd: line 101: 1248 Segmentation fault (core dumped)YAMD version 0.32Starting run: /usr/src/test/test2/test2Executable: /usr/src/test/test2/test2Virtual program size is 1380 K...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal allocation of this blockAddress 0x4002be00, size 512ERROR: Crash...Tried to write address 0x4002c000Seems to be part of this block:Address 0x4002be00, size 512...Address in question is at offset 512 (out of bounds)Will dump core after checking heap.Done.

MEMWATCH와 YAMD 모두 유용한 디버깅 툴이고 다른 접근 방식이 필요하다. MEMWATCH를 사용해서는 include 파일인 memwatch.h에 추가하고 두 개의 컴파일 타임 플래그를 켜야 한다. YAMD는 link 문에 -g 옵션만 필요했다.




위로


Electric Fence

대부분의 리눅스 배포판에는 Electric Fence용 패키지가 포함되어 있지만 다운로드 할 수도 있다. Electric Fence는 Bruce Perens가 작성한 malloc() 디버깅 라이브러리이다. 여러분이 할당한 메모리 바로 뒤에 보호를 받는 메모리를 할당한다. fencepost 에러가 있다면(어레이 끝에서 실행됨) 프로그램은 프로텍션(protection) 에러를 사용하여 즉시 종료된다. Electric Fence와 gdb를 결합하면 어떤 라인이 보호되는 메모리에 접근했는지 정확히 추적할 수 있다. Electric Fence는 메모리 누수를 탐지할 수 있다.




위로


시나리오 2: strace 사용하기

strace 명령어는 사용자 공간 프로그램에서 실행되는 모든 시스템 호출을 보여주는 강력한 툴이다. strace는 호출에 대한 인자를 디스플레이 하고 상징적인 형태로 값을 리턴한다. strace는 커널에서 정보를 받고 커널이 특별한 방식으로 빌트인 될 필요가 없다. strace 정보는 애플리케이션과 커널 개발자에게 유용하다. Listing 6에서 파티션의 포맷은 실패했고 listing은 make file system(mkfs)에 대한 strace의 시작을 보여준다. strace는 어떤 호출이 문제를 일으키는지 결정한다.


Listing 6. mkfs에서의 strace 시작
		execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], & ... open("/dev/test1", O_RDWR|O_LARGEFILE) = 4 stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0 ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument) write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning - cannot set blocksize on block device /dev/test1: Invalid argument )  = 98 stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0 open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5 ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument) write(2, "mkfs.jfs: can't determine device"..., ..._exit(1)  = ?

Listing 6을 보면 ioctl 호출이 파티션을 포맷하는데 사용되었던 mkfs 프로그램이 실패하는데 원인이 된다는 것을 알 수 있다. ioctl BLKGETSIZE64는 실패한다. (BLKGET-SIZE64ioctl을 호출하는 소스 코드에 정의된다.) BLKGETSIZE64 ioctl은 리눅스 상의 모든 장치들에 추가되고 있고, 이 경우, 논리적 볼륨 매니저는 이것을 지원하지 않는다. 따라서 mkfs 코드는 BLKGETSIZE64 ioctl 호출이 실패할 경우 오래된 ioctl을 호출해야 한다. 이것은 mkfs가 논리적 볼륨 매니저와 작동하도록 한다.




위로


Scenario 3: gdb와 Oops 사용하기

Free Software Foundation의 디버거인 gdb 프로그램을 사용할 수 있다. 명령행 또는 Data Display Debugger (DDD) 같은 그래픽 툴에서 사용할 수 있다. gdb를 사용하여 사용자 공간 프로그램이나 리눅스 커널을 디버깅 할 수 있다. 이 섹션에서는 gdb용 명령행에 대해서만 설명하겠다.

gdb program name 명령어를 사용하여 gdb를 시작한다. gdb는 실행 파일의 심볼을 로딩하고 인풋 프롬프트를 디스플레이 하면, 여러분은 디버거를 사용할 수 있다. gdb 프로세스를 볼 수 있는 세 가지 방법이 있다.

  • attach 명령어를 사용하여 이미 실행중인 프로세스를 본다. attach는 프로세스를 중지시킬 것이다.

  • run 명령어를 사용하여 프로그램을 실행하고 시작할 때 프로그램을 디버깅 한다.

  • 기존 코어 파일을 보고 프로세스가 종료되었을 때의 상태를 파악한다. 코어 파일을 보려면 다음 명령어를 사용하여 gdb를 시작한다.

    gdb programname corefilename

    코어 파일을 사용하여 디버깅 하려면 프로그램 실행 파일과 소스 파일 그리고 코어 파일이 필요하다. 코어 파일로 gdb를 시작하려면 -c 옵션을 사용한다.

    gdb -c core programname

    gdb는 어떤 라인의 코드가 문제를 일으켰는지를 코어 덤프에 보여준다.

프로그램을 실행하거나 이미 실행중인 프로그램에 붙기 전에 버그가 있다고 생각하는 소스 코드의 리스트를 만들고, 중단점을 설정하고, 프로그램 디버깅을 시작한다. gdb 온라인 도움말과 help 명령어를 사용하여 상세한 튜토리얼을 참조할 수 있다.




위로


kgdb

kgdb 프로그램(gdb를 통한 원격 호스트 리눅스 커널 디버거)은 gdb를 사용하여 리눅스 커널을 디버깅 하는 메커니즘을 제공한다. kgdb 프로그램은 커널의 확장으로서, 원격 호스트 머신에서 gdb를 실행할 때 kgdb 확장 커널을 실행하는 머신에 연결할 수 있다. 커널로 들어가서 중단점을 설정하고 데이터를 검사할 수 있다. (애플리케이션 프로그램에서 gdb를 사용하는 것과 비슷하다.) 이 패치의 주요 기능들 중 하나는 gdb를 실행하는 원격 호스트가 부팅 프로세스 동안 목표 머신(디버깅 될 커널을 실행하고 있음)으로 연결된다는 점이다. 때문에 디버깅을 최대한 빨리 시작할 수 있다. 이 패치는 리눅스 커널에 이 기능을 추가하여 gdb가 리눅스 커널을 디버깅하는데 사용될 수 있도록 한다.

kgdb를 사용하려면 두 개의 머신이 필요하다. 하나는 개발 머신이고 다른 하나는 테스트 머신이다. 시리얼 라인(null-modem cable)은 시리얼 포트를 통해 머신으로 연결된다. 여러분이 디버깅 하고자 하는 커널은 테스트 머신 상에서 실행된다. gdb는 개발 머신에서 실행된다. gdb는 시리얼 라인을 사용하여 여러분이 디버깅 하고 있는 커널과 통신한다.

다음 단계를 통해서 kgdb 디버그 환경을 설정한다.

  1. 리눅스 커널 버전에 맞는 패치를 다운로드 한다.

  2. 커널에 컴포넌트를 구현한다. 이것은 kgdb를 사용하는 가장 쉬운 방법이다. (커널의 컴포넌트를 구현하는 두 가지 방법이 있다. 모듈로서 구현하거나 직접 커널에 구현하는 방법이 있다. 예를 들어, Journaled File System (JFS)은 모듈로서 구현되거나 커널에 직접 구현될 수 있다. gdb 패치를 사용하면 JFS를 커널에 직접 구현할 수 있다.)

  3. 커널 패치를 적용하고 커널을 재구현 한다.

  4. .gdbinit 라고 하는 파일을 만들고 이것을 커널 소스 하위 디렉토리에 둔다.(/usr/src/linux) .gdbinit 파일에는 다음과 같은 네 가지 라인이 있다.
    • set remotebaud 115200
    • symbol-file vmlinux
    • target remote /dev/ttyS0
    • set output-radix 16

  5. append=gdb 라인을 lilo에 추가한다. 이것은 부트 로드로서 커널을 부팅할 때 사용될 커널을 선택한다.
    • image=/boot/bzImage-2.4.17
    • label=gdb2417
    • read-only
    • root=/dev/sda8
    • append="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0"

Listing 7은 개발 머신에 구현한 커널과 모듈을 테스트 머신으로 가져오는 스크립트 예제이다. 다음 아이템들을 수정해야 한다.

  • best@sfb: Userid와 머신 이름
  • /usr/src/linux-2.4.17: 커널 소스 트리의 디렉토리
  • bzImage-2.4.17: 테스트 머신 상에 부팅 될 커널의 이름
  • rcp and rsync: 커널이 구현된 머신 상에서 실행되어야 함.


Listing 7. 커널과 모듈을 테스트 머신으로 가져오는 스크립트
		set -xrcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17rm -rf /lib/modules/2.4.17rsync -a best@sfb:/lib/modules/2.4.17 /lib/moduleschown -R root /lib/modules/2.4.17lilo

커널 소스 트리가 시작되는 디렉토리를 수정함으로서 개발 머신 상에서 gdb 프로그램을 시작할 준비가 되었다. 이 예제에서, 우리의 커널 소스 트리는 /usr/src/linux-2.4.17에 있다. gdb를 입력하여 프로그램을 시작한다.

모든 것이 작동한다면 테스트 머신은 부팅 프로세스 동안 중지된다. gdb 명령어 cont를 입력하여 부팅 프로세스를 계속 진행한다. 한 가지 문제는 null-modem 케이블이 그릇된 시리얼 포트에 연결될 수도 있다는 점이다. gdb가 시작하지 않으면 포트를 두 번째 시리얼로 전환하여 gdb를 시작한다.




위로


kgdb를 사용하여 커널 문제 디버깅하기

Listing 8은 jfs_mount.c 파일의 소스에서 수정되었던 코드이다. 무효 포인터 예외를 만들어서 109 라인에 세그멘테이션 오류를 만들었다.


Listing 8. 수정된 jfs_mount.c 코드
		int jfs_mount(struct super_block *sb){...int ptr; 			/* line 1 added */jFYI(1, ("nMount JFSn"));/ ** read/validate superblock* (initialize mount inode from the superblock)* /if ((rc = chkSuper(sb))) {		goto errout20;	}108 	ptr=0; 			/* line 2 added */109 	printk("%dn",*ptr); 	/* line 3 added */

Listing 9는 파일 시스템에 대한 mount 명령어가 실행된 후에 gdb 예외를 디스플레이 하고 있다. kgdb에서 사용할 수 있는 여러 명령어들이 있다. 데이터 구조와 변수 값을 디스플레이 하고, 시스템의 모든 태스크들이 어떤 상태에 있는지를 보고, 이들이 수면하는 곳은 어디이며, CPU를 소비하고 있는 곳은 어디인지를 알 수 있다. Listing 9는 백 트레이스가 제공하는 정보를 보여준다. where 명령어는 백 트레이스를 수행하는데 사용된다. 코드에서 중지된 장소로 가기위해 실행되었던 호출을 보여준다.


Listing 9. gdb 예외와 백 트레이스
		mount -t jfs /dev/sdb /jfsProgram received signal SIGSEGV, Segmentation fault.jfs_mount (sb=0xf78a3800) at jfs_mount.c:109109 		printk("%dn",*ptr);(gdb)where#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109#1 0xc01a0dbb in jfs_read_super ... at super.c:280#2 0xc0149ff5 in get_sb_bdev ... at super.c:620#3 0xc014a89f in do_kern_mount ... at super.c:849#4 0xc0160e66 in do_add_mount ... at namespace.c:569#5 0xc01610f4 in do_mount ... at namespace.c:683#6 0xc01611ea in sys_mount ... at namespace.c:716#7 0xc01074a7 in system_call () at af_packet.c:1891#8 0x0 in ?? ()(gdb)

다음 섹션에서는 디버거 설정 없이 같은 JFS 세그멘테이션 오류 문제에 대해 살펴보도록 하겠다. 비-kgdb 환경에서 Listing 8의 코드를 실행할 때 커널이 만들어내는 Oops를 사용한다.




위로


Oops 분석

Oops (또는 panic) 메시지에는 CPU 레지스터 같은 시스템 오류 상세들이 포함되어 있다. 리눅스에서, 시스템 충돌을 디버깅하는 전통적인 방법은 Oops 메시지의 상세를 분석하는 것이었다. 상세를 포착하면 메시지는 ksymoops 유틸리티로 전달된다. 이것은 코드를 명령어로 변환하고 스택 값을 커널 심볼로 변환한다. 많은 경우에, 오류의 원인을 파악할 수 있는 충분한 정보가 된다. Oops 메시지에는 코어 파일이 포함되어 있지 않다.

시스템이 Oops 메시지를 만들어냈다고 가정해보자. 코드의 작성자로서, 문제를 해결하고 무엇이 Oops 메시지의 원인이 되었는지 파악하고 싶을 것이다. 아니면 Oops 메시지를 디스플레이 했던 코드의 개발자에게 문제 정보를 알려주고 문제 해결을 받게 될 것이다. Oops 메시지는 방정식의 한 부분이지만 ksyoops 프로그램을 통해서 실행하지 않는 한 도움이 되지 않는다. 아래 그림은 Oops 메시지를 포맷팅 하는 과정이다.


Oops 메시지 포맷팅하기
Formatting an Oops message

ksymoops가 필요로 하는 여러 가지 아이템들이 있다. Oops 메시지 아웃풋, 실행중인 커널의 System.map 파일, /proc/ksyms, vmlinux, /proc/modules 등이 필요하다. 커널 소스 /usr/src/linux/Documentation/oops-tracing. txt 또는 ksymoops 매뉴얼 페이지에는 ksymoops를 사용하는 방법이 자세히 나와있다. Ksymoops는 코드 섹션을 분해하고, 오류 명령어를 가리키고, 코드가 호출되었던 방법을 보여주는 트레이스 섹션을 디스플레이 한다.

우선, Oops 메시지를 파일에 넣어 ksymoops 유틸리티를 통해 이를 실행한다. Listing 10은 mount에 의해 생성된 JFS 파일 시스템에 대한 Oops를 보여준다. JFS의 mount 코드에 추가되었던 Listing 8의 세 개의 라인에서 야기된 문제가 포함되어 있다.


Listing 10. ksymoops에 의해 처리된 후의 Oops
		ksymoops 2.4.0 on i686 2.4.17. Options used... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference atvirtual address 0000000... 15:59:37 sfb1 kernel: c01588fc... 15:59:37 sfb1 kernel: *pde = 0000000... 15:59:37 sfb1 kernel: Oops: 0000... 15:59:37 sfb1 kernel: CPU:    0... 15:59:37 sfb1 kernel: EIP:    0010:[jfs_mount+60/704]... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688] [get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208][do_page_fault+0/1264]... 15:59:37 sfb1 kernel: Call Trace: []...... 15:59:37 sfb1 kernel: [>EIP; c01588fc  <=====...Trace; c0106cf3 Code; c01588fc 00000000 <_EIP>:Code; c01588fc   <=====   0: 8b 2d 00 00 00 00 	mov 	0x0,%ebp    <=====Code; c0158902    6:  55 			push 	%ebp

다음에는, jfs_mount에서 문제를 일으킨 라인이 어떤 것인지를 파악해야 한다. Oops 메시지를 보면 문제는 offset 3c 에 있는 명령어에서 비롯되었다는 것을 알 수 있다. 이것을 다루는 한 가지 방법은 jfs_mount.o 파일에 objdump 유틸리티를 사용하여 offset 3c를 보는 것이다. Objdump는 모듈 함수를 분해하고 어떤 어셈블러 명령어가 C 소스 코드에 의해 만들어 졌는지를 보는데 사용된다. Listing 11은 objdump에서 볼 수 있는 것을 보여주고 우리는 jfs_mount 에 대한 C 코드를 보고 109 라인에 의해 무효가 발생했다는 것을 볼 수 있다. Offset 3c는 Oops 메시지가 문제의 원인을 규명했던 위치이기 때문에 중요하다.


Listing 11. jfs_mount의 어셈블러 리스팅
		109	printk("%dn",*ptr);objdump jfs_mount.ojfs_mount.o: 	file format elf32-i386Disassembly of section .text:00000000 :   0:55 			push %ebp  ...  2c:	e8 cf 03 00 00	   call	   400   31:	89 c3 	  	    	mov     %eax,%ebx  33:	58		    	pop     %eax  34:	85 db 	  	    	test 	%ebx,%ebx  36:	0f 85 55 02 00 00 jne 	291   3c:	8b 2d 00 00 00 00 mov 	0x0,%ebp << problem line above  42:	55			push 	%ebp




위로


kdb

Linux kernel debugger (kdb)는 리눅스 커널용 패치이고, 시스템이 실행 중일 때 커널 메모리와 데이터 구조를 검사하는 방식을 제공한다. kdb는 두 개의 머신까지는 요구하지는 않지만 kgdb 처럼 소스 레벨 디버깅을 수행할 수 없다. 추가 명령어를 사용하여 중요한 시스템 데이터 구조를 포맷팅 및 디스플레이 한다. 다음 과 같은 명령어 세트를 사용해서 커널 작동을 제어할 수 있다.

  • Single-stepping a processor
  • Stopping upon execution of a specific instruction
  • Stopping upon access (or modification) of a specific virtual memory location
  • Stopping upon access to a register in the input-output address space
  • Stack back trace for the current active task as well as for all other tasks (by process ID)
  • Instruction disassembly

메모리 오버런 추적하기

수 천 개의 호출 뒤에 발생하는 할당 오버런 같은 상황에 직면하기 원치 않을 것이다.

우리 팀은 이상한 메모리 오염 문제를 파악하느라 오랜 시간을 보냈다. 이 애플리케이션은 우리의 개발 워크스테이션에서는 작동했지만 새로운 워크스테이션에서 malloc()에 2백만 개의 호출을 한 후에 중지되었다. 실제 문제는 백만 호출과 관련한 오버런이였다. 새로운 시스템은 malloc() 영역이 다르게 레이아웃 되었기 때문에 문제가 생긴 것이다. 따라서 문제 메모리를 다른 장소에 배치했고 오버런을 수행했을 때와는 다른 것을 파괴했다.

우리는 이 문제를 다양한 기술들을 적용하여 해결했다. 디버거를 사용하기도 했고 소스 코드에 트레이싱을 추가하기도 했다. 이와 동시에 메모리 디버깅 툴을 찾기 시작했고 이것이 이러한 유형의 문제를 더욱 빠르고 효율적으로 해결한다는 것을 발견했다. 새로운 프로젝트를 시작할 때 내가 가장 먼저 하는 일은 MEMWATCH와 YAMD를 실행하여 이들이 메모리 관리 문제를 지적할 수 있는지를 확인하는 것이다.

메모리 누수는 애플리케이션에서는 일반적인 문제지만 이 글에서 설명한 툴을 사용하여 문제를 해결할 수 있다.




위로


Scenario 4: 키 시퀀스로 추적하기

키보드가 여전히 작동하고 있고 리눅스 상에서 행(hang)이 걸렸다면 다음 방식을 사용하여 행 문제를 해결하라. 다음 단계들을 따라가면 매직 키 시퀀스를 사용하여 현재 실행중인 프로세스와 모든 프로세스들에 대한 역추적 내용을 디스플레이 할 수 있다.

  1. 실행중인 커널에는 CONFIG_MAGIC_SYS-REQ가 빌트인 되어야 한다. 또한 텍스트 모드에 있어야 한다. CLTR+ALT+F1를 사용하면 텍스트 모드가 되고 CLTR+ALT+F7은 X Windows로 돌아간다.
  2. 텍스트 모드에 있을 때, 을 누른 다음 을 누른다. 이 키스트로크는 각각 현재 실행 중인 프로세스와 모든 프로세스들에 대한 스택 트레이스를 제공한다.
  3. /var/log/messages를 검사해 보라. 모든 것이 올바르게 설정되었다면 시스템은 상징적인 커널 주소로 변환되었을 것이다. 백 트레이스가 /var/log/messages 파일에 작성될 것이다.




위로


결론

리눅스 상에서 프로그램을 디버깅하는데 사용할 수 있는 많은 툴이 있다. 이 툴들은 많은 코딩 문제를 해결하는데 도움이 된다. 메모리 누수 위치, 오버런 등을 보여주는 툴은 메모리 관리 문제도 해결할 수 있고 MEMWATCH와 YAMD도 도움이 된다.

리눅스 커널 패치를 사용하여 gdb를 리눅스 커널 상에 작동시키는 것도 문제 해결에 도움이 된다. 또한 strace 유틸리티는 시스템 호출 동안 파일시스템 유틸리티에 어떤 오류가 생겼는지 파악하는데 도움이 된다. 다음 시간에는 이 글에 소개된 툴을 사용하여 버그를 없애는 방법을 설명하겠다.

기사의 원문보기




위로


참고자료




위로


필자소개

Steve Best는 Linux Technology Center(Austin, Texas)에서 일하고 있다. 현재 Journaled File System (JFS) 작업을 하고 있다. 운영 체계 개발과 파일 시스템, 국제화, 보안 분야에서 많은 작업중이다

'Utilities' 카테고리의 다른 글

개발 서버에 gdb 설치 하고 사용하기  (0) 2006.08.10
[응용] Makefile 작성법  (2) 2006.08.10
디버깅을 익히자!  (0) 2006.08.10
리눅스 개발환경의 개요도  (0) 2006.08.10
CVS 사용  (0) 2006.07.26