diff --git a/images/roundedcorners.jpg b/images/roundedcorners.jpg new file mode 100644 index 0000000..6fc080f Binary files /dev/null and b/images/roundedcorners.jpg differ diff --git a/posts/2021-11-13-gdb-scripting.md b/posts/2021-11-13-gdb-scripting.md new file mode 100644 index 0000000..24a572f --- /dev/null +++ b/posts/2021-11-13-gdb-scripting.md @@ -0,0 +1,258 @@ +--- +author: Sanchayan Maity +title: Automate debugging using GDB scripting +tags: linux, gdb, gdb scripting, gstreamer +--- + +Recently I was working on a GStreamer +[plugin](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/572) +in Rust. The plugin basically rounds the corners of an incoming video, +something akin to the `border-radius` property in CSS. Below is how it looks +like when running on a video. + +![](/images/roundedcorners.jpg) + +The GStreamer pipeline for the same. + +```bash +gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! decodebin ! videoconvert ! video/x-raw,format=I420 ! roundedcorners border-radius-px=100 ! video/x-raw,format=A420 ! videoconvert ! gtksink +``` + +This was my first time working on a video plugin in GStreamer. Had a lot to +learn on how to use the `BaseTransform` class from GStreamer, among other +things. Without getting into the GStreamer specific details here, I basically +ran into a problem for which I needed to do some debugging for figuring out +what was going on in the internals of GStreamer. + +Now, while I never had problems using GDB from the command line, but, the way I +was using it earlier was just not good enough. I would start the pipeline, then +attach gdb to a running process, place breakpoints by manually typing out the +whole thing and then start. For one off debugging sessions, where may be you +just want to quickly inspect the backtrace from a crash or may be look into a +deadlock condition where your code hanged, this could be fine. However, when +you have to repeat this multiple times, do a source code change, compile and +then check again, it becomes frustrating. + +## GDB Dashboard + +Looking for a better way, I first stumbled on +[gdb-dashboard](https://github.com/cyrus-and/gdb-dashboard). +This is quite useful since it can give the needed information without having to +type anything. Using gdb +[hooks](https://git.sr.ht/~sanchayanmaity/dotfiles/tree/master/item/gdb/.gdbinit.d/hooks), +the dashboard can be triggered when appropriate. See the rest of my +[gdb configuration](https://git.sr.ht/~sanchayanmaity/dotfiles/tree/master/item/gdb/.gdbinit.d) +to get an idea. I use this in scenarios like where code is stuck due to a +deadlock, I need to look at the backtrace of a crash or any such one off simple +investigation. + +## Construct breakpoint command in neovim & copy to clipboard + +The next small improvement I did was more specific to my use of neovim. Am +generally navigating source code using neovim, which would be opened in one +kitty tab and gdb would be running in terminal in next tab or a split. Wanted +to be able to quickly place a breakpoint without having to type anything out on +the gdb prompt. Wrote a small piece of vimscript code which generates the gdb +command, I would have to type on the gdb prompt to enable a breakpoint, +considering the current line and file on which my cursor is at in the source +when opened in neovim. + +```vimscript +function! CopyBpLocToClipboard() abort + let linenumber = line(".") + let filepath = expand("%") + let breakpoint = "break " . filepath . ":" . linenumber + silent execute "!wl-copy " . breakpoint +endfunction + +nnoremap yb :call CopyBpLocToClipboard() +``` + +So I can hit the key binding above and a command like below will be copied to +the clipboard which I can paste on gdb prompt. + +```bash +break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104 +``` + +Nifty!!! + +## GDB scripting + +Now imagine a scenario where may be one wants to look at multiple places in the +source code and when the program is running, inspect certain variables or just +print out a back trace each time a specific code point is reached. + +The dumb way to do this and the way which I was also doing it earlier, was to +load the executable in gdb or attach to a running process, place a break point, +run, inspect the local variables or print stack trace, place the next break +point and repeat this whole process. Just time consuming and a waste of time. + +GDB can completely automate the above process. Let's see how. + +Below is the `.gdbinit` file I came up with for my problem. This is what is +called a command file by gdb. + +```bash +set confirm off +set breakpoint pending on +set logging on +set logging overwrite on +set print pretty on +set pagination off +break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104 if meta->n_planes == 4 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:228 +break subprojects/gstreamer/gst/gstbuffer.c:1410 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:231 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:237 +break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:136 + +commands 1 + print i + print *frame + enable 2 + enable 3 + enable 4 + enable 5 + enable 6 + continue +end +commands 2 + print offset + continue +end +commands 3 + print offset + print size + continue +end +commands 4 + print *(GstBufferImpl *)buffer + print idx + print length + print skip + continue +end +commands 5 + disable 2 + disable 3 + disable 4 + print *(GstBufferImpl *)buffer + print info->data + print skip + print *data + continue +end +commands 6 + print *frame + quit +end + +disable 2 +disable 3 +disable 4 +disable 5 +disable 6 +run +``` + +The command I was using to debug my GStreamer plugin in this pipeline with gdb. + +```bash +gdb --nx -x .gdbinit --args env RUST_BACKTRACE=1 GST_DEBUG=3,basetransform:6 GST_PLUGIN_PATH=$GST_PLUGIN_PATH:~/GitSources/gst-plugins-rs/target/debug gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! decodebin ! videoconvert ! video/x-raw,format=I420 ! roundedcorners border-radius-px=100 ! video/x-raw,format=A420 ! videoconvert ! gtksink +``` + +In the command above, the `-x` parameter tells gdb to use the command file. The +`--nx` flag tells gdb to not read any any `.gdbinit` files in any directory, as +I did not want to use `gdb-dashboard` for this. `--args` is how I tell gdb what +to run, which is my GStreamer pipeline. You can see `gdb --help` for details on +the flags. + +Now, let's understand what the command file does. The ones below are just some +settings we want gdb to use. Note that we have turned on logging and pretty +printing. + +```bash +set confirm off +set breakpoint pending on +set logging on +set logging overwrite on +set print pretty on +set pagination off +``` + +Next we specify the breakpoints. We have six breakpoints. These are the +locations which were of interest to me. + +```bash +break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104 if meta->n_planes == 4 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:228 +break subprojects/gstreamer/gst/gstbuffer.c:1410 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:231 +break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:237 +break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:136 +``` + +Breakpoints can be enabled conditionally. The `if meta->n_planes == 4` implies +to consider this breakpoint only when we get a video frame with 4 planes. + +We can now tell gdb what should it do when each of the breakpoint above is hit. + +```bash +commands 1 + print i + print *frame + enable 2 + enable 3 + enable 4 + enable 5 + enable 6 + continue +end +``` + +`commands 1` implies these are the commands for gdb to execute when breakpoint +1 is hit. When breakpoint 1 is hit, it will print the value of `i` and `frame`. +The other breakpoints get enabled only after the first one is hit. This is +because at the end of command file, we have + +```bash +disable 2 +disable 3 +disable 4 +disable 5 +disable 6 +``` + +which tells gdb to start with these breakpoints disabled. They will get enabled +only when we hit breakpoint 1. The `continue` just tells gdb to continue, as we +do not want to stop on hitting a breakpoint and only want to inspect in the end +using gdb log. + +Similarly we have for other breakpoints. + +The `run` at the end tells gdb to start running immediately. In normal usage +one would have to explicitly type `run` on the gdb prompt to make gdb start +debugging. + +Now, since we turned on logging after running the below on the terminal + +```bash +gdb --nx -x .gdbinit --args env RUST_BACKTRACE=1 GST_DEBUG=3,basetransform:6 GST_PLUGIN_PATH=$GST_PLUGIN_PATH:~/GitSources/gst-plugins-rs/target/debug gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! decodebin ! videoconvert ! video/x-raw,format=I420 ! roundedcorners border-radius-px=100 ! video/x-raw,format=A420 ! videoconvert ! gtksink +``` + +gdb will run the pipeline, considering the command file it was passed and log +whatever it was asked to log when each breakpoint is encountered. And now since +we had turned on logging and pretty printing, gdb will nicely log everything in +default `gdb.txt` file. You can see the exact log text file +[here](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/572#note_1107146), +where I have attached the `gdbinit` and the other two log files. + +Now, one can comfortably look at this log and see what is going on. Once the +command file is written, the whole debugging process is completely automated. +Run, sit back and then look at the logs. + +Using gdb is now a breeze and hassle free experience. Being able to automate +and log the debugging process like this, also means you could share your +command file and someone else can replicate this. Wish I would have learned +this sooner.