DAN ARMENDARIZ: Hi.
I'm Dan Armendariz .
Today we're going to be looking at debugging.
Not only are we going to talk about some techniques,
but also we're going to look at some of the features contained
within the CS50 IDE that allow you to easily debug a program.
>> Just one example of something that can go wrong,
and it's actually something that we've already seen before.
In this case, this is a C program that accepts an integer from the user,
divides it by 2, and provides the output back to the user.
Now, from what we've seen earlier in lectures,
we know that this will actually cause specific types of division problems
when we have odd numbers.
Specifically, it will just throw away anything after the decimal point.
>> Now, we know that this happens to be the case.
And if we run it we can confirm our suspicions first by compiling
and then by running and entering an odd number.
This is nothing new, but this is actually
an example of a bug that can exist within a larger program that
becomes harder to track down.
Even though we know what the issue is, the true crux of the matter
might be trying to identify specifically where the error occurs,
identifying what that problem is, and then fixing it.
So I provide this as an example of what might be something
that we already know but can be buried within other elements of the code.
>> So opening this other source code file as an example,
this division problem is now part of a larger program.
Still might be a little bit contrived and we
might be able to easily identify it, especially
since we were just discussing this, but we
can figure out that this problem can exist on a larger scale.
If I compile this and now run it, enter an odd number,
we can see that we don't get precisely the output that we may have expected.
In this particular case, we might say that we
want to count all the numbers from 1 up to some specific number.
And we can see that we have a variety of issues
here if we're putting simply 0 and 1 when we provide an input of 5.
>> So we already know that there's a problem here.
But we may not know precisely where this issue actually exists.
Now, one of the ways that we can try to fix this
is something that we've already been introduced to,
we can just use it on a larger scale.
On line 14 we have this printf function which
allows us to print out the state of various pieces of information.
And this is something that you should leverage within your program
to try to figure out exactly what's happening in various lines of code.
>> So even if this is not the final output that we actually
want to produce out of this program, we still
might have some debug statements where we
can try to figure out precisely what is happening inside of our code.
So in this case I will printf with a debug tag.
In this case this is just a debug string that I'm
outputting so that it becomes very clear in the output of my code
what it is that I want to show.
And output here, the number that we have computed.
>> In this case, I might want to know precisely what is happening
before and after some specific computation
so I might use a printf before and after that line of code.
In this case I could even make it a little bit more clear
by saying debug before and debug after so
that I don't confuse myself with multiple lines that look identical.
Now, if we recompile this and run it, enter a number like 5 again,
we can see that we have now output before and after
and find that we have not done a clear division or a clear having
of the number that we actually want to do.
>> Now, in this case, this is not really a clear output.
It's not really a clear outcome that we want out of this particular program.
And this is, again, a little bit contrived.
But perhaps one of the things that we could
do if the specification said that we want to divide this by 2
and add 1-- so in other words, we want to round up--
then we might know that we could do that particular thing in this case.
>> Now, here we know that we will be able to add 1 to our halved number.
Let's recompile this and confirm that this
is behaving the way that we want to.
We can see that now before having we have the number 5,
after having we have the number 3.
Which, according to our specification, is what we wanted to do.
But if we look at the output here we can see
that we might have another bug altogether, which is
that we are starting our count from 0.
Now again, this is something that we have seen in the past
and we can fix quite readily.
But in this case we also had the benefit of using the printf statement directly
inside of the for loop to know precisely where that error was occurring.
>> So printf statements are very useful in helping
you determine where precisely in your source code
a specific error is occurring.
And it's also important to realize that as we're writing code,
we might have assumptions about the state of a program
or we might have assumptions about what part of the program
is actually correct or incorrect.
When later on as we build on that program
and make it part of a complex and larger program,
that we realize that some aspect of that is actually buggy.
>> Using printf can really help narrow down and identify
the regions of a program that may not be behaving exactly the way that we
expect based on our assumptions.
But there's other tools available as well
that allow us to try to figure out where an error is occurring.
And also, specifically, what things are happening inside of the program.
So using printf is very useful when we want
to identify specific areas of a program that have some bug.
But also becomes tedious after a while.
In this case, this is a relatively simple program
with just one or two variables and it becomes very easy for us
to print out the value of those variables
in the context of the larger program.
>> But we might have a different program that has many variables
and it may not be quite so easy to use printf
to try to evaluate what is happening to each one of those variables
as the program is executing.
There's a program that exists called a debugger program.
In this case, the one that we will use is the gnu debugger, or GDB,
that allows us to inspect the internal workings of a program in a much more
detailed way.
We can actually execute GDB from the command line
here by simply typing GDB and the command that we want to debug.
In this case, count.
>> Now this case we can see that it brings us to a prompt that says GDB
and we can actually execute commands to GDB to actually begin execution
of the program, stop it at certain points, evaluate the variables,
and inspect the variables that exist in the program state
at that particular moment, and so on and so forth.
It provides a lot of power to us.
>> But it just so happens that the CS50 IDE also
provides a GUI, or a user interface, for GDB
that allows us to do this without needing the command line
interface whatsoever.
Or at all, even.
The way that I can access that is by using the Debug button
at the very top above the CS50 IDE.
Now, in the past, what we have seen is that we use the command
line to compile and then run a program.
The Debug button does both of those steps,
but it also will bring up the Debugger tab on the far right
that allows us to inspect a variety properties of the program
as it is executing.
>> If I click Debug, in this case, it will bring up
a new tab in the console window at the very bottom.
And you can see that this tab has some information at the very top
and we can largely ignore this.
But one of the things that we want to notice
is that it outputs the same thing that we
would get if we tried to run make on the C program in the terminal window.
Here we can see it's running Clang and it has a variety of flags
and it is compiling our count.c file which was the selected tab at the time
that I hit Debug.
>> So this is very useful because now, using this Debug button,
we can see simultaneously compile and then execute the program
that we actually want to run.
One of the flags that is important in this case we've actually
been using for the longest time but also just did some hand waving at,
which is this one right here.
In clang it says -ggdb3.
In this case, what we are telling Clang, our compiler,
is that we want to compile our program but also provide
what are called symbol information so that the compiler actually
has access to a lot of the underlying information contained
within the program.
Most specifically, the number of functions that I have,
the names of those functions, the variables, the types
that those variables are, and a variety of other things that help the debugger
perform its operation.
Now, there's something else that's important to mention
when we're discussing running a program in this way.
Notice that it has actually brought up a new tab in our console
along the bottom.
We no longer have to interact directly with the terminal window,
but this new tab is actually terminal window,
it just is specific to the running program that we have created.
>> Notice that at the bottom, in combination
with some output by Clang, the compiler, and GDB, which we can largely ignore,
it actually shows the output of our program at the very bottom.
Now, it's important to realize that this one window actually
will show you the output from your program
but also can accept input for that program as well.
So notice that it says, please enter a number, which
is the same output that we had had in the terminal window before
but is now shown in this new tab.
I can input a number and it will actually
function as we expect showing us our debug output, the output that
might be buggy-- as we've seen before-- and at the very bottom
it actually has some additional output from GDB just saying
that this program has completed.
>> Now, as you saw in this particular run through, it wasn't particularly useful.
Because even though we had the debugger menu come up,
this was still a running program.
At no point did actually pause execution for us
to be able to inspect all of the variables contained within.
There's something else that we have to do in order
to get GDB to recognize that we want to pause execution of the program
and not just allow it to proceed normally as we would in any other case.
>> In order to pause execution at some specific line,
we need to create what's called a breakpoint.
And a breakpoint is very easily created in the CS50 IDE by taking your mouse
and clicking directly to the left of some specific line number.
Once I do that, a red dot appears which indicates that that line is now
a breakpoint, and the next time that I run GDB,
it will stop execution at that breakpoint
when it reaches that line of code.
>> Now, this is an important thing to realize.
That it's not necessarily the case that every line of code
is actually accessible.
If I were to create a function up here, for example,
void f, and just do a print line here, hello world,
if I never call this function, it will be the case that if I set a breakpoint
here the function will never be called and therefore
this particular breakpoint will never actually pause
execution of the program.
>> So let's say that I correctly create a breakpoint on some line of code
that will actually be executed.
Now, in this case, this is the first line in the main function
so it will certainly be the case that as soon as I begin execution
the very first line will be reached, GDB will pause execution,
and then I will be able to interact with the debugger.
You can set multiple lines as breakpoints if you would like.
We can also create a line up here in this segment of code
that will never be reached.
And we can also set one further below.
The reason that we would want to do this we'll
go into a little bit more detail in just a moment.
So for now, let me just disable these additional breakpoints
so that we can look at what happens when I have
one single breakpoint in my program.
I have made some changes to this program so I need to save it.
I will click Debug so that I can begin the compilation and then
execution of the debugger.
We will see that after moments the line that we selected as the breakpoint
is highlighted in yellow.
We can also notice that in the upper right in the Debug panel
that the Pause icon has turned into a little Play icon.
This means that we have paused execution in this particular case,
and hitting the Play button will allow us to resume execution
at that specific point.
>> Notice that there's a couple of other buttons available in this Debug panel
as well.
Step Over, which allows me to execute that one line of code
and step over to that line to the next one.
Which in this case would mean that the printf statement is executed
and it will then pause execution on line 13 like so.
And there's also a Step Into function which
is useful if I have created other functions elsewhere in the source code
and I want to step into those functions rather than
execute that function as a whole.
But we'll look more at this Step Into function in just a moment.
Now, notice some other things that actually exist within this Debug panel.
We have this panel called the Call Stack which shows us where exactly we are.
In this case, we are inside of the main function, our script is called count.c,
and we happen to be on line 13 column one.
Which is precisely what the highlighted region of the source code
indicates as well.
>> Now notice that this also shows under the Local Variables section
all of the variables that exists within this function.
It's important to note that all of the variables
will appear in this Local Variable section within a function
even before they are defined.
We can see here that we have a variable called num that has a default
value of 0 and it is of type int.
Now before we actually initialize all of these variables,
we're not necessarily guaranteed to see a value of 0.
And depending on other executions that you've performed
and the state of your memory when you actually run this program,
you might find that you don't see values of 0
and instead some other crazy numbers.
But don't worry about that.
It's not going to be relevant until you actually initialize the value.
>> Now, in this case, we can see that I have performed some outputs
and I'm right now paused execution.
But in this case, what I really want to do
is to now step over this line of code so that I can actually
query the user for that int that we want to use in our program.
Now, in this case, when I hit Step Over, notice
that the Pause-- rather the Resume button has changed to this Pause button
because this code is actually executing.
What is happening right now is that it is
waiting for us to input some information as we can see by our output text
at the very bottom.
>> So right now this is not actually paused even though it sort of appears
to be because nothing is happening.
But it just so happens that in my specific case, on line 13,
I'm waiting for user input and so GDB is not
able to inspect a program as it is running.
Now, the next time that I enter some input--
so I'll enter the number 5 as we've seen in the past,
hit Return-- we notice that, immediately, GDB pauses
and again highlights the next line.
>> But notice that now, as a result of our inputting a value,
we have updated that value inside of our local variables, which
is very useful to know precisely what that number was in memory.
Now, I can allow this program to continue
playing until the end of its execution by hitting Resume.
We can see that, very quickly, just does program finish executing
with the same output that we had before.
The debugger closes and now this program has stopped completely.
>> I show that only for the purposes of seeing what
happens when we actually hit Resume.
But we actually are going to want to go back into this program
so that we can try to debug precisely what is happening.
Now that I'm using the debugger I may not need these debug printf statements.
So I could remove them as I will do now.
Just to go back to our simpler code that we had a moment ago.
>> Now, when I save the program and execute it,
it will again go to that initial breakpoint that I had on line 11
and I will be able to inspect my variables as I want to do.
It just so happens that this part isn't very interesting.
And I know with that I'm going to print out this statement--
please enter a number-- and then I know that I'm
going to ask the user for that integer, so perhaps I
actually want to move my breakpoint a little bit further down.
You can remove breakpoints by clicking, again, directly
to the left of that line number.
That red dot will disappear indicating that that breakpoint is now gone.
>> Now, in this case, execution has been paused
and so it's not actually going to resume in that particular instance.
But I can set a breakpoint a little bit later.
And when I now resume my code, it will resume
until the point of that breakpoint.
Again, I hit Resume.
Doesn't seem like anything is happening but that's
because my code is waiting for input.
I will enter a number 5, hit Enter, and now the next breakpoint will be hit.
>> Now in this case, this is the line of code that before we knew
happened to be buggy.
So let's evaluate what happens at this particular point in time.
When a line is highlighted, this line has not yet been executed.
So in this case, we can see that I have a number which--
I have an integer with-- called num that has a value 5
and I'm going to be performing some math on that number.
If I step over that, we can notice that the value for num
has changed in accordance with the arithmetic that we've actually done.
And now that we are inside of this for loop,
or now that the for loop itself is highlighted,
we see that we have a new variable called i that
is going to be used in that for loop.
>> Now, remember before that I mentioned that sometimes you're
going to see some kind of crazy numbers until-- as default before that number
or that variable is actually initialized.
We can see that precisely here in this variable
called i which has not yet been initialized
at the time of highlighting, but we can see
that it has some number that we wouldn't actually expect.
That's OK.
Don't worry about it because we have not actually
initialized that number until I step over this line and the value
i has been initialized to the value 1.
>> So to see that that's actually the case, let's step over.
We can now see that that line has been executed
and we are now highlighting this printf line.
And we can now see how our values of i and 3 have changed over time.
This is very useful to do, in fact, is to step over lines repeatedly.
And you can find what actually happens inside of your for loop
and what happens to the variables inside of that for loop
as that program execution occurs one step at a time.
>> Now, at this point I stepped over just enough
that I now am at the end of my program.
If I step over that, it will actually cease execution
as we have seen in the past.
Let me restart this yet again so that I can point something else out as well.
In this case, it is now asking me, again,
for a number which I will again enter.
But this time I'm going to enter in a larger number so that the for loop
will iterate more times.
In this case I'm going to enter a value of 11.
>> Now, again, because I'd set a breakpoint at line 15,
it's going to highlight that line.
We can see that our number 11 is correctly
represented in our local variables.
Stepping over that we can now watch what happens to our value of i
as we proceed inside of this for loop.
It gets incremented every time we reach the top of that for loop.
>> Now, one of the things that might be useful to do during execution
of this program is really to actually change the variables midstream
to see what happens to my program.
In this case I can actually double click the value.
Notice that it becomes a text field.
Now I can enter a different value altogether
to see how my program behaves when I've changed that variable.
>> Now in this case, the variable i now contains the value 10
but the program is still paused in execution.
When I step over, I see that i-- the value i which I entered as 10
is now greater than the value of num which immediately causes the for loop
to stop executing.
Now that's not the only reason why you would
want to modify the variable in place.
You might actually want to try to modify it so
that you can continue execution of a loop
or so that you can modify some value before it
reaches some specific set of arithmetic that you are about to perform.
So now that we've actually changed the value of i
as the program was executing, it caused the for loop
to quit prematurely because all a sudden i happened
to be greater than the value of num.
Meaning that that for loop no longer needed to be executed.
Further, it happened to be the case that we changed the value of i
when the line 17 was highlighted which was the point in time
that the for loop execution was actually being evaluated.
If I had changed the value of i on a different line, say 19,
we would have seen different behavior because line 19 would
have executed before the loop condition was reevaluated.
>> Now at this point, I'm, again, at the end of this program
and I can allow this to proceed to allow my program to quit naturally.
But there's a couple of things that are important to take away
from this particular discussion.
You need to evaluate your own assumptions
about how the code should be behaving.
Any time you think that some piece of code you know happens to work,
that might be a red flag to go back and evaluate and be sure
that your assumption of how that code is operating
is actually true to how it is expressed in your source code.
>> But even more to point was when we were using the debugger.
You can put breakpoints at different lines of code
which will cause the debugger to pause execution at each of those lines
so that you can evaluate the memory or even change it in place.
And again, remember that you can create multiple breakpoints so that you
can also resume execution, skip over large portions of code,
and it will automatically pause at the next breakpoint.
>> There's actually more advanced features of the debugger as well.
But we'll have to refer you to some subsequent videos
in order to really tease apart how to use those particular functions.
For now, thank you very much for watching and good luck debugging.