Skip to content
Courses

Follow Values Through Function Calls

Debugging FoundationsLesson 2 of 575 minutesLessonBuilds on: Stop a Program and Look InsideNo lab next

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:

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

The normal run should print 14. Now in GDB:

break calculate
run

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

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:

next
print adjusted
step
print value

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

backtrace

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

up
print input
print adjusted
down
print value

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

finish
next
print result
continue

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

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.

  • A local looks nonsensical: check whether its initialization has executed. Restart at calculate and advance deliberately.
  • A name isn’t in scope: use backtrace and select its owning frame. Don’t change the source to make the debugger’s current view convenient.
  • You stepped into library code: restart at calculate if you’re unsure where you are. Use next over 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 0 returns to the innermost frame, but does not restart execution.

Knowledge check

Question 1 of 1

While stopped in twice, you use up to select calculate. What changed?