Linux中断处理之系统调用

来源:互联网 发布:魔兽世界数据库2.43 编辑:程序博客网 时间:2024/05/15 14:11

 一:前言

  有时候,用户空间为了满足某些要求,要从内核空间去进行操作,比例建立文件,建立socket,查看内核数据等等.因此操作系统必须提供一种方式.供用户态转入内核态.我们在前面分析过tarp_init()函数.只有异常跟系统调用才能从用户空间转入到内核空间(PL值为3).但是异常通常带有很大的随意性,用户程序不好控制异常的发生点.所以,系统调用就成了沟通用户空间与内核空间的一座重要的桥梁.

  二:系统调用在用户空间的调用方式.

  在前面分析过.系统调用的中断号为0x80.所以,只要在用户空间通过int 0x80软中断方式就可以陷入内核了.为了区分不同的系统调用.必须为每一个调用指定一个序号.即系统调用号.通常,在用int 0x80中断之前,先将中断号放入寄存器eax.

  三:系统调用的参数传递方式

  系统调用是可以传递参数的.例如:int open(const char *pathname, int flags),那这些参数是如何传递的呢?系统调用采用寄存器来传值,这样,进入内核空间之后,取值非常方便.这几个寄存器依次是:ebx,ecx,edx,esi,edi,ebp.如果参数个数超过了6个,或者参数的大小大于32位,可以用传递参数地址的方法.陷入到内核空间之后,再从地址中去取值.回忆一下:我们在前面分析过的系统调用过程:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ENTRY(system_call)
   pushl %eax      # save orig_eax(系统调用号)
   SAVE_ALL
   ……
SAVE_ALL:
#define __SAVE_ALL
   cld;
   pushl %es;
   pushl %ds;
   pushl %eax;
   pushl %ebp;
   pushl %edi;
   pushl %esi;
   pushl %edx;
   pushl %ecx;
   pushl %ebx;
   movl $(__USER_DS), %edx;
   movl %edx, %ds;
   movl %edx, %es;

  发现了吧,系统调用时,把ebp到ebx压栈,再调用系统调用处理函数.这里其实是模拟了一次函数调用过程.在系统调用处理函数中,会根据处理函数的参数个数,到当前堆栈中去取参数值.

  既然在系统调用的时候可以用地址作为参数,那就必须要检查这个地址的合法性了.在以前的内核中.会对地址进行严格的检查.即会查对进程的vma判断此线性地址是否属于进程所拥有.权限是否合法.这个过程是相当耗时的.其实虽然有地址非法的错误,但毕竟是少数.犯不着为少数错误降低整个系统的效率.那还要不要检查呢?当然要了.地址非法访问会产生页面异常,推迟到页面异常程序中再处理

  四:系统调用相关代码分析:

  在前面我们在<< linux中断处理之初始化>>一文中分析过系统调用的进入和返回过程.再来看下代码:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ENTRY(system_call)
   pushl %eax      # save orig_eax(系统调用号)
   SAVE_ALL
   GET_THREAD_INFO(%ebp) #当前进程的task放入ebp
              # system call tracing in operation
   #如果定义了系统调用跟踪标志
   testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
   jnz syscall_trace_entry
   #判断系统调用号是否合法(是否超过NR_syscalls).在x86中,这个值为285
   cmpl $(nr_syscalls), %eax
   #如果非法.跳至syscall_badsys:即返回-ENOSYS
   jae syscall_badsys
syscall_call:
   //调用sys_call_table中寻找第eax项(第项占四字节).
   call *sys_call_table(,%eax,4)
   movl %eax,EAX(%esp)     # store the return value
syscall_exit:
   cli          # make sure we don't miss an interrupt
              # setting need_resched or sigpending
              # between sampling and the iret
   movl TI_flags(%ebp), %ecx
   testw $_TIF_ALLWORK_MASK, %cx   # current->work
   jne syscall_exit_work
restore_all:
   RESTORE_ALL

  Sys_call_table定义如下:

双击代码全选
1
2
3
4
5
ENTRY(sys_call_table)
   .long sys_restart_syscall  /* 0 - old "setup()" system call, used for restarting */
   .long sys_exit
   .long sys_fork
   ……
这个表通常被称为系统调用表.如系统调用号1对应的处理函数为sys_exit.

  下面以sys_sethostname为例进行分析:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
