You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

x86_64 Linux下可处理SIGSEGV的上下文要求咨询

关于用户态处理SIGSEGV时RSP寄存器的上下文要求及内核栈限制问题

我想知道是否有文档说明用户态处理器处理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不在此列,可能会引发额外的问题。

解决步骤

  1. 为自定义栈添加MAP_GROWSDOWN标志
    修改分配栈的mmap调用,添加MAP_GROWSDOWN,让内核识别这是一个可向下扩展的栈:

    char* stack = mmap(NULL, 4096 * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0);
    

    注意要分配更大的初始空间,因为栈是向下增长的,需要预留足够的空间给内核存储信号上下文。

  2. 设置信号处理专用栈(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,不会依赖你的业务栈空间。

  3. 修复信号处理函数中的不安全调用
    把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);
        
        // ... 其他逻辑 ...
    }
    
  4. 避免使用MAP_FIXED进行动态映射
    你的handler中用MAP_FIXED映射0x12341234所在的页,这会覆盖该地址的现有映射(如果有的话),更安全的做法是不指定固定地址,让内核自动分配,然后修改导致SIGSEGV的指针指向新映射的地址。不过如果你的业务逻辑必须映射到特定地址,需要确保该地址没有被其他占用。

关于RSP寄存器的文档说明

内核对用户态RSP的要求主要体现在:

  • RSP必须指向一个被内核识别为合法栈的内存区域(带有MAP_GROWSDOWN标志,或者是进程的默认栈)。
  • 栈空间必须有足够的剩余空间来存储信号上下文结构,否则内核无法执行信号处理函数。
  • 当在信号处理函数中时,RSP会被内核切换到信号栈(如果设置了sigaltstack),处理完成后恢复原RSP。

这些规则没有单独的文档,而是分散在Linux内核源码和POSIX标准中,核心逻辑是内核需要确保信号处理的执行环境安全可用。


内容的提问来源于stack exchange,提问作者Mike Liao

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 14:20:23