You can add the debugBreak
function to any
program. When the debugBreak
function is called, it
creates a debugger process with the initialization needed to
connect the debugger (via attach
) to the program. This
process invokes the debugger, positioned at the prompt, with control
over the program just after the debugBreak
call.
This chapter contains
code for the
debugBreak
function that you can add to your program
and the
procedure for implementing it.
It also notes several restrictions.
The following example shows the output from a trivial C program
called c_debugBreak
, which is started without the
debugger. When the debugBreak
function is called, the
debugger starts up, taking control:
$ c_debugBreak
Example: about to invoke debugBreak!
Welcome to the Ladebug Debugger Version 4.0-61 (built Mar 31 2000)
------------------
object file name: ./c_debugBreak
Reading symbolic information ...done
Attached to process id 6266 ....
stopped at [<opaque> __usleep_thread(...) 0x3ff800f1058]
Information: An <opaque> type was presented during execution of the previous command. For complete type information on this symbol, recompilation of the program will be necessary. Consult the compiler man pages for details on producing full symbol table information using the -g (and -gall for cxx) flags.
stopped at [void debugBreak(const char* const):237 0x120001ab0]
237 sleep(1);
stopped at [int main(int, char**):15 0x120001548]
9 debugBreak(argv[0]);
>0 0x120001548 in main(argc=1, argv=0x11fffe368) "../src/debugBreakExample.c":15
#1 0x120001488 in __start(0x11fffe228, 0x0, 0x0, 0x0, 0x0, 0x100000) in ./c_debugBreak
5 ** This code demonstrates how to use debugBreak.
6 */
7 #include <stdio.h>
8 #include "debugbreak.h"
9
10 int main(int argc, char* argv[])
11 {
12 fprintf(stdout, "Example: about to invoke debugBreak!\n");
13 fflush(stdout);
14
> 15 debugBreak(argv[0]);
16
17 fprintf(stdout, "Example: should be under debugger control!\n");
18 fflush(stdout);
19
20 return 0;
21 }
22
(ladebug) record io out
(ladebug) quit
Example: should be under debugger control!
$
debugBreak
, complete the following steps:
debugBreak
function in strategic
locations. The debugBreak
function takes one argument:
the name of the program executable file.
debugBreak(argv[0]); /* Something is wrong! */
argv[0]
provides the correct information for most programs. If you
do not want to use argv[0]
, pass the
pathname of your executable as a literal string, for example:
/usr/users/smith/foo
-g
) to include these
changes.
debugBreak
is called.
The debugger connects to the program via attach
, which
means:
detach
from the program.
rerun
the program.
attach
command.
debugBreak
:
.dbxinit
and
.ladebugrc
initialization files.
(If these files contain useful commands, they can be sourced at the command
line after the debugger has started.)
$stoponattach
set to 1 at startup,
which is not the default.
debugBreak
if the
program runs to completion under the debugger
($exitonterminationofprocesswithpid
is set to the process ID
of the process calling debugBreak
). This is supported in Ladebug
versions 4.0-61 and higher.
debugBreak
code has been tested as is.
Modifying it may have unintended consequences.
debugBreak
code must be compiled and linked with
-g
, so that the debugger can find (and modify)
variables. The compiler-generated debugging information must also
be present in the objects for the sources that call
debugBreak
.
debugBreak
function is written in C, for widest compatibility.
You can compile it into a .o
file for inclusion by entering the
following:
$ cc -c -g -o debugBreak.o debugBreak.C
This produces a debugBreak.o
file that must be included
in the link step for your program. You can also compile it to
start the debugger with the GUI, as follows:
$ cc -c -g -o debugBreak.o -DGUI=1 debugBreak.C
The debugbreak.c
code follows:
(ladebug) sh cat ../src/debugbreak.c
/*
** debugBreak - a mechanism to invoke the Ladebug debugger at an interesting
** point in the user's code.
**
** How to use this code: At problematic places in your program, add a call
** to the debugBreak subroutine coded below. The subroutine will create a
** Ladebug debugger process that automatically attaches to your program. The
** debugger will then step your code to just after the debugBreak call. Once
** there, you can use the features of the Ladebug debugger to figure out what
** is going wrong.
**
** Important note: Because the debugBreak subroutine must attach to your
** program, the specific Ladebug options below are necessary. Do not change
** them! In particular:
**
** - The -c option overrides your .dbxinit commands to step properly out of
** the debugBreak code.
**
** - The -i option overrides your .ladebugrc commands to insure that the
** attach works correctly.
**
** - The -pid option tells the debugger how to attach to your program.
**
** There are references in the code to Stevens. These are references to
** W. Richard Stevens' excellent "Advanced Programming in the UNIX Environment",
** 1992, Addison-Wesley Publishing Company.
**
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
/* DEBUGGER can be pointed at a specific version of ladebug, if needed. */
#ifndef DEBUGGER
#define DEBUGGER "ladebug"
#endif
#define FILENAMESIZE L_tmpnam
#define PIDSTRINGSIZE 32
/* Call this routine with the name of your executable binary */
void debugBreak(char const * const executableName)
{
static int callsToDebugBreak = 0;
volatile int changeMeToZeroToContinue = 1;
pid_t firstChildPid = -1;
FILE* initFile = NULL;
char initializationFileName[FILENAMESIZE];
char pidArgumentString[PIDSTRINGSIZE];
FILE* preinitFile = NULL;
char preinitializationFileName[FILENAMESIZE];
pid_t processToDebugPid = -1;
int returnStatus = -1;
pid_t secondChildPid = -1;
if (callsToDebugBreak++ > 0) {
/* Recursion in debugBreak is a bad idea. Don't allow it. */
fprintf(stderr, "debugBreak: Already connected to the debugger\n");
fflush(stderr);
return;
}
/* Create the temporary files used to pass commands to the debugger. */
tmpnam(initializationFileName);
tmpnam(preinitializationFileName);
/* By unlinking here, the files remain around, and will be deleted when
** the program calling debugBreak terminates. See Stevens, section 4.15
** for details.
*/
unlink(initializationFileName);
unlink(preinitializationFileName);
/* Create a child process to set up the Ladebug debugger process. */
if ((firstChildPid = fork()) < 0) {
fprintf(stderr, "debugBreak: first fork failed\n");
fflush(stderr);
return;
} else if (firstChildPid == 0) {
/* This is the (first) child
**
** We have to be careful here. Because of the asynchronous nature of
** the system, we cannot assume anything about ordering of events
** here. Furthermore, because it is possible for the debugger to
** affect the process state of all of the processes in this mechanism,
** we have to carefully manage this parent-child process relationship.
** We don't want to leave any zombie processes about.
**
** Fortunately, there is a solution in Stevens. (Section 8.6) We fork
** again, letting the first child create a second. Then, when the
** first child ends, the second becomes reparented to the init process.
** This eliminates the possibility of zombies.
**
** Hang on to the pid of interest here.
*/
processToDebugPid = getppid();
/* Create the grandchild that will actually be the debugger */
if ((secondChildPid = fork()) < 0) {
fprintf(stderr, "debugBreak: second fork failed\n");
fflush(stderr);
exit(1);
} else if (secondChildPid == 0) {
/* This is the second child.
**
** The process that called debugBreak is our grandparent. Our
** parent will exit shortly, in the else clause below. When it
** does, then our parent will become the init process.
**
** Thus, we can continue, knowing that the original process that
** started this all isn't going to be hung up waiting for us if we
** exit early. If it exits early, then we're still OK, because it's
** no longer our parent.
**
** The initialization file tells the debugger what to do once it is
** attached.
*/
initFile = fopen(initializationFileName, "w");
if (initFile == NULL) {
fprintf(stderr,
"debugBreak: could not create init file [%s]\n",
initializationFileName);
fflush(stderr);
exit(1);
}
#ifndef GUI
/* It is still possible for the debugger process to outlast the
** process that creates it via debugBreak. In that case, the
** debugger will lose access to the terminal, unable to get any
** further I/O.
**
** Tell the debugger to exit if the original process terminates.
** This prevents the debugger from hanging while "sharing" a prompt
** with the shell.
**
** The GUI gives an alternative path for commands, allowing the
** user to operate and issue commands after the original process
** terminates. The GUI also gives a graceful exit in this case.
** We don't need to set this variable if the GUI is used.
*/
fprintf(initFile, "set $exitonterminationofprocesswithpid = %d\n",
processToDebugPid);
#endif
/* These commands step the debugger out of the debugBreak spin loop,
** back to the caller of debugBreak.
*/
fprintf(initFile, "assign changeMeToZeroToContinue = 0\n");
fprintf(initFile, "return debugBreak\n");
fprintf(initFile, "return\n"); /* to debugBreak caller */
#ifndef GUI
/* These commands are helpful when debugging without the GUI.
** They provide some user context when control stops after the
** debugBreak routine.
*/
fprintf(initFile, "where\n");
fprintf(initFile, "W\n");
#endif
fclose(initFile);
/* The preinitialization file tells the debugger to stop on
** attach.
*/
preinitFile = fopen(preinitializationFileName, "w");
if (initFile == NULL) {
fprintf(stderr,
"debugBreak: could not create preinit file [%s]\n",
preinitializationFileName);
fflush(stderr);
exit(1);
}
fprintf(preinitFile, "set $stoponattach = 1\n");
fclose(preinitFile);
/* Convert the pid to a string */
sprintf(pidArgumentString, "%d", processToDebugPid);
/* Invoke the debugger. */
returnStatus = execlp(DEBUGGER, /* What to exec */
DEBUGGER, /* Also argv[0] */
executableName,
"-pid",
pidArgumentString,
"-c",
initializationFileName,
"-i",
preinitializationFileName,
#ifdef GUI
"-gui",
#endif
(char *)0);
/* If we get here, then something really bad happened. */
fprintf(stderr,
"debugBreak: Can't invoke [%s], status (%d)\n",
DEBUGGER,
returnStatus);
fflush(stderr);
exit(returnStatus);
} else {
/* This is the first child process - the parent from the second
** fork. We want this process to exit normally, so that its child
** process (the debugger) gets reparented to init.
*/
exit(0);
}
} else {
/* This is the original process
**
** We have to wait for the first child to exit. It will go away
** relatively quickly.
*/
if (waitpid(firstChildPid, NULL, 0) != firstChildPid) {
fprintf(stderr, "debugBreak: waitpid for [%d] failed\n",
firstChildPid);
fflush(stderr);
return;
}
/* This is where we wait for the debugger. When the debugger connects,
** it will change the value of the loop test, so that the loop exits.
*/
while (changeMeToZeroToContinue) {
sleep(1);
}
}
}
To simplify adding calls to debugBreak
in the code, you can
include the debugbreak.h
file where needed to
provide the proper function prototype:
(ladebug) sh cat ../src/debugbreak.h
/*
** debugBreak - a mechanism to invoke the Ladebug debugger at an interesting
** point in the user's code.
*/
#ifndef DEBUGBREAKH
#define DEBUGBREAKH 1
/* executableName is the name of the user program to debug. */
extern void debugBreak(char const * const executableName);
#endif