mikejsavage.co.uk • About • Archive • RSS • Thanks for blocking ads! Blocking ads owns: AdGuard for Safari / uBlock Origin for everything else
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.