We have already seen the basics of debugging assembly code with GDB. We covered assembling our code with debug symbols, setting break points, stepping through the code as it executes and inspecting the contents of registers. Now it is time to learn some more advanced techniques!
Command Line Arguments
Sometimes when we run an executable we pass in command line arguments. We can also do this when debugging with GDB. There are two ways to do this. We can put the command line arguments after the run command r
. So if echo_input
was the name of a binary, and we wanted to debug it with the command line arguments “Hello World” we would, load it into GDB as normal, and then start execution with
r Hello World
Alternatively we can load the executable with our command line arguments directly by using the --args
option. So, in our example we would execute:
gdb --args ./echo_input Hello World
This is very useful when debugging the code in our previous posts covering command line arguments!
Inspecting Memory
We know how to read the data stored in registers, but when we’re debugging we often want to read values stored in memory. Suppose, for example, we have a register that we are using as a pointer. We can use the info registers
command to see what memory address is stored in the register. To see what data is stored in that memory address we can use the x
command.
The x
command prints out the value at a given memory address. We provide a suffix to specify how much memory to read and how to display it.
Let’s have a look at an example. Suppose we have defined a byte in our data section with value 12, and that we have moved the address of this byte into the register rax
. In GDB
we use the command
info registers rax
to read this value from rax
. Let’s say that the output of this command is
rdi 0x40200b 4202507
So the address of the our data is 4202507
, or 0x40200b
in hex. We can read the value stored at this memory address with the command
x/bd 0x40200b
The output of this will be 0x40200b: 12
, that is, the address followed by the value 12, as we would expect.
The suffix bd
tells gdb
to read a byte (b) of memory and display the result as a decimal (d). We can display value as hexadecimal with x, octal with o, binary with t and unsigned decimal with u. We can specify the size to read as a byte with b, 2 bytes with h, 4 bytes with w and 8 bytes with g.
Let’s say we have defined a short in our data section named numShort that has value 256. This will take up more than one byte, it will appear in memory as the byte 00000000 followed by 000000001. So the command x/bt
will read the first byte, 00000000, and the command x/ht
will read two bytes giving 0000000100000000.
We can also output more than one value at once, by adding a multiplier to the suffix. So if we apply the command x/2bt to the memory address referenced by the name numShort
our output will be:
0x40200c: 00000000 00000001
We can also read and output character values. Suppose we have declared a string in our data section with the value “outputFile”. If we inspect the address of this memory with x/bx
, we will get 0x6f
. If you look this value up in an ascii table, you will see this is the hex value of the character ‘o’. This is, or course, the first character of our string.
To output these values as characters directly we use the c
suffix like so:
x/bc 4202496
the output will be: 111 'o'
. That is the decimal ascii value of the character ‘o’ and the character ‘o’. We can even read multiple values at once. For example the command
x/5bc 4202496
will output:
111 'o' 117 'u' 116 't' 112 'p' 117 'u'
that is, we have read five characters starting at the memory address 4202496
. Of course reading individual characters like this would be a little tedious, and we can just read entire strings out of memory. The command:
x/s 4202496
will output the entire string: "outputFile"
.
The x
command doesn’t work just on the data section, we can use it to inspect any memory address! For example we can check the values in our buffers defined in the .bss
section with x
.
Our instructions are also stored in memory, and we can read those with x
as well. The register rip is the instruction pointer and contains the address of the next instruction that will be executed. So, we can get that address in GDB with the command:
info registers rip
If we use x/i
on the returned address we will see something like:
=> 0x401000 <_start>: mov $0x32,%rax
Once we specify a particular size and format, when we execute x without a suffix, it will use the same size and format. The default is to read 4 bytes and display in hexadecimal.
In our example we read the memory addresses from registers. However, we can also use the names of memory addresses directly with the &
operator. For example:
x/s &filename
will print the content of the memory address named filename. This works for buffers defined in the .bss
section and for data declared in the .data
section. We can also use arithmetic expressions when specifying the memory address to inspect. For example to read the byte that is located 3 bytes pas the memory address 0x40200b
we would use the command
x/bx 0x40200b + 3
The units we are counting in are the same as the units we are reading, so if we wanted to read the fifth 2 byte value after the memory address 0x40200b
we would use:
x/hx 0x40200b + 5
Conditional BreakPoints and Watches
To help with the monotony of debugging we use conditional breakpoints. These are breakpoints that come with a logical condition on a register. The breakpoint will only stop execution when the condition is satisfied. Suppose our source code is in a file named, echo_input.s, and we would like to break at line 12 whenever the register rbx
has value 4. We can do that with the following command:
b echo_input.s:12 if $rbx == 4
Note, we put a dollar sign $ before the name of the register. This is a little confusing, as we usually use dollar signs for constant values. We can use any of the typical binary comparison operators, ==, !=, <, >, ,<= and >= when specifying the condition. One thing that can catch you out here, is that the break point condition is evaluated before the instruction on that line executes.
We can also set watches, which are very similar to conditional breakpoints but more general. With a watch, we specify a condition on a register and code execution will halt whenever that condition is satisfied. We do not have to specify a particular line of code. To set a watch that will halt whenever the rcx
is greater than 5, we use the command:
watch $rcx > 5
note, we don’t specify the file name, and we still use the dollar sign.
I have covered some pretty technical stuff in this post, I’d recommend you experiment with it all yourself to get a feel for these techniques!