As the title says - rip
from ucontext_t
does not point to the int3
that raised a SIGTRAP
. Instead it points to the next instruction.
This deviates from my (naive) expectations that every faulted instruction will be retried upon return from signal handler (if context was not changed and process did not explicitly terminate while in the handler).
On the other hand when getting SIGILL
- context points to the bad instruction. also on ARM and Aarch64 - SIGTRAP
context also points to the related bkpt #0
/bpt #0
.
Test program:
/* sigtest.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <ucontext.h>
extern void do_int3(void);
extern void do_ud2(void);
void sighandler(int signo, siginfo_t* info, void* context) {
struct ucontext_t* uctx = context;
/* Yes, printf() here is bad. I promise to never ever do this in real programs */
printf("Got signal %d:\n"
"\tsi_addr: %p\n"
"\tcontext RIP: %p\n",
signo,
info->si_addr,
(void*)uctx->uc_mcontext.gregs[REG_RIP]);
}
int main(int argc, char** argv) {
struct sigaction sa = {};
sa.sa_flags = SA_SIGINFO | SA_ONESHOT;
sa.sa_sigaction = sighandler;
sigemptyset(&sa.sa_mask);
sigaction(SIGTRAP, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
void (*fn)(void) = 0;
if (argv[1][0] == 't') {
fn = do_int3;
}
if (argv[1][0] == 'u') {
fn = do_ud2;
}
printf("call function at %p\n", fn);
fn();
printf("call returned\n");
}
; do_int3.S
.text
.globl do_int3
.type do_int3, @function
do_int3:
int3
ret
.size do_int3, .-do_int3
; do_ud2.S
.text
.globl do_ud2
.type do_ud2, @function
do_ud2:
ud2
ret
.size do_ud2, .-do_ud2
Compile and run:
$ cc sigtest.c do_int3.S do_ud2.S
$ ./a.out t
call function at 0x5620b87c6908
Got signal 5:
si_addr: (nil)
context RIP: 0x5620b87c6909
call returned
$ ./a.out u
call function at 0x557d1bc1590a
Got signal 4:
si_addr: 0x557d1bc1590a
context RIP: 0x557d1bc1590a
Illegal instruction (core dumped)
It is easy to notice that for SIGTRAP
rip
value point to the next instruction rather than to the int3
.
Why is instruction pointer adjusted in SIGTRAP
context to not retry related int3
?
CodePudding user response:
Why is instruction pointer adjusted in SIGTRAP context to not retry related int3?
It's not adjusted by the kernel; the exception-return address pushed by hardware is the one after an int
instruction, including int3
.
Keep in mind that int3
is only slightly different from the normal case of int n
such as int 0x80
. int
is designed as being like a syscall or far-call, so the (exception) return address is the one after the int
. Otherwise an int 0x80
system call would re-run itself forever unless the kernel edited the exception-return info before iret
.
So why doesn't Linux's int3
/ int 3
handler decrement the saved RIP by 1? For one thing, debuggers can do that in software if they want to, and keeping the kernel simple is better. (For both maintenance, efficiency, and less demangling for software that does want to know the address pushed by hardware.)
For another, a fixed offset of 1 byte wouldn't always be correct: 2-byte CD 03 int 3
raises the same exception as 1-byte CC int3
. And even if you wanted to try, x86 machine code doesn't uniquely decode backwards, so obfuscated machine code could give an address that wasn't the actual start of the instruction that executed. (Although it would run as int 3
or int3
if decoded from there). e.g. 2-byte rep int3
is indistinguishable from add al, 0xf3
/ int3
if looking backwards.
Software like GDB that inserts an int3
will know what it inserted, and needs to know about the target machine details, so can deal with the offset.