无libc多线程独立程序sigaction注册信号后触发内核SIGSEGV问题
核心原理:信号处理的上下文恢复流程
当Linux内核向用户态进程递送信号时,会先把当前进程(或线程)的完整上下文(寄存器值、栈指针、程序计数器等)保存到用户栈上,然后跳转到你注册的信号处理函数执行。
问题出在处理函数执行完毕后:内核不会自动帮你恢复上下文,必须通过特定机制告诉内核“我处理完了,把之前的状态找回来”。这个机制就是rt_sigreturn系统调用。
有libc vs 无libc的差异
在带libc的程序里,glibc、musl等库会自动帮你设置好sa_restorer——它们会提供一个封装了rt_sigreturn的恢复函数,同时自动加上SA_RESTORER标志,你完全感知不到这个过程。但你写的是无libc独立程序,没有这个默认逻辑,所有细节都得自己处理。
为什么会触发SIGSEGV?
当你没设置sa_restorer时,信号处理函数执行完后,CPU没有正确的返回目标,会继续执行栈上的垃圾数据(或者尝试从被破坏的栈地址取指令),直接触发非法内存访问,也就是内核发的SIGSEGV。
你移除子线程的futex等待后程序能正常退出,是因为进程直接终止了——不需要恢复上下文回到futex等待的代码流,自然不会暴露这个问题。多线程场景下,每个线程的栈和上下文独立,信号递交给等待futex的线程时,缺失恢复逻辑的后果会立刻显现。
sa_restorer与SA_RESTORER的具体作用
sa_restorer:是一个函数指针,指向你自己实现的恢复函数,这个函数的唯一作用就是调用rt_sigreturn系统调用。内核会在信号处理函数执行完毕后,自动跳转到这个恢复函数。SA_RESTORER标志:告诉内核“我已经提供了自定义的恢复函数,不要使用默认的(无libc场景下默认恢复函数根本不存在)”。
比如musl的实现里,恢复函数就是一段简单的汇编:
restorer: mov $__NR_rt_sigreturn, %rax syscall
你需要把这个函数的地址赋值给struct sigaction的sa_restorer成员,同时设置sa_flags |= SA_RESTORER。
总结
无libc的Linux程序必须手动处理信号的上下文恢复,这是内核信号机制的硬性要求——内核只负责保存上下文,恢复的动作需要用户态通过rt_sigreturn触发。多线程场景下,线程上下文的独立性让这个问题更突出,缺失恢复逻辑会直接导致内存访问错误。
内容的提问来源于stack exchange,提问作者Beepboop

