linux信号机制 - 用户堆栈和内核堆栈的变化

标签: linux 信号 用户 | 发表时间:2011-07-26 18:27 | 作者:hex108 liyuan
出处:http://www.cppblog.com/
此文只简单分析发送信号给用户程序后,用户堆栈和内核堆栈的变化。没有分析实时信号,当然整个过程基本一致。很多参考了<情景分析>,所以有些代码和现在的内核可能不同,比如RESTORE_ALL,但大体的机制是类似的。

1. 一个信号小例子

hex@Gentoo ~/signal $ cat sigint.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

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 *0x80482e8
Breakpoint 1 at 0x80482e8: file sigint.c, line 6.
(gdb) handle SIGINT noprint pass
SIGINT is used 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/sigint

2.2 向该程序发送信号: kill -INT 此程序的pid号
hex@Gentoo ~/signal $ kill -INT 4639

2.3 该程序收到信号后停在断点处
Breakpoint 1, sig_int (signo=2) at sigint.c:6
6    {
(gdb) i r esp
esp            0xbfffe7ec    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 <main+58>    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    %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 <main+58>:    jmp    0x8048336 <main+58>
(gdb) i r esp
esp            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.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" 只是一段小插曲。程序从原处开始运行。


hex108 2011-07-26 18:27 发表评论

相关 [linux 信号 用户] 推荐:

linux信号机制 - 用户堆栈和内核堆栈的变化

- liyuan - C++博客-首页原创精华区
此文只简单分析发送信号给用户程序后,用户堆栈和内核堆栈的变化. 没有分析实时信号,当然整个过程基本一致. 很多参考了<情景分析>,所以有些代码和现在的内核可能不同,比如RESTORE_ALL,但大体的机制是类似的. 2.1 编译运行该程序,并设置断点在sig_int函数开头(0x80482e8),并设置SIGINT信号的处理方式.

linux内核中的信号机制--一个简单的例子

- - CSDN博客推荐文章
linux内核中的信号机制--一个简单的例子. 信号机制是类UNIX系统中的一种重要的进程间通信手段之一. 我们经常使用信号来向一个进程发送一个简短的消息. 例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置为可中断等待状态(TASK_INTERRUPTIBLE),此时我们已经失去耐心,想提前结束这个进程,于是可以通过kill命令想这个进程发送KILL信号,内核会唤醒该进程,执行它的信号处理函数,KILL信号的默认处理是退出该进程.

Linux 中用 strace 追踪系统调用和信号值

- - 博客园_iTech's Blog
原文地址: http://www.dbabeta.com/2009/strace.html. 打开man strace,我们能看到对strace的最简洁的介绍就是”strace – trace system calls and signals”. 实际上strace是一个集诊断、调试、统计与一体的工具,我们可以使用strace对应用的系统调用和信号传递的跟踪结果来对应用进行分析,以达到解决问题或者是了解应用工作过程的目的.

linux下踢出已登录用户

- - CSDN博客互联网推荐文章
通过xshell登录到linux,看到如下所示,有3个用户,但是前面两个不知在哪登录的了,那就踢出吧. 顺便注意一下“whoami”和“who am i”的不同. 本文旨在用于自己日后查阅方便,也希望能帮助到看到此篇文章的朋友. 作者:wzzfeitian 发表于2013-7-13 21:34:53 原文链接.

中国至少有200W以上linux用户

- Terry - UbuntuSoft
一直以来都对中国的Linux用户数很感兴趣,但是从搜索结果中仅得到仅是09年袁萌教授的一个估测(详情请查看这里),之后就没有一个比较明确的说法. 在一次偶然的机会发现ubuntusoft所使用的网站统计CNZZ有一个数据中心,里面统计了每个月的各种情况,如浏览器份额、操作系统份额,所以就打起了通过CNZZ数据中心来获取Linuxer数目的心思.

高效Linux用户需要了解的命令行技能

- - 灵犀志趣
最近在Quora上看到一个问答题目, 关于在高效率Linux用户节省时间Tips. 将该题目的回答进行学习总结,加上自己的一些经验,记录如下,方便自己和大家参考. 下面介绍的都是一些命令行工具,这些工具在几位回答者的日常工作中都很有用. 对于任何不了解的命令,请使用“man “查看,或者使用Google.

Linux 下强制中断其他用户的登陆连接

- - 开心平淡对待每一天。热爱生活
    有没有试过,登陆上 VPS 的时候发现已经有其他人在登陆了. 这个时候你是不是很希望把对方T掉. 因为他很可能没有经过你的授权,不知道通过什么途径获得了 ROOT 密码,然后登陆到你的 VPS 上. 又或者,当你需要运行一些程序、执行一些命令或者其他不希望让其他人在旁窥探的事情时,发现已经有人登陆到服务器上了,这个时候你是不是也很想把他立即清理出去.

支付宝为苹果与Linux用户准备无控件支付方案

- shan - cnBeta.COM
裸奔与虚拟机时代过去了,现在苹果与Linux用户也能安全又便捷地使用支付宝. 即日起,已有安全产品(宝令、数字证书、手机动态口令任一)的用户可在无控件的情况下实现安全的支付,解决了上述用户长期面临的支付困境.

通过8个技巧让你成为一个超强的Linux终端用户

- - 极客范 - GeekFan.net
使用Linux终端不仅仅是只输入命令. 学习这些基本的技巧,你就会逐渐掌握Bash shell,这个在大多数Linux发行版上默认使用的终端工具. 这篇文章是写给缺乏经验的新手的,我相信大多数高级用户已经知道了所有的这些技巧. 但是,你仍然可以看看,也许能学到你一直以来都忽略了的东西. 使用Tab键自动补全是基本的技巧.