GNU Debugger Tutorial

gdb helps you trace and understand how your code really runs. In projects like xv6, relying on printf() to debug can be a mess. With gdb, you can debug faster and understand how xv6 works. This tutorial focuses on the basics, but if you’d like to dive deeper, check out the official gdb manual.
How to use gdb with xv6
Run make qemu-gdb:
$ make qemu-gdb
*** Now run 'gdb' in another window.It will compile xv6 and launch the hypervisor, QEMU, in debug mode.
Now, run gdb-multiarch in another terminal:
$ gdb-multiarchwarning: File "/workspaces/{YourDirName}/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path /workspaces/xxx/.gdbinit
line to your configuration file "/home/vscode/.config/gdb/gdbinit".To avoid typing too many commands when you open gdb, if you see the message above, run the following command outside gdb (the same environment you $ make qemu), and remember to replace {YourDirName} with your own directory name:
$ echo "add-auto-load-safe-path /workspaces/{YourDirName}/.gdbinit " >> ~/.gdbinitOn Github Codespace, this would be:
$ echo "add-auto-load-safe-path /workspaces/xv6-lab-syscall-tracing/.gdbinit " >> ~/.gdbinitIf you add your safe path correctly, when you open gdb next time you should see the following without typing any command:
0x0000000000001000 in ?? ()
(gdb)Now we add symbol file:
(gdb) file kernel/kernel
Reading symbols from kernel/kernel...If you need to inspect kernel code and user code at the same time, run the following also:
(gdb) add-symbol-file user/_sh
add symbol table from file "user/_sh"
Reading symbols from user/_sh...Basic (but extremely useful) Commands
Although we provide examples, it is still better that you try these commands on your own to understand and feel how they work. Let’s set a breakpoint after you added symbol files: Now gdb will pause at the breakpoint 1 after we decide to continue our program: You can also set a breakpoint on a given line of code in a program: With With With With Use You can even assign a condition to commands: It is very common that you inspect a function but you don’t know who calls it, so we need With More intuitive ways to jump between stack frames are Run Run For Press If you forget to set a breakpoint or your program continues executing but you want to pause now, Type Breakpoints & Continue
(gdb) b syscall
Breakpoint 1 at 0x800027f0: file kernel/syscall.c, line 133.(gdb) c
Continuing.
[Switching to Thread 1.2]
Thread 2 hit Breakpoint 1, syscall () at kernel/syscall.c:133
133 {(gdb) b trap.c:189
Breakpoint 4 at 0x800024ce: file kernel/trap.c, line 189.d, we can delete all breakpoints. You can also specify which breakpoint you want to delete:d 1Inspecting values
Info
i r we can view register contents at the current step of code. With i b we can view the status of breakpoints. In fact, there are a lot of information you can inspect by command i. Type i to see lists for detailed explanation.(gdb) i r
ra 0x800025ac 0x800025ac <usertrap+118>
sp 0x3fffffdfe0 0x3fffffdfe0
gp 0x505050505050505 0x505050505050505
tp 0x1 0x1 <getcmd+1>
t0 0x80002536 2147493174
t1 0x8000000000087fff -9223372036854218753
t2 0x505050505050505 361700864190383365
fp 0x3fffffe000 0x3fffffe000
s1 0x80012770 2147559280(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000800027f0 in syscall at kernel/syscall.c:133
breakpoint already hit 1 timePrint
p, we can print the value of a variable at the current step of code:Thread 2 hit Breakpoint 4, devintr () at kernel/trap.c:189
189 if(scause == 0x8000000000000009L){
(gdb) p scause
$1 = 9223372036854775813p/x, we can print the value of a variable in Hexadecimal:210 } else if(scause == 0x8000000000000005L){
(gdb) p/x scause
$4 = 0x8000000000000005Watch
watch to make GDB pause whenever a given variable changes:(gdb) watch ticks
Hardware watchpoint 6: ticks
(gdb) c
Continuing.
Thread 1 hit Hardware watchpoint 6: ticks
Old value = 2326
New value = 2327
clockintr () at kernel/trap.c:169
169 wakeup(&ticks);(gdb) watch ticks if ticks==2400
Hardware watchpoint 7: ticks
(gdb) c
Continuing.
Thread 1 hit Hardware watchpoint 7: ticks
Old value = 2399
New value = 2400
clockintr () at kernel/trap.c:169
169 wakeup(&ticks);Backtrace
bt:Thread 1 hit Breakpoint 8, clockintr () at kernel/trap.c:165
165 {
(gdb) bt
#0 clockintr () at kernel/trap.c:165
#1 0x0000000080002538 in devintr () at kernel/trap.c:212
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
#3 0x000000008000520c in kernelvec ()
#4 0x0000000080001d66 in scheduler () at kernel/proc.c:473
#5 0x0000000080000e9e in main () at kernel/main.c:44Frame
frame, we can jump between stack frames:Thread 3 hit Breakpoint 8, clockintr () at kernel/trap.c:165
165 {
(gdb) bt
#0 clockintr () at kernel/trap.c:165
#1 0x0000000080002538 in devintr () at kernel/trap.c:212
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
#3 0x000000008000520c in kernelvec ()
#4 0x0000000080001d66 in scheduler () at kernel/proc.c:473
#5 0x0000000080000e9e in main () at kernel/main.c:44
(gdb) frame 2
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
147 if((which_dev = devintr()) == 0){up and down:Thread 2 hit Breakpoint 8, clockintr () at kernel/trap.c:165
165 {
(gdb) bt
#0 clockintr () at kernel/trap.c:165
#1 0x0000000080002538 in devintr () at kernel/trap.c:212
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
#3 0x000000008000520c in kernelvec ()
#4 0x0000000080001d66 in scheduler () at kernel/proc.c:473
#5 0x0000000080000e9e in main () at kernel/main.c:44
(gdb) up
#1 0x0000000080002538 in devintr () at kernel/trap.c:212
212 clockintr();
(gdb) up
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
147 if((which_dev = devintr()) == 0){
(gdb) up
#3 0x000000008000520c in kernelvec ()
=> 0x000000008000520c <kernelvec+44>: 6082 ld ra,0(sp)
(gdb) down
#2 0x0000000080002640 in kerneltrap () at kernel/trap.c:147
147 if((which_dev = devintr()) == 0){Stepping
Next
n or next to execute commands step by step. When we encounter a function, n will execute that function without stepping into it:Thread 1 hit Breakpoint 1, getcmd (buf=buf@entry=0x2020 <buf> "", nbuf=nbuf@entry=100) at user/sh.c:136
136 {
(gdb) n
137 write(2, "$ ", 2);
(gdb) n
138 memset(buf, 0, nbuf);
(gdb) n
139 gets(buf, nbuf);Step
s or step to execute commands step by step. When we encounter a function, s will stepping into it:Thread 2 hit Breakpoint 1, getcmd (buf=buf@entry=0x2020 <buf> "adgad\n", nbuf=nbuf@entry=100) at user/sh.c:136
136 {
(gdb) n
137 write(2, "$ ", 2);
(gdb) n
138 memset(buf, 0, nbuf);
(gdb) s
memset (dst=0x2020 <buf>, c=0, n=100) at user/ulib.c:51
51 for(i = 0; i < n; i++){Other useful stuff
s,n,b and c, you can execute them multiple times with given number:Thread 3 hit Breakpoint 1, getcmd (buf=buf@entry=0x2020 <buf> "sdgd\n", nbuf=nbuf@entry=100) at user/sh.c:136
136 {
(gdb) n 3
139 gets(buf, nbuf);ENTER without typing anything will execute the previous command:Thread 3 hit Breakpoint 3, getcmd (buf=buf@entry=0x2020 <buf> "wrghg\n", nbuf=nbuf@entry=100) at user/sh.c:136
136 {
(gdb) n
137 write(2, "$ ", 2);
(gdb)
138 memset(buf, 0, nbuf);
(gdb)
139 gets(buf, nbuf);Control+C:(gdb) c
Continuing.
^C
Thread 3 received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:455
455 intr_on();
Conclusion

This is just the tip of the iceberg. There are still many powerful or useful functions of gdb we can’t cover in this introductory article. However, these essential commands should be enough for you to deal with all the xv6 labs. Make good use of gdb will save you a great amount of time. Good luck.