Paul Richards <paul@FreeBSD.org>, Jörg Wunsch <joerg@FreeBSD.org> 저
박일균 <ikpark@dcn.ssu.ac.kr> 번역
본 장에서는 커널 디버깅을 위해 충돌 덤프를 이용하는 몇가지 방법을 설명합니다. 이를 위하여 여러분의 시스템에는 충돌 덤프를 위한 충분한 스왑 공간을 필요로 합니다. 만약 여러 개의 스왑 파티션 중에 첫번째 파티션의 공간이 덤프를 저장하기에 충분치 않을 때에는 커널 설정을 변경하거나(config kernel 라인 수정), dumpon(8) 명령을 사용하여 다른 스왑 파티션에 덤프를 저장하도록 지정해줄 수 있습니다. dumpon(8) 명령을 사용할 경우에는 /etc/rc.conf 파일 내에 dumpdev 변수를 지정해주는 것이 제일 좋은 방법입니다. 보통 /etc/fstab에 명시된 스왑 장치들 중 하나를 지정해주면 됩니다. 현재 테이프와 같은 비 스왑 장치에 대한 덤프는 지원하지 않습니다. config -g 명령을 이용하여 커널을 설정하기 바랍니다. FreeBSD 커널 설정에 관한 자세한 내용은 FreeBSD 커널 설정하기를 참고하시기 바랍니다.
어느 장치로 덤프할 것인지 커널에 지정해주기 위해서는 dumpon(8) 명령을 사용합니다(주: swapon(8) 명령으로 스왑 공간으로 사용할 파티션을 지정해준 다음에 사용해야 합니다). 보통 /etc/rc.conf 파일 및 /etc/rc 파일에 순서 대로 설정되어 있습니다. 다른 방법으로는 커널 설정 파일의 config 라인 내에 있는 dump 절에 덤프할 스왑 장치를 하드 코딩할 수도 있습니다. 이는 권장되지 않는 방법이며, 부팅 과정 중에 손상되는 커널로부터 충돌 덤프를 얻고 할 때만 사용해야 합니다.
Note: 이제부터 gdb 라는 용어는 "커널 디버깅 모드"에서 동작하는 gdb 디버거를 가리킵니다. gdb 명령을 -k 옵션과 같이 실행시키면 커널 디버깅 모드로 디버깅할 수 있습니다. 커널 디버깅 모드에서, gdb는 프롬프트를 (kgdb)로 바꿉니다.
Tip: FreeBSD 3이나 그 이전 버전에서는 크기가 큰 디버깅용 커널 대신 디버깅용 심볼을 제거한 복사본을 만들어서 설치해야 합니다.
# cp kernel kernel.debug # strip -g kernel반드시 필요한 작업이 아니지만 실행할 것을 추천합니다. (FreeBSD 4나 그 이후 릴리즈에서는 커널 make 과정의 마지막에 자동으로 실행됩니다.) 이러한 과정을 거쳐 커널의 디버깅용 심볼이 제거되고 나면, make install 명령을 실행하여 커널을 설치할 수 있습니다.
예전의 FreeBSD 릴리즈(3.1 이전 버전)는 기본적으로 a.out 커널을 사용하고 있으며, 물리적인 메모리에 심볼 테이블을 항상 상주시키고 있음을 주의하기 바랍니다. 디버깅 심볼이 제거되지 않은 디버깅용 커널은 보다 큰 심볼 테이블을 가지게 되므로 메모리가 더 낭비됩니다. 최근의 FreeBSD 릴리즈는 이런 문제를 일으키지 않는 ELF 커널을 사용하고 있습니다.
부트 프롬프트에서 새로운 커널의 이름을 입력하면 새로운 커널을 테스트 할 수 있으며, 테스트 도중 충돌이 발생하여 다른 커널로 시스템을 살릴 경우에는 부트 프롬프트에서 -s 플래그를 입력하여 단일 유저 모드로 들어갑니다. 그리고 다음 명령들을 실행합니다.
# fsck -p
# mount -a -t ufs # /var/crash의 저장을 위해 파일 시스템을 쓰기 가능하게 바꿉니다.
# savecore -N /kernel.panicked /var/crash
# exit # 멀티 유저 모드로 진입...이렇게 함으로써 savecore(8)는 다른 커널의 심볼의 이름을 추출하게 됩니다. 기본적으로는 현재 동작하는 커널이 심볼 이름 추출의 대상이 될 것이고, 충돌 내용이 덤프된 커널의 심볼과 다르기 때문에 아무 것도 할 수 없게 됩니다.
충돌 덤프 후에는 /sys/compile/WHATEVER로 가서 gdb -k 명령을 실행합니다. gdb에서 다음을 수행합니다.
symbol-file kernel.debug
exec-file /var/crash/kernel.0
core-file /var/crash/vmcore.0
이로써, 다른 일반 프로그램을 디버깅할 때와 같이 커널 소스를 이용하여 충돌
내용이 덤프된 커널을 디버깅할 수 있게 됩니다.
아래에 디버깅 과정을 설명하기 위한 kgdb 세션의 스크립트 로그가 있습니다. 가독성을 높이기 위해 길이가 긴 줄은 다음 줄로 내리고 각 줄마다 줄번호를 붙였지만, 아래의 로그는 실제 pcvt 콘솔 드라이버 개발 과정 중의 오류 추적 중에 얻어진 것입니다.
1:Script started on Fri Dec 30 23:15:22 1994
2:# cd /sys/compile/URIAH
3:# gdb -k kernel /var/crash/vmcore.1
4:Reading symbol data from /usr/src/sys/compile/URIAH/kernel
...done.
5:IdlePTD 1f3000
6:panic: because you said to!
7:current pcb at 1e3f70
8:Reading in symbols for ../../i386/i386/machdep.c...done.
9:(kgdb) where
10:#0 boot (arghowto=256) (../../i386/i386/machdep.c line 767)
11:#1 0xf0115159 in panic ()
12:#2 0xf01955bd in diediedie () (../../i386/i386/machdep.c line 698)
13:#3 0xf010185e in db_fncall ()
14:#4 0xf0101586 in db_command (-266509132, -266509516, -267381073)
15:#5 0xf0101711 in db_command_loop ()
16:#6 0xf01040a0 in db_trap ()
17:#7 0xf0192976 in kdb_trap (12, 0, -272630436, -266743723)
18:#8 0xf019d2eb in trap_fatal (...)
19:#9 0xf019ce60 in trap_pfault (...)
20:#10 0xf019cb2f in trap (...)
21:#11 0xf01932a1 in exception:calltrap ()
22:#12 0xf0191503 in cnopen (...)
23:#13 0xf0132c34 in spec_open ()
24:#14 0xf012d014 in vn_open ()
25:#15 0xf012a183 in open ()
26:#16 0xf019d4eb in syscall (...)
27:(kgdb) up 10
28:Reading in symbols for ../../i386/i386/trap.c...done.
29:#10 0xf019cb2f in trap (frame={tf_es = -260440048, tf_ds = 16, tf_\
30:edi = 3072, tf_esi = -266445372, tf_ebp = -272630356, tf_isp = -27\
31:2630396, tf_ebx = -266427884, tf_edx = 12, tf_ecx = -266427884, tf\
32:_eax = 64772224, tf_trapno = 12, tf_err = -272695296, tf_eip = -26\
33:6672343, tf_cs = -266469368, tf_eflags = 66066, tf_esp = 3072, tf_\
34:ss = -266427884}) (../../i386/i386/trap.c line 283)
35:283 (void) trap_pfault(&frame, FALSE);
36:(kgdb) frame frame->tf_ebp frame->tf_eip
37:Reading in symbols for ../../i386/isa/pcvt/pcvt_drv.c...done.
38:#0 0xf01ae729 in pcopen (dev=3072, flag=3, mode=8192, p=(struct p\
39:roc *) 0xf07c0c00) (../../i386/isa/pcvt/pcvt_drv.c line 403)
40:403 return ((*linesw[tp->t_line].l_open)(dev, tp));
41:(kgdb) list
42:398
43:399 tp->t_state |= TS_CARR_ON;
44:400 tp->t_cflag |= CLOCAL; /* cannot be a modem (:-) */
45:401
46:402 #if PCVT_NETBSD || (PCVT_FREEBSD >= 200)
47:403 return ((*linesw[tp->t_line].l_open)(dev, tp));
48:404 #else
49:405 return ((*linesw[tp->t_line].l_open)(dev, tp, flag));
50:406 #endif /* PCVT_NETBSD || (PCVT_FREEBSD >= 200) */
51:407 }
52:(kgdb) print tp
53:Reading in symbols for ../../i386/i386/cons.c...done.
54:$1 = (struct tty *) 0x1bae
55:(kgdb) print tp->t_line
56:$2 = 1767990816
57:(kgdb) up
58:#1 0xf0191503 in cnopen (dev=0x00000000, flag=3, mode=8192, p=(st\
59:ruct proc *) 0xf07c0c00) (../../i386/i386/cons.c line 126)
60: return ((*cdevsw[major(dev)].d_open)(dev, flag, mode, p));
61:(kgdb) up
62:#2 0xf0132c34 in spec_open ()
63:(kgdb) up
64:#3 0xf012d014 in vn_open ()
65:(kgdb) up
66:#4 0xf012a183 in open ()
67:(kgdb) up
68:#5 0xf019d4eb in syscall (frame={tf_es = 39, tf_ds = 39, tf_edi =\
69: 2158592, tf_esi = 0, tf_ebp = -272638436, tf_isp = -272629788, tf\
70:_ebx = 7086, tf_edx = 1, tf_ecx = 0, tf_eax = 5, tf_trapno = 582, \
71:tf_err = 582, tf_eip = 75749, tf_cs = 31, tf_eflags = 582, tf_esp \
72:= -272638456, tf_ss = 39}) (../../i386/i386/trap.c line 673)
73:673 error = (*callp->sy_call)(p, args, rval);
74:(kgdb) up
75:Initial frame selected; you cannot go up.
76:(kgdb) quit
77:# exit
78:exit
79:
80:Script done on Fri Dec 30 23:18:04 1994위 스크립트에 대한 설명입니다.
위 스크립트는 DDB(다음 섹션 참고) 내에서 얻은 덤프입니다. 패닉 메시지 "because you said to!"와 약간 긴 스택 추적문을 볼 수 있습니다. 커널이 페이지 오류 트랩에 걸려서 DDB로 빠졌습니다.
여기는 스택 추적문 중에서 trap() 함수의 위치를 나타냅니다.
새로운 스택 프레임을 강제로 사용하게 합니다. 이는 현재 더 이상 필요하지 않습니다. 트랩에 잡힌 경우에도 스택 프레임은 올바른 위치를 가리키고 있다고 가정합니다. (저는 새 커널에 대하여 <g>를 이용한 유용한 덤프를 가지고 있지 않습니다. 제 커널은 꽤 오랜 시간 패닉에 빠진 적이 없었습니다.) 소스 코드의 403 라인에서 볼 수 있는 바와 같이, "tp"를 가리키는 포인터가 엉켰거나, 배열 억세스에서 경계를 넘어섰을 가능성이 높습니다.
포인터가 의심스럽지만, 올바른 위치를 가리키고 있습니다.
그러나, 분명히 포인터는 쓸모 없는 영역을 가리키고 있고, 따라서 이 부분에서 에러를 찾아낼 수 있었습니다. (이런 특수한 코드에 대해 익숙치 못한 분들을 위해 설명하면, tp->t_line은 콘솔 장치의 라인 규칙을 나타내는 것이며, 작은 정수 값을 가져야 합니다.)