Chapter 8
Stopping time
A program that goes wrong will not explain itself. A debugger lets you stop it in the middle, look at every value, and walk forward one line at a time. It is the difference between guessing and looking.
Every debugger is four ideas
Debuggers look complicated because they have many commands. They are not. Nearly everything you will ever do is one of four things:
| Idea | What it means |
|---|---|
| Breakpoint | Stop here when you reach this line |
| Step | Move forward one line, then stop again |
| Inspect | Show me the value of this thing, right now |
| Call stack | How did the program get to this line? |
Learn those four and you can use any debugger. The names of the commands change between languages. The four ideas do not.
A first look, with sum.c
Start with the program from chapter 6.
Build it with -g and start the debugger:
$ clang -g sum.c -o sum
$ lldb ./sum
Line 16 is where the two numbers are added. Stop there, and the program will have read both:
(lldb) b sum.c:16
(lldb) run
It asks for the numbers as usual. Type 3 and 4, and
the program stops before line 16 runs. Now look at what it holds:
(lldb) frame variable
(int) num1 = 3
(int) num2 = 4
Move forward one line, and look again:
(lldb) n
(lldb) p num1
(int) 7
You have just watched a variable change. Not guessed, not printed — watched. That is the whole point of a debugger.
Why the -g, at last
Chapter 6 said -g keeps
debug information, without saying what that means. Here it is.
The finished program holds machine instructions. It does not hold your C. The
processor has no idea that a certain instruction came from line 16, or that
one of its numbers is called num1.
-g tells clang to leave a map behind: this
instruction came from that line, this piece of memory is the variable you
called num1. The debugger reads the map.
Build with plain clang sum.c -o sum and the debugger still
works — but b sum.c:16 has nothing to attach to, and
frame variable has no names to show. You are left reading
assembly.
Compile with -g whenever you might want to debug. It costs nothing while the program runs.
A program with a function of your own
One function is not enough to show what a debugger is for. Here is a second program. It finds the most significant digit of a number — the leftmost one — by dividing by ten until only one digit is left. Then it adds the results for two numbers.
You can download it, or type it out:
#include <stdio.h>
int most_significant_digit(int number)
{
while (number >= 10)
{
number = number / 10;
}
return number;
}
int main()
{
int num1;
int num2;
int msd1;
int msd2;
int total;
printf("To add the most significant digits of two numbers.\n");
printf("Enter the first number: ");
scanf("%i", &num1);
printf("Enter the second number: ");
scanf("%i", &num2);
msd1 = most_significant_digit(num1);
msd2 = most_significant_digit(num2);
total = msd1 + msd2;
printf("Sum of MSDs(Most Significant Digits) of two numbers is %i.\n", total);
return 0;
}
$ clang -g msd.c -o msd
$ ./msd
To add the most significant digits of two numbers.
Enter the first number: 345
Enter the second number: 52
Sum of MSDs(Most Significant Digits) of two numbers is 8.
3 from 345, 5 from 52, and 8 altogether. It works. Now take it apart and watch it work.
Stepping into your own function
A breakpoint does not have to be a line number. Give it a function name and it stops every time that function is entered:
$ lldb ./msd
(lldb) b most_significant_digit
(lldb) run
Type 345 and 52, and it stops:
* thread #1, name = 'msd', stop reason = breakpoint 1.1
frame #0: msd`most_significant_digit(number=345) at msd.c:5:5
3 int most_significant_digit(int number)
4 {
-> 5 while (number >= 10)
6 {
7 number = number / 10;
Read that carefully. Three useful things are on screen already:
most_significant_digit(number=345)— the function, and the value it was givenmsd.c:5— exactly where it is- the arrow
->marks the line about to run, with the source around it
s and n — the difference that mattersStop at line 30, where the function is called the second time, and try both:
n(next) runs the whole call and stops on the following line. You never see inside.s(step) goes into the function and stops on its first line.
Use n for code you trust, s for code you suspect.
(lldb) b msd.c:30
(lldb) run
(lldb) s
frame #0: msd`most_significant_digit(number=52) at msd.c:5:5
You are now inside the function, with the second number.
Watching the loop do its work
This is where a debugger earns its keep. Stop at the top of the function and
press n repeatedly, printing number as you go:
(lldb) b most_significant_digit
(lldb) run
(lldb) p number
(int) 345
(lldb) n
(lldb) n
(lldb) p number
(int) 34
(lldb) n
(lldb) n
(lldb) p number
(int) 3
345, then 34, then 3. You are watching the loop divide the
number down, one turn at a time. No printf was added. The
program was not changed at all.
If that loop had a mistake — the wrong comparison, a division by the wrong number — you would see it here, in the values, rather than reasoning about it on paper.
The call stack: how did I get here?
When the program stops inside a function, bt shows the chain of
calls that led there:
(lldb) bt
* frame #0: msd`most_significant_digit(number=345) at msd.c:5:5
frame #1: msd`main at msd.c:29:12
frame #2: libc.so.6`__libc_start_main + 152
frame #3: msd`_start + 48
Read it from the bottom upwards, and it is the story of the program so far:
_start— where every program begins__libc_start_main— the setup code the linker joined on in chapter 6main, at line 29 — your programmost_significant_digit— where you are now
Frame #1 is the useful one. It tells you the call came from
line 29, so this is the first of the two calls, with
num1.
When a program crashes and prints a long list of function names, that is a call stack. The same thing Python prints as a Traceback. You are reading one on purpose here, instead of waiting for something to go wrong.
Running back out
You have seen enough of the function and want its answer.
finish runs the rest of it and stops the moment it returns:
(lldb) finish
Return value: (int) $0 = 3
frame #0: msd`main at msd.c:29:10
Return value: (int) $0 = 3. The debugger caught the answer on
its way out and showed it to you — before it was even stored in
msd1.
A small surprise worth understanding
Stop at line 30, before the second call, and ask for every local variable at once:
(lldb) b msd.c:30
(lldb) run
(lldb) frame variable
(int) num1 = 345
(int) num2 = 52
(int) msd1 = 3
(int) msd2 = 0
(int) total = -1
num1, num2 and msd1 hold what you
expect. But look at msd2 and total. Those lines
have not run yet, so nothing has been put in them — and they are not empty.
They hold whatever numbers happened to be lying in that memory already.
total = -1 is not a value anybody chose. On another machine,
or another day, it might be 0, or a huge number. Use a
variable before you set it and your program behaves differently each time.
Step 11 shows how to make the compiler warn you about exactly this.
The commands worth knowing
| Idea | Command | Short |
|---|---|---|
| Breakpoint | breakpoint set --name main | b main |
breakpoint set --file msd.c --line 30 | b msd.c:30 | |
| start the program | run / r | |
| Step | next line, over calls | next / n |
| next line, into calls | step / s | |
| run out of this function | finish | |
| carry on to the next breakpoint | continue / c | |
| Inspect | one value | print number / p number |
| every local variable | frame variable / v | |
| Call stack | how did I get here | bt |
| Leave | quit the debugger | quit / q |
Why not just add printf?
You can find most bugs with printf. People do it every day. But
compare the two ways of working.
| Adding printf | Using a debugger | |
|---|---|---|
| To ask a new question | edit, rebuild, run again | type it at the prompt |
| You see | only what you thought to print | anything you like, at any point |
| Your code | fills with lines to remove later | never changes |
| Call stack | you work it out yourself | bt |
The real difference is the loop. With printf, every question
costs an edit and a rebuild. With a debugger the program is already stopped,
and you can keep asking until you understand.
Try it
Build the program with the map left in:
$ clang -g msd.c -o msd && lldb ./msd
Then, inside lldb, work through these in order:
b most_significant_digit # stop whenever the function is entered
run # type 345 then 52
bt # which call is this - line 29 or 30?
p number # 345
n # press a few times, printing as you go
p number # watch it become 34, then 3
finish # see the return value
frame variable # which locals are set, which are junk?
c # continue - it stops again for the second number
bt # now the call came from line 30
q
Then try one thing more. Rebuild without -g and repeat
the first two commands. Seeing what you lose is the fastest way to remember
what -g is for.
$ clang msd.c -o msd-nomap && lldb ./msd-nomap
You can now explain
- The four ideas every debugger is built from
- What
-gactually leaves in the program, and what breaks without it - The difference between
sandn, and when to use each - How to watch a loop change a value, without touching the code
- How to read a call stack, and which frame tells you where a call came from
- What
finishshows you that you could not otherwise see - Why a variable you have not set holds junk rather than zero
- Why a debugger beats adding
printf, and when printf is still fine
