Linux下如何处理多次SIGSEGV?内核内存扫描程序故障求助
嘿,我之前在做内核内存扫描工具的时候也踩过这个SIGSEGV的坑,你的情况核心问题出在信号处理的默认行为和进程上下文的恢复逻辑上,咱们一步步解决:
为什么第二次SIGSEGV会直接崩溃?
首先,如果你用的是标准的signal()函数注册信号处理程序,很多系统的实现会在信号处理函数执行完毕后,自动把该信号的处理方式重置回默认(SIG_DFL)。这就导致第一次触发SIGSEGV时,你的处理逻辑能正常执行,但处理完后,SIGSEGV的处理方式已经变回“终止程序”,第二次触发时自然直接崩溃——哪怕你以为自己重新注册了,大概率是注册时机不对(比如在处理函数里注册,但此时信号已经触发,内核已经用了默认处理逻辑)。
另外还有个更关键的点:当SIGSEGV触发时,CPU的指令指针(rip)会停在导致非法内存访问的那条指令上。如果你的处理函数只是简单返回,内核会重新执行这条指令,再次触发SIGSEGV。如果此时处理方式已经被重置,直接就崩了。
解决步骤
1. 用sigaction()替代signal()注册信号
sigaction()是POSIX标准的信号注册接口,行为更可控,不会自动重置处理方式。记得设置正确的标志位:
#include <signal.h> #include <ucontext.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void segv_handler(int sig, siginfo_t *info, void *context); int main() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = segv_handler; // 用sa_sigaction才能获取上下文信息 sa.sa_flags = SA_RESTART | SA_NODEFER; // SA_NODEFER:允许在处理函数中再次接收SIGSEGV(避免嵌套问题但必要时可用) // SA_RESTART:让被信号打断的系统调用自动重启 sigemptyset(&sa.sa_mask); if (sigaction(SIGSEGV, &sa, NULL) == -1) { perror("Failed to set SIGSEGV handler"); exit(EXIT_FAILURE); } // 你的内核内存扫描逻辑... return 0; }
2. 在处理函数中修改进程上下文,跳过非法指令
要让程序继续执行,必须把指令指针(rip)移动到非法指令的下一条,避免重复触发SIGSEGV。通过ucontext_t结构体可以修改CPU寄存器:
void segv_handler(int sig, siginfo_t *info, void *context) { ucontext_t *uc = (ucontext_t *)context; // 跳过当前非法指令(x86_64架构下,REG_RIP对应指令指针) // 这里的偏移量要根据你扫描代码中内存访问指令的长度调整,比如如果是movq指令,偏移8字节 uc->uc_mcontext.gregs[REG_RIP] += 8; printf("Skipped invalid memory page at address: %p\n", info->si_addr); }
注意:不同架构的寄存器名称不一样,比如x86下是
REG_EIP,ARM架构是REG_PC,需要根据你的目标平台调整。
3. 优化扫描逻辑,按页跳转
既然是扫描内存页,每次触发SIGSEGV后,直接跳过整个页(比如4KB),而不是单字节调整指令指针。这样能减少信号触发的频率,提升扫描效率——毕竟内核的不可访问内存都是按页对齐的。
最后再提醒一句
即使是root权限,现代内核有很多内存保护机制(比如SMAP、KASLR),有些内存区域是完全无法访问的,所以你的扫描逻辑要做好边界检查,避免无意义的无限循环。
内容的提问来源于stack exchange,提问作者Mike

