SysAdminMag.com
Debugging in the Command Line with dbx
Arnaud Aubert
Feeling comfortable with the debugger you use is absolutely essential for any developer. Most developers are familiar with the basic principles: step-by-step execution, inspecting variables, setting breakpoints, etc., but problems may arise when facing a command-line debugger for the first time. If you are used to working with integrated development environments, you may need some help to start working with dbx.
The dbx debugger was initially developed at UC Berkeley and is now provided on AIX and Solaris. Sun also provides an implementation for Linux included in Sun Studio. In this article, I will use the source code example shown in Listing 1 to provide an introduction to dbx. The example uses POSIX threads to show how multithreaded debugging can be done.
Debugging the First Program
The debugger will need your compiler to include debugging information to work correctly. Usually, adding -g to the parameters of your compiler is enough to do so. To start debugging, simply run dbx and type debug demodbx. This will load the process image without starting the software.
Controlling Breakpoints
Once the debugger has been started, you can run it using the run command. You can then provide command-line arguments and redirect the standard streams. Before typing run, you should specify at what point the program should stop during execution. This can be done using the stop command, which uses “at” to specify a line number or “in” a function name. Breakpoint expressions can be really complex and include Boolean conditions; you should read dbx’s docs to discover how powerful these can be. For example, you can set a breakpoint in the function incrementCounter used only if iCounter is greater than 50:
# dbx For information about new features see `help changes' To remove this message, put `dbxenv suppress_startup_message 7.5' \ in your .dbxrc (dbx) debug demodbx Reading demodbx Reading ld.so.1 Reading libpthread.so.1 Reading libthread.so.1 Reading libc.so.1 (dbx) stop in incrementCounter -if iCounter > 50 (2) stop in incrementCounter -if iCounter > 50 (dbx) status (2) stop in incrementCounter -if iCounter > 50 (dbx) run iCounter=1 iCounter=2 .... iCounter=50 iCounter=51 t@2 (l@2) stopped in incrementCounter at line 16 in file "demodbx.c" 16 pthread_mutex_lock(&mutex);Browsing the Call Stack and Inspecting/Changing Variables The program has now been stopped in the second thread. You can analyze the value of the local and global variables using the print var command. You can even change the value using assign “var”=”value” as follows:
(dbx) print iCounter iCounter = 51 (dbx) assign iCounter=90 (dbx) print iCounter iCounter = 90 (dbx) print arg dbx: "arg" is not defined in the scope `demodbx`demodbx.c`incrementCounter` dbx: see `help scope' for detailsThe arg variable cannot be viewed, because it is not accessible from within the scope context of the debugger. However, you can ask for its value by switching the debugger’s context to the caller with the up and down commands on the call stack. You can check the current context (specified with the “=>” characters) by typing where to view the call stack.
(dbx) where current thread: t@2 =>[1] incrementCounter(), line 16 in "demodbx.c" [2] myThread(arg = (nil)), line 30 in "demodbx.c" [3] _thr_setup(0xcfea2400), at 0xcff4f93e [4] _lwp_start(), at 0xcff4fc20 (dbx) up Current function is myThread 30 incrementCounter(); (dbx) print arg arg = (nil) (dbx) where current thread: t@2 [1] incrementCounter(), line 16 in "demodbx.c" =>[2] myThread(arg = (nil)), line 30 in "demodbx.c" [3] _thr_setup(0xcfea2400), at 0xcff4f93e [4] _lwp_start(), at 0xcff4fc20The where command is used to see the call stack of the current thread, but you may switch the debugger context (not the execution one) at any time using thread id. The threads command can be used to see currently running threads.
(dbx) threads t@1 a l@1 ?() running in ___nanosleep() *>t@2 a l@2 myThread() breakpoint in incrementCounter() (dbx) thread t@1 Current function is main 56 usleep(500); t@1 (l@1) stopped in ___nanosleep at 0xcff4ffc5 0xcff4ffc5: ___nanosleep+0x0015: jae ___nanosleep+0x23 \ [ 0xcff4ffd3, .+0xe ] (dbx) where current thread: t@1 [1] ___nanosleep(0x8047cc8, 0x0), at 0xcff4ffc5 [2] _usleep(0x1f4), at 0xcff446fb =>[3] main(), line 56 in "demodbx.c" (dbx) print tid tid = 2UWith the context switched to the first thread, it is easy to inspect the main function’s local variables like “tid”. Step-by-Step Execution Now you know how to check variables from the current functions and their callers, let’s see how to control the execution of your program. If you type cont, the execution will continue until a new breakpoint is reached. You can also ask the system to execute until the next line of source code in the same scope is reached (a.k.a. stepping over) using the next command. Stepping out (i.e., executing until the current function exits) can be done using the step up command. You can also ask the system to execute until control is given to a specified function (without needing a breakpoint) using step [function_name] [thread_id]:
(dbx) step up iCounter=91 incrementCounter returns t@2 (l@2) stopped in myThread at line 30 in file "demodbx.c" 30 incrementCounter(); (dbx) next t@2 (l@2) stopped in myThread at line 30 in file "demodbx.c" 30 incrementCounter();Checking Locks While debugging a multi-threaded application, you can use the syncs command to get a report of locks known to the multi-threading library and their status:
(dbx) syncs All locks currently known to libthread: mutex (0x08060cd4): thread mutex(unlocked) _xftab+0x40 (0xcff7b1a8): usync_? mutex(unlocked) libc_malloc_lock (0xcff7a4d0): thread mutex(unlocked) 0xcfe1a080 (0xcfe1a080): thread mutex(unlocked) 0xcfe1a098 (0xcfe1a098): thread condition variable __uberdata+0xfc0 (0xcff7d680): usync_? mutex(unlocked) __uberdata+0xe40 (0xcff7d500): thread mutex(unlocked) __uberdata+0xd40 (0xcff7d400): thread mutex(unlocked) __uberdata+0x80 (0xcff7c740): thread mutex(unlocked) __uberdata+0x40 (0xcff7c700): thread mutex(unlocked) __uberdata+0x58 (0xcff7c718): thread condition variable __uberdata (0xcff7c6c0): thread mutex(unlocked) __sbrk_lock (0xcff7c568): thread mutex(unlocked)Detecting Memory Leaks The debugger can also be configured in a special mode to check for memory leaks (i.e., blocks that have been allocated but never freed by your process). To enable the mode, simply use check -leaks and run the program from the debugger. Upon exit, a detailed report will be displayed as follows:
(dbx) kill (dbx) status *(2) stop in incrementCounter -if iCounter > 50 (dbx) delete 2 (dbx) check -leaks leaks checking - ON (dbx) run ... iCounter=99 iCounter=101 Checking for memory leaks... Actual leaks report (actual leaks: 1 total size: 100 bytes) Total Num of Leaked Allocation call stack Size Blocks Block Address ========== ====== =========== ======================================= 100 1 0x8060d10 main Possible leaks report (possible leaks: 0 total size: 0 bytes) execution completed, exit code is 0The report gives a list of all blocks never freed. Each line also specifies the function that did allocated non-freed blocks. It may be very helpful to know where to debug your code and understand how to solve the problem. Attaching a Running Program The debugger can also be used on programs that started initially out of its context. Let’s consider that little source code:
#include <unistd.h> int main() { long l = 0; while (1) { l++; usleep(500); } return 0; }Let’s attach dbx to that program running in the background. Dbx can receive a process identifier in argument and will then attach to it. Upon attachment, the process will be paused and you will have full control on its execution using the previously described commands (e.g., inspecting the value of l):
# ./attach & [2] 19433 # dbx attach $! For information about new features see `help changes' To remove this message, put `dbxenv suppress_startup_message 7.5' in your .dbxrc Reading attach Reading ld.so.1 Reading libc.so.1 Attached to process 19433 stopped in ___nanosleep at 0xcff7ffc5 0xcff7ffc5: ___nanosleep+0x0015: jae ___nanosleep+0x23 \<br> [ 0xcff7ffd3, .+0xe ] Current function is main 9 usleep(500); (dbx) where [1] ___nanosleep(0x8047d30, 0x0), at 0xcff7ffc5 [2] _usleep(0x1f4), at 0xcff746fb =>[3] main(), line 9 in "attach.c" (dbx) print l l = 289Analyzing a Core File Core files can also be analyzed by dbx. In that case, you can only inspect the process but no longer control its execution because it is no longer running. You are, however, free to control the debugging context (threads and call stack switching). Consider that small program dividing a number by 0:
#include <stdio.h> int main() { for (int x = 10; x >= 0; x--) printf("10/x=%d\n", x); return 0; }Start the program to generate a core file and start the debugger by specifying the core file name instead of the process identifier:
# ./coretest 10/10=1 10/9=1 ... 10/2=5 10/1=10 Arithmetic exception (core dumped) # ls -l core -rw------- 1 root root 1303923 oct 11 04:03 core # dbx coretest core For information about new features see `help changes' To remove this message, put `dbxenv suppress_startup_message 7.5' \ in your .dbxrc Reading coretest core file header read successfully Reading ld.so.1 Reading libc.so.1 program terminated by signal FPE (integer divide by zero) Current function is main dbx: warning: File `/article/coretest.c' has been modified more \ recently than `/article/coretest' 7 printf("10/x=%d\n", x); (dbx) print x x = 0 (dbx) next dbx: can't continue execution -- no active process
Conclusion
I hope that this article showed you how simple it can be to use dbx once you become familiar with its basic commands. Once you understand the concepts, you can dive into the reference documentation to find how powerful and complex your breakpoint conditions can be.
Arnaud Aubert is an independent consultant and trainer working from France. He contributes regularly to IT publications. He can be reached at: http://www.magesi.com.