Placeholder Image

Subtitles section Play video

  • 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