Reputation: 399
Say that a process is running within the main method when a signal is received and handled. When the signal handler is finished, does the process return back to the line in main where the signal was received, or does it return to the signal call?
P.S. I know I can test this quickly myself but this thought occurred to me while I am without access to my PC.
Thanks.
Upvotes: 7
Views: 5153
Reputation: 384304
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 Dashboard 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.
Upvotes: 2
Reputation: 1986
It returns back to where it was in your code when the signal was triggered.
Many libraries and applications exploit the same mechanisms to implement threadless multitasking (for instance libmill).
Upvotes: 7
Reputation: 6739
After the signal handler completes, your code continues executing at the point where it was when the signal was received. If your code was executing a system call, it may fail with EINTR. See signal(7) for more info.
Upvotes: 3