mikejsavage.co.uk / blog

RSS feed

07 Oct 2017 / C++ tricks: autogdb

One of the nice things about developing on Windows is that if your code crashes in debug mode, you get a popup asking if you want to break into the debugger, even if you ran it normally.

With some crap hacks we can achieve something pretty similar for Linux:

#pragma once

#include <sys/ptrace.h>
#include <sys/wait.h>

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <err.h>

static void pause_forever( int signal ) {
        while( true ) {
                pause();
        }
}

static void uninstall_debug_signal_handlers() {
        signal( SIGINT, SIG_IGN );
        signal( SIGILL, pause_forever );
        signal( SIGTRAP, SIG_IGN );
        signal( SIGABRT, pause_forever );
        signal( SIGSEGV, pause_forever );
}

static void reset_debug_signal_handlers() {
        signal( SIGINT, SIG_DFL );
        signal( SIGILL, SIG_DFL );
        signal( SIGTRAP, SIG_DFL );
        signal( SIGABRT, SIG_DFL );
        signal( SIGSEGV, SIG_DFL );
}

static void prompt_to_run_gdb( int signal ) {
        uninstall_debug_signal_handlers();

        const char * signal_names[ NSIG ];
        signal_names[ SIGINT ] = "SIGINT";
        signal_names[ SIGILL ] = "SIGILL";
        signal_names[ SIGTRAP ] = "SIGTRAP";
        signal_names[ SIGABRT ] = "SIGABRT";
        signal_names[ SIGSEGV ] = "SIGSEGV";

        char crashed_pid[ 16 ];
        snprintf( crashed_pid, sizeof( crashed_pid ), "%d", getpid() );
        fprintf( stderr, "\nPID %s received %s. Debug? (y/n)\n", crashed_pid, signal_names[ signal ] );

        char buf[ 2 ];
        read( STDIN_FILENO, &buf, sizeof( buf ) );
        if( buf[ 0 ] != 'y' ) {
                exit( 1 );
        }

        // fork off and run gdb
        pid_t child_pid = fork();
        if( child_pid == -1 ) {
                err( 1, "fork" );
        }
        reset_debug_signal_handlers();

        if( child_pid == 0 ) {
                execlp( "cgdb", "cgdb", "--", "-q", "-p", crashed_pid, ( char * ) 0 );
                execlp( "gdb", "gdb", "-q", "-p", crashed_pid, ( char * ) 0 );
                err( 1, "execlp" );
        }

        if( signal != SIGINT && signal != SIGTRAP ) {
                waitpid( child_pid, NULL, 0 );
                exit( 1 );
        }
}

static bool being_debugged() {
        pid_t parent_pid = getpid();
        pid_t child_pid = fork();
        if( child_pid == -1 ) {
                err( 1, "fork" );
        }

        if( child_pid == 0 ) {
                // if we can't ptrace the parent then gdb is already there
                if( ptrace( PTRACE_ATTACH, parent_pid, NULL, NULL ) != 0 ) {
                        if( errno == EPERM ) {
                                printf( "! echo 0 > /proc/sys/kernel/yama/ptrace_scope\n" );
                                printf( "! or\n" );
                                printf( "! sysctl kernel.yama.ptrace_scope=0\n" );
                        }
                        exit( 1 );
                }

                // ptrace automatically stops the process so wait for SIGSTOP and send PTRACE_CONT
                waitpid( parent_pid, NULL, 0 );
                ptrace( PTRACE_CONT, NULL, NULL );

                // detach
                ptrace( PTRACE_DETACH, parent_pid, NULL, NULL );
                exit( 0 );
        }

        int status;
        waitpid( child_pid, &status, 0 );
        if( !WIFEXITED( status ) ) {
                err( 1, "WIFEXITED" );
        }

        return WEXITSTATUS( status ) == 1;
}

static void install_debug_signal_handlers( bool debug_on_sigint ) {
        if( being_debugged() ) return;

        if( debug_on_sigint ) {
                signal( SIGINT, prompt_to_run_gdb );
        }
        signal( SIGILL, prompt_to_run_gdb );
        signal( SIGTRAP, prompt_to_run_gdb );
        signal( SIGABRT, prompt_to_run_gdb );
        signal( SIGSEGV, prompt_to_run_gdb );
}

Include that somewhere in your code and stuff #if PLATFORM_LINUX install_debug_signal_handlers( true ); #endif at the top of main. Then when your program crashes you will get a prompt like PID 19418 received SIGINT. Debug? (y/n).

GDB often crashes and if you break with ctrl+c you can get problems when you quit GDB, but when it does work it's nice and it's definitely better than nothing.

BTW I wrote this ages ago and I can't remember many of the details around signal handling so don't ask me.