[读源码]看libc里面实现的strcpy
这个libc选用的是dietlibc-0.32,是一位老大介绍的,说是比较简单供学习只用.看后大呼上当....(代码写的个人觉得很糟糕)
为什么说这个strcpy呢?略微有那么一点理由:
1. 考试几乎必考.而考试给的标准答案很怪异....
2. 这个strcpy有提供了两个实现,第二个实现的效率比VC9的汇编实现的strcpy效率还要高,我粗略的测算,要高30%.
这在很大程度上获得了我的注意力:-D(NND,哪个程序员不希望自己的程序跑的贼快)
3. 也看看别人的代码是怎么写出来的,学习一下
说明一下,这个libc代码的来源貌似比较复杂,有来自BSD的,有来自GNU,还有一些其他的....(大杂烩%>_<%)
OK,上代码:
#define UNALIGNED(x,y) (((unsigned long)x & (sizeof (unsigned long)-1)) ^ ((unsigned long)y & (sizeof (unsigned long)-1))) #define STRALIGN(x) (((unsigned long)x&3)?4-((unsigned long)x&3):0) # define MKW(x) (x|x<<8|x<<16|x<<24) # define GFC(x) ((x)&0xff) # define INCSTR(x) do { x >>= 8; } while (0); char * strcpy (char *s1, const char *s2) { char *res = s1; #ifdef WANT_SMALL_STRING_ROUTINES while ((*s1++ = *s2++)); return (res); #else int tmp; unsigned long l; if (UNALIGNED(s1, s2)) { while ((*s1++ = *s2++)); return (res); } if ((tmp = STRALIGN(s1))) { while (tmp-- && (*s1++ = *s2++)); if (tmp != -1) return (res); } while (1) { l = *(const unsigned long *) s2; if (((l - MKW(0x1ul)) & ~l) & MKW(0x80ul)) { while ((*s1++ = GFC(l))) INCSTR(l); return (res); } *(unsigned long *) s1 = l; s2 += sizeof(unsigned long); s1 += sizeof(unsigned long); } #endif }
那些需要的宏我也给拎出来了,好看一些.
先来看这个简单的实现:WANT_SMALL_STRING_ROUTINES
while ((*s1++ = *s2++));
return (res);
就两行代码,也确实够简单的....这个不说.
再看后面的实现,前面两个宏的判断我也不想看,貌似是什么是否是四字节对齐的判断,猜的...
真正有意思的在这里while(1)这里,这里才是问题的关键.
DEBUG几次大约就能知道if语句里面判断字符串是否包含'\0'.
如果包含'\0',那么就用while把剩余的字符串拷贝过去;
如果不包含'\0',那么就用long拷贝,因为一次可以拷贝四个字节.
if语句外面的,基本上大家都知道意思了,问题就在里面的,里面猜是能猜到拷贝剩余的(不足四个字节的)字符串.
先来看:
while ((*s1++ = GFC(l))) INCSTR(l);
return (res);
这两句代码吧.
while( *si++ = GetFirstChar(l) ) //获取l的第一个字符
INC_STR(l); //往后偏移一个字节
那么他是怎么判断这个long里面是否有'\0'的呢?问题的关键就在
((l - MKW(0x1ul)) & ~l) & MKW(0x80ul)
这句代码上!
这句代码看这个很费解,我看了很长时间才看懂了.OK,提个问题,你怎么判断一个字节是否是0呢?
你也许会用==0,可是这样的代码只能对一个字节有效,这个libc用了一种比较复杂的办法:
byte i;
((i - 0x1) & ~i) & 0x80
他是这么判断的.
那句代码可以这么写(i-1) & (0xFF-i) & 0x80,我们画一个表就明白是怎么回事了.
i 0 1 2 .... 127 128 129 .... 253 254 255
i-1 255 0 1 .... 126 127 128 .... 252 253 254
255-i 255 254 253 .... 128 127 126 .... 2 1 0
& 255 <=0 <=1 .... <=126 <=127 <=126 .... <=2 <=1 <=0
有一个命题,N >= M,那么,(N & M) <= M 必然成立.(谁帮忙证明一下^_^)
那么(i-1) & (255-i)里面最大的数,也就指望中间这就几个数了,很可惜
127 & 127 = 127 < 128 = 0x80
所以他就用这个算法,去衡量一个byte是否是'\0',也就是0.
是0的话,会返回255;否则返回0.
一个long里面有四个byte,只要有一个byte出现0,也就是出现字符串的终结符,都会是那个表达式变成一个非0的数字,从而他的目的达到了.
OK,再来回顾一下他是怎么做的呢?
1. 读取四个字节,构成一个long
2. 判断这个long里面有没有包含C String的终结符'\0'
3. 包含的话,按byte拷贝
4. 不包含的话,按long拷贝
5. 回到1
挺犀利的,但是昨天晚上睡觉想到了一个问题,这个有问题.
1. 我看过FreeBSD的strcpy,在FreeBSD的libkern里面实现的那个
2. VC的strcpy是汇编实现的,MS在有可能的情况下,可能会极限优化程序
他们为什么都不用这个算法呢??
先来看看FreeBSD里面怎么实现strcpy的:
char * strcpy(char * __restrict to, const char * __restrict from) { char *save = to; for (; (*to = *from); ++from, ++to); return(save); }
这段代码和MS的strcpy虽然很不同,但是效率没多少差别,至少不会差别30%,也就是说MS的汇编,跟FreeBSD的算法上面是一样的.
他们都选择读取一个字节的!
刚才说那个libc实现的strcpy有问题,问题在哪里呢??
他一次读四个字节,可能字符串所占的空间不可能老是四的倍数,所以,他这个会越界!!!有可能会是你的程序Down掉....
验证一下:
char src[4]={1,0,1,1};
char dest[10]={0};
strcpy(dest,src);
然后你下一个断点试一试,看看long的值是不是0x01010001.
所以,看着这个算法的效率很高,实际是不可靠的.
PS:
1. dietlibc-0.32的string.h里面,有好几个函数都是用这个算法实现
2. dietlibc-0.32的代码风格很糟糕,跟FreeBSD的相比,s1,s2这样的命名...有时候很难看懂%>_<%
3. 底层库,效率固然重要,但是正确是前提
4. 另外问一个问题,我new char[1]和malloc(1)会给我分配四个字节么?标准应该没有类似的说法吧!
PS:
回去看了一下C99的文档,文档里面这么描述:
The malloc function allocates space for an object whose size is specified by size and whose value is indeterminate.
所以,我打算维持我之前所做的判断,这个有可能会越界,因为他所做的假设,不一定完全成立.