glibc--strcpy源码分析
strcpy是各大计算机考试和面试中几乎不可少的考点。我们平时看到最多的是下面这个版本(不考虑参数检查等操作)
char * strcpy (char * dst, const char * src)
{
char * cp = dst;
while( *cp++ = *src++ )
; /* Copy src over dst */
return( dst );
}
不得不说的是,诸如*cp++ = *src++之类的代码太泛滥了,我甚至听有的人说这是老鸟才能写出的代码,⊙﹏⊙b汗!
但我们要明白的是代码最终是要机器来运行的,这样的语句翻译成机器码后,机器没有得到任何好处 -- 具体原因下文会分析... ...
glibc中的strcpy的效率要高于上面的代码,代码如下
#include <stddef.h>
#include <string.h>
#include <memcopy.h>
#include <bp-checks.h>
#undef strcpy
/* Copy SRC to DEST. */
char *
strcpy (dest, src)
char *dest;
const char *src;
{
char c;
char *__unbounded s = (char *__unbounded) CHECK_BOUNDS_LOW (src);
const ptrdiff_t off = CHECK_BOUNDS_LOW (dest) - s - 1;
size_t n;
do
{
c = *s++;
s[off] = c;
}
while (c != '\0');
n = s - src;
(void) CHECK_BOUNDS_HIGH (src + n);
(void) CHECK_BOUNDS_HIGH (dest + n);
return dest;
}
libc_hidden_builtin_def (strcpy)
/* Int 5 is the "bound range" exception also raised by the "bound"
instruction. */
# define BOUNDS_VIOLATED int $5
/* Verify that pointer's value >= low. Return pointer value. */
# define CHECK_BOUNDS_LOW(ARG) \
(((__ptrvalue (ARG) < __ptrlow (ARG)) && BOUNDS_VIOLATED), \
__ptrvalue (ARG))
/* Verify that pointer's value < high. Return pointer value. */
# define CHECK_BOUNDS_HIGH(ARG) \
(((__ptrvalue (ARG) > __ptrhigh (ARG)) && BOUNDS_VIOLATED), \
__ptrvalue (ARG))
不要被以上的代码给吓了,其实很多已经不用了。首先我们了解下bounded pointer(只是了解下)
GCC支持bounded类型指针(bounded指针用__bounded关键字标出,若默认为bounded指针,则普通指针用__unbounded标出),这种指针占用3个指针的空间,在第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。__ptrvalue、__ptrlow、__ptrhigh分别返回这3个值,有了3个值以后,内存越界错误便很容易查出来了。并且要定义了__BOUNDED_POINTERS__这个宏才有作用,否则这3个宏定义都是空的。
不过,尽管bounded指针看上去似乎很有用,但是这个功能却在2003年被去掉了。因此现在所有关于bounded指针的关键字其实都是一个空的宏。
不过还是分析下这几个宏的作用吧。__ptrvalue (ARG) < __ptrlow (ARG)判断目标指针是否小于合法指针的下界,如果其结果为真,即指针越界,则执行 && 后面的BOUNDS_VIOLATED 陷入中断程序;反之,指针没有越界,则不执行BOUNDS_VIOLATED,整个表达式的值为逗号表达式后面的值,即__ptrvalue (ARG),即目标指针的值。 这么写主要是为了后面可以直接对CHECK_BOUNDS_LOW(ARG) 进行操作。
好了,鉴于此, 以上代码等价于一下代码:
char *
strcpy(char *dest, const char *src)
{
char c;
char *s = src;
const ptrdiff_t off = dest - s - 1;
do {
c = *s++;
s[off] = c;
} while (c != '\0');
return dest;
}
值得指出的是:此算法利用了进程平坦的内存模型,虚拟内存平坦铺开,于是任意两个指针的差就是两者之间的距离。得到地址间的相对距离off后,就不需要再用绝对地址寻址了,这样在每一次的循环中可以少一次dest++操作,而多出来的相对地址操作则完全可以用寄存器高效地完成!
接下来我们通过具体的实验来看看到底glibc的实现是否真的高效!
实验代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
char *strcpy_mcrt(char *dest, const char *src);
char *strcpy_glibc(char *dest, const char *src);
int main(int argc, const char *argv[])
{
int i = 0;
char dest[20] = {0};
char src[20] = {"hello world~!"};
for (; i < 100000000; i++)
{
strcpy_mcrt(dest, src);
strcpy_glibc(dest, src);
}
return 0;
}
char *strcpy_mcrt(char *dest, const char *src)
{
char *ret = dest;
while (*dest++ = *src++);
return ret;
}
char *strcpy_glibc(char *dest, const char *src)
{
char c;
char *s = (char *)src;
const ptrdiff_t off = dest - src - 1;
do {
c = *s++;
s[off] = c;
} while (c != '\0');
return dest;
}
第一次测试strcpy_mcrt函数:
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m5.925s
user 0m5.852s
sys 0m0.020s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m5.906s
user 0m5.872s
sys 0m0.016s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m5.908s
user 0m5.872s
sys 0m0.016s
平均下来有5.913s
再来测试strcpy_glibc函数:
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m6.061s
user 0m6.024s
sys 0m0.016s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m6.058s
user 0m6.028s
sys 0m0.016s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m6.092s
user 0m6.040s
sys 0m0.016s
平均下来有6.070s
这是怎么回事? strcpy_glibc函数并不高效,反而效率更低? 不能够啊
于是我在strcpy_glibc中将变量c改成register char c,再来看看结果如何?
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m3.933s
user 0m3.908s
sys 0m0.008s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m3.333s
user 0m3.316s
sys 0m0.008s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m3.941s
user 0m3.912s
sys 0m0.016s
平均下来有3.735s
哇!效果很明显啊!效率提高的原因也很简单:编译器将变量c放入一个寄存器,省去了每次循环中对变量c的两次内存访问。
Dump of assembler code for function strcpy_glibc:
0x080484e9 <+0>: push ebp
0x080484ea <+1>: mov ebp,esp
0x080484ec <+3>: push ebx
0x080484ed <+4>: sub esp,0x10
0x080484f0 <+7>: mov eax,DWORD PTR [ebp+0xc]
0x080484f3 <+10>: mov DWORD PTR [ebp-0x8],eax
0x080484f6 <+13>: mov edx,DWORD PTR [ebp+0x8]
0x080484f9 <+16>: mov eax,DWORD PTR [ebp+0xc]
0x080484fc <+19>: mov ecx,edx
0x080484fe <+21>: sub ecx,eax
0x08048500 <+23>: mov eax,ecx
0x08048502 <+25>: sub eax,0x1
0x08048505 <+28>: mov DWORD PTR [ebp-0xc],eax
0x08048508 <+31>: mov eax,DWORD PTR [ebp-0x8]
0x0804850b <+34>: movzx ebx,BYTE PTR [eax]
0x0804850e <+37>: add DWORD PTR [ebp-0x8],0x1
0x08048512 <+41>: mov eax,DWORD PTR [ebp-0xc]
0x08048515 <+44>: add eax,DWORD PTR [ebp-0x8]
0x08048518 <+47>: mov BYTE PTR [eax],bl
0x0804851a <+49>: test bl,bl
0x0804851c <+51>: jne 0x8048508 <strcpy_glibc+31>
0x0804851e <+53>: mov eax,DWORD PTR [ebp+0x8]
0x08048521 <+56>: add esp,0x10
0x08048524 <+59>: pop ebx
0x08048525 <+60>: pop ebp
0x08048526 <+61>: ret
End of assembler dump.
可以看到,编译器将变量c放入寄存器ebx了。
可是想想不对啊,这不公平,这就相当于变相做了优化!
我的疑问有:
1、为什么要用一个中间变量c?
2、为什么定义指针s代替指针src,而不直接使用src?
于是我将代码修改成如下所示:
char *strcpy_glibc(char *dest, const char *src)
{
// register char c;
// char *s = (char *)src;
const ptrdiff_t off = dest - src;
do {
src[off] = *src;
} while (*src++ != '\0');
return dest;
}
编译却出现如下错误提示:
strcpy_test.c: In function ‘strcpy_glibc’:
strcpy_test.c:42: error: assignment of read-only location ‘*(src + (unsigned int)off)’
这就解释了为什么需要定义指针s而不能直接使用指针src了!因为src[off] = *src是写操作(尽管写的内容并不是src指针指向的真实内容)。
继续修改代码:
char *strcpy_glibc(char *dest, const char *src)
{
// char c;
char *s = (char *)src;
const ptrdiff_t off = dest - src;
do {
s[off] = *s;
} while (*s++ != '\0');
return dest;
}
测试strcpy_glibc结果如下:
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m4.795s
user 0m4.764s
sys 0m0.016s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m4.787s
user 0m4.764s
sys 0m0.012s
astrol@astrol:~/c++/9c++$ time ./strcpy_test
real 0m4.881s
user 0m4.844s
sys 0m0.016s
平均下来有4.821s
可见这种利用进程平坦内存模型的办法的确比机械的复制要高效,反汇编如下:
Dump of assembler code for function strcpy_glibc:
38 {
0x080484e9 <+0>: push ebp
0x080484ea <+1>: mov ebp,esp
0x080484ec <+3>: sub esp,0x10
39 // char c;
40 char *s = (char *)src;
0x080484ef <+6>: mov eax,DWORD PTR [ebp+0xc]
0x080484f2 <+9>: mov DWORD PTR [ebp-0x4],eax
41 const ptrdiff_t off = dest - src;
0x080484f5 <+12>: mov edx,DWORD PTR [ebp+0x8]
0x080484f8 <+15>: mov eax,DWORD PTR [ebp+0xc]
0x080484fb <+18>: mov ecx,edx
0x080484fd <+20>: sub ecx,eax
0x080484ff <+22>: mov eax,ecx
0x08048501 <+24>: mov DWORD PTR [ebp-0x8],eax
42
43 do {
44 s[off] = *s;
0x08048504 <+27>: mov eax,DWORD PTR [ebp-0x8]
0x08048507 <+30>: add eax,DWORD PTR [ebp-0x4]
0x0804850a <+33>: mov edx,DWORD PTR [ebp-0x4]
0x0804850d <+36>: movzx edx,BYTE PTR [edx]
0x08048510 <+39>: mov BYTE PTR [eax],dl
45 } while (*s++ != '\0');
0x08048512 <+41>: mov eax,DWORD PTR [ebp-0x4]
0x08048515 <+44>: movzx eax,BYTE PTR [eax]
0x08048518 <+47>: test al,al
0x0804851a <+49>: setne al
0x0804851d <+52>: add DWORD PTR [ebp-0x4],0x1
0x08048521 <+56>: test al,al
0x08048523 <+58>: jne 0x8048504 <strcpy_glibc+27>
46
47 return dest;
0x08048525 <+60>: mov eax,DWORD PTR [ebp+0x8]
48 }
0x08048528 <+63>: leave
0x08048529 <+64>: ret
为了作对比,输出strcpy_mcrt函数的反汇编如下:
Dump of assembler code for function strcpy_mcrt:
30 {
0x080484b6 <+0>: push ebp
0x080484b7 <+1>: mov ebp,esp
0x080484b9 <+3>: sub esp,0x10
31 char *ret = dest;
0x080484bc <+6>: mov eax,DWORD PTR [ebp+0x8]
0x080484bf <+9>: mov DWORD PTR [ebp-0x4],eax
32 while (*dest++ = *src++);
0x080484c2 <+12>: mov eax,DWORD PTR [ebp+0xc]
0x080484c5 <+15>: movzx edx,BYTE PTR [eax]
0x080484c8 <+18>: mov eax,DWORD PTR [ebp+0x8]
0x080484cb <+21>: mov BYTE PTR [eax],dl
0x080484cd <+23>: mov eax,DWORD PTR [ebp+0x8]
0x080484d0 <+26>: movzx eax,BYTE PTR [eax]
0x080484d3 <+29>: test al,al
0x080484d5 <+31>: setne al
0x080484d8 <+34>: add DWORD PTR [ebp+0x8],0x1
0x080484dc <+38>: add DWORD PTR [ebp+0xc],0x1
0x080484e0 <+42>: test al,al
0x080484e2 <+44>: jne 0x80484c2 <strcpy_mcrt+12>
33
34 return ret;
0x080484e4 <+46>: mov eax,DWORD PTR [ebp-0x4]
35 }
0x080484e7 <+49>: leave
0x080484e8 <+50>: ret
End of assembler dump.
计算出off值的内存访问可以忽略不计。其实不看汇编也可以得到结论:每一次循环中省去了*dest++操作,当字符串很长,或者像上面一样,重复很对此,那么这种优势就显现出来了。
那么,为什么glibc要用中间变量c呢?这岂不是吃力不讨好吗?
百度了下,在http://blog.chinaunix.net/uid-23629988-id-2853291.html中找到了答案:
参考链接:http://blog.chinaunix.net/space.php?uid=23629988&do=blog&frmd=147168&classid=116191&view=me
参考链接: http://blog.csdn.net/dog250/article/details/5302947