x86_64 Linux下可处理SIGSEGV的上下文要求咨询
我想知道是否有文档说明用户态处理器处理SIGSEGV的上下文要求?我特别关注RSP寄存器的相关规则。
当我通过mmap或malloc分配内存区域并将RSP指向该区域后,触发内存错误时会收到两次无法处理的SIGSEGV:
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x12341234} --- --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
第二次看起来是内核强制发送的信号,请问内核对栈的大小或内存区域有特定要求吗?
以下是我的代码:
C代码
#define _POSIX_C_SOURCE 200809L #include <fcntl.h> #include <sys/mman.h> #include <sys/stat.h> #include <signal.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> const ptrdiff_t shm_size = 4096 * 1024; static ptrdiff_t shm_offset = 0; static int shm_fd = -1; void panic(char* s) { perror(s); exit(1); } static void handler(int sig, siginfo_t* info, void* _) { uint64_t bad_addr = (uint64_t)info->si_addr; printf("Handling SIGSEGV at %p\n", bad_addr); void* new_page_addr = (void*)((bad_addr >> 12) << 12); printf("Maping new page %p\n", new_page_addr); if (MAP_FAILED == mmap(new_page_addr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED | MAP_STACK , shm_fd, shm_offset)) { panic("mmap failed\n"); } shm_offset += 4096; if (shm_offset > shm_size) { panic("overflow\n"); } } void init_sigsegv_handler() { shm_fd = shm_open("bb_shared_mem", O_RDWR | O_CREAT, S_IRWXU | S_IRWXG); if (shm_fd == -1) { panic("shm_open"); } shm_unlink("bb_shared_mem"); ftruncate(shm_fd, shm_size); struct sigaction sa; memset(&sa, 0, sizeof(sigaction)); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = handler; if (sigaction(SIGSEGV, &sa, NULL)) { panic("sigaction"); } } extern void test_switch_stack(void * rsp); int main() { init_sigsegv_handler(); char* stack = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); if (MAP_FAILED == stack) { panic("mmap\n"); } // stack = malloc(4096); test_switch_stack(stack + 2048); printf("success\n"); return 0; }
汇编代码
.global test_switch_stack .section .data rsp_value: .quad 0 .section .text test_switch_stack: mov %rsp, rsp_value mov %rdi, %rsp mov $0x12341234, %rdi movb $1, (%rdi) mov rsp_value, %rsp ret
核心原因:信号处理时的栈可用性问题
第二次SI_KERNEL来源的SIGSEGV,是因为内核在尝试触发你的信号处理函数时,无法找到可用的栈空间。
当程序触发第一次SIGSEGV时,内核需要切换到信号处理栈来执行handler。此时内核会检查当前的RSP指向的栈是否合法:
- 你手动切换的栈是通过普通mmap分配的,没有设置
MAP_GROWSDOWN标志,内核不认为这是一个可自动扩展的栈区域。 - 更关键的是,当信号发生时,内核需要在当前栈上预留足够的空间来存储信号上下文(ucontext_t结构),如果当前栈的剩余空间不足,或者栈本身不被内核识别为合法栈,内核会尝试使用进程的默认信号栈(如果设置过
sigaltstack),如果没有设置默认信号栈,内核就会直接发送SI_KERNEL的SIGSEGV,导致程序崩溃。
另外,你的handler里使用printf是不安全的——信号处理函数中只能调用异步信号安全的函数,printf不在此列,可能会引发额外的问题。
解决步骤
为自定义栈添加
MAP_GROWSDOWN标志
修改分配栈的mmap调用,添加MAP_GROWSDOWN,让内核识别这是一个可向下扩展的栈:char* stack = mmap(NULL, 4096 * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0);注意要分配更大的初始空间,因为栈是向下增长的,需要预留足够的空间给内核存储信号上下文。
设置信号处理专用栈(sigaltstack)
显式为信号处理设置一个独立的栈,避免和自定义业务栈冲突:void init_sigsegv_handler() { // ... 原有代码 ... stack_t ss; ss.ss_sp = malloc(SIGSTKSZ); ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; if (sigaltstack(&ss, NULL) == -1) { panic("sigaltstack"); } }这样内核会在触发信号时切换到这个专用栈执行handler,不会依赖你的业务栈空间。
修复信号处理函数中的不安全调用
把printf替换为异步信号安全的write:static void handler(int sig, siginfo_t* info, void* _) { char buf[64]; int len = snprintf(buf, sizeof(buf), "Handling SIGSEGV at %p\n", info->si_addr); write(STDOUT_FILENO, buf, len); // ... 其他逻辑 ... }避免使用
MAP_FIXED进行动态映射
你的handler中用MAP_FIXED映射0x12341234所在的页,这会覆盖该地址的现有映射(如果有的话),更安全的做法是不指定固定地址,让内核自动分配,然后修改导致SIGSEGV的指针指向新映射的地址。不过如果你的业务逻辑必须映射到特定地址,需要确保该地址没有被其他占用。
关于RSP寄存器的文档说明
内核对用户态RSP的要求主要体现在:
- RSP必须指向一个被内核识别为合法栈的内存区域(带有
MAP_GROWSDOWN标志,或者是进程的默认栈)。 - 栈空间必须有足够的剩余空间来存储信号上下文结构,否则内核无法执行信号处理函数。
- 当在信号处理函数中时,RSP会被内核切换到信号栈(如果设置了
sigaltstack),处理完成后恢复原RSP。
这些规则没有单独的文档,而是分散在Linux内核源码和POSIX标准中,核心逻辑是内核需要确保信号处理的执行环境安全可用。
内容的提问来源于stack exchange,提问作者Mike Liao

