Skip to content
Courses

Connect Source to Live Machine State

Debugging FoundationsLesson 3 of 590 minutesLessonBuilds on: Follow Values Through Function CallsNo lab next

The disassembly of classify contains instructions for adding two and subtracting two. It has to: either path might be needed. Seeing an addition in the executable doesn’t establish that this run executed it, any more than seeing an else in the source establishes that the condition was false.

We can now watch one execution. Keep the original decision.c from the first debugging lesson; no new program is needed. This lesson builds on frame selection, the instruction lesson’s registers, and the rule that a pointer is useful only while it refers to valid storage.

First leave any previous GDB session with quit. In the shell, rebuild the unchanged baseline and check its identity:

Terminal window
cd ~/underdunn-course/exercises/debugging
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g decision.c -o decision
sha256sum decision.c decision
gdb -q ./decision

The saved GDB settings select Intel instruction notation automatically. Predict the default argument and which source branch will run. GDB input:

break classify
run
print value
print &value
x/1dw &value

print value should show 12. &value asks for the address of the local argument storage in this stopped frame. The examine command, x, reads memory: /1dw requests one decimal, four-byte word. On this supported build, that memory should also contain 12.

The address will vary between runs. More importantly, this observation is valid because we’re inside the active function with the variable in scope. It is not permission to keep reading that address after the function returns. Accessible memory and a live C object are different claims.

We’ve now connected a source expression to storage in this run. GDB used debugging information to make that connection; it did not recover the author’s intent from an arbitrary number.

Inspect the function and the next instruction:

disassemble classify
x/i $pc
stepi
x/i $pc

disassemble shows the function’s instructions. x/i decodes one instruction at the supplied address. $pc is GDB’s program-counter name, identifying the instruction that will execute next at this stop.

Before issuing stepi, use the displayed instruction to predict whether execution will continue sequentially or take a branch. Then compare the new instruction location with the prediction. A branch destination can move backward or forward, so “the address changed” is not an explanation of the instruction’s effect.

stepi executes one machine instruction. step, from the previous lesson, advances by source location and can execute several instructions before stopping. A C comparison doesn’t become exactly one debugger step merely because it occupies one line of your file.

Continue with stepi until you’ve observed the decision’s path and the arithmetic instruction implementing it. Stop before the function’s ret instruction, which returns to the caller. Keep this bounded to classify; we’re not single-stepping through the library’s printing implementation. Check the actual branch and arithmetic instructions from your build rather than copying an address from another run.

The GDB machine-stepping reference distinguishes stepi from nexti, which steps over a call at instruction level. For this short function, stepi is enough.

While still inside classify, predict its return value. Then execute the rest of the function:

finish
info registers eax
continue

Expect GDB to report a returned value of 14, and eax to show hexadecimal 0xe and decimal 14 immediately after the return. The final program output should agree.

Why inspect eax here? The calling convention you studied earlier uses it for this int result. That is a claim about the call boundary, not a promise that the register always represents the C variable value. Another instruction can overwrite it later. Reading every register and looking for a familiar number would lose the relationship we came here to establish.

Use run low at the existing function breakpoint. Predict the argument, compare it with valid local memory, then follow the decision again. Observe the return register at the same boundary as before.

  • Addresses differ: that’s expected. Resolve the function by name and use the current $pc or &value, not an address copied from an earlier session.
  • A variable is unavailable or optimized away: rebuild the exact -O0 -g baseline and load that executable. More optimized programs need a less literal source-to-state model.
  • You already executed the return: don’t use finish in the caller and expect another observation of classify’s return. Restart at the function breakpoint.
  • Memory cannot be read: check the frame and lifetime. Restart inside classify; do not disable protections or try nearby addresses.

Knowledge check

Question 1 of 1

You observe eax=14 immediately after classify returns. Which conclusion is supported?