DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Waiting for... a Debugger

Introduction

When using a debugger such as gdb or lldb, typically you:

  1. Run the debugger specifying the name of your program’s executable.
  2. Set breakpoints.
  3. 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 );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 in main() to be the base name of argv[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:

  1. Via a command-line option, e.g., --debug.
  2. Via the existence of a file, e.g., ~/.foo_debug (where foo would be replaced with the name of your program).
  3. 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();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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 );
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Conclusion

Conditionally using SIGSTOP is a good way to debug a program that’s launched in ways other than the command-line.

Top comments (0)