If it was raised by raise
it continues just after the raise()
More precisely, the Linux kernel returns to the very first instruction after the syscall
instruction.
Let's GDB it.
signal_return.c
#include <stdio.h> /* puts */
#include <stdlib.h> /* EXIT_SUCCESS */
#include <signal.h> /* signal, raise, SIGSEGV */
#include <unistd.h> /* write, STDOUT_FILENO */
void signal_handler(int sig) {
(void)sig;
const char msg[] = "signal received\n";
write(STDOUT_FILENO, msg, sizeof(msg));
/* The handler automatically disables handling of future signals.
* So we set it again here. */
signal(SIGSEGV, signal_handler);
}
int main(int argc, char **argv) {
(void)argv;
signal(SIGSEGV, signal_handler);
if (argc > 1) {
*(int *)0 = 1;
} else {
raise(SIGSEGV);
}
puts("after");
return EXIT_SUCCESS;
}
Compile:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o signal_return.out signal_return.c
Run without arguments to do a raise(SIGSEGV);
:
./signal_return.out
output:
signal received
after
Now I run it through GDB with GDB Dashboar to make it easier to interpret things:
gdb \
-ex 'set pagination off' \
-ex 'handle all nostop' \
-ex 'dashboard -layout source assembly' \
-ex 'b signal_handler' \
-ex 'run' \
-ex 'fin' \
signal_return.out
handle all nostop
stops GDB from stopping at signals for us: How to handle all signals in GDB
After the fin
we are left in some internal signal handling machinery, so I just ni
a bunch of times.
The first time we come out back to main is:
26 puts("after");
27 return EXIT_SUCCESS;
28 }
─── Assembly ─────────────────────────────────────────────────────────────
0x0000555555555280 main+56 jmp 0x55555555528c <main+68>
0x0000555555555282 main+58 mov $0xb,%edi
0x0000555555555287 main+63 call 0x555555555090 <raise@plt>
0x000055555555528c main+68 lea 0xd71(%rip),%rax # 0x555555556004
0x0000555555555293 main+75 mov %rax,%rdi
0x0000555555555296 main+78 call 0x5555555550a0 <puts@plt>
0x000055555555529b main+83 mov $0x0,%eax
──────────────────────────────────────────────────────────────────────────
>>>
we are at the lea
. We can see where that fits into main
with:
disas /m
which gives:
18 int main(int argc, char **argv) {
0x0000555555555248 <+0>: endbr64
0x000055555555524c <+4>: push %rbp
0x000055555555524d <+5>: mov %rsp,%rbp
0x0000555555555250 <+8>: sub $0x10,%rsp
0x0000555555555254 <+12>: mov %edi,-0x4(%rbp)
0x0000555555555257 <+15>: mov %rsi,-0x10(%rbp)
19 (void)argv;
20 signal(SIGSEGV, signal_handler);
0x000055555555525b <+19>: lea -0x99(%rip),%rax # 0x5555555551c9 <signal_handler>
0x0000555555555262 <+26>: mov %rax,%rsi
0x0000555555555265 <+29>: mov $0xb,%edi
0x000055555555526a <+34>: call 0x5555555550d0 <__sysv_signal@plt>
21 if (argc > 1) {
0x000055555555526f <+39>: cmpl $0x1,-0x4(%rbp)
0x0000555555555273 <+43>: jle 0x555555555282 <main+58>
22 *(int *)0 = 1;
0x0000555555555275 <+45>: mov $0x0,%eax
0x000055555555527a <+50>: movl $0x1,(%rax)
0x0000555555555280 <+56>: jmp 0x55555555528c <main+68>
23 } else {
24 raise(SIGSEGV);
0x0000555555555282 <+58>: mov $0xb,%edi
0x0000555555555287 <+63>: call 0x555555555090 <raise@plt>
25 }
26 puts("after");
=> 0x000055555555528c <+68>: lea 0xd71(%rip),%rax # 0x555555556004
0x0000555555555293 <+75>: mov %rax,%rdi
0x0000555555555296 <+78>: call 0x5555555550a0 <puts@plt>
27 return EXIT_SUCCESS;
0x000055555555529b <+83>: mov $0x0,%eax
28 }
0x00005555555552a0 <+88>: leave
0x00005555555552a1 <+89>: ret
so it is clear that we returned to the very first instruction after the call to the C library function signal
:
call 0x555555555090 <raise@plt>
This appears to happen right at the instruction level. By using: https://askubuntu.com/questions/487222/how-to-install-debug-symbols-for-installed-packages/1434174#1434174 we can step into raise
until the syscall with some effort:
0x00007ffff7e07a7a __pthread_kill_implementation+240 syscall
0x00007ffff7e07a7c __pthread_kill_implementation+242 mov %eax,%r13d
After +240 we go into the signal. And after the signal handler returns, the very first instruction executed is +242. TODO: compile a minimal freestanding assembly example to finish it off ;-)
If it was raised by an instruction, it goes back and re-runs the instruction
Running the program with an argument:
./signal_return.out 1
leads to an infinite loop of:
signal received
signal received
signal received
We therefore understand that we must be going back to the very offending instruction.
Let's try to walk into the instruction ourselves this time:
gdb \
-ex 'set pagination off' \
-ex 'handle all nostop' \
-ex 'dashboard -layout source assembly' \
-ex 'b signal_handler' \
-ex 'start' \
-args signal_return.out 1
We are trying to write to address 0 at line 22:
18 int main(int argc, char **argv) {
19 (void)argv;
20 signal(SIGSEGV, signal_handler);
21 if (argc > 1) {
22 *(int *)0 = 1;
23 } else {
24 raise(SIGSEGV);
25 }
26 puts("after");
27 return EXIT_SUCCESS;
28 }
─── Assembly ─────────────────────────────────────────────
0x000055555555526f main+39 cmpl $0x1,-0x4(%rbp)
0x0000555555555273 main+43 jle 0x555555555282 <main+58>
0x0000555555555275 main+45 mov $0x0,%eax
0x000055555555527a main+50 movl $0x1,(%rax)
Now after I si
here, we hit the breakpoint.
After I fin
we are left at a weird:
0x00007ffff7db3520 __restore_rt+0 mov $0xf,%rax
0x00007ffff7db3527 __restore_rt+7 syscall
0x00007ffff7db3529 __restore_rt+9 nopl 0x0(%rax)
which makes system call number 0xf = 15. A quick peek at the syscall table teaches us that this is rt_sigreturn
, and man rt_sigreturn
tells us a bit about the code injection madness done by the kernel.
After two more si
s we are out and back to the exact offending instruction:
22 *(int *)0 = 1;
23 } else {
24 raise(SIGSEGV);
25 }
26 puts("after");
27 return EXIT_SUCCESS;
28 }
─── Assembly ─────────────────────────────────────────────
0x000055555555526f main+39 cmpl $0x1,-0x4(%rbp)
0x0000555555555273 main+43 jle 0x555555555282 <main+58>
0x0000555555555275 main+45 mov $0x0,%eax
0x000055555555527a main+50 movl $0x1,(%rax)
Returning to the offending instruction allows us both to handle the signal and get a core dump
As mentioned at: Linux: handling a segmentation fault and getting a core dump this default behavior is not bad, because what you might want to do on SIGSEGV
is:
For this to work like that you would want to remove the signal(SIGSEGV, signal_handler);
call from the handler. This way it comes out and blows up as desired instead of looping forever. That was only for demonstration purposes.
Tested on Ubuntu 22.04 x86_64.