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