字幕列表 影片播放 列印英文字幕 DOUG LLOYD: So if you're anything like me, when you write code it's not always going to be perfect. There's usually going to be something wrong with it. And that is known as writing bugs. And part of being a computer programmer is learning how to debug, or remove the bugs, from programs. And there are a lot of different techniques and strategies that one might use to do this. And what I want to do in this video is show you three of the tools that are baked into CS50 IDE that you can use to help with your debugging, or help trying to figure out what's going on in your system. And these three tools, which I'm going to just display the names of here, are known as debug50, eprintf, and help50. We're actually going to cover those in reverse order. So we're going to start with help50. So I have a couple of files pre-configured in my IDE. I'm going to navigate into my help50 directory. And just clear the terminal window here. And what help50 basically does, it is designed to be sort of a virtual teaching fellow of sorts. It basically is trying to give you clues and hints about what might be going on in your program or when you're trying to work with your program and something goes wrong without just giving you the answer outright. So it's trying to nudge you along in the right direction in the hopes that it's an issue that, with a little bit of encountering and having the computers sort of give you a little bit of back and forth, it's an issue that you'll learn to be able to not recreate in the future. So I have a couple of example programs here. All of them are written in C. And let's just try and compile the first one. So let's try and make example one. Error. So this is where help50 is going to come in handy. Generally it's going to be used when you're working. It's not so much a bug in your program that is during runtime, a runtime error. This is going to be a logical bug, or some kind of bug that prevents your program from being compiled is going to be the most common use case of help50. So here I'm getting told I'm implicitly declaring library function printf with blah, blah, blah. There's a whole lot of text there. Now, what help50 tries to do is it looks at that error message for you. And if there are multiple ones it will look at the first error message for you. And it will try and translate that kind of arcane language into something a little more, hopefully, intuitive to use. And the way we use help50 is pretty straightforward. We first type help50, and then we do exactly the command we tried to do. So in this case it's help50, make example1. Let's see if maybe help50 can give me a little more information about what I might be doing wrong in my program. So I run it again. Now, I'm getting this sort of hint from the virtual TF, which is written here in yellow. "Did you forget to #include stdio.h in which printf is declared atop your file?" Well, let's take a look. So in example 1.c, it looks like I did kind of forget to do that. So if I just go ahead and add that, and I save, and I try and make example one again maybe. Hey, it compiled. It worked just fine. So I can now run the example1 program, hello world. Now, of course, I have a logical bug here where I didn't put a backslash n. So it kind of ran into a new line like that. But that's OK. That's something I can go ahead and fix pretty easily. So let's try and compile example two. And we'll go through this again where we won't look at the source code beforehand. We'll just try and compile it, thinking we're pretty close, and see what happens. So we'll try and make example2. Oh. Seven errors generated. That's not good. But, again, help50 can help us here. And what it's going to do in this case is it's going to look at the very first one and give us a hint for that one. Now, oftentimes when you get a lot of errors from compiling it's not necessarily that I've done seven things wrong. In fact, if you look at the program you'll see that it's not that many lines of code when we do. But really it's just one error that sort of cascades and creates another one because it doesn't understand what's happening before. And if you can fix that one error at the beginning, it might resolve one or more, or all, of the remaining errors. So I'll clear my terminal window. And we'll try this again. But we'll preface this by saying help50 first. So help50 make example2. Let's see. Use of undeclared identifier string. Did you mean standard in? Well, I'm assuming probably not. Let's take a look at my code. No. I meant to use string there on line five. So by undeclared identifier, Clang means you've used a name string on line five which hasn't been defined. Did you forget to #include cs50.h in which string is defined atop your file? Well, here, again, I did. I did forget to include cs50.h this time. So it gave me another clue here. But I had seven errors. So hopefully this resolves most of them. Lets clear this and see if it does. We'll try and make example2 again. And it did. So even though it said I had seven errors, I really didn't. I just had the one. And because that one kind of threw everything else off, it just kind of cascaded down and really messed us up. All right. Let's take a look at example three. We'll try and make example three. Seven errors again. Ugh, man. Let's see what I got here. Looks like I have a for loop that's going from 1 to 10. And it's supposed to print out a new integer on each line. That looks OK to me, at least at first glance. I don't really know what the issue could be here. But help50 might be able to help us out again. So we'll try one more time help50 make example3. And, again, you'll see that help50 is commonly going to be used most of the time-- although it's not all it can do. And we'll take a look at a couple of examples of that in a second. You're going to use help50 most of the time when there are compile time errors that prevent your program from being compiled and have to be resolved first. So we'll try and help50 this one. Looks like it says on line five it looks like you're trying to declare a variable that's already been declared elsewhere. I don't see that. But then it tells me a little bit more. If you meant to create a for loop be sure that each part is separated with a semicolon rather than a comma. So here I've got commas, and I'm supposed to have semicolons. So it's, again, little things like this where it's little syntax things where if you were able to get in touch with somebody you could just kind of look at your code really quickly and be like, oh, well, you really just need to put in a semicolon here instead of a comma. That's the kind of thing where it stinks to get stuck in a jam where you're just waiting for somebody to give you that sort of shining moment right where suddenly the error is revealed. You can use help50 to try and help you along and accelerate this so that you don't get stuck waiting, and you can get a little resolution for your errors a little more quickly. Of course, we'll just confirm with trying to make it again. And it compiled no problem. I could run this program. And it will print out the numbers 1 through 10, each on separate lines. We can't see them all here because my terminal window is not big enough to hold them, but they're all there. All right. I got one more example here. And then I'll show you two examples of using help50 not in the make context necessarily, or in the Clang context. So let's try and make example4. I have one error. "Expression result unused, n times 5." Not really sure what that means. So let's just quickly pop open example4 here at the top. Looks like what I want to do is have this print out 50. So I set n as equal to 10. I multiply n by 5 to get 50. And I want to print out 50. So I wonder what the issue could be. So let's help50 make example4. On line six of example 4.c you are performing an operation, but not saving the result. Did you mean to print or store the result in a variable? So remember, when we are doing mathematical manipulations to variables, we always have to assign the result to something. And here what I probably meant to say is either n equals n times 5, to change n from 10 into 50, or the more shorthand version, n times equals 5, which would also work just fine. So I can save this. Hopefully this fixes it. I'll try and recompile it. And I'll try and run it to see if I get 50 printed out. So that's a couple of uses of, in particular this is help50 helping with Clang. Although every time we were doing this we were typing make. Remember that make is actually just a wrapper for Clang, which is the name of the actual compiler that we're using in CS50 IDE. What happens though if I try and do this? There's no file called example5. So make example5. Unlike the previous four, this is actually going to be a help50 that helps with a message from make as opposed to a message from Clang. Now, obviously it's not there. So you can probably figure out what it means. But in case you didn't, I could preface it with help50. "Do you actually have a file called example5.c in the current directory?" Well, I don't. So I'm trying to compile a program that doesn't exist. So this is just help50, again, giving me a clue, like, well, maybe you're making a little bit of a mistake here. And it's true. I am making a bit of a mistake. One more example here. Let's say that I want to change directories into another directory. So I want to ls example6. There is, again, no example6 here. "ls cannot access example6, no such file or directory." Again, help50 might be able to help with this. So I'll preface it one more time. Are you sure example6 exists? Did you misspell example6? It doesn't exist. Maybe I had typed it wrong and I thought it was working. So it's just a reminder. Did you check that? Oh, maybe I had misspelled it. Maybe I meant to do something else, and then I can take a look. Now, help50 is a tool that is still in development. We actually have an open source GitHub repository. So as you go through the class and you become more comfortable, particularly as you start to learn Python, which is the tool that help50 is written in, you can actually head to CS50's public GitHub repositories and take a look and see what types of error messages that are a little more arcane that we're trying to catch and have help50 explain in more human understandable terms so that if you want to contribute to that you actually can. And if you encounter an error that you're not getting, that help50 doesn't understand, and you want to report that error and hope that somebody else maybe can help match it, you can do that through that platform as well. So this is definitely a collaborative tool. And we welcome you to continue to contribute either solutions as you become more comfortable, or issues that we can try and resolve to make it a more robust tool. All right. So another tool that we have is one called eprintf. Now, it's really common when you are starting to debug your programs to kind of pepper them with printf statements. It's sort of like a sanity check where you're checking, OK, well, this line printed. And then I have some lines of code that run. And then this line printed. Like you're trying to figure out where things are going wrong. The trouble with printf is it doesn't necessarily tell you where the program is going wrong. You have to kind of go back and scroll through your source code to figure out which error message corresponds to that. That's not always that helpful. It's not a huge deal. But we have this tool that makes it a little bit easier. So it's called eprintf, which stands for error printf. And I'm just going to navigate into my eprintf directory where I have just one example file. I'm going to open that up here and show you what it does. So not much to this. I'm going to scroll up here. Basically what I'm doing is I'm asking the user to give me an integer. And then I actually get that integer from them. And then I have eprintf, integer obtained. Now, this means nothing to the user. It's an error message to make sure for me, as the person who's testing this program, that I got to this point in the program. And we'll see exactly what eprintf does that gives it a little bit of a leg up over printf, which is just going to display to the terminal. Then I'm doing a couple of different things. If x is less than 0, I'm going to print x is negative. If it's greater than zero, I print x is positive. Otherwise I print x is 0. And then I have one more eprintf call at the bottom here which is got through the program. So if I see that eprintf, that means that I've succeeded and I've gotten through every single line of the program without crashing. So let's just quickly compile this with make eprintf. And we'll try and run ./eprintf. Please give me an integer. Let's give it 50. And look at the little difference here. So instead of just printing out what I said, it tells me where it came from. So it tells me what line it came from and what file it came from. As we write more and more complex programs later in the course, we might write programs that span multiple .c files. We might have eprintfs throughout them. So it's just a useful way to figure out exactly from where in your program, or as things are going through, where in your program the error message that you're seeing, or sort of the printf prompt that you're seeing comes from. So it's definitely a good tool to familiarize yourself with, particularly earlier on where you might not need the more complex tools of a debugger, which we're going to talk about in a second. But you just want to get sort of a sanity check to make sure that things are working the way you expect them to. OK. And the third and final tool that we're going to cover in this video is debug50. Now, debug50 is a GUI or Graphical User Interface wrapper for a tool known as GDB, which is a very richly developed debugging platform. And it will really help you take a look underneath the hood, inspect what is going on in your program, and try and figure out where things might be going wrong. In this example we're going to cover three different buggy videos. And I'm going to show you how to use debug50 to sort of give you some clues as to what you might need to do. So the first thing I'm going to do here is navigate into my debug50 directory where I have three source files and three programs that are previously compiled. So these have compiled. I don't need help50 to tell me what's going on here because I've already got the compiled binaries here. But there's something going wrong in these programs that is not what I expect. So the first thing I want to do is run debug1's program, and then we'll take a look at debug1's source code. And then we'll take a look at how debug50 can give us a little bit more information about what might be going wrong here. So here's what we're going to do. We're going to go, and I'm going to clear my terminal really quick, and we're going to run debug1's binary. Provide a number. OK. I'll provide one. You gave a one. Seems pretty good. Let's try this again. Provide a number. I'll give it two. You gave a one. That's not right. Zero? You gave a one. So it's always saying, I gave a one. I did. I gave a one the first time. So let's maybe take a look at what's going on here. So I have my program. I'm providing a number. OK. That works. If x is one, say you gave a one. Otherwise, apparently I'm not doing anything. It's not supposed to print anything if I didn't give it a one. So that's weird. So at this point if you're familiar with the code you might see where the issue is here. But let's pretend that we don't. Or maybe you don't. And that's totally OK as well. This debug50 tool can hopefully give a little bit more information and we can see what's happening. So what I want to do is the first thing you need to do when you're working with a debugger like GDB is to provide a breakpoint. What a breakpoint is is basically your program will execute until it hits that line of code. And then it will stop, or break, and wait for something to happen. If you don't know where to break in your code, because you don't even know where the issue might be happening, it's totally reasonable to break at main. The way that you set a breakpoint is right here to the left of the line numbers you'll notice that my cursor turned into a little like pointer finger there. If you click once, you get this sort of red light, or a stop light. That's basically your indicator that you have set a breakpoint. So my program will compile, and it will run until it gets to line six. At which point it's going to kind of freeze. And from there I can make very slow steps through my program. Usually when we're running our program it's like you compile it, and it's done. Here we can really force it to slow down, and look what it's doing line, by line, by line, and see if we can see where the issue might lie. So I've set a breakpoint. Now I need to run debug50. debug50 ./debug1. Cause that's the name of the program that I wanted to test. Hit Enter. And if you've watched our CS50 IDE tutorial introduction video you noticed I talked about the debugger tab. You'll notice the debugger tab on the right has popped open. And I'm giving a little bit of information here. I have information about the call stacks. So if I had multiple functions being called it would tell me which function I'm in at the time, and which functions are waiting for stuff to happen. Local variables in my program. There are values. What type they are, and so on. Really useful stuff. And my program has frozen, right. It has not printed out "please provide a number." It's just waiting for me to do something. And the way I can control what my program does is up here at the top. I have resume, which is basically just going to speed through my program again, ignoring any more breakpoints. I have step over, which is going to move one line ahead. And generally you're going to want to step over any functions that you did not write, because if you step into them, which is the next example, it's actually going to go and look at printf's code, and start executing printf's code one line at a time. And we can probably assume that printf is OK. And we can probably assume that getint, for example, is OK. So we don't need to step into those. We can step over them. If you're going into a block, for example, like an if, or a function that you wrote, you might want to step into it, unless you happen to know for sure that it's not an issue in that function. So you can start to proceed through those a line at a time. And then, lastly, there's step out. We're not in anything right now. So we can't step out. But if you step into a function and you realize, oh, I really shouldn't have done this, you can step out, which will just proceed through the rest of the function, and then pick up again at the next logical spot. So what I want to do here is start to proceed one line at a time. So I want this to print out "provide a number." So I'm going to step over this line, which is going to execute it. You can see it at the terminal there at the bottom. Then it's prompting me to provide a number. So I'll give it a three. One seems to work. So I'm going to give it a three. And I'll hit Enter. And then I will again step over. If x equals one printf. You gave a one. So I kind of want to step into this. And in particular, notice what's going to happen here. Notice that right now it says my variable x has a value of three, which is what I expect. But as I step into this, the value of x has changed to one. It's no longer three. So this statement is now true. It says, well, x is one. So you gave a one. And if I step over, that is what it will do. You gave a one. And if I stop one more time, the program finishes executing. Did you happen to see what the error there was? It's a silly little syntax thing. But I used a single equal sign instead of a double equal sign. So what I really did here is I assigned x to be one. Regardless of what x was at this point, I said x is equal to one. I assigned x to one, when I really meant to do this. If x equals equals one. And now let's just save this program really quickly. And we'll recompile it, because we have to recompile a program if we want to rerun it. Use equals to turn this comparison into an equality assignment. Now, the reason that this works is because I had put this extra set of parentheses in here. Generally if you tried to do this it wouldn't work. But I had to force it for purposes of this example. So I had to get rid of those parentheses as well. We'll try one more time to compile this program. Now it compiled successfully. We'll clear our terminal window. And let's try and debug50 one more time, just to make sure that everything is working the way we expect. We still have our breakpoint set at line six. So it's still going to freeze at the first possible opportunity after that. I want to step over this to provide a number. I'll give it three again. Step over. x is now equal to three. You can see here on the right. Now, let's step into this. Can't step into it. Why? Because x is not one anymore. So that next line of code on line 13 there is not going to execute because the condition is no longer true. So before I was assigning x to be one. And that's why it would go in. And thanks to debug50 I was able to see, oh, wait, this three is unexpectedly changing into a one. Maybe there's something going on wrong in that area. So that's one way that debug50 was able to help isolate where in the program things were going awry. All right. So I'm going to hit Resume here just to end the program, have it finish doing its thing. Didn't print anything because we didn't have a one. And now let's take a look at program number two. So I'll clear my terminal. And let's try and execute the debug2 program. "What percentage grade did you get as a number?" Well, let's say maybe I wasn't doing so great in CS50 and I got a 50%. My grade is an E. OK. But let's say maybe I'm doing a little bit better and I got a 75. So that should hopefully be like a C. Eh, I got an E. Wait a minute. What if I got like an A, I got everything? E. Well, again, this isn't doing exactly what I expected it to do. So let's take a look at the source code. And then let's take a look at debug50 and see if maybe it can give us some clues here. So I'll open up the source code for debug2. And I'll just close this one here. And I'll also just clear the terminal screen one more time. All right. So what percentage of the grade did you get as a number? Get an integer. What's happening here on line eight is I'm just transforming whatever the user gave me into a multiple of 10, so that divided by 10 times 10, that's a trick that works because of integer division. But say the user gives me 93. 93 divided by 10 is nine, not 9.3. Remember, its integer division. And then 9 times 10 is 90. So I'm just basically dropping off the ones place of every number here and just transforming it into a multiple of 10. And then I have a letter character for a grade. It will get stored there. And then I'm using a switch statement, which is one form of conditional statement, to look over the different possibilities. So if I have a 90 or a 100 after executing lines seven, eight, grade should be an A. If I have an 80, it should be a B, a C, a D, and an E, depending on what my scores are. But it seems like every time I'm doing this I'm getting an E. And, again, if you're familiar with switch statements, you might see what the issue is here. And if you're not familiar with switch statements, that's OK. Because switch statements are a little bit strange the first time you use them. But debug50 can help us figure out what is happening here. So first thing we got to do, of course, is we got to set that breakpoint. So I got to figure out where I want to set it. Well, it looks like I might have already set it here at line 11. And that seems like a fair enough place to do it. So let's set our breakpoint there. So our program will not freeze and pop open for us until we get to that point in the code. So we'll use debug50 on the debug program number two. "What percentage of the grade did you get as a number?" So let's say I got 100. And now we're frozen. And notice over here, grade has been set to negative one. There's no value there. But that makes sense, cause we haven't assigned grade equals something yet. Percent is 100 and 10th is 100. So we have the value we're expecting. 10th is 100, which means when we get to line 13, grade A should get triggered. So let's step in one line at a time. All right. So notice that my grade is A. And then it's B. And then it's C. And then it's D. And then it's E. And then if I step over, that's what now gets printed. And that's not what I expect. So it seems like by using debug50 to step slowly through my program, what was happening is I wasn't just triggering the one case that I wanted to hit. I seemed to be hitting all of the different cases. And the grade kept changing. It started out at the right thing. It started out as A. And then it became B, and a C, and a D, and an E. And the reason for that, and again, we'll cover this on our video on conditionals if you haven't seen it already, is that at the end of every case option for a switch I needed to have a break. I only ever want one of these things to be true. Now, it's true that you might end up in a situation where you actually want the behavior of falling through the cases, as it's known. But that's not what I want here. I want explicitly to be assigned one value. And then I don't want to look at the rest of the options. So what happened here is I just fell through. I kept executing every line of code from case 100 down. If I had started out at 80, it would have given me a B, and then a C, and then a D, and then an E. And if I started out at like a 60, it would have given me a D and then an E. So I don't want that. So I have to include break statements at the end of all of my different options within the switch. And if I save this and I recompile it, and I clear my terminal and run it one more time, if I get 100, now I have an A. All right. That's good. If I get an 80, I have a B, and so on. And so, again, by using the tools this time of seeing that I was hitting all of these different lines of code, not necessarily that variables were changing-- although it was useful to see that A was turning into B and turning into C. What was more useful for me there anyway was seeing that I was hitting all of these lines of code that I shouldn't have been hitting. I should have only been hitting the very one, which was case 90, colon, case 100 colon. Because that was what I had typed in, a 90 or a 100. So it was strange that I ended up falling through. And being able to see it go line by line, as opposed to just the program running so quickly because it's so short, that was a good clue for me. We have one more buggy program. And let's take a look at that one and see if debug50 can help us figure that one out as well. So I'll close this code window here. And let's run debug3. How many stars? Five. Give me six. How many stars? Zero. it gives me one. How many stars? 40. Well, I'm not going to count that, but it's 41 is what it is. So I'm getting a few too many stars. But if I look at my debug3 source code, I'm going from zero to x, and I'm printing out a star for every time. So it seems like that should be working just fine, but it's not. So maybe the debugger can help us figure this out a little bit more cleanly. So let's pop it open. Let's first set a breakpoint. It seems like the integer part is maybe not working. So instead of setting it to breakpoint after that, maybe it somehow is turning five into six? I don't know. Let's set our breakpoint before that. Let's just set our breakpoint right at main. And there was a breakpoint here. We'll just get rid of it. You can double click on a breakpoint to make it go away if you don't want to use that one anymore. And you can also set multiple breakpoints. We're not doing it in these examples. But I could set a breakpoint at first spot, and then maybe another one a little bit later on. It would stop at that point as well. All right. So let's try and compile this. So really quickly recompile. And then we'll run debug50 to take a look at what's happening in this program. All right. How many stars? So, again, we broke at main. So it's going to freeze at line six and wait. If I step over this line, it will print out how many stars. Let's say that I want five. Then we'll step over this. Because we don't need to step in to get in presumably. And when we get through, I's value is 32,677, which is actually OK. It hasn't been set yet, because line nine has not executed. But x is five. It didn't somehow transform into six. So what I now know at least is that my error somewhere happens from line nine down. It's not line six. It's not line seven. It's got to be after that, because x has the correct value. It has five. It didn't somehow get transformed into six. But I's value is 32,767. This is a red herring. This is not a bug. Like I said, line nine has not executed yet. Once line nine executes then I will be set at least to zero, as we'll see right now. So we'll step into this loop. Notice now I is zero, which is what we expect. Then it will print a star. I'm going to step over that, because I don't want to step into printf. So we'll step over. Notice that it printed one star to the terminal. Step again. I is now one. That's what we expect. We went through one iteration of the loop. I incremented by one. I'm going to step over. Print. OK. Two. Three. Four. Now, at this point I should be breaking, right. I'm going from I is equal to zero to I is less than or equal to x. Uh-oh. I see my bug now already, right. I'm counting I is equal to zero to I is less than or equal to x. So I saw it even before debug50 saw it for me. But it's true if I step into this loop, now I's value is five. So it should have stopped if I did what I intended, which is I is less than x. But it actually went into the loop, which means it's going to execute this line of code again. And now it's true that if I step into this, I goes away, right. Because if I became six, this loop would no longer run. So it goes away. And I break out of the loop. And I jump down to line 12. But I've printed out six stars instead of five. Now, again, these examples are a little bit contrived for the purposes of showing you how to use the debugger. It's not supposed to necessarily find the bug for you. But it's supposed to slow the computer down enough that you can see the steps that it's doing. So the values on the right, being able to track what values variables have is useful. Also I didn't show this, but you can actually change the value of variables if you want. I could say x is equal to 23. I could do this in the middle of my program running, between doing different lines to change things, to see if the behavior is affected in any way. It wouldn't have been that useful here because I've already broken out of the loop. But if I wanted to somehow make it artificially print more and more because I haven't figured it out yet, I can do that. You can do that with any local variable there as well. You can use this to inspect strings and arrays as well, which we're not going to get into here, because this is just supposed to be a general tutorial. But, again, the goal is not so much to be, here's the bug. It's to slow things down enough that you can see exactly what the computer is doing, and see if at any point it deviates from what you expect it to be doing. And that's what all these debugging tools are used for, particularly this GDB based one called debug50. But eprintf. Make sure that your program is printing out sort of sanity check messages in the right spot. help50 is supposed to give you some clues about where you might have compile time errors. And debug50 is supposed to help you figure out where your runtime errors are. So use all these tools in conjunction, and you'll be well on your way to debugging code like an expert in no time. I'm Doug Lloyd. This is CS50.