跑 busybox sh 时发现一个诡异的问题:Ctrl+C 能杀掉进程,但 handler 执行完之后程序直接崩了——寄存器全乱了。调查下去,发现 ch53 的信号系统有三个根本缺陷。
问题一:寄存器没有保存
ch53 的 signal_dispatch 只保存了 user_rip 和 user_rsp:
uint64_t new_rsp = *user_rsp - 8;
*(uint64_t *)(phys + ...) = *user_rip; // 仅压返回地址
*user_rsp = new_rsp;
*user_rip = handler;
handler 执行期间会用到 r15, r14, …, rbx, rbp——这些全都没有保存。handler 返回时弹出的 rip 确实是原来的位置,但所有调用者保存的寄存器都被 handler 破坏了,接下来的代码跑的是垃圾数据。
问题二:sigaction 是空 stub
case SYS_SIGACTION:
case SYS_SIGPROCMASK:
case SYS_SIGRETURN:
return 0; // 什么都没做
musl 的 signal() 底层调用 sigaction,返回 0 让它以为设置成功了,实际上 handler 根本没有注册进去。
问题三:没有 signal mask
handler 执行期间,如果同一个信号再次到来,会再次触发 dispatch,handler 递归执行,栈很快就溢出了。
解法:signal_frame_t + 真正的 sigaction
核心数据结构
把所有寄存器打包成一个结构体,压到用户栈:
typedef struct {
uint64_t r15, r14, r13, r12, r11, r10, r9, r8;
uint64_t rdi, rsi, rdx, rcx, rbx, rbp, rax;
uint64_t rip, rsp, rflags;
sigset_t saved_mask; // 恢复用
uint32_t sig;
} signal_frame_t;
进程结构体换掉旧的 signal_handlers[]:
sigset_t sig_mask;
k_sigaction_t sig_actions[NSIG]; // handler + sa_flags + sa_mask
syscall_entry.asm:传整个帧
原来只传两个指针,现在传整个内核栈帧:
push rax ; 保存 syscall 返回值
lea rdi, [rsp + 8] ; frame* 指向 r9 slot
lea rsi, [rsp + 0] ; &retval
call signal_dispatch_frame
pop rax
内核栈从低到高的布局是 r9, r8, r10, rdx, rsi, rdi, r15, r14, r13, r12, rbx, rbp, rflags, rip, user_rsp——正好对应一个 kframe_t 结构体,C 代码直接读写。
signal_dispatch_frame
void signal_dispatch_frame(kframe_t *kf, uint64_t *retval) {
uint32_t deliverable = current->pending_signals & ~current->sig_mask;
// ...
// 计算用户栈上的 frame 位置(16 字节对齐)
uint64_t frame_sp = (kf->user_rsp - sizeof(signal_frame_t)) & ~0xFULL;
// 把所有寄存器复制到用户栈
signal_frame_t frame = { .r15=kf->r15, ..., .rip=kf->rip, ... };
frame.saved_mask = current->sig_mask;
copy_to_user_bytes(current->pml4, frame_sp, &frame, sizeof(frame));
// 屏蔽当前信号(默认不递归)
current->sig_mask |= sa->sa_mask | (1u << sig);
// 如果有 sa_restorer(musl 的 __restore_rt),压到返回地址
if (sa->sa_restorer) {
uint64_t ret_sp = frame_sp - 8;
// 写 sa_restorer 到 [ret_sp]
kf->user_rsp = ret_sp;
}
kf->rip = handler;
kf->rdi = sig; // handler 的第一个参数
}
SYS_SIGRETURN:还原
musl 的 __restore_rt 会执行 syscall SYS_SIGRETURN,内核从用户栈上读回 signal_frame_t,把所有寄存器写回内核栈帧:
case SYS_SIGRETURN: {
kframe_t *kf = (kframe_t *)syscall_frame;
signal_frame_t uf;
copy_from_user((uint8_t *)&uf, kf->user_rsp, sizeof(uf));
kf->r15 = uf.r15; /* ... 全部还原 */
kf->rip = uf.rip;
kf->user_rsp = uf.rsp;
kf->rflags = uf.rflags;
current->sig_mask = uf.saved_mask; // 还原 mask
return uf.rax; // 还原 syscall 返回值
}
sysret 之后,进程在被信号打断的那条指令继续执行,所有寄存器都回来了。
SIGCHLD + 僵尸进程自动回收
子进程退出时自动发 SIGCHLD 给父进程:
// proc_exit 里
k_sigaction_t *sa = &procs[ppid].sig_actions[SIGCHLD];
if (sa->sa_flags & SA_NOCLDWAIT) {
current->state = PROC_UNUSED; // 父进程不需要 wait,直接回收
} else {
current->state = PROC_ZOMBIE;
signal_send(ppid, SIGCHLD); // 通知父进程
}
shell 注册 SA_NOCLDWAIT 后,sh -c "cmd" 的子进程退出时自动回收,不会堆积僵尸。
SIGPIPE
之前用硬编码数字 13,改成用宏,顺便修返回值:
if (p->read_fds == 0) {
signal_send(current->pid, SIGPIPE);
return -EPIPE; // 原来返回 -1,现在返回正确的错误码
}
程序可以 signal(SIGPIPE, SIG_IGN) 忽略它,让 write 直接返回 -EPIPE 而不是崩掉。
完整信号流程
kill(pid, SIGUSR1)
→ signal_send: procs[pid].pending |= (1<<SIGUSR1)
syscall 返回前 signal_dispatch_frame:
→ 计算 frame_sp = user_rsp - sizeof(signal_frame_t)
→ 把所有寄存器写入用户栈 frame_sp
→ sig_mask |= (1<<SIGUSR1)(防递归)
→ kf->rip = handler, kf->rdi = sig
→ [ret_sp] = sa_restorer
sysret → 跳到 handler(SIGUSR1)
→ handler 执行,可以用所有寄存器
→ 执行完,ret 弹出 sa_restorer 地址
sa_restorer(musl __restore_rt):
→ syscall SYS_SIGRETURN
SYS_SIGRETURN:
→ 从用户栈读 signal_frame_t
→ 还原所有寄存器 + sig_mask
→ sysret → 回到被中断的指令
验收
/ # kill -USR1 $$ ← 发信号给 shell 自身
caught SIGUSR1 ← handler 打印
/ # ← shell 继续正常工作,寄存器没乱
/ # ls | grep bin ← SIGPIPE 正常处理,不崩溃
bin