鸿 网 互 联 www.68idc.cn

当前位置 : 服务器租用 > 服务器相关 > 批处理 > >

__do_strncpy_from_user()源码分析

来源:互联网 作者:佚名 时间:2015-09-27 08:31
在用户层发起系统调用时,有时需要向 内核 传递字符串。 内核 通常不会直接操作用户传入的字符串,而是先拷贝到 内核 缓冲区中。因为是用户态传入的指针,并不能保证在拷贝的过程中不会发生异常,所以 内核 必须保证这种异常不会影响到 内核 的运行。 内核

  在用户层发起系统调用时,有时需要向内核传递字符串。内核通常不会直接操作用户传入的字符串,而是先拷贝到内核缓冲区中。因为是用户态传入的指针,并不能保证在拷贝的过程中不会发生异常,所以内核必须保证这种异常不会影响到内核的运行。内核通常会使用copy_from_user()或者strncpy_from_user()等类似的函数来执行拷贝操作。我们这里以strncpy_from_user()(64位系统)为例进行分析。strncpy_from_user()函数最终是调用__do_strncpy_from_user宏来完成拷贝,这个宏是通过内嵌汇编的方式定义的,源码如下:

/*
* Copy a null terminated string from userspace.
*/
#define __do_strncpy_from_user(dst,src,count,res)               \
do {                                       \
    long __d0, __d1, __d2;                           \
    might_fault();                               \
    __asm__ __volatile__(                           \
        "    testq %1,%1\n"                       \
        "    jz 2f\n"                       \
        "0:    lodsb\n"                       \
        "    stosb\n"                       \
        "    testb %%al,%%al\n"                   \
        "    jz 1f\n"                       \
        "    decq %1\n"                       \
        "    jnz 0b\n"                       \
        "1:    subq %1,%0\n"                       \
        "2:\n"                               \
        ".section .fixup,\"ax\"\n"                   \
        "3:    movq %5,%0\n"                       \
        "    jmp 2b\n"                       \
        ".previous\n"                           \
        _ASM_EXTABLE(0b,3b)                       \
        : "=&r"(res), "=&c"(count), "=&a" (__d0), "=&S" (__d1),       \
          "=&D" (__d2)                           \
        : "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \
        : "memory");                           \
} while (0)
  首先看下面的指令:  

        testq %1,%1\n"
            jz 2f\n
  test指令都比较熟悉,就是对两个操作数执行AND操作,然后根据结果设置ZF、SF、PF寄存器,AND操作的结果会被丢弃。这里的两个操作数都是%1,也就是输出参数count。假设count为0的话,两个count值执行AND操作的结果也是0,此时ZF寄存器会被置位;同理,如果count不为0,则ZF寄存器就不会置位。如果ZF寄存器置位的话,会跳转到符号2的位置,也就是说要拷贝的字符串长度count为0,此时不执行任何拷贝操作,直接返回。这里有两个地方需要注意,这里的“f“的意思是forward,也就是向前跳转,f可以省略,默认就是向前跳转;第二个地方是,跳转到符号2的位置后,不会执行.section .fixup之后的代码,后面再说这个段。
  如果count不为0,则从符号0处开始执行。lodsb指令是将地址为DS:(E)SI的字节加载到寄存器AL中,同时si寄存器会自动增加或减小1,取决于方向标志位DF。64位系统下,lodsb是加载地址为%rsi的字节到AL中。src是作为输入参数,和编号为3的参数使用相同的寄存器或内存,也就是和__d1(编号是从0开始)使用相同的寄存器或内存。__d1的约束是"=&S",其中"&"约束只能用于Output操作表达式,向GCC表明"GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器";"="约束说明当前的操作表达式是Write-Only的,只能用作输出,而不能作为输入;"S"约束是寄存器约束,表示要使用寄存器%rsi/%esi/%si。因为src和__d1使用相同的寄存器约束,所以src的地址会使用%rsi寄存器来存储。这里lodsb是将src指向的字符串中的字符加载到寄存器AL中,注意,这里lodsb使用的是寄存器si的值作为地址值,所以递增的是si,不是src指针值。
  现在是把字符加载到寄存器AL,接着使用stosb指令将AL寄存器存储的字符存储到dst执行的内存中。stosb指令的作用是将AL的内容存储到地址为ES:(E)DI的字节处,同时di寄存器会自动增加或减小1,取决于方向标志位DF。64位系统下,将AL寄存器的内容存储到地址为%rdi的字节处。同src的原理一样,dst使用的是和__d2相同的寄存器%rdi,也就是将内容存储到dst中。
  lodsb和stosb两个指令结合起来就是把字符从src拷贝到dst中,每次都是只拷贝一个字符。
  在拷贝完一个字符后,会使用下面的指令来来检查拷贝的字符是否是空字符,如下所示:
    testb %%al,%%al
            jz 1f
  在内联汇编中,要访问寄存器要使用两个%符号,如果是空字符的话,则跳转到符号1的位置,开始计算拷贝的字符的数量,后面再讨论。如果不是空字符,则执行下面的指令:

        decq %1
            jnz 0b
  使用decq指令将编号为1的参数(初始值为count)减1。如果编号为1的参数的值不为0,则跳转到符号0的位置,重复上面拷贝字符的操作。编号为1的参数值如果为0,则说明已经拷贝了最大数量的字符,此时可以结束执行,到符号1的位置计算拷贝的字符数量,然后返回。这里的0b中字符"b"的意思是backward,就是向后跳转,因为默认情况是向前跳转,所以这里的b不能省略,因为符号0在这条指令前边。
  符号1处的指令如下所示:

"1:    subq %1,%0\n"
  这条指令相当于是执行%0=%0-%1。编号为0的参数是res,但是在前边我们没有看到对res初始化的地方,res本身就是用来存储结果的,那res的初始值是多少呢?我们首先看输入参数"0"(count),也就是编号为6的参数,这个输入参数使用编号为0的参数相同的寄存器约束,也就是res所使用的寄存器。所以res的值和编号为6的参数相同,也就是说res的初始值就是要拷贝的字符的最大数量count,而每拷贝一个字符编号为1的参数(初始也为count)值都会减1,所以%0-%1就是已经拷贝的字符数量,结果存储在%1中,也就是res中。到这里,拷贝操作顺利执行完成。
  接下来看.fixup段之后的的指令,如下所示:
    .section .fixup,\"ax\"
        3:    movq %5,%0
        "    jmp 2b
        ".previous
        _ASM_EXTABLE(0b,3b) 
 其中_ASM_EXTABLE宏是向异常表中插入一项(这个宏比较简单,这里就不替换了),在符号0处的代码发生异常时,会跳转到符号3处处理异常,也就是.fixup段中的指令。
 .section .fixup之后的代码会被插入到.fixup段中,“ax”用来表示这段代码必须被加载到内存中,并且其中包含可执行的代码。如果符号0处的指令引发了Page Fault异常,会跳转到.fixup段中处理。这里的处理就是将res的值设置为-EFAULT,然后跳转到符号2处返回。

网友评论
<