7 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.