服务器 频道

FreeBSD 5内核源代码分析

    【IT168 服务器学院】在内核中,用户态和内核态之间的数据拷贝主要通过copyin()和copyout()
    两个函数完成。与普通的数据拷贝不同,用户态和内核态之间的数据拷贝必须考虑到用户给出的地址是否有效,即该地址是否有真正的地址映射。同时又要考虑到效率。因此也不可能对用户给出地址的每个字节检查一遍。

    FreeBSD和linux一样(linux中是copy_from_user()和copy_to_user()),
    都是先拷贝,出错以后再进行错误处理,有着异曲同工之妙。

    本文所有代码,如未注明,均来自sys/i386/i386/support.s

    copyin()由汇编语言写成,我们逐句来看。

    代码:
    /*
    * copyin(from_user, to_kernel, len) - MP SAFE
    */
    ENTRY(copyin)
    MEX99vCOUNT
    jmp *copyin_vector

    copyin()有三个参数,from_user为用户态数据地址,to_kernel为内核缓冲区地址,len为数据长度。它们分别位于堆栈的位置是:
    代码:
    from_user: 12(%esp)
    to_kernel: 16(%esp)
    len: 20(%esp)

    这里copyin_vector定义为
    代码:
    .globl copyin_vector
    copyin_vector:
    .long generic_copyin

    定义一个数据copyin_vector,其值是generic_copyin

    这里*copyin_vector的值就是generic_copyin,因此将跳转到generic_copyin。

    代码:
    ENTRY(generic_copyin)
    movl PCPU(CURPCB),%eax
    movl $copyin_fault,PCB_ONFAULT(%eax)

    先将curpcb地址存入%eax,然后将curpcb->pcb_onfault置为copyin_fault,这里copyin_fault也是一个程序标号,拷贝出错时将跳转到这里。下面我们将看到。
    代码:
    pushl %esi
    pushl %edi
    movl 12(%esp),%esi /* caddr_t from */
    movl 16(%esp),%edi /* caddr_t to */
    movl 20(%esp),%ecx /* size_t len */

    分别将from_user, to_kernel, len存入寄存器%esi, %edi, %ecx

    代码:
    /*
    * make sure address is valid
    */
    movl %esi,%edx
    addl %ecx,%edx
    jc copyin_fault

    这几句看from_user+len是否有整数溢出。

    代码:
    cmpl $VM_MAXUSER_ADDRESS,%edx
    ja copyin_fault

    相加之和是否在用户有效地址空间内。

    代码:
    #if defined(I586_CPU) && defined(DEV_NPX)
    ALIGN_TEXT
    slow_copyin:
    #endif
    movb %cl,%al
    shrl $2,%ecx /* copy longword-wise */
    cld
    rep
    movsl

    先以4字节为单位拷贝。

    代码:
    movb %al,%cl
    andb $3,%cl /* copy remaining bytes */
    rep
    movsb

    再拷贝剩余的字节(最多3字节),如果有的话。

    代码:
    #if defined(I586_CPU) && defined(DEV_NPX)
    ALIGN_TEXT
    done_copyin:
    #endif
    popl %edi
    popl %esi
    xorl %eax,%eax
    movl PCPU(CURPCB),%edx
    movl %eax,PCB_ONFAULT(%edx)
    ret

    拷贝完成,恢复寄存器,并清除curpcb->pcb_onfault,返回。由于%eax用做函数返回值,这就是说,如果成功拷贝就返回0

    事情总是有意外发生,如果用户给出的地址段
    [from_user, from_user+len]
    有问题的话,copyin()将发生异常,进入异常处理函数,

    代码:
    (sys/i386/i386/trap.c)
    ---------------------------
    /*
    * Exception, fault, and trap interface to the FreeBSD kernel.
    * This common code is called from assembly language IDT gate entry
    * routines that prepare a suitable stack frame, and restore this
    * frame after the exception has been processed.
    */

    void
    trap(frame)
    struct trapframe frame;
    {
    struct thread *td = curthread;
    struct proc *p = td->td_proc;
    ......

    type = frame.tf_trapno;
    ......

    if(it is user trap){
    ......

    }else{ /* kernel trap */
    ......
    switch (type) {
    ......

    case T_PROTFLT: /* general protection fault */
    case T_STKFLT: /* stack fault */
    ......
    /* FALL THROUGH */
    case T_SEGNPFLT: /* segment not present fault */
    ......
    if (PCPU_GET(curpcb) != NULL &&
    PCPU_GET(curpcb)->pcb_onfault != NULL) {
    frame.tf_eip =
    (int)PCPU_GET(curpcb)->pcb_onfault;
    goto out;
    }
    break;
    ......
    } /* end switch */
    ......
    } /* end else */
    ......

    out:
    return;
    }

    对于copyin()产生的错误,"general protection fault"或"stack fault"
    或"segment not present fault",都由这段代码处理。
    由于在进入copyin()时设置了curpcb->pcb_onfault,这里将异常处理程序退出时继续运行的eip指针设置为copyin_fault,于是,当异常返回后,程序控制将到达copyin_fault。

    代码:
    ALIGN_TEXT
    copyin_fault:
    popl %edi
    popl %esi
    movl PCPU(CURPCB),%edx
    movl $0,PCB_ONFAULT(%edx)
    movl $EFAULT,%eax
    ret

    在这里,恢复寄存器,清除curpcb->pcb_onfault,返回EFAULT.

    注意,此时的核心栈与拷贝成功时的核心栈是相同的,这是因为前述trap()函数修改%eip后,程序只是将本来应该继续执行拷贝错误语句改为执行copyin_fault,异常处理程序返回后的核心栈并没有变化。因此,内核将顺着copyin()后的代码执行,就好象根本没有发生过异常一样。

    参考文献:
    [1] Sinan "noir" Eren, "Smashing The Kernel Stack For Fun And Profit", phrack60-06 

0
相关文章