GNU Debugger Tutorial

Nini the detective

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-multiarch
warning: 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 " >> ~/.gdbinit

On Github Codespace, this would be:

$ echo "add-auto-load-safe-path /workspaces/xv6-lab-syscall-tracing/.gdbinit " >> ~/.gdbinit

If 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.

Breakpoints & Continue

Let’s set a breakpoint after you added symbol files:

(gdb) b syscall
Breakpoint 1 at 0x800027f0: file kernel/syscall.c, line 133.

Now gdb will pause at the breakpoint 1 after we decide to continue our program:

(gdb) c
Continuing.
[Switching to Thread 1.2]

Thread 2 hit Breakpoint 1, syscall () at kernel/syscall.c:133
133     {

You can also set a breakpoint on a given line of code in a program:

(gdb) b trap.c:189
Breakpoint 4 at 0x800024ce: file kernel/trap.c, line 189.

With d, we can delete all breakpoints. You can also specify which breakpoint you want to delete:

d 1

Inspecting values

Info

With 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 time

Print

With 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 = 9223372036854775813

With p/x, we can print the value of a variable in Hexadecimal:

210       } else if(scause == 0x8000000000000005L){
(gdb) p/x scause
$4 = 0x8000000000000005

Watch

Use 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);

You can even assign a condition to commands:

(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

It is very common that you inspect a function but you don’t know who calls it, so we need 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:44

Frame

With 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){

More intuitive ways to jump between stack frames are 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

Run 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

Run 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

For 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);

Press 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);

If you forget to set a breakpoint or your program continues executing but you want to pause now, Type Control+C:

(gdb) c
Continuing.
^C
Thread 3 received signal SIGINT, Interrupt.
scheduler () at kernel/proc.c:455
455         intr_on();

Conclusion

Good luck

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.

Back to top