信号处理函数是如何返回的(1)? 您所在的位置:网站首页 stack函数调用 信号处理函数是如何返回的(1)?

信号处理函数是如何返回的(1)?

2023-05-06 18:55| 来源: 网络整理| 查看: 265

从程序入手, 信号中断的基本逻辑

下面的程序说明了一个基本的信号处理函数如何建立:

123456789101112131415161718192021222324252627#define _GNU_SOURCE#include #include #include void sighandler(int signo, siginfo_t *info, void *ctx) { printf("处理函数\n");}int main() { stack_t s; s.ss_flags = 0; s.ss_size = 40000; s.ss_sp = malloc(40000); sigaltstack(&s, NULL); struct sigaction action; action.sa_flags = SA_SIGINFO|SA_ONSTACK; sigfillset(&action.sa_mask); action.sa_sigaction = sighandler; sigaction(SIGINT, &action, NULL); while (1) { }}

此处我们使用了信号自定义栈, 在跳入信号处理函数时, 内核将使用这个栈作为信号处理函数的栈。在main函数中, 有一个无限循环, 如果我们在程序运行时按CTRL+C, SIGINT信号将被发送给进程, 并保存在此进程的task_struct中。内核在调度此进程时, 查到此进程的task_struct里面有未决的且未被阻塞的信号, 便向这个自定义栈插入栈帧(就是向栈顶插入一些信息, 包括一些寄存器信息, 以及原来的栈信息), 并跳入预设的信号处理函数中。在系统设置的栈帧中包含了足够的寄存器信息, 在信号处理函数中调用sigreturn, 内核将弹出栈帧并恢复原来的执行流执行, 此时各种寄存器都会被恢复, 包括栈也会被恢复, 这样就完整恢复了上下文, 回到原来的执行流了。

下面是这个栈帧的示意图。此外, 这里有篇文章, 通过gdb调试探究了栈帧的结构, 值得一读。

进入信号处理函数时栈的结构

这个图片是我从别人的博客上扣下来的, 实际上黄色的那部分是sigaction设置中的sa_restorer函数的地址, 在我们使用glibc编程时, 我们其实是没法自定义sa_restorer的(即使我们设置成自己的, glibc也会用自己设计好的restorer代码, 可见, struct sigaction的sa_restorer是冗余字段, 没有任何意义的嘛!), 这个代码很简单, 只是简单的几行汇编, 如下:

123__restore_rt: mov $15, %rax syscall

可见, 它只是调用了15号系统调用sigreturn, 内核会弹出栈帧中的信息, 并用来为进程设置新的上下文, 此外, 栈帧中保存的信号屏蔽字也将被恢复。

这种原子操作很适合做跳转, 它能原子性地设置信号屏蔽字并执行跳转, 是很牛的系统调用, 通过精心设计的栈帧, 它能跳转到任何地方, 并原子地设置信号屏蔽字, 这不就是在用户态模拟内核态的硬件中断吗?

神秘的第三个参数:

我们注意到sa_sigaction, 也就是sigaction的信号处理函数签名如下:

1void(*) (int, siginfo_t *, void *)

其中第一个参数是信号的号码, 比如9代表SIGKILL。

第二个参数为信号自身附带的信息, 比如我们知道, 一个支持多任务的shell程序能够获悉任务的运行情况, 看下面的测试:

12markity@mycom ~/D/Blog (main)> cat &markity@mycom ~/D/Blog (main)> fish: Job 1, 'cat &' has stopped

既然shell能获得子进程的执行情况, 那么必然是内核提供的某种通知机制。好吧, 其实这个机制的秘密就是这个第二个参数, 里面有SIGCHLD信号的专属信息(当然其它类型的信号也有专属的额外信息), 比如进程id, 这里不过多展开, 点到为止。

