Notes on using the debugger gdb

IC 220

Why gdb or gdbgui?

The program gdb (“GNU debugger”) is a very important and useful utility for examining and stepping through compiled programs. But it’s a command-line utility that can feel a little user-unfriendly first. This guide is intended to help you get started.

Quick start

You will use gdb to run your program. You can do things like set breakpoints, step one instruction at a time, look at registers, and look at the stack.

To start gdb, you first compile your program and then run the command gdb <myprogram>, like so:

$ gcc -g myprogram.s -omyprogram
$ gdb myprogram
...
(gdb)

Now your program is ready to go and you have a (gdb) prompt where it is waiting for you to tell it what to do. For the especially impatient, here are some of the most useful commands:

A little more details on commands

Getting help

Most commands in gdb have a long form like help or a short form, which is usually just the first letter. The list above lists the short form, which you can use if your fingers get tired of typing out the whole words.

To read the documentation within gdb, just type

(gdb) help

For help on a specific command (like a man page), you would for example do

(gdb) help run

which would display more details on the run command.

Work flow

It’s useful to have two windows open when using gdb: your text editor with the .s source file, and your terminal window that’s compiling and running gdb.

Remember, every time you edit your source code, you have to re-create the executable before gdb will see any changes. So a good work-flow is something like this:

  1. (In the editor window) Open myprog.s file in text editor
  2. (In the editor window) Make some changes to myprog.s
  3. (In the editor window) Save myprog.s in the text editor
  4. (In the terminal window) Run something like gcc -g myprog.s -omyprog to create the executable
  5. (In the terminal window) Run gdb myprog to start the debugger
  6. (Working in both windows) Use gdb to examine your program while looking through the source code. Figure out what you need to change.
  7. (In the terminal window) Quit gdb and go back to the shell.
  8. Go back to step 2 and repeat until your program works perfectly!

Breakpoints

A breakpoint in your program is a line of the assembly code where you want gdb to stop once it reaches that point. Usually you would put a breakpoint at main, or at the beginning of some function you are trying to debug.

To set a breakpoint for a function called myfun (which must be a label in your assembly program), type

(gdb) break myfun

You can also set a breakpoint for a specific line of code even if it doesn’t have a label. Like to set a breakpoint at line 20, you would do

(gdb) break 20

To remove a breakpoint, you clear it:

(gdb) clear myfun

Running your program

To start running your program, or to restart from the beginning, do

(gdb) run

It will keep going until you interrupt with Ctrl-C, or until it reaches a breakpoint. Two key notes on this:

Once the program is paused at a breakpoint, you can step through one instruction at a time. There are two commands for this. The first one:

(gdb) step

Goes to the next line of code, no matter where that is. That’s good as long as the next line of code is your code. But if the next line is a subroutine call to some library function like puts, you would rather do

(gdb) next

which basically keeps running until the control reaches the next line, which normally means until after the subroutine returns.

In summary, to proceed with gdb in small pieces you should use:

  1. If the next line of code is not a function call (bl): use step or next, doesn’t matter which

  2. If the next line of code is a function call (bl), then

    • Use step if you want to step into the function, or

    • Use next if you want to skip over the function call (by executing it and stopping when it is finished). You would do this if the branch is to someone else’s function that you don’t care about examining, or if the branch is to your own function, but you think it works and just want to execute it and skip to the final result

If you are tired of stepping one line at a time and want to just keep running until the next breakpoint, you do

(gdb) continue

You can also halt the program at any time with

(gdb) kill

Looking at registers and stack

One of the most useful things in gdb is to examine what is the current state of your registers. To see all of them, do

(gdb) info registers

Or, to see just one register like x19, do

(gdb) info registers x19

(Remember you can use the shorthand for this also, like i r x19.)

You can use the examine command (shorthand x) to look at values in memory. For us, it will be convenient to use the g modifier for “giant” 8-byte values, since most of the values we deal with will be 8 bytes.

