Hello and welcome.
In this lecture I'd like to talk about debugging.
This is a very important concept that you need to understand specifically, you need to understand some
of the tools and some of the methods that you can employ to find and fix bugs.
So debugging is the process of finding and fixing errors in a program.
Usually logic errors.
And I'm going to tell you the difference between logic and syntax errors, but it can also include compiler
syntax errors.
It's the entire process for syntax errors.
Debugging is understanding what the compiler is telling you.
You always want to focus on fixing the first problem detected.
I want to go through some of the common compiler errors and warnings that you may may get, but one
of the things you always want to know about the compiler is that the compiler may give you a number
of errors.
You always want to focus on the first error because that first error may result in other errors.
So it can be very misleading if you see five errors in a in a program that the compiler is telling you,
you might only have one error and that first error might be causing the other errors.
So it's important to always look at that first top error and fix that one first.
In this very simplistic example, we have multiple errors in this code.
We don't have an.
We don't have a beginning left bracket, curly brace.
We don't have a semicolon.
We're returning a string.
And so when we save this and we go to compile it and you actually see some red errors, when we go to
compile it, it actually tells us we have three errors.
You never want to just select the last error on line seven, even though in this case it's an actual
error.
You always want to just fix the first error.
So you see the first error listed here and you fix that first error and then you recompile and you run
it again.
But the reason to do this is only to fix that top level error is because that error may cause other
problems.
So always focus on fixing that first error debugging and the process of finding and fixing bugs can
can be very complex from finding simple errors to collecting large amounts of data for analysis.
It all depends.
Sometimes you don't have any information provided with you except logging, and in that case you have
to analyze things.
Sometimes you'll have bugs that aren't repeat repeatable.
You can't reproduce it because it happens in such a rare occasion.
That's usually when you have to analyze logs or some some variation of that.
The ability to debug by a programmer is an essential skill.
It's really a problem solving skill that can save you a tremendous amount of time and money.
So it's an important skill to learn.
It's an important skill to develop.
It takes practice.
You're not just going to it's not just going to come to you.
There are certain techniques that you can apply and methods that you can apply to make you better at
debugging.
I know quite a few number of developers that are really good at writing software writing code.
They understand the language very well, but their debugging skills are very poor.
They may get a bug and their first inclination to solving that bug is really kind of off track.
And again, this comes with practice.
You'll get better and better at debugging and understanding how to find and fix problems, but it is
something you have to have to learn as well.
The maintenance phase is the most expensive phase of the software life cycle.
The maintenance phase is after you release your product and you now have to maintain it.
Maintaining it means fixing any new bugs that come in and also adding new features.
So, for example, when Windows releases a new operating system such as Windows 10, they're not done
producing that software.
It's going to be out in the field for probably ten years.
And it's still Microsoft's responsibility when any new bugs come in or they find new bugs for that software
to fix it.
And that's why they push auto updates and they do things like that.
And then they'll also notice that the operating system will come out new features as the market changes
and as technology changes.
So maintenance is very important.
And as a result, finding and fixing your bugs is very important.
Understand, though, that bugs are unavoidable.
All software have bugs.
Nobody can write perfect code and it's not even necessarily about writing perfect code.
It's just things change over time.
So that may result in bugs as well.
You have to understand that just because software has bugs does not mean it's poorly written code.
It's kind of unavoidable.
So it's okay to have bugs in, you know, when you when you find and fix them, it's even greater because
you actually feel like you're accomplishing something.
Common problems.
And we're going to talk about more about specific problems related to see.
But common problems when you're trying to debug are logic errors.
Logic errors have to do with not with the opposite of kind of syntax errors your code actually compiles.
It works, but it doesn't do what it's intended to do.
So there's some kind of logic or intended functionality that's incorrect.
Maybe you have an IF statement and you're jumping into that if statement on the wrong condition.
Maybe you have a loop and the loop is not executing the right number of times or you're not sorting
correctly.
Functionality is broken, but the compiler says, Hey, everything looks good.
Here's an example of a logic error.
A logic error is where the syntax is correct, but the desired results are not correct due to some kind
of logic problem.
So in this code you see the for loop has a semicolon at the end that is syntactically correct.
But you're expecting this for loop to run five times.
But it's not.
It's only going to one run time.
So if we say build and we build this project, we do see a warning, which is nice and you should always
fix all your warnings.
But it did compile successfully.
So when we run this project, you're going to notice that it actually only runs one time and prints
out Hello World once.
So this is an example of a logic error.
Syntax errors are directly related to the compiler.
These are things not following the rules of the language.
You don't have a semicolon at the end of a statement.
You don't have an ending, right?
Parentheses with a left, parentheses.
You're missing a bracket.
You're not using the right keyword, things like that.
Syntax errors are usually caused by the compiler Logic Errors are going to be caused by the programmer
during the testing phase.
Memory corruption is also a common problem, especially in C.
If you're not freeing your memory after allocating it correctly, you can have things like dangling
references and you can also have memory issues related to memory that's not being used.
You can have memory leaks.
Here is an example of a memory error.
In this particular code, we actually have a memory leak, so we haven't talked about pointers yet.
We haven't talked about dynamic memory allocation.
But in C you can allocate memory while your program is running.
You can use a malloc function.
And so here we're marking, we're allocating memory, but we're never freeing it.
So we can build this code and we can run it and everything's good to go, but we're not actually freeing
this error.
So what we'd have to actually do to correct this memory error is we'd have to say free and then we have
to say free and X to free that pointer.
But whenever you allocate memory, you don't free it.
That's a memory leak.
And then another example would be if we don't allocate the memory, if we create a pointer and then
we try to assign it, we're going to have memory corruption because that memory doesn't have any allocation
for it and we're trying to assign a value to it.
So that's just a little, little tidbit of some examples for memory, corruption, performance and scalability.
This could be a problem where your functionality is intended as it is and your code syntactically is
correct, but it's not very efficient or you don't have the hardware to support it.
So if you're running a server and you don't have enough hardware to support all the requests, it's
going to run really, really slow.
Or if you wrote your program to work with ten users, but when it gets a lot of users, maybe it's not
as fast because the database gets bogged down or whatever the case may be.
Code works, but it's not designed correctly to handle many, many amounts of users or whatever the
case may be.
Lack of cohesion.
This can be a problem in your code that reduces quality related to your code is doing too much or a
function is doing too much.
It's not a single focus or even an application has too much functionality, has features that are never
used, and so this would probably require a redesign.
Here is an example of a program that would be not modularized, that's not cohesive.
You have one big function that's doing way too many things hundreds of lines of code, thousands of
lines of code.
You want to break a function down into helper functions, separate modules.
That makes your program much more cohesive, easier to maintain.
So instead of having like a big main method that does initialization reading input, find some, find
difference, find product, find quotient, you could create all this code and put it in helper modules
and separate the code and so the separation can help find and fix your bugs much easier.
Cohesiveness is always important in C and it usually is resolved where you have high cohesion by providing
helper methods.
Tight coupling.
This also has to do with quality of code.
This is where you have a lot of dependencies.
Coupling is different than cohesion.
Cohesion is your single focus.
Coupling is where you have dependencies between maybe functions or between objects.
We talked about coupling, we talked about global variables.
If a global variable variable is being used in a lot of different functions, it can cause dependencies
between those functions.
So what you want to strive for when you write code and you're writing high quality code is you want
to have very high cohesion and very low coupling.
The debugging process.
The first thing you have to do before you can find and fix in any error is you have to understand the
problem.
This is relatable to just don't just dive in and start looking at the code, sit down with the tester,
understand the requirements, understanding what's going on.
Because if you don't understand what's going on, you're not even going to be able to.
You shouldn't even attempt to try to solve it.
Once you understand the bug, try to reproduce it because reproducing the bug can help you gather more
information.
It's not always possible to reproduce bugs because they may occur in really rare circumstances.
So for example, bugs that have like race conditions or even memory corruption may only happen in a
very specific use cases and they may be intermittent problems.
And is that the case?
And you can't reproduce it.
It's going to become harder and harder to debug.
If you're able to reproduce it, you can usually gather enough information to fix it.
Most problems are fixable.
Also, you can have problems with not being able to reproduce a problem if you're multithreaded environment,
parallel processes or threading problems.
Those are also very hard to debug, but definitely try to reproduce it once you've figured out what
the problem actually is in terms of you're able to understand the once you're able to reproduce, if
you're not, then you want to try to approach apply some kind of divide and conquer.
You want to try to isolate the bug, simplify the problem, because being able to reduce is going to
give you information back, but it might not necessarily tell you what part of the code the bug is in.
So what you can do to try to simplify the problem is remove parts of the original test case so that
you're not testing too much.
You can isolate the problem even even smaller by saying, Oh, it only happens in this specific situation.
You can also comment out code.
We talked about functions just having stubs and not having implementation.
Sometimes that's employed in testing so that it can limit the problem set.
Also, if you comment out code, you're going to have less code where the bug can actually happen.
So as you're commenting out code or backing out changes, the bug may go away and if the bug goes away,
that tells you that the the code that you commented out may have been causing it.
What you essentially want to do though, is turn a large program into a lot of small programs, either
via testing or commenting out.
Another common technique is to have unit tests around your code.
If a unit test fails, that can tell you exactly where the logic or the problem might be in your code.
Once you isolate the bug, you and that will help you find the origin of the problem in the code, the
specific code that's causing the problem.
Sometimes this will employ using debugging tools such as a debugger.
We're going to talk about a debugger in specifics, but usually depending on what IDE integrated development
you use, there's debuggers that come with the IDE.
There's also debuggers that can land standalone.
The big important debugger that we're going to use for C programming is called GDB.
It's the GNU debugger, so it's a nice tool that's actually a graphical tool where you can stop the
execution of your program and add in breakpoints.
And then as you stop the execution, you can go line by line in your code and look at the value of variables.
So you can see exactly what's going on in your code while it's running.
It's an invaluable tool.
It's just it's so useful.
We're going to run through the debugger.
I'll show you how to add breakpoints.
I'll show you how to set up a watch so you can look at variables as they change and also then step through
the code.
After you've identified the origin of the problem, you then have to fix it.
This takes a lot of experience and practice.
Just because you found the origin of the problem, you still may not be able to fix it just based on
your experience.
This sometimes includes redesign or refactoring of other code that that especially with scalability
problems or performance problems, you may have to redesign to allow your software to be more scalable.
So it's not always easy just finding the problem and then fixing it.
The problem sometimes, especially even more even harder problems like threading or parallel processing,
those are even more difficult to fix.
But as you write more and more code, this is going to become easier to do.
And then lastly, you got to test like crazy.
Always test test many different boundaries.
Good cases, bad cases in between cases to verify that you have indeed fixed the problem.
Some of the techniques and tools that are available.
One common and very, very used tool is to use print statements.
You can use print statements to basically trace your code.
So in C you can use the print F statement and you can put it at multiple lines in your code to signify
where you are.
This is also useful, very useful when your program crashes.
It can help identify where it crashed.
As you're adding these print statements tracing through your code, you can add output in the print
statements to see the value of variables because the value of variables may be affecting state and may
be causing logic issues.
So just adding print statements is very, very useful.
It can show the flow of execution and it can help isolate the error.
Debuggers as I mentioned one in GDB, those can monitor the execution of the program.
You can stop it, you can run line by line, restart it, set breakpoints, watch variables and memory.
Very, very helpful when trying to diagnose where a problem resides.
Log files.
Log files are also very useful, especially for bugs that cannot be recreated.
You can analyze them at a later date to see exactly what was going on when the bug happened.
So this means that you have to provide good logging statements in your code itself in order for this
to be useful.
So you may create your own log file and then write to that log as the program is running so that if
you do have an error, you can send those logs out to developers.
We employ this quite a bit at Xerox because when we encounter issues, we can't always reproduce them
or they may be encountered in the field.
We need to be able to access those logs to see what was happening when that bug happened.
Monitoring software.
There's software that can monitor and have runtime analysis of memory usage, network traffic threat
and object information.
This can tell you if maybe you're allocating too much memory and not deleting it.
If you have a tool for memory usage network traffic that can tell you whether or not you're sending
too many packets out or whether or not your software is scalable, if you have an increase in network
traffic and you see that your your your response time is degrading.
You can also monitor threads and objects information for parallel processing to see what's going on
in certain threads and what what's going on in another thread.
That will help you diagnose any thread kind of problems.
Some other common debugging tools that you can employ to find and fix your errors.
You can use exception handling.
Many programming languages have exception handling.
Unfortunately, C does not an exception handler will actually prevent crashes and do a lot of things
at runtime.
It's doesn't apply to see, but it's still a good thing to understand.
Trycatch blocks, things like that.
Static analyzers.
There are many static analyzers available for C static analyzers.
Analyze the source code without the actual executable.
Running the program doesn't need to run.
It goes line by line and looks at the source code.
And as it's parsing the source code, it looks for any known problems.
So it can check semantics.
It can make sure that.
That's it.
You don't have any uninitialized variables if you're if you're allocating new memory that you delete
it.
If you have any dead code where code is not actually being executed, you can delete that code.
It also analyzes things like race conditions and deadlocks.
We employ a couple static analyzers where I work.
We have one called Coverity, which is pretty popular, and there's a code sonar tool that we use as
well, and we'll run this on our entire baseline, our entire source set.
And whenever we introduce new files and new code, that tool will be run on that code and will be able
to identify immediate issues with the code as far as uninitialized variables, memory leaks and so forth.
It does not analyze syntax.
That is what the compiler is for.
There's also a number of test suites that run a set of comprehensive system end to end tests that you
can run on your software.
Testing is always important, but if you can automate it somehow to run certain tests every time you
do a new build, for example, that can be very helpful and this can actually prevent errors, but it
also helps diagnose errors as well.
Debugging the program after it has crashed.
We're going to talk about something called the call stack.
This is a very useful mechanism to see, especially in a language like C, where you have a crash and
all you the only information that you get is something like segmentation fault.
When you have that crash, what happens a lot of time, depending on how it crashed, a core file will
get generated and you can analyze the core file to look at the call stack to see exactly where that
program died.
It will tell you the line of code where the program died.
And so we're going to learn about the call stack.
And as far as analyzing core files, that's more of an advanced technique.
But in C, a core file can be generated and you can analyze it.
That's a little more advanced technique, but that's another way you can do debugging on your software.
What about preventing errors?
We know that debugging is finding and fixing errors, but if you can prevent errors, that's even better.
One of the ways that you can prevent errors is you can write high quality code.
This is really not a focus on many software engineers.
A lot of software engineers write the code, and as long as it works, that's all they're concerned
about or as long as the output is correct, then they consider themselves done.
But there is such a thing as poor quality code and you don't want to write that.
I've demonstrated some of this during the lectures, but things like using meaningful variable names
very important.
Or if you write inefficient code where you're using loops maybe more than you need to, or using an
if statement or you don't need to do that.
All of these types of things constitute poor quality code.
If you have a function that's doing 100 other things and it's not cohesive, if you have 5000 lines
in a function, if your code is not organized in a modular fashion or if you have many dependencies
between functions, again, all of this is poor quality code and you want to strive not to do this.
You want to employ techniques where your program is organized.
It's easy to maintain, it's highly cohesive, it has low coupling, and you use meaningful variable
names.
Very, very important.
Again, it's very important also because the maintenance phase is so, so expensive.
If you write poor quality code, it becomes much harder to maintain.
It's harder to isolate errors and it's harder to add new features.
So employee good high quality techniques when writing your code.
Unit tests.
Unit tests help prevent errors tremendously.
Unit tests are executed usually automatically when you compile unit tests can help avoid regression.
That's one of the most important factors.
Regression is when you write new code and you introduce a bug on something that was previously working.
You broke old code.
You don't want to do that.
If some software is working and then something new that you did broke it, you want to immediately be
notified and unit tests can help do that because unit tests will run on that old code and it will say,
Hey, this was working, now it's failing.
It also can find errors in new code before it's delivered, because again, they run after you build
and it will immediately tell you something's wrong.
There's a concept of test driven development which is getting really, really popular.
This is where you write your tests first and then you develop your code after.
And the goal here is when you write your first test, your test is failing and then you write the code
to get it to pass.
There's a lot of advantages to TDD and it's kind of a reverse way of thinking.
Normally people when they write code, they write the code first and then they write the test after.
But with TDD, it actually gives you a different way of thinking so that you're working in really small
chunks and you tend to write very cohesive code and you don't have the code, doesn't have a lot of
dependencies because guess what?
If you're able to test code, it means that the code doesn't have as many dependencies.
If your unit test is really, really large and you're mocking out all these different things, it's
usually a big indicator that your code is very, very dependent on other things.
So by employing TDD, it forces you to to write very loosely coupled code that doesn't have dependencies
and it forces you to have your functions be very cohesive.
There are a lot of advantages of TDD.
You also want to provide good documentation and you want to do proper planning.
Don't just dive into code and start implementing.
Write down your design on paper.
Utilize pseudocode.
Right.
Plan your code.
If you plan your code, you understand the requirements and you have a good thoughtful design, you
can prevent many, many errors.
And I've mentioned this throughout the course, but it's very, very important to have a divide and
conquer type approach, work in small steps and constantly test after each step, do not write 1000
lines of code and then say, okay, compile and test.
That is not a good approach.
Avoid too many changes at once.
When making changes, apply them incrementally, add one change, then test thoroughly before starting
the next step.
This can help reduce all sorts of bugs and it also limits your problem set.
So if you do have a bug, you immediately know where it is in the code because you just compiled and
you just added that line or number of lines.
Again, I cannot emphasize the importance of debugging writing high quality code, preventing errors.
These problem solving techniques will save you a ton of time and make you a much better programmer top
tier programmer.
So in the next few lectures, I want to talk about some of the common errors that we see in c c programming
language.
Also, I want to talk a little about the call stack.
I want to go through the debugger that introduce you to the menu options and how you can stop and run
watches and so forth.
And I also want to help identify some common compiler errors that you see how to interpret the compilers,
compiler warnings and errors.
What does it mean if the compiler says this?
What does it mean if the compiler gives you this warning?
Thank you.