第三个参数, 也是最神秘的参数, 其实它真实的类型不是void *, 它其实是ucontxt_t *, 它经过严谨的设计, 它其实就是栈帧的地址。这个参数可以让我们自己修改栈帧, 从而跳转到别的地方去。但如果我们不修改它, 那么栈帧就不会被修改, 最终返回时就会跳转到被中断的原处。下面的程序尝试修改这个结构, 不跳转回原来的地方, 而是跳转到另外的地方。

1234567891011121314151617181920212223242526272829303132#define _GNU_SOURCE#include #include #include #include void restore_here() { printf("哈哈\n");}void sighandler(int signo, siginfo_t *info, void *ctx) { makecontext(ctx, restore_here, 0);}int main() { stack_t s; s.ss_flags = 0; s.ss_size = 40000; s.ss_sp = malloc(40000); sigaltstack(&s, NULL); struct sigaction action; action.sa_flags = SA_SIGINFO|SA_ONSTACK; sigfillset(&action.sa_mask); action.sa_sigaction = sighandler; sigaction(SIGINT, &action, NULL); while (1) { }}

运行时, 按CTRL+C, 打印哈哈\n后退出。如果熟悉ucontext应该能理解这个程序, 如果不太熟悉可以查下ucontext这个设施, 也可以阅读此篇查看ucontext怎么使用。

如何重启系统调用?

下面是一段代码, 中断后它总能恢复之前的系统调用, 无论我们按多少次CTRL+C, 程序也不结束:

12345678910111213141516171819202122232425262728293031#define _GNU_SOURCE#include #include #include #include #include #include #include void sighandler(int signo, siginfo_t *info, void *ctx) {}int main() { stack_t s; s.ss_flags = 0; s.ss_size = 40000; s.ss_sp = malloc(40000); sigaltstack(&s, NULL); struct sigaction action; action.sa_flags = SA_SIGINFO|SA_ONSTACK|SA_RESTART; sigfillset(&action.sa_mask); action.sa_sigaction = sighandler; sigaction(SIGINT, &action, NULL); getchar();}

相反, 下面这个程序按CTRL+C就直接结束了:

12345678910111213141516171819202122232425262728293031#define _GNU_SOURCE#include #include #include #include #include #include #include void sighandler(int signo, siginfo_t *info, void *ctx) {}int main() { stack_t s; s.ss_flags = 0; s.ss_size = 40000; s.ss_sp = malloc(40000); sigaltstack(&s, NULL); struct sigaction action; action.sa_flags = SA_SIGINFO|SA_ONSTACK|SA_INTERRUPT; sigfillset(&action.sa_mask); action.sa_sigaction = sighandler; sigaction(SIGINT, &action, NULL); getchar();}

所以, sa_flags中的SA_RESTART可以告知内核, 中断恢复后是否重启系统调用。这个重启的原理是什么呢?

在发生信号中断后, 内核能根据task_struct的信息判断此进程对某个中断的处理策略, 如果对此信号采取重启系统调用的策略, 那么内核会在设置信号处理函数的栈帧时编辑一下rip的值, 使之回到syscall那一行。比如在我的x86_64架构下, syscall指令占了两个字节, 那么设置栈帧时, rip就会被设置为origin_rip-2, 从而在sigreturn跳转时, 回到syscall指令的那一行。从这里可以找到线索。

意义?

了解了sigreturn系统调用, 我们可以更优雅地了解阻止信号处理函数重入的原理, 从而写出不可能发生重入的平坦化的代码。之前的文章中, 我们用swapcontext做了上下文切换。然而swapcontext和setcontext很拉跨, 它先调用setprocmask, 再设置了rip寄存器进行跳转, 在极端情况下, 这就可能重入。然而, 有了sigreturn, 就能规避这个风险, 那就很完美了!

继续阅读下一篇

信号处理函数是如何返回的(2)?

参考资料 sigreturn-orient攻击概览, sigreturn原理探究 被中断的系统调用如何重启?


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有