To look at the 8-byte number stored at [sp, 0], you do

(gdb) x/dg $sp

The d is for decimal number format, i.e. normal base-10 numbers. To see it in hex instead, you would do

(gdb) x/xg $sp

You can also do offsets here. To see the 8-byte value at [sp, 24] in hex format, you would do

(gdb) x/xg $sp+24

Looking at the code within gdb

When stepping through your program, you might forget which line you’re on. To see what line will be executed next, do

(gdb) frame

If you want to see a broader context of the source code around that line, you can then do

(gdb) list

To see 10 lines around the current one. You can run list more times to see more lines of code, or you can give an argument like

(gdb) list myfun

to see a program listing around that label (or function name) that you specified.

Debugging example

Here is an example program that is trying to flip a coin five times. The C code we are trying to emulate is something like this:

#include <stdio.h>
#include <stdlib.h>

void coin_flip();

int main() {
    srand(time(0));
    for (int i=0; i < 5; ++i) {
        coin_flip();
    }
    return 0;
}

void coin_flip() {
    int c = rand() % 2;
    if (c != 0)
        puts("heads");
    else
        puts("tails");
}

Here is an attempt at writing an assembly program (download: coin.s) to do this coin flipping. The loop control is a little different, but we’re accomplishing the same thing. Focus on the coin_flip function at the bottom.

        .arch armv8-a





        .text
strHeads:
        .string "heads"
strTails:
        .string "tails"

        .align 2
        .global main
main:
        // prepare stack
        sub     sp, sp, 16
        stur    lr, [sp, 0]

        // seed random number generator based on current time
        movz    x0, 0
        bl      time
        bl      srand

        // call coin_flip() five times
        movz    x19, 5
loop0_top:
        bl      coin_flip
        sub     x19, x19, 1
        cbnz    x19, loop0_top

        // restore stack and return
        ldur    lr, [sp, 0]
        add     sp, sp, 16
        br      lr
        .size   main, .-main       // gives extra info about where main() ends to gdb
// function to call rand() and flip a coin
coin_flip:
        // prepare stack
        sub     sp, sp, 16
        stur    lr, [sp, 0]
        // (no real reason to use saved reg, just want to make debugging interesting)
        stur    x19, [sp, 8]

        // get random number
        bl      rand

        // random number mod 2 by AND'ing with 0x1
        and     x19, x0, 1

        // if/else to print heads or tails
        cbz     x19, coin_tails
        adr     x0, strHeads
        b       coin_end
coin_tails:
        adr     x0, strTails
coin_end:

        // actually print out heads or tails here
        bl      puts

        // restore stack and return
        ldur    x19, [sp, 8]
        add     sp, sp, 16
        br      lr

There is something horribly wrong about this program. Do you see it? Probably not! So let’s compile and run it:

$ gcc -g coin.s -o coin
$ ./coin
heads
Segmentation fault

Oh no! The dreaded seg fault! Where did it come from and what can we do about it?

Of course, I’m going to tell you to start the debugger! Here’s how you would start gdb and just try running the program:

$ gdb coin
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
... bunch of stuff ...
Reading symbols from coin...done.
(gdb) run
Starting program: /home/pi/220ex/coin
tails

Program received signal SIGSEGV, Segmentation fault.
coin_end () at coin.s:65
65              ldur    x19, [sp, 8]
(gdb)

Already this is great information, just using the simple run command. The debugger is telling us exactly which line caused the seg fault - it’s the last ldur at the end of the coin_flip function.

Already you might be able to see what’s happening, but pretend for the moment that you do not see it. Below is a gdb session that shows us:

(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) break coin_flip
Breakpoint 1 at 0xaaaaaaaaa830: file coin.s, line 42.
(gdb) run
Starting program: /home/pi/220ex/coin 

