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

如何捕获SIGSEGV信号并定位触发它的代码行号?

解答你的SIGSEGV捕获与行号定位问题

我来一步步拆解你的问题,刚好之前做过类似的段错误监控工具,把经验分享给你:

关于si_addr的真实含义

你猜的方向有点偏差——si_addr不是触发错误的变量/函数本身的地址,而是程序试图访问但没有权限或无效的内存地址。举两个例子:

  • 如果你访问空指针*NULL,si_addr就是0x0;
  • 如果越界访问数组arr[100](假设数组只有10个元素),si_addr就是&arr[100]这个非法地址。

它记录的是引发段错误的内存操作目标地址,而不是执行该操作的指令地址(指令地址才是用来找源码行号的关键)。

用sa_sigaction捕获SIGSEGV的正确姿势

你说得对,必须设置SA_SIGINFO标志才能启用sa_sigaction回调,它能拿到比sa_handler多得多的上下文信息。这里给你一个可运行的子进程示例,里面包含信号处理和获取指令地址的逻辑:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <ucontext.h>

// 全局变量存管道写端,方便信号处理函数写入地址
int pipe_write_fd;

void sigsegv_handler(int sig, siginfo_t *info, void *ucontext) {
    ucontext_t *uc = (ucontext_t *)ucontext;
    // 不同架构的指令指针寄存器名不一样:
    // x86_64用REG_RIP,x86_32用REG_EIP,ARM用REG_PC
    void *fault_instr_addr = (void *)uc->uc_mcontext.gregs[REG_RIP];
    void *fault_mem_addr = info->si_addr;

    printf("=== SIGSEGV Triggered ===\n");
    printf("Attempted to access invalid memory: %p\n", fault_mem_addr);
    printf("Instruction causing fault: %p\n", fault_instr_addr);

    // 把指令地址传给父进程
    write(pipe_write_fd, &fault_instr_addr, sizeof(fault_instr_addr));
    exit(1);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pipe-write-fd>\n", argv[0]);
        exit(1);
    }
    pipe_write_fd = atoi(argv[1]);

    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;  // 必须设置这个标志才能启用sa_sigaction
    sa.sa_sigaction = sigsegv_handler;
    sigemptyset(&sa.sa_mask);  // 清空信号掩码

    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("Failed to set SIGSEGV handler");
        exit(1);
    }

    // 触发段错误的测试代码
    int *null_ptr = NULL;
    *null_ptr = 42;  // 这里会触发SIGSEGV

    return 0;
}

注意:编译这个子进程时必须加-g参数,保留调试信息,否则后续无法把地址转换成源码行号。

父进程监控子进程并定位错误行号

你已经有父进程监控的代码,核心要做的是:

  1. 父进程创建管道,fork子进程时把管道写端传给子进程;
  2. 子进程触发SIGSEGV后,把出错的指令地址传给父进程;
  3. 父进程拿到地址后,用addr2line工具把地址转换成源码行号(完全不用gdb,符合你的需求)。

下面是父进程的完整示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <stdint.h>

int main() {
    int pipe_fd[2];
    if (pipe(pipe_fd) == -1) {
        perror("Pipe creation failed");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        close(pipe_fd[0]);  // 子进程关闭读端
        // 把管道写端作为参数传给子进程
        char fd_buf[16];
        snprintf(fd_buf, sizeof(fd_buf), "%d", pipe_fd[1]);
        execl("./segv_child", "segv_child", fd_buf, NULL);
        perror("Execl failed");
        exit(1);
    } else {
        close(pipe_fd[1]);  // 父进程关闭写端
        uintptr_t fault_instr_addr;
        ssize_t read_len = read(pipe_fd[0], &fault_instr_addr, sizeof(fault_instr_addr));
        
        if (read_len == sizeof(fault_instr_addr)) {
            printf("\nLocating error line in child process...\n");
            // 调用addr2line解析行号,-f参数还会显示函数名
            char cmd[256];
            snprintf(cmd, sizeof(cmd), "addr2line -f -e ./segv_child %p", (void*)fault_instr_addr);
            system(cmd);
        } else {
            printf("Failed to receive fault address from child\n");
        }

        // 等待子进程彻底终止
        waitpid(pid, NULL, 0);
    }
    return 0;
}

关键注意事项

  • 不同架构的指令指针寄存器要对应正确,比如x86_64是REG_RIP,ARM平台要改成REG_PC;
  • addr2line是GNU binutils自带工具,几乎所有Linux发行版都预装,无需额外安装;
  • 如果子进程直接被内核终止(没走自定义信号处理),父进程通过waitid拿到的si_addr是内存访问地址,不是指令地址,还是自定义信号处理传指令地址更可靠。

补充吐槽

《The Linux Programming Interface》确实没覆盖sa_sigaction,我当初学的时候也纳闷,后来查man sigaction才搞明白——手册页虽然枯燥,但这类细节确实得看它。

内容的提问来源于stack exchange,提问作者Aleksa Jovanovic

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 10:06:37