|
Level: Advanced |
Hariprasad Nellitheertha
Software engineer, IBM
2003년 6월 5일
커널 문제들을 디버깅할 때, 커널 실행을 추적하고 이것의 메모리와 데이터 구조를 검사할 수 있도록 한다면 매우 유용할 것이다. 리눅스의 빌트인 커널 디버거인 KDB가 바로 이러한 기능을 제공한다. 이 글에서 KDB가 제공하는 기능을 사용하는 방법과 리눅스 머신에 KDB를 설치 및 설정하는 방법을 배울것이다.
리눅스 커널 디버거(KDB)를 사용하여 리눅스 커널을 디버깅 할 수 있다. 이렇듯 적절한 이름이 붙은 툴은 해커들이 커널 메모리와 데이터 구조에 접근하도록 하는 커널 코드에 대한 패치이다. KDB의 큰 장점 중 하나는 디버깅을 위한 추가 머신이 필요하지 않다는 점이다. 따라서 실행 중에 커널을 디버깅 할 수 있다.
KDB용 머신을 설정할 때에는 커널이 패치가 되고 재컴파일 되어야 하므로 약간의 작업이 필요하다. 리눅스 커널 컴파일링 관련하여 참고자료에 보다 자세한 내용이 있다.
KDB 패치 다운로드, 패치 적용, 커널 (재)컴파일링, KDB 시작하기 등으로 이 글은 시작한다. 그런 다음 KDB 명령어를 연구하고 자주 사용되는 명령어를 검토할 것이다. 마지막으로 설정 및 디스플레이 옵션을 자세하게 보겠다.
시작하기
KDB 프로젝트는 Silicon Graphics (참고자료)에서 관리하며, FTP site에서 커널 버전에 맞는 패치를 다운로드 해야 한다. 사용할 수 있는 최신 KDB는 지금 이 글을 쓰고있는 당시 4.2이다. 두 개의 패치를 다운로드 하여 적용해야 한다. 하나는 "common" 패치로서 일반적인 커널 코드의 변경사항을 포함하고 있고 다른 패치는 아키텍쳐 스팩의 패치이다. 이 패치들은 bz2 파일로 사용할 수 있다.
이 글에서 제공하는 모든 예제들은 i386 아키텍쳐와 2.4.20 커널 기준이다. 머신과 커널 버전에 근거해 알맞게 변경해야 한다. 또한 다음과 같은 작동을 수행하려면 루트(root) 권한이 필요하다.
파일들을 /usr/src/linux 디렉토리에 복사하고 bzip 파일에서 패치 파일을 추출한다:
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2
#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2
kdb-v4.2-2.4.20-common-1과 kdb-v4.2-2.4-i386-1 파일을 보게 된다.
이제 패치를 붙인다:
#patch -p1 <kdb-v4.2-2.4.20-common-1
#patch -p1 <kdb-v4.2-2.4.20-i386-1
이 패치들을 깨끗하게 붙여야 한다. .rej로 끝나는 모든 파일을 찾아보라. 이는 잘못된 패치이다. 커널 트리가 깨끗하다면 패치는 아무 문제없이 적용될 것이다.
그런 다음 커널은 KDB가 실행될 수 있도록 구현되어야 한다. 첫 번째 단계는 CONFIG_KDB 옵션을 설정하는 것이다. 선호하는 설정 방법(xconfig, menuconfig)으로 한다. "Kernel hacking" 섹션으로 가서 "Built-in Kernel Debugger support" 옵션을 선택한다.
다른 옵션들이 두 가지 더 있다. "Compile the kernel with frame pointers" 옵션이 있다면 선택하여 CONFIG_FRAME_POINTER 플래그를 설정한다. 스택 추적이 보다 나아진다. 프레임 포인터 레지스터가 범용의 레지스터 보다는 프레임 포인터로서 사용된다. "KDB off by default" 옵션을 선택할 수 있다. 이것은 CONFIG_KDB_OFF 플래그를 설정하고 KDB를 디플트로 종료한다. 다음 섹션에서 자세히 다루겠다.
설정을 저장하고 종료한다. 커널을 재컴파일 한다. 커널을 구현하기 전에 "청소"를 반드시 하기 바란다. 일반적인 방법으로 커널을 설치하고 부팅한다.
환경 변수의 초기화와 설정
KDB 초기화 과정 중에 실행될 KDB 명령어를 정의할 수 있다. 이 명령어들은 kdb_cmds라는 평이한 텍스트 파일로 정의되어야 한다. 리눅스 소스 트리의 KDB 디렉토리에 있다. 이 파일은 환경 변수를 정의하는데 사용되어 디스플레이 및 프린트 옵션을 설정할 수 있다. 파일 시작에 있는 주석은 파일 편집에 대한 지시이다. 이 파일을 사용할 때의 단점이라면 파일을 변경할 때 재설치되어야 한다는 점이다.
KDB 활성화
CONFIG_KDB_OFF가 컴파일 진행 중에 선택되지 않았다면 KDB는 디폴트로 활성화 된다. 그렇지 않다면 이를 반드시 활성화 해야한다. kdb=on 플래그를 부팅중에 커널로 전달하거나 /proc이 마운트되면 이를 수행한다:
#echo "1" >/proc/sys/kernel/kdb
위 단계를 반대로 하면 KDB를 비활성화 하는 것이 된다. kdb=off 플래그를 커널로 전달하거나 아니면 다음과 같이 KDB를 비활성화한다:
#echo "0" >/proc/sys/kernel/kdb
부팅 중 커널로 전달될 수 있는 또 다른 플래그가 있다. kdb=early 플래그는 부팅 프로세스 중 매우 빨리 KDB에 전달되면서 제어를 받게된다. 부팅 프로세스 중 일찍 디버그 할 때 도움이 된다.
KDB를 호출하는 다양한 방법이 있다. KDB가 실행중이면 커널에 패닉이 있을때마다 자동으로 호출된다. 키보드 상의 PAUSE 키를 눌러도 KDB가 호출된다. 또다른 호출 방법은 시리얼 콘솔을 이용하는 것이다. 물론 이렇게 하려면 시리얼 콘솔(참고자료)를 설정하고 시리얼 콘솔을 읽는 프로그램이 필요하다. Ctrl-A를 누르면 시리얼 콘솔에서 KDB가 호출된다.
KDB 명령어
KDB는 메모리 및 레지스터 수정, 적용 중단점, 스택 트레이싱 같은 다양한 작동들을 가능하게 하는 매우 강력한 툴이다. 이를 기반으로 KDB 명령어는 여러 카테고리로 구분될 수 있다. 여기에서는 가장 일반적으로 사용되는 명령어를 구분했다.
메모리 디스플레이 및 수정
이 카테고리에서 가장 빈번히 사용되는 명령어는 md, mdr, mm, mmW 이다.
md 명령어는 address/symbol과 line-count용 주소에서 시작하는 메모리를 디스플레이한다. line-count가 지정되지 않으면 환경 변수에서 지정된 디폴트가 사용된다. 주소가 지정되지 않으면 md는 프린팅 된 마지막 주소 부터 지속된다. 이 주소는 앞부분에서 프린트되고 캐릭터 변환은 마지막에 프린트된다.
mdr 명령어는 address/symbol과 바이트 카운트를 취하고 바이트의 byte-count 수를 위한 지정된 어드레스에서 시작하는 메모리의 원래 콘텐트를 디스플레이한다. 근본적으로 md와 같으나 시작 어드레스와 마지막에 캐릭터 변환을 디스플레이하지 않는다. mdr 명령어는 사용량이 적다.
mm 명령어는 메모리 콘텐트를 수정한다. 매개변수로서 address/symbol과 콘텐츠를 취하고 new-contents를 가진 어드레스에서 콘텐츠를 대체한다.
mmW 명령어는 주소에서 시작하는 W 바이트를 변경한다.
예시
0xc000000에서 시작하는 15 라인의 메모리 디스플레이하기:
[0]kdb> md 0xc000000 15
0xc000000 메모리 위치의 콘텐츠를 0x10으로 바꾸기:
[0]kdb> mm 0xc000000 0x10
레지스터 디스플레이 및 수정
이 카테고리의 명령어로는 rd, rm, ef 등이 있다.
rd 명령어는 (어떤 인자 없이도) 프로세서 레지스터의 콘텐츠를 디스플레이한다. 세 개의 인자를 선택적으로 취한다. c 인자가 통과되면, rd는 프로세서의 제어 레지스터를 디스플레이한다. d 인자로는 디버그 레지스터를 디스플레이한다. u 인자로는 커널에 마지막으로 들어갈 때 현재 태스크의 레지스터 세트가 디스플레이 된다.
rm 명령어는 레지스터의 콘텐츠를 수정한다. 인자로서 레지스터 이름과 new-contents를 취하고 new-contents로 레지스터를 수정한다. 레지스터 이름은 특정 아키텍쳐에 따라 다르다. 현재 제어 레지스터는 수정될 수 없다.
ef 명령어는 인자로서 어드레스를 취하고 지정된 어드레스에 예외 프레임을 디스플레이한다.
예시
일반적인 레지스터 세트 디스플레이:
[0]kdb> rd
레지스터 ebx 콘텐츠를 0x25로 설정하기:
[0]kdb> rm %ebx 0x25
중단점
일반적으로 사용되는 중단점 명령어는 bp, bc, bd, be, bl 이다.
bp 명령어는 인자로서 address/symbol을 취하고 어드레스에 중단점을 붙인다. 이 중단점이 적용되면 실행은 멈추고 KDB에 제어가 생긴다. 이 명령어에 두 가지의 유용한 변수가 있다. bpa 명령어는 SMP 시스템에 있는 모든 프로세서에 중단점을 붙인다. bph 명령어는 지원하는 시스템상의 하드웨어 레지스터를 사용토록 한다. bpha 명령어는 하드웨어 레지스터 사용을 강요하는 것을 제외하고 bpa 명령어와 비슷하다.
bd 명령어는 특정 중단점을 실행불가로 만든다. 인자로서 중단점 내에 숫자를 취한다. 이 명령어는 중단점 테이블에서 중단점을 제거하지 않는다. 다만 실행불가능한 상태로 만들 뿐이다. 중단점 숫자는 0 부터 시작하고 가능한 순서대로 중단점에 할당된다.
be 명령어는 중단점을 실행가능하게 한다. 이 명령어에 대한 인자 역시 중단점 숫자이다.
bl 명령어는 현재 중단점 세트를 리스팅한다. 실행불가/실행가능 중단점 모두 포함되어 있다.
bc 명령어는 중단점 테이블에서 중단점을 제거한다. 인자로서 특정 중단점 숫자를 취하거나 *을 취한다. 모든 중단점을 제거할 경우에 그렇다.
예시
sys_write()함수에 중단점 설정하기:
[0]kdb> bp sys_write
중단점 테이블에 모든 중단점 리스팅하기:
[0]kdb> bl
중단점 숫자 1 제거하기:
[0]kdb> bc 1
스택 트레이스
주요 스택 트레이싱 명령어로는 bt, btp, btc, bta.
bt 명령어는 현재 쓰레드에 스택에 있는 정보를 제공한다. 인자로서 스택 프레임 어드레스를 선택적으로 취한다. 어떤 어드레스도 제공되지 않으면 현재 레지스터로 스택을 추적한다. 그렇지 않으면 유효 스택 프레임 시작 어드레스로 제공된 어드레스를 시작하고 추적을 시도한다. CONFIG_FRAME_POINTER 옵션이 커널 컴파일 동안 설정된다면 프레임 포인터 레지스터는 스택을 관리하기 위해 사용된다. 따라서 스택 추적은 정확하게 수행될 수 있다. bt 명령어는 CONFIG_FRAME_POINTER가 설정되지 않을 경우 정확한 결과를 낼 수 없다.
btp 명령어는 인자로서 프로세스 ID를 취하고 특정 프로세스를 위해 스택 추적을 수행한다.
btc 명령어는 CPU상에서 실행되는 프로세스에 대한 스택 추적을 수행한다. 첫 번째 활성 CPU에서 시작하여 bt를 수행하고 다음의 활성 CPU로 전환하는 식이다.
bta 명령어는 특정 상태에 있는 모든 프로세스에 대한 추적을 수행한다. 인자 없이도 모든 프로세스에 대한 추적을 수행한다. 다양한 인자들이 명령어에 전달 될 수 있다. 특정 상태에 있는 프로세스들은 인자에 의존해 프로세스 될 것이다. 옵션과 상태 설명은 다음과 같다:
예시
현재 활성 쓰레드에 대한 스택 추적:
[0]kdb> bt
ID 575 프로세스용 스택 추적:
[0]kdb> btp 575
기타 명령어
커널 디버깅에 유용하게 쓰일 KDB 명령어들이 있다.
id 명령어는 address/symbol을 인자로 취하고 그 어드레스에서 시작하는 명령을 역어셈블링한다. 환경 변수 IDCOUNT는 얼마나 많은 아웃풋 라인이 디스플레이 될지를 결정한다.
ss 명령어는 KDB에 대한 제어를 리턴한다. 이 명령어에 대한 변종으로는 ssb 인데 현재 명령 포인터 어드레스에서 온 명령을 실행한다. 브랜치를 발생할 명령을 만날 때 까지 지속한다. 전형적인 브랜치 명령으로는 call, return, jump가 있다.
go 명령어는 시스템이 정상 실행을 지속할 수 있도록 한다. 중지점이 적용될 때까지 실행을 지속한다.
reboot 명령어는 시스템을 즉각적으로 재부팅한다. 시스템을 깔끔하게 종료하는 것이 아니므로 결과는 예견 불가능하다.
ll 명령어는 인자로서 어드레스, 오프셋, KDB 명령어를 취한다. 링크된 리스트의 명령어를 반복한다. 실행되는 명령어는 인자로서 그 리스트의 현재 엘리먼트의 어드레스를 취한다.
예시
루틴 스케줄에서 시작하는 명령의 역어셈블. 디스플레이된 라인의 수는 환경 변수 IDCOUNT에 의존한다:
[0]kdb> id schedule
브랜치 조건을 만날 때 가지 명령 실행하기:
[0]kdb> ssb
0xc0105355 default_idle+0x25: cli
0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax
0xc0105359 default_idle+0x29: test %eax, %eax
0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
팁 & 트릭
문제를 디버깅 하는 것에는 디버거를 사용하여 문제의 원천(source)을 찾아내고 소스 코드를 사용하여 문제의 근원을 트래킹하는 것이 포함된다. 문제를 규명하기 위해 소스 코드 만을 사용하는 것은 매우 위험한 일이며 전문 커널 해커에게나 가능한 일이다. 초보자는 버그 픽스를 할 때 디버거에 지나치게 의존하는 경향이 있다. 이러한 접근방식은 때로는 그릇된 솔루션을 초래할 수 있다. 그와 같은 접근방식이 실제 문제 보다는 어떤 현상을 픽스하는것으로 끝날것이 우려된다.
코드를 연구하고 디버깅 툴을 사용하는 이중적 접근방식은 문제 규명과 해결을 위한 최상의 방법이다.
디버거의 기본적인 사용처는 버그의 위치를 찾고 현상을 확인하며 변수 값을 결정하고 프로그램을 어떻게 다룰것인지를 결정하는 것이다. 숙련된 해커는 문제에 따른 디버거를 파악하고 디버깅에 필요한 정보를 빠르게 수집하며 원인을 규명할 코드를 분석한다.
다음은 위에 언급한 대로 KDB를 사용하여 빠른 결과를 얻을 수 있는 팁이다. 물론 디버깅의 속도와 정확성은 경험, 연습, 시스템 지식에 달려있다는 것을 명심하라.
Tip #1
KDB 에서 프롬프트에 어드레스를 타이핑하면 가장 근접한 심볼 매치를 리턴한다. 이는 스택 분석과 글로벌 데이터의 어드레스/값과 함수 어드레스를 결정하는데 유용하다. 심볼 이름을 타이핑하면 가상 어드레스를 리턴한다.
예시
sys_read 함수가 0xc013db4c 어드레스에서 시작함을 나타내기:
[0]kdb> 0xc013db4c
0xc013db4c = 0xc013db4c (sys_read)
유사한 것으로, sys_write가 0xc013dcc8 어드레스에 있다는 것 나타내기:
[0]kdb> sys_write
sys_write = 0xc013dcc8 (sys_write)
이는 스택을 분석하는 동안 글로벌 데이터와 함수 어드레스의 위치를 찾는데 도움이 된다.
Tip #2
KDB로 커널을 컴파일 하는 동안 CONFIG_FRAME_POINTER 옵션을 사용하라. 이때 커널을 설정하면서 "Kernel hacking" 섹션 밑에 있는 "Compile the kernel with frame pointers" 옵션을 선택해야 한다. 프레임 포인터 레지스터가 정확한 추적을 할 수 있도록 사용되고 있음을 확인하는 것이다. 사실 프레임 포인터 레지스터의 콘텐츠를 수동으로 덤핑하고 전체 스택을 추적할 수 있다. 예를 들어 i386 머신에서, %ebp 레지스터는 전체 스택 추적에 사용될 수 있다.
rmqueue()함수에 첫 번째 명령을 실행한 후에 스택은 다음과 같다:
[0]kdb> md %ebp
0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000
0xc74c9f48 08053328 c0425238 c04253a8 00000000
0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25
0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc
0xc74c9f78 c014fe45 c74c8000 00000000 08053328
[0]kdb> 0xc0136c40
0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
[0]kdb> 0xc0136a25
0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
[0]kdb> 0xc0136d6d
0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
rmqueue()가 __alloc_pages에 의해 호출되었음을 알 수 있다. 차례대로 _alloc_pages에 의해 호출되었다.
모든 프레임의 첫 번째 두 개의 단어는 다음 프레임을 나타내고 여기에 즉시 호출 함수 어드레스가 따라온다. 따라서 스택 트레이싱은 매우 쉬운 일이 된다.
Tip #3
go 명령어는 매개변수로서 어드레스를 취한다. 특정 어드레스에서 실행을 지속하려면 매개변수로서 어드레스를 제공할 수 있다. 다른 대안으로는 rm 명령어를 사용하여 명령 포인터 레지스터를 수정하고 go를 타이핑하는 것이다. 특정 명령이나 문제를 일으킬 것으로 보이는 명령을 스킵할 때 유용하다. 이 명령을 사용할 때 주의하지 안으면 심각한 문제가 발생한다.
Tip #4
defcmd 이라는 유용한 명령어로 자신만의 명령어 세트를 정의할 수 있다. 예를 들어 중단점을 히트할 때 마다 특정 변수 검사, 레지스터의 콘텐츠 검사, 스택 덤핑 등을 동시에 수행하고 싶을 때도 있다. 일반적으로 이를 동시에 수행할 수 있는 명령어 시리즈를 타이핑해야 한다. defcmd는 자신만의 명령어를 정의할 수 있도록 해주며 한 개 이상의 사전정의된 KDB 명령어로 구성되어 있다. 단지 하나의 명령어가 필요할 분이다. 신택스는 다음과 같다:
[0]kdb> defcmd name "usage" "help"
[0]kdb> [defcmd] type the commands here
[0]kdb> [defcmd] endefcmd
예를 들어 0xc000000에서 시작하는 한 줄의 메모리를 디스플레이하고, 레지스터 콘텐츠를 디스플레이하며, 스택을 덤핑할 새로운 명령어 hari를 정의할 수 있다:
[0]kdb> defcmd hari "" "no arguments needed"
[0]kdb> [defcmd] md 0xc000000 1
[0]kdb> [defcmd] rd
[0]kdb> [defcmd] md %ebp 1
[0]kdb> [defcmd] endefcmd
이 명령어의 결과는:
[0]kdb> hari
[hari]kdb> md 0xc000000 1
0xc000000 00000001 f000e816 f000e2c3 f000e816
[hari]kdb> rd
eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
....
...
[hari]kdb> md %ebp 1
0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
[0]kdb>
Tip #5
bph와 bpha 명령어는 일기 및 쓰기 중단점을 적용하기 위해 사용될 수 있다. 데이터가 특정 어드레스에서 읽히거나 쓰여질 때마다 제어를 할 수 있다는 것을 의미한다. 데이터/메모리 오염 문제를 디버깅할 때 매우 편리하다.
예시
To enter the kernel debugger whenever four bytes are written into address 0xc0204060:
[0]kdb> bph 0xc0204060 dataw 4
To enter the kernel debugger when at least two bytes of data starting at 0xc000000 are read:
[0]kdb> bph 0xc000000 datar 2
결론
KDB는 커널 디버깅에 필요한 간편하고 강력한 툴이다. 다양한 옵션을 제공하고 메모리 콘텐츠와 데이터 구조 분석도 가능하다. 무엇보다도 디버깅을 수행할 추가 머신이 필요없다.
출처 : http://blog.naver.com/awol11/80008744930
|