asmlinkage long sys_sethostname(char __user *name, int len)
{
   int errno;
   char tmp[__NEW_UTS_LEN];
   //检查是否为特权用户?
   if (!capable(CAP_SYS_ADMIN))
      return -EPERM;
   //参数长度检查
   if (len < 0|| len > __NEW_UTS_LEN)
      return -EINVAL;
   down_write(&uts_sem);
   errno = -EFAULT;
   //将用户空间的值copy到内核空间中
if (!copy_from_user(tmp, name, len)) {
   //如果成功的话,设置system_utsname.nodename
      memcpy(system_utsname.nodename, tmp, len);
      system_utsname.nodename[len] = 0;
      errno = 0;
   }
   up_write(&uts_sem);
   return errno;
}

Copy_from_user()是一个通用的api.详细分析一下

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
   //判断from,n的合法性
   if (access_ok(VERIFY_READ, from, n))
      n = __copy_from_user(to, from, n);
   else
      //参数非法
      memset(to, 0, n);
   return n;
}
Access_ok()用来初步检查参数的合法性.定义如下:
#define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0))
#define __range_ok(addr,size) ({
   unsigned long flag,roksum;
   __chk_user_ptr(addr);
   asm("addl %3,%1 ; sbbl %0,%0; cmpl %1,%4; sbbl $0,%0"
      :"=&r" (flag), "=r" (roksum)
      :"1" (addr),"g" ((int)(size)),"rm" (current_thread_info()->addr_limit.seg));
   flag; })

  实际上,在此只是检查了add+size 是否大于current_thread_info()->addr_limit.seg(进程所允许的最大数据段)

  转入__copy_from_user():

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
   //可能会引起睡眠.例如发生缺页异常
   might_sleep();
   if (__builtin_constant_p(n)) {
      unsigned long ret;
      //对特殊情况的优化
      switch (n) {
      case 1:
         __get_user_size(*(u8 *)to, from, 1, ret, 1);
         return ret;
      case 2:
         __get_user_size(*(u16 *)to, from, 2, ret, 2);
         return ret;
      case 4:
         __get_user_size(*(u32 *)to, from, 4, ret, 4);
         return ret;
      }
   }
   return __copy_from_user_ll(to, from, n);
}

  __get_user_size的代码比较简单,我们转入到__copy_from_user_ll()以便分析更普通的情况

双击代码全选
1
2
3
4
5
6
7
8
9
10
unsigned long __copy_from_user_ll(void *to, const void __user *from,
              unsigned long n)
{
   //在没有定义CONFIG_X86_INTEL_USERCOPY的情况下,此函数恒为1
   if (movsl_is_ok(to, from, n))
      __copy_user_zeroing(to, from, n);
   else
      n = __copy_user_zeroing_intel(to, from, n);
   return n;
}

  跟踪进__copy_user_zeroing()

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define __copy_user_zeroing(to,from,size)          
do {
  
int __d0, __d1, __d2;              
   __asm__ __volatile__(              
      "   cmp $7,%0n"             
      "   jbe 1fn"            
      "   movl %1,%0n"             
      "   negl %0n"            
      "   andl $7,%0n"             
      "   subl %0,%3n"             
      "4:  rep; movsbn"             
      "   movl %3,%0n"             
      "   shrl $2,%0n"             
      "   andl $3,%3n"             
      "   .align 2,0x90n"         
      "0:  rep; movsln"             
      "   movl %3,%0n"             
      "1:  rep; movsbn"             
      "2:n"                   
      ".section .fixup,"ax"n"         
      "5:  addl %3,%0n"             
      "   jmp 6fn"            
      "3:  lea 0(%3,%0,4),%0n"          
      "6:  pushl %0n"              
      "   pushl %%eaxn"            
      "   xorl %%eax,%%eaxn"          
      "   rep; stosbn"             
      "   popl %%eaxn"             
      "   popl %0n"            
      "   jmp 2bn"            
      ".previousn"                
      ".section __ex_table,"a"n"          
      "   .align 4n"              
      "   .long 4b,5bn"            
      "   .long 0b,3bn"            
      "   .long 1b,6bn"            
      ".previous"                 
      : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) 
      : "3"(size), "0"(size), "1"(to), "2"(from)    
      : "memory");                
}
首先,我们先思考一个问题.怎么将用户空间的数据拷贝到内核空间?我们知道,32位平台上,1~3G的线性地址属于进程专用.3~4属于内核空间,所有进程共享.在前面进行内存管理的时候,我们分析过内核有内核页目录.那进程的页目录与内核的内目录有什么关系呢?从硬件的角度来看,系统调用从空户空间切换到内核空间的时候,并没有重新装载CR3寄存器,也就是说页目录没有发生改变.事实上,所有进程的高1G映射页目录都是一样的,都为内核页目录.所以在内核空间的寻址与用户空间的寻址也是一样的,所以就可以直接进行数据的拷贝了.

