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 details
The 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 0xcff4fc20
The 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 = 2U
With 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 0
The 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 \
[ 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 = 289
Analyzing 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.
|