baidu

[读源码]看libc里面实现的strcpy

[读源码]看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.

所以,我打算维持我之前所做的判断,这个有可能会越界,因为他所做的假设,不一定完全成立.

posted @ 2010-05-25 09:06  egmkang  阅读(6409)  评论(22编辑  收藏  举报