Difference between revisions of "W2601 Debugging First Steps"

From Coder Merlin
Line 121: Line 121:


=== Making your Program Pause: Breakpoints ===
=== Making your Program Pause: Breakpoints ===
One of the most fundamental things you want to do while debugging is make the program pause at a particular line or at the start of a function. These locations in a program where execution pauses are called "breakpoints."
{{notice|[[File:Oxygen480-actions-help-hint.svg|frameless|30px]]|Helpful hint:  Breakpoints must be placed in your code at a location where code actually executes.  (For example, they can't be placed on blank lines or comments.)}}
There are several different ways that we can specify the location for a breakpoint:
* By '''function name''':  <code>breakpoint set --name ''function_name''</code>
** Try it now with <code>breakpoint set --name division</code>
* By '''line number''': <code>breakpoint set --line ''line_number''</code>
** Sometimes it is necessary to specify a ''file_name'' in addition to the ''line_number''
** Try it now with <code>breakpoint set --line 3</code>
To view a list of current breakpoints, we can enter:
<syntaxhighlight lang="bash">
breakpoint list
</syntaxhighlight>
=== Viewing Source Code ===
Source code may be viewed by <code>list</code>. For example, we can specify a file name and a line number using the syntax <code>list ''file_name:line_number''</code>.  Try it now:
<syntaxhighlight lang="bash">
list main.swift:1
</syntaxhighlight>


=== Controlling Execution After a Breakpoint ===
=== Controlling Execution After a Breakpoint ===

Revision as of 20:29, 19 February 2019

Within these castle walls be forged Mavens of Computer Science ...
— Merlin, The Coder

DRAFT ICON

Debugging - First Steps[edit]

This tutorial is meant to get you used to using the LLVM debugger, lldb, a command-line debugger.

Research[edit]

Experiment[edit]

What is Debugging?[edit]

A debugger is a utility program that allows you to run a program under development while controlling its execution and examining the internal values of variables. We think of a program running "inside" a debugger. The debugger allows us to control the execution of the program by pausing its execution and then resuming it. While paused, we can find out where we are in the program, what values variables have, reset the values of variables, etc. If a program crashes, the debugger can tell you exactly where the program crashed.

Note that by definition, a debugger cannot help us find compile-time errors, because these occur prior to the successful production of a program's binary representation. It is only after we successfully compile that we are able to use a debugger.

All computer scientists should learn the basics of debugging and how to use a debugger. It will save you literally hours of time when finding and fixing problems in your programs. The few minutes of investment you put into learning how to use a debugger will pay off tremendously in a matter of weeks. Work smart!

Sample Program[edit]

Use emacs to create the following sample program. Save it as "main.swift".

func division(dividend:Int, divisor:Int) -> Int {
    let quotient =  dividend / divisor
    return quotient
}

// This program will print the quotient of a division operation
func main() {
    var dividend : Int = 0
    var divisor : Int = 0

    dividend = dividend + 114
    divisor = divisor * 2

    let quotient = division(dividend:dividend, divisor:divisor)
    print("The quotient is: ", quotient)
}

// Invoke main
main()

Put emacs to sleep; compile and execute the program.

swiftc main.swift
./main

What happens?

Preparing for Debugging[edit]

Programs normally have to be compiled with a special option to allow debugging to take place. For swift, this is the -g option. Before proceeding, examine the size of your "main" executable file:

ls -l

Write down the size of the file. Now, compile with the debugging option:

swiftc -g main.swift

Again, examine the size of the new file that was produced with the debugging option. Write down the size of this file and compare it to the version without the debugging information.

Emblem-question-green.svg Question: Is the new file smaller or larger than the original? Why?

The -g option causes the compiler to include information about the source file (main.swift) that is needed for debugging as part of the executable file. This causes the executable to be larger in size, and slightly slower, but allows for debugging. So when you run the debugger, you specify the executable file (not the source file) as the input to the debugger.

Starting the Debugger[edit]

Start the debugger by typing:

lldb main

Once in lldb, use the run command to start your program running. It will run until it completes, until it crashes, or until it reaches a breakpoint that you set (more on this later) -- and it will pause for input, of course. Once it finishes, you're still in lldb, so you can run it again from the beginning.

If your program requires command-line arguments, you can give them after the run command. If you would normally run the program on the command line by entering prog1 100 test1.dat in the debugger, you would enter: run 100 test1.dat Note, however, that the main.swift that we are editing here does not need any command line parameters, so just type:

run


Where Am I? Where Did It Crash?[edit]

One of the most frustrating things about running a program is that they normally give little useful information when they crash -- usually they just say, 'segmentation fault'. Part of the reason is that by default the executable file doesn't include information about the source code that is needed to print an error message (like the line number). But when you run a program inside a debugger, you can easily see what the current line is when a program crashes. Type f to see the current and surrounding lines. Note that this may be more or less useful depending on where you are in the code. It may be significantly less useful if your program crashed deep inside of a library. In such cases, you may only be able to view some cryptic text. But don't despair. We can walk up through the frames which invoked the procedure until we reach our own familiar code.

f

More usefully, you can see a list of the function calls that led you to this point in your program. Your program may have died deep inside a function that is called many times in your program, and you need to know which sequence of nested functions calls led to the failure. In the command-line mode, type bt, backtrace, to show this list. Examine the list closely. The frame at the bottom is the one that began execution of your program. Each frame up was the result of a new frame being introduced, often by a function invocation. Thus, the frame listed at the top is the most recent, or "deepest".

Oxygen480-actions-help-hint.svg Helpful hint: This command, bt, is one of the most important and useful debugging commands you'll see in this lesson.
bt

When a program stops, you can examine local variables, view lines of code, etc. that are local to that function. If you need to move up to the function that called this one, you need to move up to the higher frame using the up command to debug there. The down command moves you back down a frame towards where you started. The up and down commands let you move up and down the calling stack (of nested function calls) so you can issue debug commands about the function that's "active" at each level.

Type up until you see code that you recognize:

up
Oxygen480-actions-help-hint.svg Helpful hint: If you need to enter the same command again and again, you can just press the <RETURN> key instead.
Emblem-question-green.svg Question: What is the role of the frames beyond that which you recognize?


Displaying Variables and Expressions[edit]

Use the f command to again display the source lines around the current location. Note the name of the function division and the names of the parameters dividend and divisor. We can examine the value of constants and variables by using the print (or just p if you're lazy). Try:

print dividend
p dividend
p divisor
Emblem-question-green.svg Question: What happens if you try p division? Why?

To see a list of all variables defined in the frame, you can use:

frame variable

Making your Program Pause: Breakpoints[edit]

One of the most fundamental things you want to do while debugging is make the program pause at a particular line or at the start of a function. These locations in a program where execution pauses are called "breakpoints."

Oxygen480-actions-help-hint.svg Helpful hint: Breakpoints must be placed in your code at a location where code actually executes. (For example, they can't be placed on blank lines or comments.)

There are several different ways that we can specify the location for a breakpoint:

  • By function name: breakpoint set --name function_name
    • Try it now with breakpoint set --name division
  • By line number: breakpoint set --line line_number
    • Sometimes it is necessary to specify a file_name in addition to the line_number
    • Try it now with breakpoint set --line 3

To view a list of current breakpoints, we can enter:

breakpoint list

Viewing Source Code[edit]

Source code may be viewed by list. For example, we can specify a file name and a line number using the syntax list file_name:line_number. Try it now:

list main.swift:1

Controlling Execution After a Breakpoint[edit]

Changing a Variable's Value[edit]

References[edit]

Key Concepts[edit]

  • lldb is a command-line debugger
  • A debugger is a utility program that allows you to run a program under development while controlling its execution and examining the internal values of variables.
  • A stack frame is the collection of all data on the stack associated with one subprogram call and generally includes the return address, argument variables passed on the stack, local variables, and saved copies of any registers that need to be restored.
  • Command-line arguments may be specified within lldb immediately after the run command
  • A breakpoint is a location in your program where execution pauses