Breakpoint 1, coin_flip () at coin.s:42
42              sub     sp, sp, 16
(gdb) step
43              stur    lr, [sp, 0]
(gdb) step
45              stur    x19, [sp, 8]
(gdb) step
48              bl      rand
(gdb) next
51              and     x19, x0, 1
(gdb) info r x0
x0             0x2bd01422       735056930
(gdb) step
54              cbz     x19, coin_tails
(gdb) info r x19
x19            0x0      0
(gdb) step
coin_tails () at coin.s:58
58              adr     x0, strTails
(gdb) step
coin_end () at coin.s:62
62              bl      puts
(gdb) next
tails
65              ldur    x19, [sp, 8]
(gdb) step
66              add     sp, sp, 16
(gdb) step
coin_end () at coin.s:67
67              br      lr
(gdb) step
65              ldur    x19, [sp, 8]
(gdb) info registers lr
 [NOTE to IC220 students -- output of this line not shown.  You need to try this to find out!]

Notice a couple things here. We are stepping through usually using step. At each moment, gdb tells us the next line of code it is ready to execute (not a line that has already executed). When the next line indicates a call to a standard library function like rand or puts, we use next to do the function call and come back on the next line after it.

Now check out the last few steps. We finish up the coin_flip function and get to the last line br lr. This should return back to some point in main. But it doesn’t!! The next command is just back up two lines prior (to line 65), back at the end of coin_flip itself.

Do you see what went wrong yet? The problem in the original program is a common error but a deadly one: we forgot to load the link register lr back from the stack at the end of the function. That caused the br lr to return back to the same place of the most recent function call, which is the line after the bl puts.

This is an example of debugging in action. It lets you step through your program slowly, and find the first time where it does something you didn’t expect. That’s your chance to use your own knowledge and figure out what happened so that you can correct it.

Happy debugging and good luck!

Postscript – one more very useful command

The where command is not always informative, but when it is, it can be very useful.

Imagine I make a different error (I won’t say what error quite yet), and it segfaults (crashes) my program. Let’s run gdb to learn more:

(gdb) r
Starting program: /home/pi/ic220/coin.s

Program received signal SIGSEGV, Segmentation fault.
strlen () at ../sysdeps/aarch64/strlen.S:94
94      ../sysdeps/aarch64/strlen.S: No such file or directory.
(gdb)

This is saying that strlen.S is seg faulting at line 94. But you might say, “What? I didn’t write that code, and why is it segfaulting?” This situation is perfect for the where command. Let’s continue with gdb and see what where does:

(gdb) where
#0  strlen () at ../sysdeps/aarch64/strlen.S:94
#1  0x0000ffffbf5c4f70 in _IO_puts (str=0x6 <error: Cannot access memory at address 0x6>) at ioputs.c:35
#2  0x0000aaaaaaaaa858 in coin_end () at coin.s:62
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)

The where commands does a “backtrace” that attempts to look at the stack and see what sequence of function calls are currently in progress (answering, "How did we get here?). Unfortunately, it won’t completely work for us because (to keep things simple) we aren’t writing code that uses the register fp (frame pointer), so gdb’s backtrace will get confused when it reaches into our code. It will however, still give some useful information. The output above is telling us:

In reality, “coin_end” is just a label, not a function. But we can still use the information that the line of code that we actually wrote that seems to be involved is line 62. Examining the code reveals that is this line:

        bl      puts

So, we called puts(), and that caused a segfault. We have to think – what’s wrong with calling puts()? We didn’t write puts(), so it must be that we are giving an invalid argument toputs(). In fact, in this hypothetical example, just before calling puts() I replaced this (correct) line:

    adr     x0, strHeads    // correct

with this error:

    mov     x0, strHeads    // WRONG -- don't do this!

The assembler still accepts this, but it’s wrong – we must use adr to get the address of strHeads into x0. [Take note – this is a common mistake for students to make!]

Note that the where command did not tell us that the error was this line of code, but it did point us to the call to puts() being problematic, which is very useful to know.