I wrote a couple of articles about assembly before, and in order to understand what I was doing, I thought it would be useful to take a look at the contents of memory and registers to confirm my understanding was correct. I looked around, and found that GDB can help with this.

I wrote an introductory article to GDB a few months ago that you can check to get the basics. This article is going to build on top of it.

Debug information

In my introduction to assembly, I used this command to assemble my program:

1
nasm -f elf64 -o example.o example.asm

The elf64 (Executable and linkable format) parameter specifies the format of the output file. This will generate a file with enough information so the Operating System can execute it, but it doesn’t contain any information to help debugging. If we want our executable to contain debug information (information about the file and line number a program is executing) we need to say so when we assemble the program.

To see what are the formats for debug information available in your version of nasm, you can use:

1
nasm -f elf64 -y

The output when I ran it, was:

1
2
3
valid debug formats for 'elf64' output format are ('*' denotes default):
    dwarf     ELF64 (x86-64) dwarf debug format for Linux/Unix
  * stabs     ELF64 (x86-64) stabs debug format for Linux/Unix

The dwarf format is supposed to be an improvement over stabs, so I’m going to use that format:

1
nasm -f elf64 -g -F dwarf -o example.o example.asm

Debugging with GDB

Let’s look at the basics over the same program I used for my introduction to assembly article:

1
2
3
4
5
6
section .text
  global _start
_start:
  mov rax, 60
  mov rdi, 0
  syscall

If we save this in a file named example.asm, we can generate the executable with these commands:

1
2
nasm -f elf64 -g -F dwarf -o example.o example.asm
ld -o example example.o

We can now start gdb with the program loaded:

1
2
$ gdb example
(gdb)

We can use the b command to set a breakpoint. For now, let’s set it at the _start symbol:

1
2
(gdb) b _start
Breakpoint 1 at 0x400080: file example.asm, line 4.

Because the executable contains debug information, gdb can tell us in which file and line number the breakpoint was set. We can now run the program and it will stop at our breakpoint:

1
2
3
4
5
(gdb) run
Starting program: /home/adrian/example

Breakpoint 1, _start () at example.asm:4
4	  mov rax, 60

The program stops at the first executable line of our program. We can step line by line using the s command:

1
2
3
4
5
6
(gdb) s
5	  mov rdi, 0
(gdb) s
6	  syscall
(gdb) s
[Inferior 1 (process 11727) exited normally]

Use q to quit gdb.

Inspecting registers

Writing assembly code, you will find yourself moving things in and out of registers very often. It is then natural that debugging a program we might want to see their contents. To see the contents of all registers we can use info registers or the abbreviation i r. Using the same example program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(gdb) run
Starting program: /home/adrian/example

Breakpoint 1, _start () at example.asm:4
4	  mov rax, 60
(gdb) i r
rax            0x0	0
rbx            0x0	0
rcx            0x0	0
rdx            0x0	0
rsi            0x0	0
rdi            0x0	0
rbp            0x0	0x0
rsp            0x7fffffffdd10	0x7fffffffdd10
r8             0x0	0
r9             0x0	0
r10            0x0	0
r11            0x0	0
r12            0x0	0
r13            0x0	0
r14            0x0	0
r15            0x0	0
rip            0x400080	0x400080 <_start>
eflags         0x202	[ IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

By printing the registers we can see that the breakpoint takes effect before executing the line: mov rax, 60. In many cases we probably only want to see a specific register. To do this we just need to add the register name to the command: i r <register>:

1
2
3
4
(gdb) s
5	  mov rdi, 0
(gdb) i r rax
rax            0x3c	60

The first column is the hexadecimal value (0x3c) and the second is decimal (60)

Inspecting memory

Let’s introduce some variables to our program:

1
2
3
4
5
6
7
8
9
10
11
section .data
  exit_code dq 0
  sys_call dq 60

section .text
  global _start

_start:
  mov rax, [sys_call]
  mov rdi, [exit_code]
  syscall

If we stop gdb at _start, we can inspect the variables in the program:

1
2
(gdb) print (int) sys_call
$1 = 60

Note that we need to cast the variable to the correct type or we’ll get an error:

1
2
(gdb) print sys_call
'sys_call' has unknown type; cast it to its declared type

Another thing we can do is get the memory address sys_call refers to:

1
2
(gdb) info address sys_call
Symbol "sys_call" is at 0x402008 in a file compiled without debugging

We can also see the data at a memory address using an asterisk (*):

1
2
(gdb) print (int) *0x402008
$4 = 60

Conclusion

This article shows how to use gdb to debug a simple assembly program. Most commands are similar to the ones used for debugging any other programming language, but we also go over how to access registers and memory addresses, which is more commonly needed when working at assembly level.

[ computer_science  programming  assembly  debugging  ]
Error Handling in Rust
B-Trees - Database storage internals
Programming Concurrency in Rust
Introduction to Rust
Assembly - Variables, instructions and addressing modes