如何捕获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参数,保留调试信息,否则后续无法把地址转换成源码行号。
父进程监控子进程并定位错误行号
你已经有父进程监控的代码,核心要做的是:
- 父进程创建管道,fork子进程时把管道写端传给子进程;
- 子进程触发SIGSEGV后,把出错的指令地址传给父进程;
- 父进程拿到地址后,用
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
相关产品推荐
相关产品推荐

