We are going to look at a different program now:
#include <stdio.h>
#include <stdlib.h>
void f(int a) {
int b;
char c[] = "hello, world!";
void* p = &b;
printf("in f()\n");
return;
}
int main(int argc, char** argv) {
f(1);
return 0;
}When you are in a function, you can refer to local and global
variables by name. To see all of the local variables in the format that
gdb thinks makes the most sense, you can run info locals.
For example:
(gdb) info locals
b = 0
c = "hello, world!"
p = 0x7ffe2d1aac7c
You can also view the function’s arguments:
(gdb) info args
a = 1
For any of these symbols, you can print their values:
(gdb) print a
$5 = 1
(gdb) print b
$6 = 0
(gdb) print c
$7 = "hello, world!"
(gdb) print p
$8 = (void *) 0x7ffe2d1aac7c
The print command can also take expressions:
(gdb) print b
$9 = 0
(gdb) print b+2
$10 = 2
(gdb) print c[3]
$11 = 108 'l'
(gdb) print &b
$12 = (int *) 0x7fff5ad997cc
(gdb) print 1 + 5
$13 = 6
You can modify local variables and function arguments with
set variable:
(gdb) set variable b = 3
(gdb) print b
$14 = 3
This can be very useful when you’re trying to diagnose an issue, and need to trigger a particular branch that is otherwise very difficult to reach.
print, pset variable, set varRunning programs make use of the CPU’s registers for information that
needs to be accessed particularly quickly. You can see all of the
registers’ current values with info registers:
(gdb) info registers
rax 0x7fff5ad997cc 140734717597644
rbx 0x7fff5ad99938 140734717598008
rcx 0x55d505bc7db8 94373412634040
rdx 0x7fff5ad99948 140734717598024
rsi 0x7fff5ad99938 140734717598008
rdi 0x1 1
rbp 0x7fff5ad997f0 0x7fff5ad997f0
rsp 0x7fff5ad997b0 0x7fff5ad997b0
r8 0x0 0
r9 0x7f4affbe3380 139960094962560
r10 0x7fff5ad99530 140734717596976
r11 0x203 515
r12 0x1 1
r13 0x0 0
r14 0x55d505bc7db8 94373412634040
r15 0x7f4affc16000 139960095170560
rip 0x55d505bc51ab 0x55d505bc51ab <f+66>
...
Note that for many of these, we see the register’s value in
hexadecimal and decimal. For addresses, both columns are in hexadecimal,
and if the address corresponds to a known symbol, that too is included.
For instance, rip (the instruction pointer) is displayed
with <f+66>, which means 66 bytes after the beginning
of the function f.
If we only want to see particular registers, we can specify these as additional arguments:
(gdb) info registers rax rbx
rax 0x7fff5ad997cc 140734717597644
rbx 0x7fff5ad99938 140734717598008
Registers can be used in expressions, such as when calling
print, by preceding the register name with
$:
(gdb) print $rip
$15 = (void (*)()) 0x55d505bc51ab <f+66>
We can also set register values:
(gdb) set $rip = f+67
(gdb) print $rip
$16 = (void (*)()) 0x55d505bc51ac <f+67>
(gdb) set $rip = f+66
(gdb) print $rip
$17 = (void (*)()) 0x55d505bc51ab <f+66>
Note that we don’t require a subcommand for set in this
case.
info registers, info regOne extremely powerful command is x, which allows you to
examine the contents of memory. It takes an address, which might be an
expression, and an optional format string. Let’s start with a simple
example:
(gdb) x &a
0x7fff154b62ac: 0x00000001
(gdb) x &b
0x7fff154b62bc: 0x00000000
(gdb) x c
0x7fff154b62ca: 0x6c6c6568
(gdb) x p
0x7fff154b62bc: 0x00000000
In the first column, we see the address of the first byte being
displayed on that line. What appears next is the contents of memory
beginning at that address. Here, our default is to display one
word (4 bytes) of memory as an integer in hexadecimal.
Regardless of the endianness of the platform, this will be displayed in
“normal reading” format, which is big-endian. For c, which
is a string, this might look odd on a little-endian machine (as was used
here). From the ASCII character table, 6c is
l, 65 is e, and 68
is h.
For expressions, we might do something like:
(gdb) x c+2
0x7fff154b62cc: 0x2c6f6c6c
Note that 0x7fff154b62cc is 0x7fff154b62ca
plus 2.
Format strings are where x really shines. To specify a
format, we follow the x with /FMT, where
FMT is our format string. The display above is equivalent
to:
(gdb) x/wx c
0x7fff154b62ca: 0x6c6c6568
(gdb) x/xw c
0x7fff154b62ca: 0x6c6c6568
How do we build a format string? There are three components, all of which are optional. If not specified, the prior value of the component is used. First, we have a count. This is the number of “things” we want to display, which will depend on the other components. Since we haven’t specified this, we’re getting the default of 1. After this are a format letter and a size letter, which can be provided in either order.
The format letters are:
t: binaryo: octald: decimalu: unsigned decimalx: hexadecimalz: 0-padded hexadecimalf: floating-pointa: addressi: instructionc: characters: stringThe size letters are:
b: bytesh: half-word (2 bytes)w: word (4 bytes)g: giant (8 bytes)Some things to note:
Let’s take a look at a few of these, beginning with a string:
(gdb) x/s c
0x7fff154b62ca: "hello, world!"
(gdb) x/14c c
0x7fff154b62ca: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 44 ',' 32 ' ' 119 'w'
0x7fff154b62d2: 111 'o' 114 'r' 108 'l' 100 'd' 33 '!' 0 '\000'
Here we see that the s format letter tells gdb to treat
memory it sees as a null-terminated string, while c treats
each ASCII character separately, displaying both the numeric value (in
decimal) and the character or an octal code, depending on whether it’s
printable or not. We can also use these format letters with non-string
data:
(gdb) x/s &b
0x7fff154b62bc: ""
(gdb) x/s &a
0x7fff154b62ac: "\001"
Obviously, in most cases this won’t be useful.
Next, let’s look at the instructions format letter:
(gdb) x/i $rip
=> 0x5638632241ab <f+66>: lea 0xe52(%rip),%rax # 0x563863225004
(gdb) x/i 0x5638632241ab
=> 0x5638632241ab <f+66>: lea 0xe52(%rip),%rax # 0x563863225004
(gdb) x/i f+66
=> 0x5638632241ab <f+66>: lea 0xe52(%rip),%rax # 0x563863225004
(gdb) disassemble f
Dump of assembler code for function f:
0x0000563863224169 <+0>: endbr64
0x000056386322416d <+4>: push %rbp
0x000056386322416e <+5>: mov %rsp,%rbp
0x0000563863224171 <+8>: sub $0x40,%rsp
0x0000563863224175 <+12>: mov %edi,-0x34(%rbp)
0x0000563863224178 <+15>: mov %fs:0x28,%rax
0x0000563863224181 <+24>: mov %rax,-0x8(%rbp)
0x0000563863224185 <+28>: xor %eax,%eax
0x0000563863224187 <+30>: movabs $0x77202c6f6c6c6568,%rax
0x0000563863224191 <+40>: mov %rax,-0x16(%rbp)
0x0000563863224195 <+44>: movabs $0x21646c726f7720,%rax
0x000056386322419f <+54>: mov %rax,-0x10(%rbp)
0x00005638632241a3 <+58>: lea -0x24(%rbp),%rax
0x00005638632241a7 <+62>: mov %rax,-0x20(%rbp)
=> 0x00005638632241ab <+66>: lea 0xe52(%rip),%rax # 0x563863225004
(gdb) x/15i f
0x563863224169 <f>: endbr64
0x56386322416d <f+4>: push %rbp
0x56386322416e <f+5>: mov %rsp,%rbp
0x563863224171 <f+8>: sub $0x40,%rsp
0x563863224175 <f+12>: mov %edi,-0x34(%rbp)
0x563863224178 <f+15>: mov %fs:0x28,%rax
0x563863224181 <f+24>: mov %rax,-0x8(%rbp)
0x563863224185 <f+28>: xor %eax,%eax
0x563863224187 <f+30>: movabs $0x77202c6f6c6c6568,%rax
0x563863224191 <f+40>: mov %rax,-0x16(%rbp)
0x563863224195 <f+44>: movabs $0x21646c726f7720,%rax
0x56386322419f <f+54>: mov %rax,-0x10(%rbp)
0x5638632241a3 <f+58>: lea -0x24(%rbp),%rax
0x5638632241a7 <f+62>: mov %rax,-0x20(%rbp)
=> 0x5638632241ab <f+66>: lea 0xe52(%rip),%rax # 0x563863225004
Here, we see that we can inspect the current instruction pointer, the
literal address value, and the symbol f with a 66-byte
offset. We also compare the output of disassembling f with
requesting 15 instructions starting at f. For the last two,
the formats are similar, but not identical, and we don’t have the
additional options that disassemble provides.
The a format letter takes 4 bytes (for a 32-bit
architecture) or 8 bytes (for a 64-bit architecture), and displays it as
a hexadecimal number with leading zeroes removed.
(gdb) x/a &p
0x7fff154b62c0: 0x7fff154b62bc
(gdb) p p
$24 = (void *) 0x7fff154b62bc
(gdb) p &p
$25 = (void **) 0x7fff154b62c0
If we display a region of memory that includes addresses within a known function, then that will be displayed:
(gdb) x/10a $rsp
0x7fff154b62a0: 0x7e00000006 0x100000000
0x7fff154b62b0: 0x0 0x0
0x7fff154b62c0: 0x7fff154b62bc 0x2c6f6c6c65680000
0x7fff154b62d0: 0x21646c726f7720 0x9158879107457c00
0x7fff154b62e0: 0x7fff154b6300 0x5638632241ee <main+29>
Here we see that there are two “addresses” shown per line (they might
not correspond to actual addresses, this is simply how we are formatting
them), which is 16 bytes. The address at 0x7fff154b62e8,
which is 0x5638632241ee, is the instruction at 29 bytes
from the start of main. From info frame, we
can verify that this is the location of the saved instruction pointer
(that it, the return address for the function f).
The remaining things we’ll look at are the x format
letter combined with the size letters. In all cases, we will examine 64
bytes from $rsp, the top of the stack:
(gdb) x/64xb $rsp
0x7fff154b62a0: 0x06 0x00 0x00 0x00 0x7e 0x00 0x00 0x00
0x7fff154b62a8: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00
0x7fff154b62b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fff154b62b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fff154b62c0: 0xbc 0x62 0x4b 0x15 0xff 0x7f 0x00 0x00
0x7fff154b62c8: 0x00 0x00 0x68 0x65 0x6c 0x6c 0x6f 0x2c
0x7fff154b62d0: 0x20 0x77 0x6f 0x72 0x6c 0x64 0x21 0x00
0x7fff154b62d8: 0x00 0x7c 0x45 0x07 0x91 0x87 0x58 0x91
(gdb) x/32xh $rsp
0x7fff154b62a0: 0x0006 0x0000 0x007e 0x0000 0x0000 0x0000 0x0001 0x0000
0x7fff154b62b0: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x7fff154b62c0: 0x62bc 0x154b 0x7fff 0x0000 0x0000 0x6568 0x6c6c 0x2c6f
0x7fff154b62d0: 0x7720 0x726f 0x646c 0x0021 0x7c00 0x0745 0x8791 0x9158
(gdb) x/16xw $rsp
0x7fff154b62a0: 0x00000006 0x0000007e 0x00000000 0x00000001
0x7fff154b62b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff154b62c0: 0x154b62bc 0x00007fff 0x65680000 0x2c6f6c6c
0x7fff154b62d0: 0x726f7720 0x0021646c 0x07457c00 0x91588791
(gdb) x/8xg $rsp
0x7fff154b62a0: 0x0000007e00000006 0x0000000100000000
0x7fff154b62b0: 0x0000000000000000 0x0000000000000000
0x7fff154b62c0: 0x00007fff154b62bc 0x2c6f6c6c65680000
0x7fff154b62d0: 0x0021646c726f7720 0x9158879107457c00
This is a little-endian system, which we can observe in more detail
by repeating the exercise with only the 8 bytes beginning at
0x7fff154b62d0:
(gdb) x/8xb 0x7fff154b62d0
0x7fff154b62d0: 0x20 0x77 0x6f 0x72 0x6c 0x64 0x21 0x00
(gdb) x/4xh 0x7fff154b62d0
0x7fff154b62d0: 0x7720 0x726f 0x646c 0x0021
(gdb) x/2xw 0x7fff154b62d0
0x7fff154b62d0: 0x726f7720 0x0021646c
(gdb) x/1xg 0x7fff154b62d0
0x7fff154b62d0: 0x0021646c726f7720
Note that there’s no guarantee that variables will align on 8-byte
boundaries, so you might not see the byte pattern you’re looking for
starting from any particular address. When in doubt, add fixed offsets
from 1 to 7 to your starting address (eg, $rsp+3).