Linux可替换信号栈,linux信号机制 - 用户堆栈和内核堆栈的变化【转】

1. 一个信号小例子

hex@Gentoo~/signal $ cat sigint.c

#include#include#include

void sig_int(intsigno)

{

printf("hello");

}intmain()

{if(signal(SIGINT, sig_int) ==SIG_ERR){

printf("can't catch SIGINT");

exit(-1);

}for(;;)

;return 0;

}2. 用户堆栈里发生的故事2.1 编译运行该程序,并设置断点在sig_int函数开头(0x80482e8),并设置SIGINT信号的处理方式

hex@Gentoo~/signal $ gdb ./sigint

(gdb) b*0x80482e8Breakpoint1 at 0x80482e8: file sigint.c, line 6.

(gdb) handle SIGINT noprint pass

SIGINTisused by the debugger.

Are you sure you want to change it?(y or n) y

Signal Stop Print Pass to program Description

SIGINT No No Yes Interrupt

(gdb) r

Starting program:/home/gj/signal/sigint2.2 向该程序发送信号: kill -INT 此程序的pid号

hex@Gentoo~/signal $ kill -INT 4639

2.3该程序收到信号后停在断点处

Breakpoint1, sig_int (signo=2) at sigint.c:6

6{

(gdb) i r esp

esp0xbfffe7ec 0xbfffe7ec(gdb) x/40a 0xbfffe7ec

0xbfffe7ec: 0xb7fff400 0x2 0x33 0x0

0xbfffe7fc: 0x7b 0x7b 0x8048930 <__libc_csu_init> 0x80488f0 <__libc_csu_fini>

0xbfffe80c: 0xbfffed58 0xbfffed40 0x0 0x0

0xbfffe81c: 0xbfffec18 0x0 0x0 0x0

0xbfffe82c: 0x8048336 0x73 0x213 0xbfffed40

0xbfffe83c: 0x7b 0xbfffead0 0x0 0x0

0xbfffe84c: 0x0 0x0 0x0 0x0

0xbfffe85c: 0x0 0x0 0x0 0x0

0xbfffe86c: 0x0 0x0 0x0 0x0

0xbfffe87c: 0x0 0x0 0x0 0x0栈上的内容为信号栈sigframe:

根据此结构可以知道:1). 返回地址0xb7fff400,它指向vdso里的sigreturn

(gdb) x/10i 0xb7fff400

0xb7fff400 <__kernel_sigreturn>: pop %eax0xb7fff401 <__kernel_sigreturn>: mov $0x77,%eax0xb7fff406 <__kernel_sigreturn>: 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 esp

esp0xbfffed40 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里有一个变量 structsigpending 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.1setup_frame

在用户空间设置好信号栈(structsigframe)(假设设置好栈后esp的值为sigframe_esp,在本文中其值为0xbfffe7ec),即在2.3里看到的栈内容。

注:structsigframe里至少包含以下内容:

用户堆栈的SS1, 用户堆栈的ESP1, EFLAGS1, 用户空间的CS1, EIP1, ES1, DS1, EAX1, EBP1, EDI1, ESI1, EDX1, ECX1, EBX13.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 | 0x80482e8RESTORE_ALL里执行完iret后,寄存器内容为: EIP为0x80482e8(即sig_int),esp为0xbfffe7ec 。 于是用户空间到了步骤2.3

3.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" 只是一段小插曲。程序从原处开始运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值