Introduction
When using a debugger such as gdb or lldb, typically you:
- Run the debugger specifying the name of your program’s executable.
- Set breakpoints.
- Run your program from within the debugger.
However, some programs are launched via fork()
and exec()
from a daemon or as part of a pipeline. If the program is itself a daemon (or other long-running process), you can simply attach the debugger to your program’s running process. But if your program is short-lived, it will have completed execution long before you can attach the debugger to it. What’s needed is a way to run your program, but have it immediately pause and wait indefinitely for you to attach a debugger.
Waiting
The C standard library includes the raise()
function that sends a signal to the current process. POSIX defines the SIGSTOP
signal that causes the program to stop and wait indefinitely until the SIGCONT
signal is received — that just so happens to be sent by the debugger when you enter a continue
command — which is exactly what we want. Here’s a function to do just that:
void wait_for_debugger_attach( void ) {
fprintf( stderr,
"%s: pid=%d: waiting for debugger to attach...\n",
me, (int)getpid()
);
if ( raise( SIGSTOP ) == -1 ) {
perror( me );
exit( EX_OSERR );
}
}
SIGSTOP
is the same signal the debugger itself sends to a process to pause its execution upon hitting a breakpoint.
First, we print a message containing the process’ ID so you know what process to attach the debugger to. Then we wait by raising SIGSTOP
. To be robust, we check the return value from raise()
: if it’s -1
, an error occurred, so print it out via perror()
and exit.
The variable
me
is global and set inmain()
to be the base name ofargv[0]
, the executable’s name.
Conditionally Waiting
Of course you want to call wait_for_debugger_attach()
only if you’re actually debugging your program. There are a few ways to let your program know this is the case:
- Via a command-line option, e.g.,
--debug
. - Via the existence of a file, e.g.,
~/.foo_debug
(wherefoo
would be replaced with the name of your program). - Via an environment variable, e.g.,
FOO_DEBUG
.
Of these, I like #3 best since it’s the hardest to do by accident since an environment variable has to be set in the parent process’ environment in order to be inherited by your program. (You don’t want your program in production to run then immediately wait indefinitely.)
Therefore, in main()
, we can do this:
int main( int argc, char const *argv[] ) {
me = basename( argv[0] );
if ( getenv( "FOO_DEBUG" ) != NULL )
wait_for_debugger_attach();
// ...
}
where getenv()
returns non-NULL
only if the given environment variable exists. (Its value, even if empty, doesn’t matter.)
Niceties
If you want to make it even harder for your program to wait indefinitely by accident, you can check the environment variable for an “affirmative” value. We can do that by adding a couple of helper functions:
bool str_is_any( char const *s,
char const *const matches[const static 2] ) {
if ( s != NULL ) {
for ( char const *const *match = matches; *match != NULL; ++match ) {
if ( strcasecmp( s, *match ) == 0 )
return true;
}
}
return false;
}
bool str_is_affirmative( char const *s ) {
static char const *const AFFIRMATIVES[] = {
"1", "t", "true", "y", "yes", NULL
};
return str_is_any( s, AFFIRMATIVES );
}
If you don’t know what the
const static 2
does in C, see here. In a C++ program, simply omit this.
Then change the code in main()
to:
if ( str_is_affirmative( getenv( "FOO_DEBUG" ) ) )
wait_for_debugger_attach();
Conclusion
Conditionally using SIGSTOP
is a good way to debug a program that’s launched in ways other than the command-line.