专栏首页原创分享linux信号处理源码分析(基于linux0.11)

linux信号处理源码分析(基于linux0.11)

linux的信号处理时机在系统调用结束后。这里以fork系统调用函数为例子讲解这个过程。下面是fork函数的定义。

_syscall0(int,fork)
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
  // 输入输出都是eax,输入是系统调用函数在系统调用表的序号
  : "=a" (__res) \
  : "0" (__NR_##name)); \
if (__res >= 0) \
  return (type) __res; \
errno = -__res; \
return -1; \
}

假设我们有这样一个程序。

init main() {
  int pid = fork();
  return 0;
}

main初始化的用户栈结构

执行fork函数时的用户栈结构

fork调用,进入内核态时的内核栈结构。

我们从sched.c的sched_init函数中知道,中断号80对应的中断处理程序是system_call。该函数在system_call.s中定义。具体的分析可以看linux0.11系统调用过程和fork源码解析这篇文章。下面贴一下代码。

_system_call:
   // 比较参数,不合法的参数直接返回中断,错误码是-1
   cmpl $nr_system_calls-1,%eax
   ja bad_sys_call
   // 寄存器压栈,保存现场和用户传递的参数
   push %ds
   push %es
   push %fs
   // 执行系统调用的函数时用户传入的三个参数,右到左,ebx是第一个参数
   pushl %edx
   pushl %ecx    # push %ebx,%ecx,%edx as parameters
   pushl %ebx    # to the system call
   // 0x10是内核数据段的选择子
   movl $0x10,%edx    # set up ds,es to kernel space
   mov %dx,%ds
   mov %dx,%es
   movl $0x17,%edx    # fs points to local data space
   mov %dx,%fs
   // 根据参数,从系统表格里找到对应的函数,每个函数地址4个字节
   call _sys_call_table(,%eax,4)
   // 系统调用的返回值,压栈保存,因为下面需要用eax
   pushl %eax
   // 把当前进程的pcb地址赋值给eax
   movl _current,%eax
   // 判断当前进程状态,0是可执行,即判断当前进程是否可以继续执行
   cmpl $0,state(%eax)    # state
   // CMP结果为0则zf等于1,jne是zf为0则跳转,所以下面是当前进程state不为0,则跳转,即重新调度
   jne reschedule
   // 时间片用完则重新调度
   cmpl $0,counter(%eax)    # counter
   je reschedule
ret_from_sys_call:
   movl _current,%eax    # task[0] cannot have signals
   // 判断当前执行的进程是不是0号进程
   cmpl _task,%eax
   // 是的话跳到标签3
   je 3f
   cmpw $0x0f,CS(%esp)    # was old code segment supervisor ?
   jne 3f
   cmpw $0x17,OLDSS(%esp)    # was stack segment = 0x17 ?
   jne 3f
   // 把这两个字段赋值给寄存器
   movl signal(%eax),%ebx
   movl blocked(%eax),%ecx
   // 对block变量的值取反,即没有屏蔽的为变成1,表示需要处理的信号
   notl %ecx
   // 把收到的信号signal和没有屏蔽的信号,得到需要处理的信号,放到ecx中
   andl %ebx,%ecx
   /*
     Bit Scan Forward,如果ecx等于0,则zf等于1,否则zf是0
     从低位到高位扫描ecx,把等于第一个是1的位置写到ecx中,即第一位是1则位置是0
   */
   bsfl %ecx,%ecx
   // zf=1即ecx是0则跳转,代表没有需要处理的信号则跳转
   je 3f
   // 把ebx的第ecx位清0,并把1移到CF,处理了该信号,清0
   btrl %ecx,%ebx
   
   movl %ebx,signal(%eax)
   // 当前需要处理的信号加1,因为ecx保存的是位置,位置是0开始的,信号是1-32
   incl %ecx
   // 入参压栈
   pushl %ecx
   // 执行信号处理函数
   call _do_signal
   popl %eax
3:  popl %eax
   popl %ebx
   popl %ecx
   popl %edx
   pop %fs
   pop %es
   pop %ds
   iret

