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:

IdeaWhat it means
BreakpointStop here when you reach this line
StepMove forward one line, then stop again
InspectShow me the value of this thing, right now
Call stackHow 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.

Without it you get instructions, not your program

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:

s and n — the difference that matters

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

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.

This is what a crash report is

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.

This is why an unset variable is dangerous in C

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

IdeaCommandShort
Breakpointbreakpoint set --name mainb main
breakpoint set --file msd.c --line 30b msd.c:30
start the programrun / r
Stepnext line, over callsnext / n
next line, into callsstep / s
run out of this functionfinish
carry on to the next breakpointcontinue / c
Inspectone valueprint number / p number
every local variableframe variable / v
Call stackhow did I get herebt
Leavequit the debuggerquit / 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 printfUsing a debugger
To ask a new questionedit, rebuild, run againtype it at the prompt
You seeonly what you thought to printanything you like, at any point
Your codefills with lines to remove laternever changes
Call stackyou work it out yourselfbt

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