背景
最近几天看到先辈们30年前留下了一块代码,为SIGSEGV设置了handler,所以心中有了两个疑问:
- 为SIGSEGV设置handler有没有用?
- 能否跳过引起崩溃的那一句指令?
答案是没有太多用处,信号处理函数返回后会再次执行出错的指令,从而再次引发调用信号处理函数,陷入无限循环。不过信号处理函数有能力改变RIP的值从而跳过出错的指令,但用处不大,因为跳过了出错指令会影响后面代码的执行结果。
以下试验只是为了玩玩,没有太多用处。
试验代码
#include<stdio.h>
#include <unistd.h>
#include <stdint.h>
#define __USE_GNU
#include <signal.h>
int handle_count=0;
void signal_seg_handler(int sig, siginfo_t* siginfo, void* context){
char eye_chacher[16]="seg_handler";
mcontext_t* mcontext = &((ucontext_t*)context)->uc_mcontext;
write(STDERR_FILENO, "SIGSEGV caught\n", 15);
if(++handle_count<5){
uint8_t* code = (uint8_t*)mcontext->gregs[REG_RIP];
printf("SIGSEGV instruction address:%p code:%02x%02x\n", code, code[0], code[1]);//此处不应该使用非重入函数printf,仅仅为了方便。
}else{
write(STDERR_FILENO, "SIGSEGV caught more than 5 times\n", 32);
exit(-1);
}
}
static void do_segv()
{
char eye_chacher[16]="do_segv";
int *segv;
segv = 0; /* malloc(a_huge_amount); */
*segv = 1;
printf("Should not go here\n");
}
void main(){
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = signal_seg_handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &act, NULL);
do_segv();
printf("exit\n");
}
给地址0处赋值,必然引起SEGV,从而触发信号处理函数signal_seg_handler。
运行结果如下:
SIGSEGV caught
SIGSEGV instruction address:0x400779 code:c700
SIGSEGV caught
SIGSEGV instruction address:0x400779 code:c700
SIGSEGV caught
SIGSEGV instruction address:0x400779 code:c700
SIGSEGV caught
SIGSEGV instruction address:0x400779 code:c700
SIGSEGV caught
SIGSEGV caught more than 5 times
正如预期, 不停地执行0x400779处的指令,使用GDB查看此处指令:
(gdb) x /3i 0x400779
0x400779 <do_segv+42>: movl $0x1,(%rax)
0x40077f <do_segv+48>: mov $0x4008e2,%edi
0x400784 <do_segv+53>: callq 0x400520 <puts@plt>
(gdb) x /6xb 0x400779
0x400779 <do_segv+42>: 0xc7 0x00 0x01 0x00 0x00 0x00
(gdb) f 3
#3 0x0000000000400779 in do_segv () at sigsegv.c:33
33 *segv = 1;
(gdb) disass /m
Dump of assembler code for function do_segv:
...
33 *segv = 1;
0x0000000000400775 <+38>: mov -0x8(%rbp),%rax
=> 0x0000000000400779 <+42>: movl $0x1,(%rax)
所以说代码*segv = 1;一直没过去。
跳过出错指令
通过上面的分析,我们知道了出错指令的长度为6;信号处理函数signal_seg_handler中也能拿到一系列要恢复的寄存器的值,尤其是RIP。只要修改RIP=RIP+6即可。
代码修改如下:
void signal_seg_handler(int sig, siginfo_t* siginfo, void* context){
char eye_chacher[16]="seg_handler";
mcontext_t* mcontext = &((ucontext_t*)context)->uc_mcontext;
write(STDERR_FILENO, "SIGSEGV caught\n", 15);
mcontext->gregs[REG_RIP] += 6; // skip it!
return;
编译运行:
[mzhai@dendevmvascen05 cprograms]$ ./a.out
SIGSEGV caught
Should not go here
exit
打印了不应该打印的Should not go here,搞定!