xv6 디버깅

xv6 GDB로 디버깅하기

xv6은 QEMU 아래서 돌아간다. 그래서 기존 유저레벨 프로그램들과는 다르게 QEMU머신에 원격으로 접속해서 디버깅을 진행한다. 교수님이 공부하시던 옛날에는 VM 이런 거 없이 베어메탈 머신에 OS 깔아서 랜선 꼽아 디버깅을 진행했다고 한다. 얼마나 좋아진 세상인가 ㅋㅋ

기본 설정

디버깅 옵션 설정해 xv6 컴파일

이 옵션 사용해 xv6 컴파일

$ make qemu-nox-gdb

(X로 화면 출력하고 싶으면 qemu-gdb로 해도 됩니다.)

make하고 나면 다음과 같이 출력됩니다.

$ make qemu-nox-gdb
sed "s/localhost:1234/localhost:26000/" < .gdbinit.tmpl > .gdbinit
*** Now run 'gdb'.
qemu-system-i386 -nographic -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 2 -m 512  -S -gdb tcp::26000

저 포트번호(26000)을 기억해 두세요. 사람마다 약간씩 다를 수 있어요.

이렇게 띄워 놓으면 xv6는 디버깅 준비가 다 되었습니다.

xv6 gdb로 연결

터미널 창 하나 더 띄워서 여기엔 gdb를 켜 주세요.

킬 때는 kernel파일로 켜 주시면 됩니다.

$ gdb kernel

그 다음 qemu의 xv6에 연결합니다.

(gdb) target remote localhost:26000

아까 기억해 둔 포트 번호를 사용하면 됩니다.

잘 연결되었다면

Remote debugging using localhost:26000
0x0000fff0 in ?? ()

와 같이 뜰 겁니다.

이제 gdb를 사용해 디버깅할 수 있어요.

xv6 안의 유저프로그램을 디버깅하고 싶으면

(gdb) add-symbol-file _cat

와 같이 심볼파일 넣어주면 됩니다.

Example 1: Tracing syscall

In this example, we will trace how xv6 handles system calls.

We will go through the system call fork, which will be used in the first process _init to launch 'sh'.

First breakpoint - init.c:24

24th line of init.c calls fork().

To set a breakpoint in init.c, you should add symbols from _init first by add-symbol-file _init.

Then set a breakpoint using:

(gdb) break init.c:24

After setting up this breakpoint, continue until your xv6 hits this breakpoint.

After hitting this breakpoint, step few times and see how xv6 makes a trap frame.
You can always check the values of registers by typing info registers.

For your information, you will be at the kernel mode after sending int 0x40, a syscall interrupt.

Second breakpoint - syscall.c:syscall()

syscall() function is called by trap() function.

Set a breakpoint using:

(gdb) break syscall

You can check the number of system call by print myproc()->tf->eax, which would be 1(SYS_fork).

Using this syscall number, it will find out a corresponding function to handle this syscall.

Third breakpoint - sysproc.c:sys_fork

sys_fork is the corresponding function for SYS_fork. It will call fork() from proc.c,
which really forks the process.

Set a breakpoint using:

(gdb) break sys_fork

After hitting this breakpoint, step forward to see how forking actually happens in the kernel.

Example 2: Tracing the first process, init

In this example, we will trace how xv6 runs its first process.

xv6 loads its first process in userinit(), which is called in main() at main.c. Then it is launched by the scheduler.

Let's see how scheduler is called and how it runs the process.

First breakpoint - mpmain()

The function mpmain will be called with your first cpu. It will launch scheduler and fetch jobs to do next.

Set a breakpoint at mpmain by

(gdb) break mpmain

(If you want a shorter one, you can also set it with b mpmain)

Then type continue in gdb. It will hit this breakpoint right after booting up.

In mpmain, you can step some steps to examine what is going on.

Second breakpoint - scheduler()

The function scheduler will be called by mpmain. This function iterates through available processes and find
the next runnable process.

Set a breakpoint at scheduler by

(gdb) break scheduler

If you continue your execution after hitting the first breakpoint, you will hit this breakpoint.
In this context, you may see some global variables, like ptable and see how it looks like.

(gdb) p ptable
$1 = {lock = {locked = 0, name = 0x80107620 "ptable", cpu = 0x0, pcs = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, proc = {{sz = 0, 
      pgdir = 0x0, kstack = 0x0, state = UNUSED, pid = 0, parent = 0x0, tf = 0x0, context = 0x0, chan = 0x0, killed = 0, ofile = {
        0x0 <repeats 16 times>}, cwd = 0x0, name = '\000' <repeats 15 times>} <repeats 64 times>}}

Since we have two CPUs, this breakpoint may be hit by the second CPU, before setting up ptable.
You can check it using info threads. In my case, it was hit before loading the first process.
You can also move to a certain thread with a command thread #.

Third breakpoint - proc.c:342

Set a breakpoint with this command:

break proc.c:342

It will set a breakpoint at the line number 342 in proc.c file.

Based on the vanila xv6 from xv6-public repo, line 342 of proc.c will be:

      c->proc = p; // line 342
      switchuvm(p);
      p->state = RUNNING;

Change the number 342 to the corresponding line number at your xv6.

This line will be run directly after finding a RUNNABLE process.
Continue until your gdb hits this breakpoint.

When this breakpoint is hit, you can find out which process is ready to be launched.

(gdb) p *p
$7 = {sz = 4096, pgdir = 0x8dffe000, kstack = 0x8dfff000 "", state = RUNNABLE, pid = 1, parent = 0x0, tf = 0x8dffffb4, 
  context = 0x8dffff9c, chan = 0x0, killed = 0, ofile = {0x0 <repeats 16 times>}, cwd = 0x80110a14 <icache+52>, 
  name = "initcode\000\000\000\000\000\000\000"}

You can find out the name of the process is "initcode", which xv6 loaded in userinit() at main().

Fourth breakpoint - main() at init.c

To debug an user-mode program running on xv6, we need to load symbols from the binary.

You can do it with a following command:

(gdb) add-symbol-file _init

If you are debugging mostly at the user level and don't want to be confused with the kernel symbols,
you can use symbol-file _init command that changes the current symbol file.

Then you will be able to set the breakpoint in your user program.

(gdb) break init.c:main

After some more continue, your xv6 will hit this fourth breakpoint.
(Recommend deleting the third breakpoint with delete 3)

you can find out variables from _init, such as:

(gdb) p argv
$10 = {0x846 "sh", 0x0 <main>}

which will be used at executing 'sh'.

Acknowledgments

이 가이드는 제가 작성했고, UWisc-Madison 운영체제 과목에서도 사용되었습니다. 예제의 경우 시간나면 번역할게요.