85 lines
12 KiB
Markdown
85 lines
12 KiB
Markdown
# Debugging
|
|
|
|
WORK IN PROGRESS
|
|
|
|
Debugging is a term usually related to [computer](computer.md) technology (but sometimes also extended beyond it) where it stands for the practice of actively searching for [bugs](bug.md) (errors, design flaws, defects, ...) and fixing them; most typically it happens in software [programming](programming.md), but we may also talk about debugging [hardware](hardware.md) etc. Debugging is notoriously tedious and stressful, it can even take majority of the programmer's time and some bugs are extremely hard to track down, however systematic approaches can be applied to basically always succeed in fixing any bug. Debugging is sometimes humorously defined as "replacing old bugs with new ones".
|
|
|
|
Fun fact: the term *debugging* allegedly comes from the old times when it meant LITERALLY getting rid of bugs that broke computers by getting stuck in the relays.
|
|
|
|
**Spare yourself debugging by testing as you go** -- while programming it's best to at least quickly test the program is working after each small step change you make. Actually you should be writing **[automatic tests](automatic_test.md)** along with your main program that quickly tests that all you've programmed so far still works (see also [regression](regression.md)). This way you discover a bug early and you know it's in the part you just changed so you find it and fix it quickly. If you don't do this and just write the whole program before even running it, your program will just crash and you won't have a clue why -- at this point you most likely have SEVERAL bugs working together and so even finding one or two of them will still leave your program crashing -- this situation is so shitty that the time you saved earlier won't nearly be worth it.
|
|
|
|
## Debugging Software
|
|
|
|
Debugging programs mostly happens in these steps:
|
|
|
|
1. **Discovering bug**: you notice a bug, this usually happens during [testing](testing.md) but of course can also just happen during normal use etc.
|
|
2. **Reproducing it**: reproducing the bug is extremely important -- actually you probably can't move on without it. Reproducing means finding an exact way to make the bug happen, e.g. "click this button while holding down this key" etc.
|
|
3. **Locating it**: now as you have a crashing program, you examine it and find WHY exactly it crashes, which line of code causes the crash etc.
|
|
4. **Fixing it**: now as you know why and where the bug exists, you just make it go away. Sometimes a [hotfix](hotfix.md) is quickly applied before implementing a proper fix.
|
|
|
|
For debugging your program it may greatly help to make **debugging builds** -- you may pass flags to your compiler that make the program better for debugging, e.g. with [gcc](gcc.md) you will likely want to use `-g` (generate debug symbols) and `-Og` (optimize for debugging) flags. Without this debuggers will still work but may be e.g. unable to tell you the name of function inside which the program crashed etc.
|
|
|
|
Also as with everything you get better at debugging with practice, especially when you learn about common types of bugs and how they manifest -- for example you'll learn to just quickly scan for the famous *[off by one](off_by_one.md)* bugs near any loop, you'll learn that when a value grows and then jumps to zero it's an [overflow](overflow.md), that your program getting stuck and crashing after a while could mean infinite [recursion](recursion.md) etc.
|
|
|
|
The following are some of the most common methods used to debug software, roughly in order by which one typically applies them in practice.
|
|
|
|
### Testing
|
|
|
|
[Testing](testing.md) is an area of itself, it's the main method of finding bugs. There are many kind of testing like manual testing (just playing around with the program), [automatic testing](automatic_testing.md) (automatized testing by a program), security/penetration testing, stress testing, whitebox/blackbox testing, unit testing, code reviews and whatnot. **[Formal verification](verification.md)** is similar to testing that can reveal further bugs, but it's more difficult to do.
|
|
|
|
### Eyeballing
|
|
|
|
Quick way to spot small bugs is obviously to just look at the code, however this really works for the small, extremely obvious bugs like writing `=` instead of `==` etc.
|
|
|
|
### Manual Execution
|
|
|
|
In this method you try to go through the program yourself step by step, just as the computer would. By this you will find out just WHY and WHERE your program gets to a wrong result or to a line that makes it crash.
|
|
|
|
### Printing
|
|
|
|
Using print statements is extremely popular and efficient method of locating bugs; the idea is to use the language's print functions to [log](log.md) what's happening. By this you can e.g. find where exactly (which line of source code) your program crashes, you simply insert `printf("asdf\n");` somewhere and keep moving this print statement and re running the program until the text stops showing up on the screen - then you know the program crashes before it reached the print. Note that you can use the principle of [binary search](binary_search.md) (also known as *wolf fence algorithm*) to move the print in the code so that you find the crash place relatively quickly. Besides this prints can of course also show you e.g. values in variables so you can e.g. check WHERE EXACTLY the value changes to a wrong value and so on.
|
|
|
|
The advantage of this is that you don't need any extra debugger, the method works basically everywhere and is actually very effective, it may be all you will need in 99% of cases. { TBH I don't even regularly use debugger, debugging with prints just works for me. ~drummyfish }
|
|
|
|
**IMPORTANT NOTE** especially for [C](c.md) programmers: output is usually line buffered, so in each print you HAVE TO add a newline (`\n`) at the end to make it print immediately. If you don't do this, it may happen that the print will be executed but the output will stay waiting in the output buffer as the program crashes so it won't show up on your screen. Similarly in other languages you may want to call some [flush](flush.md) function etc.
|
|
|
|
Sometimes a bug can be super nasty and make the program crash always in random places, even depending e.g. on where you put the print statement, even if the print statement shouldn't really have an effect on the rest of the program. When you spot something like this, you probably have some super nasty bug related to undefined behavior or optimization, try to mess with optimization flags, use static analyzers, reduce your program to a minimum program that still bugs etc. This may take some time.
|
|
|
|
### Rubber Duck
|
|
|
|
Rubber duck debugging works like this: you try to explain your code to someone -- even someone who doesn't understand programming, for example rubber duck -- and in doing this you often spot some error in reasoning. Explaining code to a programmer may have a further advantage as he may ask you clever questions.
|
|
|
|
### Reducing Your Program To Minimum
|
|
|
|
When dealing with a super nasty bug in a complex program that's dodging solutions by the simpler methods, it is useful to just copy your program elsewhere and there strip down everything off of it while still keeping the bug in place. I.e. you just keep deleting functions and all the program does while making sure the bug you're after is still happening. This will firstly eliminate places where you have to look for the bug but mainly will usually lead you to reducing the program to just a few lines of code that behave extremely weirdly, like a function whose behavior depends on where you put a print statement of if you use a wider data type etc. Then you usually find the problematic line or whatever it is that's causing the bug and once you know the line, you can look at it really carefully, google the behavior of each operator etc. to really find the bug.
|
|
|
|
### Debuggers And Other Debugging Tools Like Profilers
|
|
|
|
There exist many software tools specialized for just helping with debugging (there are even physical hardware debuggers etc.), they are either separate software (good) or integrated as part of some development environment (bad by [Unix philosophy](unix_philosophy.md)) such as an [IDE](ide.md), web browser etc. Nowadays a compiler itself will typically do some basic checks and give you warning about many things, but oftentimes you have to turn them on (check man pages of your compiler).
|
|
|
|
The most typical debugging tool is a **[debugger](debugger.md)**, a program that lets you to play around with the program as it's running, it typically allows doing things like like:
|
|
|
|
- Step through the program line-by-line (typically there is are two options: step by lines and step by functions), sometimes even backwards in time.
|
|
- Run the program and pause it exactly where you need ([breakpoints](breakpoint.md)).
|
|
- Inspect values in RAM, CPU registers etc.
|
|
- Modify values in RAM, registers etc.
|
|
- Modify code on-the-fly.
|
|
- Assert if certain conditions hold.
|
|
- Link lines of [assembly](assembly.md) to lines in original source code.
|
|
- Warn about suspicious things.
|
|
- ...
|
|
|
|
Furthermore there many are other useful tools such as:
|
|
|
|
- **dynamic program analyzer**: A tool that will watch your program running and check for things like [memory leaks](memory_leak.md), access of unallocated memory, suspicious behavior, unsafe behavior, call of obsolete functions and many others. The most famous such tool is probably **[valgrind](valgrind.md)**, it's a good habit to just use valgrind from time to time to check our program.
|
|
- **[profiler](profiling.md)**: A kind of dynamic analyzer that focuses on statistical measuring of resource usage of your program, typically execution times of different functions, memory consumption or network usage. Basically it will watch your program run and then draw you graphs of which parts of your programs consume most resources. This usually helps [optimization](optimization.md) but can also serve to find bugs (you can spot where your memory starts leaking etc.). Some basic profiling can be done even without profiler, just inside your own program, but it can get tricky. One famous profiler is e.g. [gprof](gprof.md).
|
|
- **static source code analyzer**: Static analyzers look at the source code (or potentially even compiled binary) and try to find bugs/inefficiencies in it without running it, just by reasoning. Static analyzers often tell you about [undefined behavior](undefined_behavior.md), potential overflows, unused code, unreachable branches, unsatisfiable conditions, confusing formatting and so on. This complement the dynamic analysis. Some tools to do this are e.g. [cppcheck](cppcheck.md) and [splint](splint.md), though thanks to compilers performing a lot of static analysis themselves these seem not as widely used as dynamic analyzers nowadays.
|
|
- **[hex editor](hex_editor.md)**: Tool allowing you to mess with binary files, useful for any program that works with binary files. A simple hex viewer is e.g. `hexdump`.
|
|
- **[emulators](emulator.md), [virtual machines](vm.md), ...**: Running your program on different platform often reveals bugs -- while your program may [work perfectly fine](works_on_my_machine.md) on your computer, it may start crashing on another because that computer may have different integer size, [endianness](byte_sex.md), amount of RAM, timings, file system etcetc. Emulators and VMs allow you to test exactly this AND furthermore often allow easy inspection of the emulated machine's memory and so on.
|
|
- ...
|
|
|
|
### Shotgun Debugging
|
|
|
|
This is kind of an improper YOLO way of trying to fix bugs, you just change a lot of stuff in your program in hopes a bug will go away, it rarely works, doesn't really get rid of the bug (just of its manifestation at best) and can at best perhaps be a simple [hotfix](hotfix.md). Remember **if you don't understand how you fixed a bug, you didn't actually fix it**.
|
|
|
|
TODO: mini gdb tutorial |