Debugging C/C++ with LLDB Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey everyone welcome back to the channel today we're going to do a quick start guide into lldb this is going to be a tutorial video on working with ldb directly from the terminal uh i'm not going to go over how to install it because this video would get far too long i would recommend looking up how to install ldb or llvm the entire suite of tools for your particular operating system or linux package manager and whatnot but this tutorial will focus on using it uh setting breakpoints watch points navigating around inspecting things that sort of thing so without further ado let's dive right into it so ldb is a standalone debugger that you would run from the terminal and use you would attach it to your executables and then set some breakpoints and navigate and inspect and whatnot um most c plus ids or editors will will have their own integration with a debugger uh visual studio uses microsoft debugger xcode will use lldb directly other tools will use gdb which is another standalone debugger they usually have different modes for integrating with ids and stuff like that but what we're going to look at today is actually using it directly and make getting ourselves familiar with uh the various commands that you would use to attach to executable and to set breakpoints and to inspect variables and all that stuff so let's take a look the first thing we need is a program to debug so i've written this short c plus plus program that doesn't serve any purpose whatsoever other than to demonstrate the various features of ldb that we'll be going over so don't look for any reason uh for why any of the stuff happens why any assignments or function calls exist this is just for demonstration purposes um so let's actually switch over to the terminal and i'll show you how to compile for debug so if you're compiling your code directly with the compiler like this uh what you want to add is dash g and then your compilation unit which in our case is demo.cpp and when i hit enter gcc will compile this with debug symbols enabled if you're using clang it's the exact same command um and that will now compile it with debug enabled if you're using cmake i recommend you take a look at this variable that needs to be set in your build in order for you to turn on debug builds um what you do is you you set this to either uh debug or so actually i'll show you real quick if you're running cmake you essentially pass this in and you'll say cmake build type equals debug and then you know your cmakelist file and when you when you pass this variable in and set it to debug then what cmake will do is it'll make sure that your compiler is called with whatever flags in order to compile it with debug symbols enabled i'm not going to spend too much time on that but just know that you have to enable the debug symbols in order for lldb to be able to pick up your source code and know variable names and to just be able to provide you source code information as you're navigating around and setting break points and all that kind of stuff so the first thing we're going to do is we're going to actually start lldb and tell it what program to run so by running lodb and passing in the name of our program as a as an argument here then ldb will run and create a target that it's then going to execute and attach to whenever we we actually run the program if your program has arguments this is worth noting you you would pass those in so if you have to type something after your program like i don't know a folder name or something right if your program accepts command line parameters then you would essentially pass those in this way so you do hyphen hyphen space the name of your program and then whatever arguments afterwards and if you want to actually set those from right from within lldb there's a much more complicated and annoying to type version which i don't know why anybody would do this but you can do that right from within lldb this way and in fact if you actually run this command lodb will actually show you that it this is the command that you can run from inside of ldb to do the exact same thing um i'm not going to spend too much time on that it's this to me i i don't know why anybody would prefer this over just doing that uh but this is how you specify those parameters and then we're going to run our actual executable from within lldb by using the run command or just r for short so let's run ldb we're going to pass a dot out and now as you can see we've uh we've started ldb and like i said it creates its own little target so that it can run our program and it sets the executable to um to our actual executable here the other way to do it is to just run lldb and then you can do file a dot out so when you run ldb you're in the current folder and that's where it'll look for files so you can do file space you can do you know if you really want it you can do dot dot slash to go one folder up or whatever but in our case we're just gonna tell it to use the current directory a dot out and it does the exact same thing and now if we do run or just r it runs the application uh the other thing that i was showing earlier is that what what this program does is if you pass a parameter uh it if it checks if you pass the parameter and then it just prints it back out i did it just for testing so if i do a dot out hello then the first thing it does is it checks if i typed in anything after a dot out and and then it just prints it back out right that's what the sif is here so uh this is what i did uh if i don't put anything then obviously it won't echo it back out now if you wanted ldb to uh to use that parameter every time it runs you do this a dot out and then hello so it's the exact same thing you would type in in the terminal except uh you need to pass it in this weight into ldb so when i do that we start ldb it sets the actual parameter uh using this argument and then when i run it you'll see that it now ran it as if i typed hello after my executable this is very useful depending on what kind of utilities you're working with and you don't want to keep typing certain arguments or whatever you just specify those when you launch lldb and then you can use the same test data or same arguments every time you run your executable so obviously the next thing we want to do is we want to actually set a breakpoint and be able to stop somewhere and take a look at certain things so i'll quickly show you how to set breakpoints one thing to keep in mind is that there's a shorthand for that as well and and i'll show you how this works but the main command if you want to just know exactly what it does break is the breakpoint command set so we're setting a breakpoint and then we could do dash f to specify the file and dash l for the line number inside that file so if we actually look at our code and we say let's set a breakpoint on line 36 what we would do is then go inside lldb we'll say break set dash f and know that you could actually do tab completion here so i'll type just demo and then it'll pick up that i have that file on my project uh oftentimes it'll actually suggest all kinds of other stuff that might be picked up from uh like the the runtime library and and other operating system things that are loaded when you when you actually attach to an executable so uh you know if i do just d e then you'll see there's all this other stuff uh that that it might suggest this is these are things that are pulled in because the application requires certain libraries and certain facilities to be loaded in order for uh the application to actually operate uh but we're gonna type just demo dash l and then we said we want it line 36 and we're going to set a breakpoint at line 36. um now when i run the application there it is we have our our debug information loaded so it knows the actual code that it's stopping at line 36 is exactly what we just looked at here we want it to break here and um yeah and we've our break point works if i um exit and then actually let's go back in and then i'll show you you can do br set the exact same thing demo and then dash l and this will set it the exact same way but the way i prefer to use is just b b will uh will set a break point and here it's actually a lot easier to you don't have to do dash f f.l you can just do demo.cpp and then call in the line number and then this does the exact same thing and it's a lot more straightforward um so this is just i find this to be the quickest way to justify you know if i want to debug something i'll just hop in bspace the file name and and the line number and then it just sets it uh the other thing you got to note is that you you don't need to use just line numbers you can actually put breakpoints on symbols so you can put it on a function so we have a function called square and every time that function gets called i want a breakpoint so if i if i want to set that i can say it'll pick it up as a symbol and it knows that this function here this function here because it has all the debug information it knows that this function exists so when i do you know like i just did sq bsq it and do tab it'll complete it for me it'll know that i have this function in my program and if i do that now anytime that function gets called it'll break so if i run again obviously we haven't called it yet but that function gets called where is it it gets called right here right we're actually right at line 37 so if i go continue it'll actually break inside the function as you can see right here and it'll it'll break on the first line of that function so let's continue again and then it gets called again and then the program terminates you could also put it on a class so if you have a class with a method for example in our program we have this class right here called demo it's very simple uh it doesn't do anything but it's a cloud it's sorry it's a struct or class doesn't matter but it's a i use the struct just so everything's public by default but we have a struct called demo which has this member variable and a function or or a member method called demo now if we want to break every time this function gets called we could just go here and do what was the command it was b demo and then because it only has one function it just completes that one function if we had multiple functions then it would stop here and allow us to start typing our function name but because demo only has one member that you could actually put a breakpoint in i'll hit enter and then we have a break point there as well uh and if you have a namespace you have to take that into into account because if you're just for example let's go here and take a look at this function we have this add function here and if we wanted to break inside this function then if i just start typing b add it's it's it's not going to find it it's going to show me all this other stuff and i'll be like why can't i break on that that's because you have to factor in that it's inside of a namespace so if i go bldb demo again it's the only function that's in there so it's going to automatically complete that function but you have to use the same format to actually specify the namespace that the function is in in order to actually be able to find it and then if i do that as you can see we have another breakpoint so now we have four breakpoints which leads us to our next point which is manipulating them so if you wanted to actually list your breakpoints you can do br list or just break list we'll give you all the break points but again br list does the same thing it's more shorthand and this shows us you know we have on line 36 which is our first break point then we have square which was our second one the one that we set on the class and then the one that we set on the namespace function now i want to reset this and the way you do that if you want to actually delete a specific one i'll show you how to do that but i'm going to want to delete all these breakpoints but let's actually delete that for the fourth fourth one first so the way you do that is br del 4 and then it just deletes the breakpoint so now if we do br list we'll see that's gone but i kind of want to just clear them all so that we can do some other demo stuff if i do br dell it says about to delete them all yes gone and now be our list we have no break points and that's it the next thing we want to do is we want to be able to navigate around our code and jump in and out of functions and and actually run the program line by line so we can issue debug commands so i'll quickly show how to navigate inside the program uh and then we'll we'll move on from there so if i run it right we have no break points right now what i'm going to do is i'm going to set a breakpoint on main which you can do this and it'll it'll put the break point right on the main function which means as soon as the program starts it'll halt so if i do that it says line 31 inside demo cpp it found main and this that's the first executable line of code inside main.cpp so i told it to break on main and then what the debugger does is it goes well this is not a line of code this is just scope information but this is a line of code so then this is where it puts the first breakpoint and now if i run this application as you can see it goes to that line 31 and it stops okay so if i wanted to just uh see go to the next line i can do n right step over means to go to the next line n um if i wanted to continue navigating i'm having some trouble without tab here uh i can do n again and now we're notice it's skipping over these right these aren't lines of code this is just source code information uh these are instructions of code right uh now we're in this line here we can continue go next and now let's say we want to inspect what happens inside square what you do here is you step into it so uh what we've been doing is stepping over that's what next means generally inside a ide or an editor if you have that button to navigate with the debugger it's actually called step over or step into that's usually what these things are called so what we want to do here is we'll say okay well i want to actually see what happens inside this function without actually setting a breakpoint so what i do is i type s which is short for step into and now it's stepped into the function in here if you have multiple function it's going to step inside each ones as they're evaluated there's an order that things are evaluated in in in these languages and it's it's if i had square times square and i'm calling square twice it's going to step into it first here and then it's going to step into it again but anyway so i've stepped into this function and i can't if i do s again there's nothing for it to step into right like this isn't a function this isn't a function there's it's nothing callable so if i do s again it's just it's going to do the same thing as just doing n and now if i go next it goes over to the next line if i do s in here it's not going to actually step into anything even though the c out is here so it knows that i probably don't want to step into standard library stuff so it kind of just steps over in here i can do the same thing i can do s or or i can actually look i can just do n and it'll go right past it but then i go oh i want to step into square again i can i can do that so i think you get the gist of it it's pretty straightforward and then i can just continue navigating and then at the end when i'm like you know i just want my program to just continue writing you can do c which is continue short for continue but c for short and then just let the program resume the next thing you're going to want to do is inspect some variables so you could navigate around your code and sort of follow the logic of your program but it's often very useful to inspect variables or arrays or or anything of that sort or any structures you can you can even inspect your your objects so the way you do that is using the print command which also uses a p for short and i'll show you how that works real quick um we'll go back to the application and run it and now we're inside of uh you know we've hit our breakpoint that we had set on main so let's just step forward a little bit a couple of lines and we're now at line 36 again we haven't run line 36 yet right we're we're currently sitting there waiting and if i do print value you'll see that it's going to show a zero because we haven't actually initialized it yet if i do next and step over that and then now this line has executed and we're waiting to execute this one so um because this line has now executed if i do print value you'll see that now there's a five where before there was a zero and that's pretty you know that's pretty obvious it's what you would expect we're now sitting at this line of code here which is about to call the function square so instead of stepping over that line let's step into it and as you can see we're now at line 18 which is where this the first executable line of code inside this function and we now have a function that takes one parameter called a and returns a squared so there's this concept of a frame so every time you call a function there's some information that needs to be retained telling your application where it needs to go back to once that function executes so every time you call a function you have this call stack you know functions calling other functions and they kind of stack on top of each other and as functions return they these for this frame of information gets popped off of the stack and then you always go back to where the function was called i assume everybody knows that but oftentimes when you're navigating your breakpoints or you find a crash or something you're going to want to see what called what and what data was passed in so you're going to want to inspect the frame uh for that particular call site or or the function that uh it actually called so in this case we're inside frame zero we're now because we're at the the most at the at the latest point of execution we're at the current point of execution rather that's now at the top of the stack frame 0 right before we're here and that was frame 0 that was the old frame zero but now that we call square that call goes on the top of the stack and now we're inspecting that and now it has become frame zero so if i do frame oops frame variable right this will give me uh the variables inside this inside this frame because we only have one local variable and that's this parameter here that's all it's showing if i now go next and now that call has popped off right we no longer we're no longer inside here we have popped off the stack and now this has become frame zero again which is the top of the stack right grows downwards you got frame zero one two three blah blah and then they keep popping off and then the previous thing that was at the top is is now gone and then the thing that was second is now the top of the stack this uh stacks are a completely separate topic but just so you understand so now we're we've popped that function call off the stack and now we're getting the result from square and putting it inside result one so let's actually run that assignment operation and now we've stepped over that line of code it's run so first we stepped into this we got the value and now we're sitting there waiting for this part of the instruction to run the assignment itself then we hit n and now we're at line 38 after it has run and then we can actually print the result or the the value of result one which is a 25. that's because we got the square of five right we we assigned five to value then we called square and we got the value of that now sometimes there's so much information on the screen that you you tend to get a little bit lost and you just kind of want to see you know like if i hit tab and blah blah all this stuff starts appearing on the screen now i've sort of lost where i'm at just know that you can always type frame select or frs and frame oops frame select like this kind of like i showed here or frs and it shows you you can always go back and see where exactly you are in in the current frame so the next thing i want to explain is uh knowing how how to work with the back trace and the various frames that are present during uh the execution of your program um if there's a crash or if you hit a breakpoint you're going to want to see what was actually going on so we now know how to inspect the frame we know how to print out the contents of variables and things like that but maybe we're interested in more than just um we're interested in more than just the current frame the current call or the current function that that we're currently inside of so let's go back to the code and then put a breakpoint inside main again and run our program so we're back in here we're inside frame zero right so one thing i want to show you is i'll run bt which stands for back trace right it's tracing back the the call stack so when we look here we'll see that we're inside main right that's our the first function so this is the start of our program this other stuff here is usually os related stuff or debugger related stuff so i'm not really interested in this what i'm interested in is this function call right here main so main is the the function that gets called when your program starts and this is uh the frame that we're now at the top of the call stack and uh this is what we're interested in so if i continue here now we're at line 37 which calls square so when we call square we're going to actually put another frame on the stack which is essentially going to keep you know it's going to have some information about how to behave after we finish executing square so once square returns let's step into it actually so once this function executes or or once this statement here rather executes and we return out of square we're then going to be back here so the frame like i said includes information that we need to go back to line 37 it's stored differently than that it doesn't just it doesn't track line numbers but it knows that we need to return back to this line of execution here and continue after uh square has finished executing so now that we're in here we're inside of the square we're at frame zero so we're at the top of the stack right because now we've called square and we've pushed another thing at the top of the stack so if we do bt you'll see that we have main and we also have now another frame on top of it right this this stack uh you'll see like the most the the most current or the the most recent call or uh the function that we're currently inside of is always going to be at the top of the stack so now that's what we're seeing here we're seeing squares at the top of the stack and it's showing us uh the various parameters and and their arguments that were that were provided so we can see that we're now inside square and uh we can do frv and and actually print the value of the variables inside this uh frame if i now step over that and i go back to the call site that actually called square if i do bt we'll see that that frame has popped off and we're back to main we're back inside main so that's pretty useful right but now let's actually continue executing and now we see we're about to call this function here demo so let's step inside of it and uh do backtrace again well there it is again right we're now inside of um we're now inside of demo so we have another frame of information on top and we can see that main called it right but what if we don't want to continue executing what if we want to actually inspect what was going on in the scope outside of this call so we we want to actually go back to this frame and inspect the variables because here you know we're calling this function here and we're not seeing um [Music] we're not seeing any variables so we're like okay well let's actually hop up one frame and inspect the way you do that is by like i've listed out here is you use the frame select function so zero i'm just using as an example here but you would specify the frame number or you can just put f for short like i said these shorthands are much more straightforward and user friendly than typing this all the time but if we do frame select and we type 1 we're now back at line 41. we haven't run this code yet right like this is still sitting there waiting we're still at this uh at this point we've just halted but we can go back and inspect every frame of execution and uh now we're we're over here and uh we can actually inspect the variables inside this frame right we can do frv and inspect those variables and we'll see all the variables and local variables that were present inside of main when we call demo now like i said there's you know these are not initialized so they're just showing junk data but uh you know you have to be cognizant of that because you know you don't want to think that these are actually initialized and and make judgments based on that so let's actually do bt again and see where uh what we're currently looking at we're looking at frame um frame one let's actually go back to frame zero so f space zero for short and now we're back to the current point of execution and we can actually step over that and continue going forward right now we're out of that and we continue our execution inside main that's really all it is when it comes to the back trace the other thing i want to show you i've put this intentional crash in here i'm dividing by zero which is not allowed and i want to i want to halt the program the program and i'll show you how that works actually so let's recompile it we'll see even the compiler is warning us that we have a division by zero because these are obviously just hard coded in so the compiler is able to see hey you're asking me to divide by zero here and it's it's this is you know it's it's catching a silly error for us but let's ignore that and just run the program you'll see that we have a core dump we have an actual exception that's thrown and the program crashes so if we want to inspect that crash let's run our program inside of the ldb and then hit run boom right now instead of just seeing this exception here we're now seeing an actual crash we're seeing an uh integer divided by zero ldb is telling us exactly what thread and what frame the crash happened at and uh what line number even right line 17 and you're able to see oh silly me there was a crash there obviously if we're just looking at this we're like what the hell happened right uh i i don't know what this crash is but when you run it inside of lldb and if you have debug symbols enabled you can now actually see what point of execution caused that crash now usually in order to debug a crash you're going to want to see more than this right we're inside of a square function that's great but maybe something led up to that so let's do the back trace again and now we can start to inspect the whole program and how it executed we can say f1 right let's jump into main okay what was going on inside of main before square was called that led up to that and you can start to inspect some of the variables obviously this error isn't or this crash here isn't dependent on any of the variables i just use the divide by zero so i can trigger a crash but maybe sometimes you know what you passed into square is dependent on what was going on inside the function that called square so you want to be able to hop around between these frames and inspect things and and be able to actually see what was going on uh and what the call stack looked like that led up to that crash and bt is the way you do that it's very simple um and you know we can also do frs and actually go back to the current point of execution if you ever get lost the current point of execution of the current frame but let's actually go back to frame 0 back to the top of the stack and then it shows us exactly where we are and where the crash happened if i now try to continue here it's going to resume with an error code but we know that the program actually crashed the next thing i want to show you are watch points and they're very useful when you want to monitor how a variable is being accessed throughout the throughout the execution of your program by default you set a watch point on a variable and it will break the execution it'll stop when it sees that that variable has been changed the way these variables are implement or the way these watch points rather are implemented is by having the cpu use special a special mechanism that that can monitor changes or accesses to a certain memory location and those are limited in number on your cpu i'm not going to get into any of the details there because frankly i i still need to look into them myself but essentially that's why this program needs to be running in order to set a watch point we're not setting a specific line number which doesn't necessarily change from execution to execution of the program but because we're telling the cpu to watch a specific memory address we need to have the program running so that um the so that the debugger knows what the address of that variable is and it can actually set the watch point so i'll show you what that means in a little more detail but essentially you set a watch point on a variable using this command here watchpoint set variable and then the name of your variable or you could set how you want to monitor that variable by using hyphen w so maybe you want to watch just when that variable is read from or when it's only when it's being written to which is the default or both read and write uh so let me show you how that works we've got the debugger here now if i try and set a by the way the shorthand for setting uh for for setting a watch point instead of typing watchpoint set variable you can just do wsv watchpoint set variable and then the variable itself so if i just try and set a variable watchpoint set variable and say global variable and the variable that i want to watch is actually this global here that i have in the program it's just sitting there initialized to 50. now normally global variables like this are kind of a bad practice because you don't want to be changing global state of your application from random places in your code that leads to some silly bugs from time to time and oftentimes when you run into these kinds of logic errors it's because you have this global state and maybe your program is so big that you can't really easily track down what's changing this variable so you just want your debugger to tell you when this variable is being changed so the best solution here to inspect that is probably a watch point so let's set a watch point on that variable right so the program's now running and it's telling us invalid process that's because it's not able to actually get any information about this variable what like where in memory that variable is so it can't actually get that memory address so it can tell the cpu hey watch when this memory is being accessed right in order to to know that we have to actually start the program halt it and then we can actually um we can actually uh check for that uh or or set up that watch point so what i'm going to do is actually just set a breakpoint on main you can set it anywhere before before the the point of interest in your code so i'm just going to set it on main right when the program starts now i can do wsv global variable and now it's going to actually because the program is now running right or sorry it hasn't actually run it i need to run it now it's running now when i do wsv global variable it can actually figure out where in memory this variable is and actually tell the cpu to watch that memory so we hit enter now we're not getting that error and we know the variable is at this address and so and we have a watch point now so if i hit continue what it's going to tell me because the default behavior is the watch point will will trigger when when that variable is changed that's the default you could also set it uh like i said um on read write or both right but this is the default here so what it's going to do is it's going to it's going to break as soon as it notices that something is changing that variable so if i continue right now that this memory has changed right it can't stop here at line 47 because it needs to actually know that that memory has changed in order for um for this watch point to trigger so what it's done is it's executed that line that memory is changed and now the watchpoint has said hey that memory changed it was written to so it needs to it's it's now going to stop and it's going to say okay look this line of code here right before the line that you're currently on modified this variable and you could be like oh that's why it changed to 75. and you didn't have to actually hunt this down anywhere right you didn't have to go through all of your source code and find places to change the global variable you actually just set a watch point on that variable and you said tell me i want ldb to tell me when that variable changes um and that's it right that's pretty straightforward now let's actually kill that process and i'll show you there's also a point in the code here that sets this variable with the contents of this global variable now maybe we want to know instead of just when it's written to we want to know uh when that global variable is actually read from so let's actually run our program again so we hit our main breakpoint the program is running and we can set a breakpoint and we're going to set a breakpoint on an global variable but we want to actually know when it's read from so let's do that and hit continue and now we know that this memory is being read from in this assignment here we're not setting this variable we're not changing it but we wanted to know when it's being accessed so i think you can see how this can be very useful when you're debugging because sometimes you don't want to go through 30 sources uh or compilation units headers whatever your your pro project is and actually look for places that are using this global variable i mean it's better to not even use global variables all over the place like that because that leads to very silly bugs but if you do run into a situation where you want to know that something uh where something is being changed from or accessed from watch points are a great word a great way to do that the other thing is you know if you have a member variable uh like for example member var right we have this class here that has member var and we want to see um or or struct doesn't matter it's the same uh concept either way but um you want to know when this variable is being changed it's the same syntax right it's uh you you have to run your program so let's run the program again and uh now that we're running right we can actually get address information we can say ws oh and we we can't actually or can we i'm not sure remember var or is it member member variable so now if we continue we can see yeah there you go so that worked so we set a a watch point on this member variable here and we're able to pause as execution whenever that variable changes and it's going to be the same thing because it gets changed twice one after the other it always like i said pauses after the actual assignment has happened and i i think you can see how these are useful and how they can help you debug certain parts of your application the last thing i just want to mention is how to actually kill the process and exit ldb there's not much to explain um kill will essentially just uh kill the running process if you're at a current break point or something and you don't want to continue executing the program maybe the program is writing to the disk or reading from the network or something whatever the case is you just want to kill the process is all that is you use the kill command and then if you want to exit ldb you can just type quit or control d so if we just uh quickly go in here and then let's set a breakpoint on main again right run the program now if i don't want to actually finish executing this program i mean i can hit continue and just execute but if this program is this long running thing and i don't want to actually like have to run through anything or you know maybe it's a gui i don't want to switch to gui and exit i can just do kill kill the program and quit and that's it i hope this video was useful uh it was meant to be just a quick start guide on on making use of ldb with the command line uh like i said i know a lot of you probably use debuggers right from inside your ides but if you're like me and you like to know how these tools work just directly uh what these tools do i find you learn quite a bit more about debugging when you actually start playing around with them directly this way um i hope this was useful please leave a comment uh down below let me know if there's uh more i should have mentioned or if there's something that you were stuck on i'd love to hear your feedback and also let me know if you'd want me to dive into any of these topics separately i'd love to do more videos on this kind of stuff so let me know what your thoughts are and i hope to see you again soon
Info
Channel: constref
Views: 5,939
Rating: undefined out of 5
Keywords: c++, debugging, lldb, programming, coding, debug
Id: 2GV0K9Y2MKA
Channel Id: undefined
Length: 40min 6sec (2406 seconds)
Published: Sun Nov 01 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.