Skip to content

Debugging with GDB

Use a debugger to test a prediction about one instruction. Begin with a known result, then distinguish an implementation bug from the test detecting that bug.

Course examples on GitHub

Open this lesson's sample files · CMake build instructions. This lesson reuses the register lab and the function lab; there is no second copy to keep in sync.

Before you start

Complete C/C++ interop. Keep or recreate the two files from the register lab in that lesson's practice directory.

Outcome: observe a width transition at an exact instruction and inspect a balanced stack. Run GDB on the Pi, not on a different host's binary.

Rebuild with debug information

1
2
3
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -O0 registers.c registers.S -o registers
./registers
gdb ./registers

At the GDB prompt, enter the following debugger commands, not shell commands:

1
2
3
4
5
6
7
8
break *width_demo
run
x/3i $pc
stepi
info registers x0
stepi
info registers x0
quit

The star requests the exact symbol address. The first step executes the 64-bit all-ones write; the second executes the 32-bit destination write.

After the first step:  x0 = 0xffffffffffffffff
After the second step: x0 = 0x1234

Formatting varies; these are the values to inspect, not a literal transcript. If quitting while paused, confirm that GDB should terminate its inferior. Closing the terminal is not a deliberate test-completion policy.

Observe the contract failure

In a practice copy, replace the second MOV with mov w0, #-1. Rebuild and repeat. The second value becomes 0xffffffff, not a 64-bit all-ones value.

Continuing execution now prints Register lab check failed and exits 1. The test is working: it detects a changed implementation. Restore the source and rebuild before continuing. Do not change both the routine and its expected value while trying to isolate the fault.

Inspect a nested call

Rebuild the functions lab, then start gdb ./functions. For the first call:

break *double_plus
run
set $entry_sp = $sp
set $entry_x19 = $x19
x/10i $pc
stepi 4
info registers sp x19 x30
nexti
info registers x0 x19
stepi 3
p $sp == $entry_sp
p $x19 == $entry_x19
quit

Four instructions allocate the frame, save x19 and retain bonus. nexti runs over the helper; the next three execute the add and restore the saved state. Both equality expressions should be 1 immediately before RET. Inspect the disassembly if source edits change the instruction count.

This confirms two particular runtime properties for this path. It does not prove every input or full unwind correctness.

Investigate without hiding the failure

  • No symbol: check nm, the executable path and whether it was rebuilt.
  • Optimized C steps strangely: inspect instructions; source statements do not map one-to-one to instructions.
  • Cannot start the inferior: inspect debugger/ptrace permissions. Do not disable system-wide protections merely to run this tutorial.
  • Unexpected address: executable relocation and ASLR can change it between runs; use symbols.
  • Stopped in the wrong program: check GDB's selected executable and process.

See the GDB AArch64 documentation for target-specific facilities. Reading a register and changing it are different actions; begin with observation.

Check your understanding

Why is 'the test still fails' useful evidence after deliberately changing the MOV?

Compare your reasoning

The unchanged oracle rejects the changed result. First restore the source and demonstrate a passing run; only then try a separate change.

Continue the course

Previous: Calling Assembly from C and C++ · Course roadmap · Next: A Checked Array Project

Donate