x86, amd64: Why SIGTRAP' ucontext instruction pointer does not point to related int3

Issue

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?

Solution

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.

Answered By – Peter Cordes

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published