x86 下 strcpy 高效实现



0. 先看个通常的实现:

为了分析方便不考虑 to/from 指针为NULL情况。

char *strcpy(char *to, const char *from)
{
    char *rev = to;
    while(*to++ = *from++);
    return rev;
}


gcc -S test.c -o test.s 编译产生汇编码为:

strcpy:                                 # ebp + 8 处为to, ebp + 12 处为from, ebp + 16 ...
  pushl   %ebp
  movl   %esp, %ebp
  subl   $16, %esp               # 在栈上为局部变量分配空间
  movl   8(%ebp), %eax         # 参数to 置入 eax
  movl   %eax, -4(%ebp)         # ebp - 4 处为 rev 所在

.L6:
  movl   12(%ebp), %eax         # 参数 from 置入 eax
  movzbl (%eax), %edx        
  movl   8(%ebp), %eax
  movb   %dl, (%eax)
  movl   8(%ebp), %eax
  movzbl (%eax), %eax
  testb   %al, %al
  setne   %dl
  incl   8(%ebp)                   # to 增1, 对应 to++
  leal   12(%ebp), %eax
  incl   (%eax)                   # from 增1, 对应 from++
  testb   %dl, %dl
  jne .L6

  movl   -4(%ebp), %eax         # return rev
  leave
  ret

可以看到实现拷贝的操作由 .L6 到 jne .L6 之间操作完成。显然这个实现是很臭的。



1. 编译器优化

现用 -O 参数重新编译: gcc -O -S test.c -o test.s

汇编码为:

strcpy:
  pushl   %ebp
  movl   %esp, %ebp
  pushl   %ebx
  movl   8(%ebp), %ebx         # 参数 to 置入 ebx
  movl   12(%ebp), %ecx         # 参数 from 置入 ecx
  movl   %ebx, %edx              

.L6:
  movzbl (%ecx), %eax
  movb   %al, (%edx)
  incl   %edx
  incl   %ecx
  testb   %al, %al
  jne .L6

  movl   %ebx, %eax               # return rev
  popl   %ebx
  popl   %ebp
  ret

可以看到经过优化后的代码,实现拷贝的循环操作效率得到大幅的提高,且使用寄存器 ebx
作临时变量 rev 的所在,这比将 rev 存储于内存中要快得多。



2. 内核与glibc的实现

位于 [include/asm-i386/string.h]

inline char * strcpy(char * dest,const char *src)
{
    int d0, d1, d2;
    __asm__ __volatile__(
        "1:/tlodsb/n/t"
        "stosb/n/t"
        "testb %%al,%%al/n/t"
        "jne 1b"
        : "=&S" (d0), "=&D" (d1), "=&a" (d2)     # 此为输出节,意为将寄存器的值
                                                        # 传给变量,参看下面的汇编码

        : "0" (src), "1" (dest)                   # 此处为输入节,意为将变量的值
                                                        # 传给对应寄存器
        : "memory");
    return dest;
}


在strcpy 中直接嵌入 x86 的串操作指令: lodsb, stosb。


gcc -S test.c -o test.s 产生的汇编码为:

strcpy:
    ...
  movl   8(%ebp), %edi         # 对应输入节,dest ---> edi
  movl   12(%ebp), %esi         # src ----> esi

  cld
1:
  lodsb                             # al   <--- [esi]
  stosb                             # [edi] <--- al
  testb %al, %al                   # al 为 '/0' 时循环结束
  jnz 1b

  movl   %esi, -20(%ebp)         # 此处对应输出节, ebp-20 处为 d0
  movl   %edi, -16(%ebp)         # ebp-16 处为 d1
  movl   %eax, -12(%ebp)         # ebp-12 处为 d2
  movl   8(%ebp), %eax         # return dest
    ...
  ret

上面对应输出节的三条movl指令没有实际作用,可以加 -O 参数编译器会将其优化掉。


至于核心中strcpy的实现为何要引入 d0, d1, d2 这三个变量,请看下面的实现:

inline char * strcpy(char * dest,const char *src)
{
  asm volatile
    (
        "cld/n"
        "1:/n/t"
        "lodsb/n/t"
        "stosb/n/t"
        "testb %%al, %%al/n/t"
        "jnz 1b/n/t"
        :
        : "S"(from), "D"(to)
        : "memory"
    );

  return to;
}


在不加 -O 参数的情况下是可以正常工作的,但如果加了 -O 则:

....
  cld
1:
  lodsb
  stosb
  testb %al, %al
  jnz 1b

  movl   %edi, %eax         # error, 返回值出错,此时edi已经指向字符串尾了
....
  ret


可以看到引入 d0, d1, d2 的作用,实际上是一个局部保存的作用,旨在不改变实参的值。


有关 gcc 的内嵌汇编可以参考:

1. GCC Inline Assembly HOWTO:
    http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
2. Brennan's Guide to Inline Assembly:
    http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html


你可能感兴趣的:(x86,相关)