Przejdź do głównej zawartości

GDB - beyond basics

GDB is console debugger that every Linux-using programmer heard about. It is not however easy to learn. Greg Law in his CppCon talk presented some of obscure, but useful features.

Text user interface

Normally we use GDB with command line interface (CI). Beyond this, GDB has TUI based on Curses library. To activate it, use keyboard shortcut ctrl-x-a (hold ctrl, press x, unpress x, press a). Now you can see the code as you go through it.
  • ctrl-x-a - activate/deactivate TUI
  • ctrl-l - when screen gets messed up, use it to redraw. Happens when program prints to stdout/stderr
  • ctrl-p / ctrl-n - since you can't use arrows to reuse previously written command, use ctr-p/n instead of arrow up/down
    ctrl-f / ctrl-n are arrows left / right
    ctrl-a / ctrl-e are home / end (all those are copied from Emacs)
  • ctrl-x-2 - second window (assembly).

Shell

You can run shell commands inside GDB command line. Just use keyword "shell" at the beginning. Examples:
  • shell ps
  • shell cat temporary_file.txt
  • shell killall child_process

Python

GDB has built-in Python interpreter. You can use it like shell with keyword "python" and one-liner python commands. Alternatively write "python", enter python commands in multiple lines, write end to run them. Of course you can source python scripts written outside gdb.
Beyond basics, python has gdb module to import. With this, you can set breakpoints from python code, define custom pretty printers for structs and other things.
You can read module documentation here.

Pretty print

When printing (command 'p') contents of variables, output can be ugly. You can improve it with command "set print pretty on" which improves matters considerably. 
Alternatively you can implement pretty robust printers in Python. Sadly this is non-trivial, so for reference I suggest watching the video (starting from time 30:50)

Backward debugging

Using GDB normally we move forward with program execution. However resolving bugs usually centers around question "how did I get here?", not "what happens next?". We can tell GDB to record changes in state of program, so that after breakpoint is hit (or segmentation fault was triggered, or whatever), we can go back in time to see what caused this situation.
Basic workflow is:
  1. Start program under gdb
  2. Set up breakpoints
  3. record
  4. continue/run
  5. wait for breakpoint to stop execution
  6. reverse-step
  7. diagnose
Other commands for reverse flow are documented here.

Debugging stack overflow

Greg shown interesting and more advanced use case. You have a program that sometimes segfaults, but not on every run. In fact it is quite rare. Reading dump file (.core) does not help, because stack got overwritten and we don't know what place in the program caused the crash. Greg wanted to catch the crash "in action" with recording on, without much manual labor. He told GDB to start recording when program starts and to restart the program when it ends normally. Commands to achieve it are:
  1. break main   # creates breakpoint 2
  2. break _exit   # creates breakpoint 3
  3. command 2
    • record
    • continue
    • end
  4. commad 3
    • run
    • end
  5. run
  6. wait for crash to stop execution, diagnose (see video for details)

Cost of recording

Of course this feature costs, and GDB works much slower when recording. According to Greg, this slowdown is about 50'000 times slower. Therefore use recording with caution - set up breakpoint as close to crash as possible, run to it and only then start recording.
There is a feature in some new processors where you can use "record btrace". Slowdown is then limited to about 100 times slower, but all you can see is history of program counter (backtrace), and not program state (contents of variables).

.gdbinit

There is configuration file for gdb! Its location is ~/.gdbinit and possible contents is:
set history save on
set print pretty on
set pagination off
set confirm off

Other

Sadly Greg run out of time. Other topics that he wanted to mention were:
  • remote debugging - you can debug programs running on remote machines. For details google gdbserver
  • multiprocess debugging - when your program forks, you can tell gdb whether to follow child or parent
  • non-stop mode - by default when one thread hits breakpoint and you are at the prompt, all other threads pause. Non-stop mode allows them to continue ("set non-stop on" / "continue -a")
  • watchpoints - with "watch foo" you can set breakpoint that triggers when foo is modified. Other possibilities: when foo is read ("rwatch foo"), is modified by thread 3 ("watch foo thread 3"), when foo is changed and new value meets some criteria ("watch foo if foo > 10") 
  • hardware/software watchpoints - hardware ones work in silicon, are much faster, but are not always possible

Komentarze

Popularne posty z tego bloga

Cleaning up #includes

Cleaning up includes In previous article I described why you should care about clean #includes and what rules I suggest to achieve it. If you already have a project that doesn't follow those rules, it would be tedious manual task to implement my advises. This article describes how some tools can help you answer these questions:
How to replace include guards with pragma once in whole project?How to sort includes in whole project?How to remove redundant includes and add missing ones?How to replace angle bracket includes with quotes only for project headers? #pragma once with include-guard-convert.pyInclude-guard-convert is simple tool that will find all include guards and change them to #pragma once. Tool doesn't need installation, simply download script from github and run it on your header files:
wget https://raw.githubusercontent.com/marcusmueller/include-guard-convert/master/include-guard-convert.py chmod +x include-guard-convert.py ./include-guard-convert.py proj_path/**/*.…

Tidy #includes

Tidy #includes Having tidy includes in C++ project is often thought as low priority. It can however help you with making your code less fragile. This is doubly important when your project is a library that will be used by other people.
I propose that "good" includes should follow those rules:
Associated header (foo.hpp for foo.cpp) always comes firstAny file should have all the headers it needs. System headers are in <angle_brackets>, project headers are in "quotes"Project headers are first, STL headers are last, between them are libraries in order of decreasing locality.Use #pagma once instead of include guards Within groups decided by 4), alphabetic order is nice to have.  Associated header is first The header that declares class / methods should be the first to include. It also should have the same file name as current source file, except for extension. Some tools depend on this - for example IDEs use this to implement "go to declaration/implementatio…