Follow Values Through Function Calls
Which function changed it?
Section titled “Which function changed it?”A calculation starts with 6 and returns 14. Perhaps it doubled the wrong number, or perhaps another function changed the number first. The final output can’t distinguish those explanations. We need to follow the value across the call, rather than stare harder at the answer.
You’ve already practiced stopping and restarting a program. This lesson also uses the call stack from How Programs Work. We’ll look at one active call at a time, keeping the program small enough that you can predict the whole calculation before running it.
Build a calculation with an intermediate value
Section titled “Build a calculation with an intermediate value”In ~/underdunn-course/exercises/debugging, save this complete file as calls.c:
#include <stdio.h>
int twice(int value) { return value * 2;}
int calculate(int input) { int adjusted = input + 1; int result = twice(adjusted); return result;}
int main(void) { printf("%d\n", calculate(6)); return 0;}Follow input through adjusted, the argument to twice, and the final result. Keep the two functions’ variable names separate, even where their values happen to match.
Shell input:
cd ~/underdunn-course/exercises/debugginggcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g calls.c -o calls./callsgdb -q ./callsThe normal run should print 14. Now in GDB:
break calculaterunThe stop should be at the initialization of adjusted, with input=6. Don’t interpret adjusted yet. Its declaration is on screen, but the assignment has not executed at this stop.
Choose how far to execute
Section titled “Choose how far to execute”next advances to the next source line in the current frame, executing calls along the way without deliberately entering them. step can enter a called function with source information. We want to execute the adjustment, then enter twice to see what it received.
GDB input:
nextprint adjustedstepprint valueAfter next, expect adjusted=7, with the call to twice still ahead. After step, expect to be in twice with value=7. These stops answer different questions: one establishes the caller’s calculation, while the other establishes what arrived in the callee.
finish will execute the rest of the selected function and stop after it returns. It isn’t the return command, which can force a return and change the program’s behavior. We want evidence from the actual calculation, so we’ll use finish after looking at the call stack.
The GDB stepping reference explains those boundaries. “One step” can mean a source line or a machine instruction; this lesson uses source lines.
Select a frame without travelling back in time
Section titled “Select a frame without travelling back in time”We’re stopped in twice. Ask how we got there:
backtraceA backtrace lists active calls, beginning with the current frame as #0. Here you should see twice, then calculate, then main. Each frame gives us the context of one active call. This isn’t a complete history of every function the process has ever called.
Move to the caller’s view:
upprint inputprint adjusteddownprint valueIn calculate, input is 6 and adjusted is 7. Back in twice, value is 7. up and down change the selected frame; they don’t run the program backward or forward. That’s why the values remain consistent while the names available to print change.
GDB’s variable rules describe the relationship between scope and the selected frame. If a name isn’t available, first ask which function owns it. Guessing at a memory address is not the next sensible move.
Now execute the return:
finishnextprint resultcontinuefinish should report the returned value 14. The following next ensures the assignment to result has completed before we inspect it. Expect result=14 and final output 14. The function return and the caller’s stored result agree, but they are observations at different points.
Practice: observe less on purpose
Section titled “Practice: observe less on purpose”Restart at calculate. This time use next for both the adjustment and the call instead of stepping into twice. Inspect the resulting local value and compare the session with the earlier one.
Common failures and recovery
Section titled “Common failures and recovery”- A local looks nonsensical: check whether its initialization has executed. Restart at
calculateand advance deliberately. - A name isn’t in scope: use
backtraceand select its owning frame. Don’t change the source to make the debugger’s current view convenient. - You stepped into library code: restart at
calculateif you’re unsure where you are. Usenextover the final printing call; it isn’t the calculation we’re investigating. - The frame numbers differ: use the function names in your own trace, not copied frame numbers.
frame 0returns to the innermost frame, but does not restart execution.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 1
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.