我们直接从call _do_signal这里开始分析。这时候的内核栈结构是。

然后调用do_signal函数。

void do_signal(long signr,long eax, long ebx, long ecx, long edx,
   long fs, long es, long ds,
   long eip, long cs, long eflags,
   unsigned long * esp, long ss)
{
   unsigned long sa_handler;
   // 系统调用返回时下一句代码的地址
   long old_eip=eip;
   // 取得该信号对应的处理结构
   struct sigaction * sa = current->sigaction + signr - 1;
   int longs;
   unsigned long * tmp_esp;
   // 处理函数
   sa_handler = (unsigned long) sa->sa_handler;
   // 处理函数是忽略信号量
   if (sa_handler==1)
     return;
   // 没有处理函数又不是SIGCHLD信号则进程退出
   if (!sa_handler) {
     if (signr==SIGCHLD)
       return;
     else
       do_exit(1<<(signr-1));
   }
   // 该处理函数只处理一次信号,即只会执行一次,清空
   if (sa->sa_flags & SA_ONESHOT)
     sa->sa_handler = NULL;
   // 修改eip的值,即系统调用返回时从这执行
   *(&eip) = sa_handler;
   // SA_NOMASK即在执行当前信号的处理函数时屏蔽当前的信号,防止嵌套,不开启的时候,需要多压栈一个参数,见下面 
   longs = (sa->sa_flags & SA_NOMASK)?7:8;
   // 拓展用户栈栈顶
   *(&esp) -= longs;
   verify_area(esp,longs*4);
   // 不能修改esp,所以赋值一下
   tmp_esp=esp;
   // 压入用户栈,中断返回时执行
   put_fs_long((long) sa->sa_restorer,tmp_esp++);
   put_fs_long(signr,tmp_esp++);
   if (!(sa->sa_flags & SA_NOMASK))
     put_fs_long(current->blocked,tmp_esp++);
   put_fs_long(eax,tmp_esp++);
   put_fs_long(ecx,tmp_esp++);
   put_fs_long(edx,tmp_esp++);
   put_fs_long(eflags,tmp_esp++);
   // 用户程序的地址
   put_fs_long(old_eip,tmp_esp++);
   // 执行该信号处理函数时屏蔽某些信号
   current->blocked |= sa->sa_mask;
}

该函数的入参和内核栈一一对应。我们回到system_call函数。do_signal后执行了以下代码。

  popl %eax
3:  popl %eax
   popl %ebx
   popl %ecx
   popl %edx
   pop %fs
   pop %es
   pop %ds
   iret

这时候的内核栈是空的(iret指令和call指令的影响可以百度或者参考intel手册),用户栈结构是。iret指令会弹出ip寄存器的值,在do_signal函数里,已经把这ip的值改为sa_handler的地址(有点缓冲区溢出攻击的感觉),所以结束系统调用后,会执行sa_handler函数。这时候的用户栈是。

在执行sa_handler函数的时候,会执行。

push ebp;
mov ebp,esp

这时候的用户栈是

sa_handler函数通过ebp+8得到第一个实参,即信号的数值。通过esp+4得到第一个局部变量的值。如此类推。sa_handler执行完后。会执行。

mov esp,ebp
pop ebp

执行完后的用户栈是。

然后再执行

ret

把sa_restorer函数的值压进ip寄存器。这时候的用户栈是

接着执行sa_restorer。sa_restorer函数是库函数提供的。用于清除栈上的参数和正确返回用户进程要执行的下一条执行。 代码如下:

addl $4, %esp
popl %eax
popl %ecx
popl %edx
popfl // 即eflags寄存器
ret

执行完后的用户栈

我们发现这个栈结构和开始系统调用之前的结构是一样的。这时候回到_syscall0这个宏的代码里。把eax的值给_res变量。然后从_syscall0返回。同样是执行。

mov ebp, esp 
pop ebp

再通过ret指令。回到main函数执行return 0。整个过程结束。信号处理的时机是在进程进行系统调用的时候。假设通过kill系统调用给进程发送一个信号。

