本文共 6020 字,大约阅读时间需要 20 分钟。
转自:
此文只简单分析发送信号给用户程序后,用户堆栈和内核堆栈的变化。没有分析实时信号,当然整个过程基本一致。很多参考了<情景分析>,所以有些代码和现在的内核可能不同,比如RESTORE_ALL,但大体的机制是类似的。
1. 一个信号小例子hex@Gentoo ~/signal $ cat sigint.c #include#include #include void sig_int(int signo){ printf("hello\n");}int main(){ if(signal(SIGINT, sig_int) == SIG_ERR){ printf("can't catch SIGINT\n"); exit(-1); } for(;;) ; return 0;}2. 用户堆栈里发生的故事2.1 编译运行该程序,并设置断点在sig_int函数开头(0x80482e8),并设置SIGINT信号的处理方式hex@Gentoo ~/signal $ gdb ./sigint(gdb) b *0x80482e8Breakpoint 1 at 0x80482e8: file sigint.c, line 6.(gdb) handle SIGINT noprint passSIGINT is used by the debugger.Are you sure you want to change it? (y or n) ySignal Stop Print Pass to program DescriptionSIGINT No No Yes Interrupt(gdb) rStarting program: /home/gj/signal/sigint 2.2 向该程序发送信号: kill -INT 此程序的pid号hex@Gentoo ~/signal $ kill -INT 46392.3 该程序收到信号后停在断点处Breakpoint 1, sig_int (signo=2) at sigint.c:66 {(gdb) i r espesp 0xbfffe7ec 0xbfffe7ec(gdb) x/40a 0xbfffe7ec0xbfffe7ec: 0xb7fff400 0x2 0x33 0x00xbfffe7fc: 0x7b 0x7b 0x8048930 <__libc_csu_init> 0x80488f0 <__libc_csu_fini>0xbfffe80c: 0xbfffed58 0xbfffed40 0x0 0x00xbfffe81c: 0xbfffec18 0x0 0x0 0x00xbfffe82c: 0x8048336 0x73 0x213 0xbfffed400xbfffe83c: 0x7b 0xbfffead0 0x0 0x00xbfffe84c: 0x0 0x0 0x0 0x00xbfffe85c: 0x0 0x0 0x0 0x00xbfffe86c: 0x0 0x0 0x0 0x00xbfffe87c: 0x0 0x0 0x0 0x0栈上的内容为信号栈sigframe:根据此结构可以知道:1). 返回地址0xb7fff400,它指向vdso里的sigreturn(gdb) x/10i 0xb7fff400 0xb7fff400 <__kernel_sigreturn>: pop %eax 0xb7fff401 <__kernel_sigreturn+1>: mov $0x77,%eax 0xb7fff406 <__kernel_sigreturn+6>: int $0x80这个地址根据内核的不同而不同,我的内核版本是2.6.38。2). 信号处理程序完成后,会回到 eip = 0x8048336 的地址继续执行。2.4 执行完sig_int函数后,进入了__kernel_sigreturn,接着回到了代码0x8048336处,一切恢复了正常。(gdb) x/5i $pc=> 0x8048336 : jmp 0x8048336 (gdb) i r espesp 0xbfffed40 0xbfffed40在用户层我们能看到的只有上面这么多信息了,可能有一个地方不能理解:在上面过程c中 从0xbfffe7ec起那一块栈上的内容从哪来的?(正常情况下堆栈esp应该一直指向在过程d中显示的esp值0xbfffed40)现在来看看在上面这些现象之下,内核的堆栈发生了怎样的变化。3. 内核堆栈里发生的故事3.1 发信号时 在 2.2 里当执行kill -INT 4639后,pid为4639的程序(也就是我们运行的 ./sigint)会收到一个信号,但是信号实际都是在内核里实现的。每个进程(这里只讲进程的情况,线程类似,线程有一个tid)都有一个pid,与此pid对应有一个结构 task_struct ,在task_struct里有一个变量 struct sigpending pending,当该进程收到信号时,并不会立即作出反应,只是让内核把这个信号记在了此变量里(它里面是一个链表结构)。当然,此时与内核堆栈还没有多大关系。3.2 检测信号 如果只记录了信号,但没有相应反应,那有什么用啊。一个进程在什么 情况下会检测信号的存在呢?在 <情景分析> 里说到了:“在中断机制中,处理器的硬件在每条指令结束时都要检测是否有中断请求的存在。信号机制是纯软件的,当然不能依靠硬件来检测信号的到来。同时,要在每条指令结束时都来检测显然是不现实的,甚至是不可能的。所以对信号的检测机制是:每当从系统调用,中断处理或异常处理返回到用户空间的前夕;还有就是当进程被从睡眠中唤醒(必定是在系统调用中)的时候,此时若发现有信号在等待就要提前从系统调用返回。总而言之,不管是正常返回还是提前返回,在返回到用户空间的前夕总是要检测信号的存在并作出反应。” 因此,对收到的信号做出反应的时间是 从内核返回用户空间的前夕,那么有那些情况会让程序进入内核呢?答案是中断,异常和系统调用。简单了解一下它们发生时内核堆栈的变化。 //-----中断,异常,系统调用 : 开始 1)在用户空间发生中断时,CPU会自动在内核空间保存用户堆栈的SS, 用户堆栈的ESP, EFLAGS, 用户空间的CS, EIP, 中断号 - 256 | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP | 中断号 - 256 进入内核后,会进行一个SAVE_ALL,这样内核栈上的内容为: | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP | 中断号 - 256 | ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX 好了,一切都处理完时,内核jmp到RESTORE_ALL(它是一个宏,例:在x86_32体系结构下,/usr/src/kernel/arch/286/kernel/entry_32.S文件里包含该宏的定义) RESTORE做的工作,从它的代码里就可以看出来了: 首先把栈上的 ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX pop到对应的寄存器里 然后将esp + 4 把 “中断号 - 256” pop掉 此时内核栈上的内容为: | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP 最后执行iret指令,此时CPU会从内核栈上取出SS, ESP, ELFGAS, CS, EIP,然后接着运行。 2) 在用户空间发生异常时,CPU自动保存在内核栈的内容为: | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP | 出错代码 error_code (注:CPU只是在进入异常时才知道是否应该把出错代码压入堆栈(为什么?),而从异常处理通过iret指令返回时已经时过境迁,CPU已经无从知当初发生异常的原因,因此不会自动跳过这一项,而要靠相应的异常处程序对堆栈加以调整,使得在CPU开始执行iret指令时堆栈顶部是返回地址) 进入内核后,没有进行SAVE_ALL,而是进入相应的异常处理函数(这个函数是包装后的,真正的处理函数在后面)(在此函数里会把真正的处理函数的地址push到栈上),然后jmp到各种异常处理所共用的程序入口error_code,它会像SAVE_ALL那样保存相应的寄存器(没有保存ES),此时内核空间上的内容为: | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP | 出错代码 error_code | 相应异常处理函数入口 | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX (注:如果没有出错代码,则此值为0) 最后结束时与中断类似(RESTORE_ALL)。 3) 发生系统调用时,CPU自动保存在内核栈的内容为: | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP 为了与中断和异常的栈一致,在进入系统调用入口(ENTRY(system_call))后会首先push %eax,然后进行SAVE_ALL,此时内核栈上的内容为 | 用户堆栈的SS | 用户堆栈的ESP | EFLAGS | 用户空间的CS | EIP | EAX | ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX 最后结束时与中断类似(RESTORE_ALL)。 //-----中断,异常,系统调用 : 结束 中断,异常,系统调用这部分有一点遗漏的地方:检测信号的时机就是紧挨着RESTORE_ALL之前发生的。3.3 对检测到的信号做出反应 如果检测到有要处理的信号时,就要开始做一些准备工作了,此时内核里的内容为(进入内核现场时的内容) | 用户堆栈的SS1 | 用户堆栈的ESP1 | EFLAGS1 | 用户空间的CS1 | EIP1 | ? | ES1 | DS1 | EAX1 | EBP1 | EDI1 | ESI1 | EDX1 | ECX1 | EBX1 (注:?的值有三个选择:中断号 - 256/出错代码 error_code/出错代码 error_code) 假设将要处理的信号对应的信号处理程序是用户自己设置的,即本文中SIGINT对应的信号处理程序sig_int。 现在要做的事情是让cpu去执行信号处理程序sig_int,但是执行前需要做好准备工作: 3.3.1 setup_frame 在用户空间设置好信号栈(struct sigframe)(假设设置好栈后esp的值为sigframe_esp,在本文中其值为0xbfffe7ec),即在2.3里看到的栈内容。 注:struct sigframe里至少包含以下内容: 用户堆栈的SS1, 用户堆栈的ESP1, EFLAGS1, 用户空间的CS1, EIP1, ES1, DS1, EAX1, EBP1, EDI1, ESI1, EDX1, ECX1, EBX1 3.3.2 设置即将运行的eip的值为信号处理函数sig_int的地址(为0x80482e8),并设置用户ESP的值为sigframe_esp(为0xbfffe7ec),这是通过修改内核栈里的EIP和ESP的值实现的,因为在从系统调用里iret时,会从内核栈里取EIP,ESP。 这时内核栈的内核为: | 用户堆栈的SS1 | 0xbfffe7ec | EFLAGS1 | 用户空间的CS1 | 0x80482e8 | ? | ES1 | DS1 | EAX1 | EBP1 | EDI1 | ESI1 | EDX1 | ECX1 | EBX1 最后,进行RESTORE_ALL,内核栈上的内容为: | 用户堆栈的SS1 | 0xbfffe7ec | EFLAGS1 | 用户空间的CS1 | 0x80482e8 RESTORE_ALL里执行完iret后,寄存器内容为: EIP为0x80482e8(即sig_int),esp为0xbfffe7ec 。 于是用户空间到了步骤 2.33.4 信号处理程序完成以后 2.3 -> 2.4,进入了sig_return系统调用,在sig_return里,内核栈的内容为(每个名字后面加一个2以便与前面的1区分) | 用户堆栈的SS2 | 用户堆栈的ESP2 | EFLAGS2 | 用户空间的CS2 | EIP2 | ? | ES2 | DS2 | EAX2 | EBP2 | EDI2 | ESI2 | EDX2 | ECX2 | EBX2 sig_return要做的主要工作就是根据用户栈里sigframe的值修改内核栈里的内容,使内核栈变为: | 用户堆栈的SS1 | 用户堆栈的ESP1 | EFLAGS1 | 用户空间的CS1 | EIP1 | ? | ES1 | DS1 | EAX1 | EBP1 | EDI1 | ESI1 | EDX1 | ECX1 | EBX1 至此内核栈里的内容和进行信号处理前一样了。经过RESTORE_ALL后,用户堆栈里的内容也和以前一样(主要指ESP的值一样)。 "kill -INT 4639" 只是一段小插曲。程序从原处开始运行。 情景分析>
本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sky-heaven/p/5800105.html,如需转载请自行联系原作者