Examining Variables and Memory

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;
}

Local Variables

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.

Synonyms

Registers

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

Synonyms

Examining Memory

One 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:

The size letters are:

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