int sys_kill(int pid,int sig)
{
  struct task_struct **p = NR_TASKS + task;
  int err, retval = 0;
  // pid等于0则给当前进程的整个组发信号,大于0则给某个进程发信号,-1则给全部进程发,小于-1则给某个组发信号
  if (!pid) while (--p > &FIRST_TASK) {
    if (*p && (*p)->pgrp == current->pid) 
      if (err=send_sig(sig,*p,1))
        retval = err;
  } else if (pid>0) while (--p > &FIRST_TASK) {
    if (*p && (*p)->pid == pid) 
      if (err=send_sig(sig,*p,0))
        retval = err;
  } else if (pid == -1) while (--p > &FIRST_TASK)
    if (err = send_sig(sig,*p,0))
      retval = err;
  else while (--p > &FIRST_TASK)
    if (*p && (*p)->pgrp == -pid)
      if (err = send_sig(sig,*p,0))
        retval = err;
  return retval;
}
/*
  发送信号给进程sig是发送的信号,p是接收信号的进程,priv是权限,
  1是代表可以直接设置,比如给自己发信息,priv为0说明需要一定的权限
*/
static inline int send_sig(long sig,struct task_struct * p,int priv)
{
  if (!p || sig<1 || sig>32)
    return -EINVAL;
  // 这里使用euid,即进程设置了suid位的话,可以扩大权限,即拥有文件属主的权限
  if (priv || (current->euid==p->euid) || suser())
    p->signal |= (1<<(sig-1));
  else
    return -EPERM;
  return 0;
}

我们发现发送一个信号只是在进程的block字段打标记,然后结束kill系统调用。接着系统就会进入信号处理的流程。

本文分享自微信公众号 - 编程杂技(theanarkh),作者:theanarkh

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 通过do_execve源码分析程序的执行(下)(基于linux0.11)

    上篇讲了程序的加载。然后设置了eip,这一篇分析一下开始执行第一条指令的时候。会发生什么。 我们先看一下这时候的内存布局。

    theanarkh
  • linux进程通信之共享内存原理(基于linux 1.2.13)

    1 有一个全局的结构体数据,每次需要一块共享的内存时(shmget),从里面取一个结构体,记录相关的信息。

    theanarkh
  • nginx0.1.0之event模块初始化源码分析(1)

    nginx模块初始化的流程在下面的代码中,核心模块的初始化,各核心模块首先在create_conf中创建保存配置的数据结构,然后在ngx_conf_parse中...

    theanarkh
  • ACM,算法

    最近Topcoder的XD遇到了一个难题,倘若一个数的三次方的后三位是111,他把这样的数称为小光棍数。他已经知道了第一个小光棍数是471,471的三次方是10...

    书童小二
  • [答题赛(第8轮)]

    用户6288414
  • ZooKeeper ZAB 协议模式

    当服务框架在启动中,或是当 Leader 服务器出现网络中断、崩溃退出或重启等异常情况时,ZAB 协议就会进人恢复模式,然后选举产生新的 Leader 服务器。

    happyJared
  • 野生前端的数据结构基础练习(4)——字典

    以键值对形式存储数据的数据结构,在Javascript中更多地是直接使用对象,一般只在有排序需求的场景下会用到本篇中构造的Dictionary类,因为对象属性是...

    大史不说话
  • 步骤5 - Orchestra从微服务提供商获得结果,再发送回WebSocket服务器

    和第二步骤相匹配,第五步也是接收数据,因此是inbound处理,通过后缀Response区分这是一个响应。找到对应的Web Shop的WebSocket服务器s...

    Jerry Wang
  • day29_Hibernate学习笔记_01

      Hibernate:是一个数据持久化层的ORM框架。   Object:对象,java对象,此处特指JavaBean。   Relational:关系,二维...

    黑泽君
  • 一文解决TCGA任意肿瘤的差异lncRNA,miRNA,mRNA

    首先对TCGA的RNA表达预处理,筛选掉其中的低表达基因(count<10)进行预处理。根据GENCODE Release 29(GRCh38.p12)(htt...

    用户1359560

扫码关注云+社区

领取腾讯云代金券