Chinaunix首页 | 论坛 | 博客
  • 博客访问: 815544
  • 博文数量: 172
  • 博客积分: 3836
  • 博客等级: 中校
  • 技术积分: 1988
  • 用 户 组: 普通用户
  • 注册时间: 2011-02-10 14:59
文章分类

全部博文(172)

文章存档

2014年(2)

2013年(1)

2012年(28)

2011年(141)

分类: LINUX

2011-08-16 10:52:29

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
有时候,用户空间为了满足某些要求,要从内核空间去进行操作,比例建立文件,建立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位,可以用传递参数地址的方法.陷入到内核空间之 后,再从地址中去取值.回忆一下:我们在前面分析过的系统调用过程:
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中断处理之初始化>>一文中分析过系统调用的进入和返回过程.再来看下代码:
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定义如下:
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为例进行分析:
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.详细分析一下
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():
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()以便分析更普通的情况
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()
#define __copy_user_zeroing(to,from,size)                     \
do {                                              \
      int __d0, __d1, __d2;                             \
      __asm__ __volatile__(                             \
           "     cmp  $7,%0\n"                          \
           "     jbe  1f\n"                        \
           "     movl %1,%0\n"                          \
           "     negl %0\n"                        \
           "     andl $7,%0\n"                          \
           "     subl %0,%3\n"                          \
           "4:   rep; movsb\n"                          \
           "     movl %3,%0\n"                          \
           "     shrl $2,%0\n"                          \
           "     andl $3,%3\n"                          \
           "     .align 2,0x90\n"                  \
           "0:   rep; movsl\n"                          \
           "     movl %3,%0\n"                          \
           "1:   rep; movsb\n"                          \
           "2:\n"                                       \
           ".section .fixup,\"ax\"\n"                   \
           "5:   addl %3,%0\n"                          \
           "     jmp 6f\n"                         \
           "3:   lea 0(%3,%0,4),%0\n"                    \
           "6:   pushl %0\n"                            \
           "     pushl %%eax\n"                         \
           "     xorl %%eax,%%eax\n"                    \
           "     rep; stosb\n"                          \
           "     popl %%eax\n"                          \
           "     popl %0\n"                        \
           "     jmp 2b\n"                         \
           ".previous\n"                                \
           ".section __ex_table,\"a\"\n"                     \
           "     .align 4\n"                            \
           "     .long 4b,5b\n"                         \
           "     .long 0b,3b\n"                         \
           "     .long 1b,6b\n"                         \
           ".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映射页目录都是一样的,都为内核页目 录.所以在内核空间的寻址与用户空间的寻址也是一样的,所以就可以直接进行数据的拷贝了.
这段代码从开始一直到  ".previous\n"前面是字串的copy操作,相当于我们使用 *(int *)dst = * (int *)src的操作.后半部份涉及到gcc的扩展语法: section 把后述代码加至进程的相应段.
在前面分析过.在进行具体的拷贝之前,只是粗略的检查了一下参数.要是参数异常或者要拷贝的内存数据被交换怎么办呢?这就需要 do_page_fault()去处理了.在上面的代码中,引起do_page_fault()只可能是由标号4,0,1引起的.在页面异常的代码分析 过,我们说过,如果是一个非法的访问,就会到异常表中找相应的处理函数.回顾一下代码:
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()函数代码如下:
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, %%esp\n\t"
                 "jmp *%1\n\t"
                 : : "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()
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()的代码:
……
".section __ex_table,\"a\"\n"                     \
           "     .align 4\n"                            \
           "     .long 4b,5b\n"                         \
           "     .long 0b,3b\n"                         \
           "     .long 1b,6b\n"                         \
…….
对应到异常表就是:
如果异常处理是在标号4处发生的,那么修复地址是标号5
如果异常处理是在标号0处发生的,那么修复地址是标号3
如果异常处理是在标号1处发生的,那么修复地址是标号6
不止是在copy_from_user()有实现中用了这样的方法,在所有会引起异常的代码中,就加入了相应的异常项.也就说谁,谁可能会引起异常,谁就负表在异常表中加入相应项.在链接的时候,ld会按照地址升序将相关项插入到异常表
五:再论异常表
系统常用/异常/IRQ中断处理程序处理完后,都会经过RESTORE_ALL返回,之前为了分析整个过程,没有分析这个过程可能会引起的异常情况.
#define __RESTORE_INT_REGS \
      popl %ebx; \
      popl %ecx; \
      popl %edx; \
      popl %esi; \
      popl %edi; \
      popl %ebp; \
      popl %eax
 
#define __RESTORE_REGS \
      __RESTORE_INT_REGS; \
111:  popl %ds;  \
222:  popl %es;  \
.section .fixup,"ax"; \
444:  movl $0,(%esp);  \
      jmp 111b;  \
555:  movl $0,(%esp);  \
      jmp 222b;  \
.previous;       \
.section __ex_table,"a";\
      .align 4;  \
      .long 111b,444b;\
      .long 222b,555b;\
.previous
 
#define __RESTORE_ALL \
      __RESTORE_REGS   \
      addl $4, %esp;   \
333:  iret;      \
.section .fixup,"ax";   \
666:  sti;       \
      movl $(__USER_DS), %edx; \
      movl %edx, %ds; \
      movl %edx, %es; \
      pushl $11; \
      call do_exit;    \
.previous;       \
.section __ex_table,"a";\
      .align 4;  \
      .long 333b,666b;\
.previous
可以看到,在标号111,222,333处加入了异常处理,相应的指令分别是popl %ds, popl %es和iret.
为什么会这三条指令会引起异常呢?先看popl %ds 和popl %es
每当装载一个段寄存器的时候,CPU都要根据这个段选择码在GDT/LDT中找到相应的描述项.并加以检查.如果描 述项和选择项都正常,就会把它装入CPU的”不可见”部份.在映射的时候就不必再去取相应的值.如果选择码或者描述符无效或者不存在时,就会产生一次 “全面”保护异常.当这样的异常发生在系统空间时,都要为它建立相关的异常表项.在代码中,异常处理代码把esp置0.然后再执行pop指令,实际上把 ds,es置为零.这样就可以返回到用户空间了.至于用户空间会发生什么,那就由相应的异常处理程序去处理,最多不过把这个进程kill掉.
那iret怎么会引起异常呢?
在<>一文中分析过,中断返回时,会从系统堆栈中取值恢复 相关的寄存器.包括CS,EIP,如果运行级别不相同的会,还会恢复SS,ESP.同上面的DS,ES一样,这里也有两个段寄存器CS,SS.不 过,CS,SS不接受0值,所以不能像DS,ES那样处理了.那就没有其它的办法了.只能把当前进程kill掉,以保证整个系统的运行..
六:小结
在这一节里,主要分析了系统调用的流程,用户空间与系统空间的数据交互.可能会引起异常指令的修正处理.看完整个流程之后,我们很容易添加自己的系统调用.但是,如果是在实际项目中的话,那就得仔细权衡添加系统调用带来的利与弊了.
阅读(1130) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~