这段代码从开始一直到 ".previousn"前面是字串的copy操作,相当于我们使用 *(int *)dst = * (int *)src的操作.后半部份涉及到gcc的扩展语法: section 把后述代码加至进程的相应段.

  在前面分析过.在进行具体的拷贝之前,只是粗略的检查了一下参数.要是参数异常或者要拷贝的内存数据被交换怎么办呢?这就需要do_page_fault()去处理了.在上面的代码中,引起do_page_fault()只可能是由标号4,0,1引起的.在页面异常的代码分析过,我们说过,如果是一个非法的访问,就会到异常表中找相应的处理函数.回顾一下代码:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
asmlinkage void do_page_fault(struct pt_regs *regs,
  
unsigned long error_code)
{
   ……
   ……
   no_context:
   /* Are we prepared to handle this kernel fault? */
   if (fixup_exception(regs))
      return;
   ……
}

  Fixup_exception()函数代码如下:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int fixup_exception(struct pt_regs *regs)
{
   const struct exception_table_entry *fixup;
#ifdef CONFIG_PNPBIOS
   if (unlikely((regs->xcs & ~15) == (GDT_ENTRY_PNPBIOS_BASE << 3)))
   {
      extern u32 pnp_bios_fault_eip, pnp_bios_fault_esp;
      extern u32 pnp_bios_is_utter_crap;
     pnp_bios_is_utter_crap= 1;
      printk(KERN_CRIT "PNPBIOS fault.. attempting recovery.n");
      __asm__ volatile(
         "movl %0, %%espnt"
         "jmp *%1nt"
         : : "g" (pnp_bios_fault_esp), "g" (pnp_bios_fault_eip));
      panic("do_trap: can't hit this");
   }
#endif
   //从异常表中查找相应处理代码的地址.对应的参数是引起异常的代码地址
   fixup= search_exception_tables(regs->eip);
   if (fixup) {
      //将地址存入调用之前的eip寄存器.这样在异常返回后,就会执行对应的代码
      regs->eip = fixup->fixup;
      return 1;
   }
   return 0;
}

  转入search_exception_tables()

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
   const struct exception_table_entry *e;
   //参数:起始地址,结束地址,引起异常的地址
   e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
   if (!e)
      e = search_module_extables(addr);
   return e;
}
//利用二分法查找
const struct exception_table_entry *
search_extable(const struct exception_table_entry *first,
       const struct exception_table_entry *last,
       unsigned long value)
{
   while (first <= last) {
      const struct exception_table_entry *mid;
      mid = (last - first) / 2 + first;
      /*
      * careful, the distance between entries can be
      * larger than 2GB:
      */
      if (mid->insn < value)
        first= mid+ 1;
      else if (mid->insn > value)
         last = mid - 1;
      else
         return mid;
    }
    return NULL;
}
exception_table_entry结构如下示:
struct exception_table_entry
{
   //insn:产生异常指令的地址
   //fixup:修复地址
   unsigned long insn, fixup;
};
返回到我们上面讨论的__copy_user_zeroing()的代码:

双击代码全选
1
2
3
4
5
6
7
……
".section __ex_table,"a"n"          
      "   .align 4n"              
      "   .long 4b,5bn"            
      "   .long 0b,3bn"            
      "   .long 1b,6bn"            
…….

  对应到异常表就是:

  如果异常处理是在标号4处发生的,那么修复地址是标号5

  如果异常处理是在标号0处发生的,那么修复地址是标号3

  如果异常处理是在标号1处发生的,那么修复地址是标号6



原创粉丝点击