前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux信号处理源码分析(基于linux0.11)

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

作者头像
theanarkh
发布2019-09-02 16:57:25
4.6K1
发布2019-09-02 16:57:25
举报
文章被收录于专栏:原创分享原创分享

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

代码语言:javascript
复制
_syscall0(int,fork)
代码语言:javascript
复制
#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; \
}

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

代码语言:javascript
复制
init main() {
  int pid = fork();
  return 0;
}

main初始化的用户栈结构

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

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

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

代码语言:javascript
复制
_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函数。

代码语言:javascript
复制
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后执行了以下代码。

代码语言:javascript
复制
  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函数的时候,会执行。

代码语言:javascript
复制
push ebp;
mov ebp,esp

这时候的用户栈是

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

代码语言:javascript
复制
mov esp,ebp
pop ebp

执行完后的用户栈是。

然后再执行

代码语言:javascript
复制
ret

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

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

代码语言:javascript
复制
addl $4, %esp
popl %eax
popl %ecx
popl %edx
popfl // 即eflags寄存器
ret

执行完后的用户栈

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

代码语言:javascript
复制
mov ebp, esp 
pop ebp

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

代码语言:javascript
复制
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系统调用。接着系统就会进入信号处理的流程。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档