I thought it would be cool to add a command to gdb that will look at the instruction pointer and overwrite the current instruction with NOPs. The idea being that if you are debugging, and your program segfaults due to memory access you could use this to temporarily ignore it and continue execution (useful if your recompiles take a really long time and you know there are more issues).
However, when I do trigger a segfault in a simple test program, overwrite the bytes, and try to resume execution, it just segfaults again and the process dies. Why?
Test program:
#include <stdio.h>
int main(int argc, char** argv)
{
if(argc == 1) {
*((char*)0) = 42;
printf("Success.\n");
return 0;
}
printf("Failure.\n");
return 1;
}
Gdb plugin (you can put source /path/to/nopify.py
in your ~/.gdbinit
to enable):
import gdb
from gdb import Command
class NopifyCommand(Command):
"""Nopify the instruction at the current instruction pointer (rip/eip)."""
def __init__(self):
super(NopifyCommand, self).__init__("nopify", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
# Get the current instruction pointer
rip = int(gdb.parse_and_eval("$rip"))
# Disassemble the current and the next instruction
asm_output = gdb.execute("x/2i $rip", to_string=True)
# Extract the address of the next instruction
lines = asm_output.strip().split('\n')
if len(lines) > 1:
next_instr_line = lines[1]
next_addr_str = next_instr_line.split()[0]
next_addr = int(next_addr_str, 16)
instr_length = next_addr - rip
# Replace the instruction with NOPs
for i in range(instr_length):
cmd = f"set *((char*)({rip}+{i})) = 0x90"
print(f"Running: {cmd}")
gdb.execute(cmd)
print(f"Replaced {instr_length} bytes with NOPs")
else:
print("Could not determine the length of the instruction.")
# Register the command
NopifyCommand()
Example gdb session:
(gdb) run
Starting program: /tmp/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555167 in main ()
(gdb) x/2i $rip
=> 0x555555555167 <main+30>: movb $0x2a,(%rax)
0x55555555516a <main+33>: lea 0xe93(%rip),%rdi # 0x555555556004
(gdb) nopify
Running: set *((char*)(93824992235879+0)) = 0x90
Running: set *((char*)(93824992235879+1)) = 0x90
Running: set *((char*)(93824992235879+2)) = 0x90
Replaced 3 bytes with NOPs
(gdb) x/2i $rip
=> 0x555555555167 <main+30>: nop
0x555555555168 <main+31>: nop
(gdb) cont
Continuing.
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
I get no other output, I expect to see it print "Success." Running flush icache
before continuing makes no difference. Any ideas?
Edit: Updated version of the script incorporating the fix from below can be found here: https://github.com/jgarvin/joe-etc/blob/master/gdb/nopify.py
You don't have a signal handler for SIGSEGV. GDB is just pausing at the moment between the signal being delivered and the process dying (the default action for SIGSEGV).
This is useful when debugging to see what led to a fatal signal, but I'm not sure there's any way to set $pc = something
to have GDB change the handling of that signal after the OS has started to deliver it.
So you need your process to ignore SIGSEGV or catch it with an empty handler, so it will retry / return to the faulting instruction address. e.g. with the obsolete but simple signal(2)
or the now-standard sigaction(2)
. (I'm not sure if SIG_IGN
is allowed as the disposition for SIGSEGV, since that would lead to infinite loops in programs not being debugged.)
Apparently it is possible to have GDB intercept signals if you run a GDB command first: Continue debugging after SegFault in GDB? - handle SIGSEGV stop nopass
. Thanks @ssbssa for pointing that out.