BYTE_COPY_FWD 源码解析

2014年1月13日 · 10 years ago

今天有同事问我之前写的那篇 iOS 常见 Crash 及解决方案 里面粘贴的 GLibC 关于 memcpy 的代码怎么理解,然后我囧了一下,当时就是随手一 copy,其实没理解透,于是花了点时间看了一下,学了不少东西,写篇博客记录一下。这里真得感谢一下 @raincai 同学的提醒。之前我粘贴的代码如下:

#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes)                                      \
do {                                                                              \
    int __d0;                                                                      \
    asm volatile(/* Clear the direction flag, so copying goes forward. */    \
                 "cld\n"                                                      \
                 /* Copy bytes. */                                              \
                 "rep\n"                                                      \
                 "movsb" :                                                      \
                 "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) :                      \
                 "0" (dst_bp), "1" (src_bp), "2" (nbytes) :                      \
                 "memory");                                                      \
} while (0)

其实上面这段代码有点问题,整理一下应该是这样:

#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes)                                      \
do {  
__asm__ __volatile__ (/* Clear the direction flag, so copying goes forward. */    \
                 "cld\n"                                                      \
                 /* Copy bytes. */                                              \
                 "rep\n"                                                      \
                 "movsb" 
                 :"=D" (dst_bp), "=S" (src_bp), "=c" (__d0)                       \
                 :"0" (dst_bp), "1" (src_bp), "2" (nbytes)                      \
                 :"memory");
} while (0)

我们一步步来解,看到已经理解的直接跳过就是了。

一、关键字

  1. do while 0

    linux内核代码很多宏都要加上这个,主要是为了是为了防止被调用的时候,复杂语句有些没被执行到。

    举个栗子:

    #define SOMETHING()\
               fun1();\
               fun2();
    

    这个宏是为了能执行到 fun1 和 fun2,但是如果你调用这个宏的时候,加上了条件判断:

    if (condition == true)
        SOMETHING();
    

    那就悲剧了,预编译的时候,宏定义被代码替换掉,那就是

    if (condition == true)
        fun1();
    fun2();
    

    fun2()就掉到判断的外面去了。所以加上这个是为了保险。

  2. asm

    这个其实就是用于在 C 语言内嵌汇编的关键字 asm, 有下划线的是个宏,看源码是这样定义的:

    #ifndef __GNUC__
    #define __asm__ asm
    #endif
    
  3. volatile
    跟 asm 类似,带下划线就是个宏,其实就是 volatile 关键字:

    #define __volatile__ volatile
    

    带上这个关键字就是告诉 GCC 不要做优化,要完全保留我写的指令,不要做任何修改。所以这个关键字是可选的。

    所以总的来说,在 C 语言里面,内嵌汇编的写法就是

    __asm__ ("汇编代码段") 
    或者
    __asm__ __volatile__ (指定操作 + "汇编代码段")
    

二、汇编代码

  1. cld

    复位方向表标记位 DF,即 DF = 0。DF为 0 则源寄存器地址 ESI/EDI (源寄存器/目标寄存器) 递增,1 则递减。

  2. rep

    表示重复,repeat,当 ECX (计数器) > 0 的时候就一直 rep。

  3. movsb

    就是搬移字串,汇编搬移字串有 movsb 和 movsw 两种,movsb 就是 moving string byte,就是一次搬一个字节,mvsw就是搬移字了

  4. 还有几个寄存器关键字

    EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP等都是X86汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。

    EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
    EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
    ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
    EDX 则总是被用来放整数除法产生的余数。
    ESI/EDI 分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
    EBP 是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer).

  5. 所以上面 =D 代表设置 EDI 目标索引寄存器,=S 是 ESI 源索引寄存器,=c 是 ECX 计数器

    OK,接下来是那些冒号,插入C代码中的一个汇编语言代码片断可以分成四部分,以“:”号加以分隔,其一般形式为:

    指令部:输出部:输入部:损坏部 
    
  6. 指令部就是上面几个指令啦无需多言,我们先看输出部

    =D 这样的语句是对输出部的约束条件:

    常用约束条件一览

    m, v, o —— 表示内存单元;
    r —— 表示任何寄存器;
    q —— 表示寄存器eax、ebx、ecx、edx之一;
    i, h  —— 表示直接操作数;
    E, F  —— 表示浮点数;
    g —— 表示”任意“;
    a, b, c, d  —— 分表表示要求使用寄存器eax、ebx、ecx和edx;
    S, D  —— 分别表示要求使用寄存器esi和edi;
    I —— 表示常数(0到31)。
    

    所以 "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) 就是把 dst_bp 放进 EDI 寄存器, src_bp 放进 ESI 寄存器, __d0 放进 ECX 寄存器。

  7. 再来看输入部

    :"0" (dst_bp), "1" (src_bp), "2" (nbytes) 这里的 0, 1, 2 不属于上面约束条件的字母,而是数字,数字代表跟输出部的第 0/1/2 个约束条件是同一个寄存器,那就很好理解了,就是说 EDI 寄存器里面将会输入 dst_bp, ESI 会输入 src_bp,最后的 ECX 会输入 nbytes 这个变量。

  8. 最后看损坏部

    这里以“memory”为约束条件,表示操作完成后内存中的内容已有改变,如果原来某个寄存器(也许在本次操作中并未用到)的内容来自内存,则现在可能已经不一致。

  9. 总的来说就是使用movsb指令来按字节搬运字符串,先设置了 EDI, ESI, ECX 几个寄存器的值, 其中EDI寄存器存放拷贝的目的地址,ESI寄存器存放拷贝的源地址,ECX为需要拷贝的字节数。所以最后汇编执行完之后,EDI中的值会保存到dst_bp中,ESI中的值会保存到src_bp中。

其他版本

这个函数有几个版本的,上面是汇编版本,下面这个是 C 版本,这个就很好理解了:

do                                                                            \
    {                                                                         \
      size_t __nbytes = (nbytes);                                             \
      while (__nbytes > 0)                                                    \
        {                                                                     \
          byte __x = ((byte *) src_bp)[0];                                    \
          src_bp += 1;                                                        \
          __nbytes -= 1;                                                      \
          ((byte *) dst_bp)[0] = __x;                                         \
          dst_bp += 1;                                                        \
        }                                                                     \
    } while (0)