Recent frustrations with the more traditional desktop environment I have been using have led me to switch to a simpler window manager. This post isn’t so much about what I learned about about window managers and the X window system, but how I set everything up so I could debug a window manager and step through it line by line. Maybe it is just because I am not even close to a GUI programmer, whether for the desktop applications on any platform or the web, so I am not familiar with how the ‘pros’ to it, but I did not find it to be particularly straightforward. I am sure someone more well versed in this field than me can explain why what I am doing is ridiculous or how there are much better ways.

A quick tangent: there are two well known programmers, both of whom I admire immensely, with very different documented views on debuggers. Linus Torvalds stated “I don’t like debuggers. […] I do not condone single-stepping through code to find the bug. I do not think that extra visibility into the system is necessarily a good thing.” On the other hand, John Carmack stated “A debugger is how you get a view into a system that’s too complicated to understand. Anybody that thinks ‘just read the code and think about it’, that’s an insane statement”. At the time of writing, Linus Torvalds’ made his statement more than 20 years ago, whereas John Carmack’s statement is relatively recent. Perhaps Linus’ view has since changed, but I happen to agree with John Carmack on this one. When I want to understand a program, in addition to reading the code, I like to step through line by line and observe what is happening, which is rarely clear from just reading the code of anything but the most trivial programs. I mention these quotes because I have had some recurring bugs with KDE Plasma with X, which I’ve used as my desktop environment for at least the past 5 years. After randomly logging in and finding my desktop basically unusable a couple of times in the past few months, I decided it was time for a change, and that I didn’t want to use a massive, hard to debug desktop environment again. So, I decided to switch to dwm full time, and I was resolved to have at least a rough understanding of how it worked and how I might debug it so that I wouldn’t be completely stuck in the event that I experienced an issue in the future.

The first hurdle you will have to jump through is that you cannot simply call gdb on dwm from your graphical desktop environment/window manager. You will get an error “another window manager is already running”. So you will actually need a nested X server for this process; that’s where Xephyr comes in. For instance, I can run startx ./xinitrc_test -- /usr/bin/Xephyr -screen 800x600 -br -reset -terminate :1 2> /dev/null & and get a new dwm instance in an independent window, while running dwm as my main window manager. Note that you can press Ctrl+Shift to lock your mouse into the Xephyr window. Now, how can we attach to this new dwm process as soon as it starts? My first thought was to see if gdb had some way to preattach to a named process. This does not seem to be a built in feature, but I came across this gdb-helpers repo, from one of the gdb developers, that adds a gdb function called preattach by means of the SystemTap program. SystemTap is not included in my distribution’s package manager, so I built SystemTap from source and tried to use the preattach function in gdb. Unfortunately, I got a bunch of errors, both indicating the SystemTap did not have the necessary kernel debug information, as well as missing headers (even though I was able to build to SystemTap binary). Dealing with the missing debug information meant I was going to have to build a custom linux kernel that included the needed debug info, something I wasn’t particularly excited to do. Given that I wasn’t sure that SystemTap would work even with all the needed kernel debug info, I decided to look for other options.

A simple hack was to modify dwm to add an infinite loop to the beginning of main which I would then break out of manually after attaching to the running process. Then, I could step through all of the initialization steps until the window manager was running, and then add any other breakpoints to observe other behavior as I used the window manager. The needed diff in dwm.c is:

diff --git a/dwm.c b/dwm.c
index 03baf42..77c2f4d 100644
--- a/dwm.c
+++ b/dwm.c
@@ -276,6 +276,17 @@ static Window root, wmcheckwin;
 struct NumTags { char limitexceeded[LENGTH(tags) > 31 ? -1 : 1]; };
 
 /* function implementations */
+
+static void
+dwm_debug()
+{
+    volatile int a = 0;
+    volatile int b = 0;
+    while(a == 0){
+        b = 1;
+    }
+}
+
 void
 applyrules(Client *c)
 {
@@ -2137,6 +2148,9 @@ zoom(const Arg *arg)
 int
 main(int argc, char *argv[])
 {
+#ifdef DEBUG
+    dwm_debug();
+#endif
 	if (argc == 2 && !strcmp("-v", argv[1]))
 		die("dwm-"VERSION);
 	else if (argc != 1)

This adds a function dwm_debug that creates an infinite loop waiting for a variable a to change; something that we will do manually in gdb. Note the use of the volatile keyword to make sure your compiler does not perform any sort of optimization that would prevent our ability to manually change these variables from gdb.

In addition to the above change, we need to make a change to config.mk which contains a commented out line with C flags that are apparently used for debugging. When we want to debug dwm, we should uncomment this changed line:

diff --git a/config.mk b/config.mk
index ef8acf7..06c6c02 100644
--- a/config.mk
+++ b/config.mk
@@ -27,7 +27,7 @@ LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS}
 
 # flags
 CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS}
-#CFLAGS   = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
+#CFLAGS   = -g -DDEBUG -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
 CFLAGS   = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS}
 LDFLAGS  = ${LIBS}

Now, when building dwm with those flags, we can step into our dwm_debug function, manually change the variable a, and step through the whole program. I wrote a convenience script to help with starting the debug process (note that my xinitrc_test simply contains exec dwm):

#!/usr/bin/env sh

# Make sure to build dwm with the commented out CFLAGS that include -g
# and add -DDEBUG to those flags.

startx ./xinitrc_test -- /usr/bin/Xephyr -screen 800x600 -br -reset -terminate :1 2> /dev/null &
sleep 5
dwmpid=$(pgrep -n dwm)
sudo -E gdb -p ${dwmpid}

At this point, gdb will start up and be in the infinite loop, which you can break out of via set var a=1.

bound_data

So, so you can start stepping through all the initialization steps, and set breakpoints in whatever other functions you wish. Note that you will have some difficulties, if you, i.e., wish to debug X events directly via something like watch ev in the main event loop. Everything will just lock up once you catch an event change. This seems to be related to a common issue when debugging X applications, see here: “It’s very difficult to debug the X server from within itself; when it stops and returns control to the debugger, you won’t be able to send events to the xterm running your debugger.”. I may eventually try to figure out how to fully catch X events without locking up when returning to the debugger (perhaps that will require 2 computers), but I have found this method useful for understanding my window manager in the meantime.