字符数组的strlen(字符串函数strlen的深入研究)

还记得刚学C的时候,对字符串操作等函数记忆很深。strlen、strlen等功能的实现在各种考试中,我毕业找工作的时候,很多公司的笔测题也包含了strlen、strcpy等功能的实现。可见字符串操作功能受到老师和公司考官的青睐。所以让让我们研究一下本文中的strlen函数!

也许你你已经在BS了,以为这是唯一需要学习的东西。我可以瞬间完成,所以你写了这段代码:

[CPP]查看纯文本

intstrlen(constchar*str)

{

int length=0;

while(*str)

长度;

返回(长度);

}

哇!它你真快,一下子就写出了这么简洁精炼的文章。是的,你的C语言考试通过了,你的公司he’他的笔试通过了。恭喜你。但是,问题好像这么快就解决了,那么这篇文章怎么进行呢?让我们先来分析一下你瞬间抓拍出来的这个strlen。她it’太完美了,而且它和MS工程师写的一模一样。总的来说,它这只是几行代码。那么,为什么它只用几行就能解决问题呢?有更好的方案吗?你灵机一动,立刻想出了一种:

[CPP]查看纯文本

intstrlen(constchar*str)

{

constchar * ptr=str

while(*str)

return(str-ptr-1);

}

所谓短码不一定是最优的。当然,我们可以不要在这里参与软件工程。我们可以看到这两个实现。str逐字节后移,时间复杂度为O(n),所以这个strlen很容易完成。那么更好的解决方案是什么呢?想象一下,如果能跳过几个字节,就能更快地完成长度计算,而且会降低复杂度。让让我们拭目以待。

本系列是分析crt库中intel模块下的那些函数,所以让咱们去看看里面有没有strlen的实现啊!我找到了。它位于VC/CRT/src/Intel/strlen.asm,打开。咦,这有点头晕。但最引人注目的是,在之前的评论中,工程师女士写了一个注释版本这与您之前实现的strlen完全相同。但是,它是一个注释版本,不会被编译到程序中。然后继续看下面的汇编实现,代码如下:

[CPP]查看纯文本

CODESEG

公众论坛

strlenproc\

缓冲区:ptrbyte

OPTIONPROLOGUE:无,EPILOGUE:无。FPO(0,1,0,0,0,0)

stringeu[尤指4]

movecx,字符串;ecx字符串

testecx,3;证明32位

jeshortmain_loop

str _未对齐:

;simplebyteloopuntilstring is aligned

moval,byteptr[ecx]

addecx,1

泰斯塔勒

jeshortbyte_3

测试cx,3

jneshortstr _未对齐

addeax,dwordptr05bytenoptoalignlabelbelow

align16应该是多余的

主循环:

moveax,dwordptr[ecx];读取4字节

movedx,7efefeffh

addedx,eax

xoreax,-1

xoreax,edx

addecx,4

testeax,81010100h

jeshortmain_loop

;foundzerobyteintheloop

moveax[ecx-4]

testal,al;isitbyte0

jeshortbyte_0

特斯塔,啊;isitbyte1

jeshortbyte_1

testeax,00ff0000hisitbyte2

jeshortbyte_2

testeax,0ff000000hisitbyte3

jeshortbyte_3

jmpshortmain _ loop取24-30位清零位

;31套

字节3:

leaeax[ecx-1]

movecx,字符串

subeax,ecx

浸水使柔软

字节2:

leaeax[ecx-2]

movecx,字符串

subeax,ecx

浸水使柔软

字节1:

leaeax[ecx-3]

movecx,字符串

subeax,ecx

浸水使柔软

字节0:

leaeax[ecx-4]

movecx,字符串

subeax,ecx

浸水使柔软

strlenendp

结束

只看主要部分的汇编代码,我们一句一句的研究。

首先,它声明strlen的公共符号,以及strlen s函数参数等。OPTION是防止汇编程序生成起始代码和结束代码的代码(这个可以参考相关文献,此处不再赘述),接下来。与堆栈指针省略相关的FPO在MSDN解释如下:

FPO (cdwLocals,cdwParams,cbProlog,cbRegs,fUseBP,cbFrame)

cdwLocals:局部变量的数量,一个无符号的32位值。

cdwParams:参数的大小,一个无符号的16位值。

cbProlog:函数Prolog代码中的字节数,一个无符号8位值。

cbRegs:函数prolog代码中的字节数,一个无符号8位值。

指示eBP寄存器是否已经被分配。不是0就是1。

CbFrame:表示帧类型。这里只需要注意第二个参数,是1,表示有一个参数。Strlen本身就是一个参数。其他参数,看到上面的英文注释应该很简单,这里不做解释。你也可以点击这里查看。

继续,注意这三句话:

[CPP]查看纯文本

stringeu[尤指4]

movecx,字符串;ecx字符串

testecx,3;证明32位

jeshortmain_loop

第一句话,esp 4简单,在文章《【动态分配栈内存】之alloca内幕》中有详细讲解。这里只简单说明一下。esp 4是strlen参数的地址,属于堆栈内存空间。如果取[esp 4],就会得到strlen参数(strlen的参数是const char*)指向的地址。假设代码如下所示:

[CPP]查看纯文本

charszName[]=梅斯费

strlen(SZ name);

然后,上面[esp 4]得到的地址值就是szName数组的第一个地址。前面的字符串equ [esp 4]不生成任何代码,字符串只相当于一个宏定义(至于为什么需要这个字符串,后面你就知道了。你要相信这一切都是有道理的,这是研究的乐趣之一),所以mov ecx,string相当于mov ecx,[esp 4]。这句话直接把参数指向的地址值赋给ecx寄存器,ecx此刻是一个字符串。下一句,test ecx,3,是测试存储在ecx中的地址值是否是4字节(32位)对齐的。如果是,跳转到main_loop执行,否则继续往下。让让我们先来看看错位的情况。自然是下面的str_misaligned部分:

[CPP]查看纯文本

str _未对齐:

moval,byteptr[ecx]

addecx,1

泰斯塔勒

jeshortbyte_3

测试cx,3

jneshortstr _未对齐

addeax,dwordptr05bytenoptoalignlabelbelow

align16应该是多余的

唐让我们先看看这段代码。让我们先推断一下。我们讨论了错位的情况。一般对于操作系统来说,内存的分配总是对齐的,所以这里strlen一进来就检查是否对齐。那么什么时候是错位的情况呢?如下所示:

[CPP]查看纯文本

charszName[]=梅斯费

char * p=szName

p;//将P向后移动一个字节,假设它与4个字节对齐。移动后,它不再与4个字节对齐。

strlen(p);

当然,我是故意这么写的。在实践中,还有其他情况。例如,如果结构中有一个字符串,该结构是字节对齐的,并且该字符串的位置不确定,则该字符串的第一个地址可能不是字节对齐的。继续之前的推断。如果没有对齐,就先对齐,然后继续找长度。如果在重新排列的过程中发现终止符,它会停止并立即返回长度。好吧,那就是它。看看上面的汇编代码,它it’真的是这样做的。

先从ecx指向的内存中取一个字节到al中,然后ecx加1并后移一个字节,再判断al是否为0。如果为0,跳转到byte_3段,否则继续测试ecx的当前地址值是否对齐,否则继续取一个字节值,如果没有对齐,再加上ecx,直到对齐或者碰到终止符。当没有遇到终止符,并且ecx中存储的地址值对齐时,下面这句话add eax,dword ptr 0后面跟一个注释,表示这段代码没有实际意义。Align 16和前面的add共同作用,将代码对齐16个字节,后面的main_loop就是16字节对齐开始的地址(再次感受到MS工程师的聪明,想的很周到)。

接下来,它是时候进入main_loop了,显然是指主循环,是strlen的核心。这里这是一个聪明的算法。首先,分析代码的前半部分:

[CPP]查看纯文本

moveax,dwordptr[ecx];读取4字节

movedx,7efefeffh

addedx,eax

xoreax,-1

xoreax,edx

addecx,4

testeax,81010100h

jeshortmain_loop

首先,第一句从ecx指向的内存中读入4个字节到eax中,显然是打算处理一次。然后看第二句,把edx赋给0x7efefeff。这个数有什么规律,有什么用?看看这个数的二进制:

0111111111111111111111111111111看这个数的二进制。我们注意到有四个红色的零,它们都有一个特点,都在每个字节的左边。什么这有什么用?再想想,左边,什么时候修改?很明显,当右边有进位时,会被修改到这个零,或者这些零与另一个数运算时位置会发生变化。唐不要先忙于分析。让看下一句,加edx,eax。这句话是把ecx指向的内存中取出的4字节整数加到0x7efefeff上。它很奇怪。什么这一附加的目的是什么?仔细考虑后,我很惊讶。原理是你可以通过把这个4字节的整数中的哪一个或哪几个字节加在一起,知道它们是0。0达到strlen的目的,就是找到终结符,然后返回长度。看加法的过程。加法的目的是改变上面四个红零中的一部分。如果有任何一个零没有变,零的最高位也没有变,说明这四个字节中有一部分或几个是0。这些红色的零可以叫洞,也很形象。例如:

字节3字节2字节1字节0

?00000000 ??//eax

01111110 11111111111111111111//EDX=0x 7 efefeffff以上假设两个数相加,问号代表0或1,但整个字节不全是0。eax的byte2全是0,加上EDX的byte2。无论byte1和byte0如何相加,最后一个进位最多只能是1,所以Byte2以此类推,如果byte0是0,byte1的最低位永远不能变。byte0只有一位不为0,byte1的最低位会接收进位,这也是edx的byte0为0xff的原因。所有字节由进位判断。只要右边没有进位,就一定有一个字节为0。

往下看,xor eax,-1是eax的取反(来自ecx指向的内存的4个字节)。然后xor eax,edx,这句话的用意是把前一次相加后值没有变化的那些位拿出来(加edx,eax后EDX的值)。继续,加ecx,4表示将ecx后移4个字节,方便下一步操作。然后,一个测试eax,81010100h,这个0x81010100是前面0x7efefeff的反转,也就是几个孔的位置都是1。与前面取出的相加值(addedx,eax后edx的值)中未改变的位进行比较:如果结果为0,说明相加值(addedx,EAX后edx的值)与原值EAX(取出的原字符串的4个字节)进行比较,处于0x7efefeff中4个0(hold)的位置,每个0的位置(hole)发生变化(或者每个1的位置(hole)相对于0x81010100中的4个1发生变化如果不是0,同理,发现有些字节是0。从这个角度来看,用0x81010100测试的目的是为了确定从字符串中提取的四个字节的hold位置相对于原来的四个字节的hold位置有哪些漏洞位置发生了变化。如果改变每个孔的位置,测试结果为0,表示没有字节为0;否则,它表示存在为0的字节。

当发现一个字节为0时,需要从取出的4个字节中确定哪个字节为0,如下所示:

[CPP]查看纯文本

moveax[ecx-4]

testal,al;isitbyte0

jeshortbyte_0

特斯塔,啊;isitbyte1

jeshortbyte_1

testeax,00ff0000hisitbyte2

jeshortbyte_2

testeax,0ff000000hisitbyte3

jeshortbyte_3

jmpshortmain _ loop取24-30位清零位

;31套

如上,第一句【ecx-4】的原因是ecx前面加了4,所以需要减去4重新开始前4个字节,然后逐字节确定哪个字节是0。代码很简单,所以我赢了这里就不详细解释了。如果在这里发现一个字节为0,则跳转到相应的尾部,如下所示:

[CPP]查看纯文本

字节3:

leaeax[ecx-1]

movecx,字符串

subeax,ecx

浸水使柔软

字节2:

leaeax[ecx-2]

movecx,字符串

subeax,ecx

浸水使柔软

字节1:

leaeax[ecx-3]

movecx,字符串

subeax,ecx

浸水使柔软

字节0:

leaeax[ecx-4]

movecx,字符串

subeax,ecx

浸水使柔软

以byte_3为例,即取出的四个字节中,第四个字节为0,前三个字节不为0,那么eax应该等于ecx-1,然后ecx重新赋值为字符串的第一个地址(这里应该明白为什么需要宏字符串了)。最后,subeax和ecx直接得到字符串的长度。然后ret返回上层。整个风暴将会结束。

通过前面的分析,我们已经知道了strlen的原理,对算法之美有了更深的体会。我们可以把strlen的这个汇编版本翻译成C语言版本,如下:

[CPP]查看纯文本

size_tstrlen(constchar*str)

{

constchar * ptr=str

for(;((int)ptr0x03)!=0;ptr)

{

if(* ptr==\0')

return ptr-str;

}

unsigned int * ptr _ d=(unsigned int *)ptr;

unsignedintmagic=0x 7 EFE feff;

while(真)

{

unsignedintbits 32=* ptr _ d;

如果((((bits 32 magic)^(bits32^-1))~magic)!=0)//bits32-1相当于~bits32

{

ptr=(const char *)(ptr _ d-1);

if(ptr[0]==0)

return ptr-str;

if(ptr[1]==0)

return ptr-str 1;

if(ptr[2]==0)

return ptr-str 2;

if(ptr[3]==0)

return ptr-str 3;

}

}

}

好了,strlen快分析完了,C语言的最后一个版本可以改了,比如可以根据字符集的编码进行专门化。但是它这通常是不必要的。它it’一般情况下用比较好。我做了一个测试,比较了本文开头的C语言版本,结尾的C语言版本,以及crt的汇编版本的性能。求同一个字符串的长度,10,000,000次,开启O2优化。三者的平均时间是:

c语言版本:723毫秒

以下翻译的c版本:315ms

crt的装配版:218ms可见,后两者性能有一定提升。这里需要说明一下,CRT的strlen函数属于内函数,所谓内函数,可以称为内部函数,有点类似于inline函数,但不代表inline。内联不是强制的,编译器编译的时候不一样。内在函数相当于编译器在编译时根据上下文决定是否在汇编级内联函数代码,同时对其进行优化,既节省了函数调用的开销,又使优化更加直接。编译器熟悉内部函数的内部函数,通常称为内置函数。因此,编译器可以更好地集成和优化,目的只有一个,在特定环境下选择最佳解决方案。以strlen为例,这段代码:

[CPP]查看纯文本

intmain(intargc,char**argv)

{

int len=strlen(argv[0]);

printf(% d ,len);

return0

}

当在debug下禁用优化,在release下禁用优化,或者在release下最小化大小(/O1)时,可以强制打开内部函数(/Oi)选项。开启此功能后,上述strlen函数将不再调用crt的汇编版本函数,而是直接嵌入到主函数代码中,如下(调试或释放下禁用优化,开启内部函数(/Oi)):

[CPP]查看纯文本

int len=strlen(argv[0]);

0042D8DEmoveax,dwordptr[argv]

0042D8E1movecx,dwordptr[eax]

0042D8E3movdwordptr[ebp-0D0h],ecx

0042D8E9movedx,dwordptr[ebp-0D0h]

0042D8EFaddedx,1

0042D8F2movdwordptr[ebp-0D4h],edx

0042D8F8moveax,dwordptr[ebp-0D0h] -

0042D8FEmovcl,byteptr[eax]|

042d900movbyteptr [EBP-0d5h],cl |//逐字节计算

0042D906adddwordptr[ebp-0D0h],1|

0042D90Dcmpbyteptr[ebp-0D5h],0|

0042D914jnemain 38h(42D8F8h)//-

0042D916movedx,dwordptr[ebp-0D0h]

0042D91Csubedx,dwordptr[ebp-0D4h]

0042D922movdwordptr[ebp-0DCh],edx

0042D928moveax,dwordptr[ebp-0DCh]

0042d 92 emovdwodptr[len],eax

如果在release下打开了最小大小(/O1)并且打开了内部函数(/Oi),则编译后的代码如下:

[CPP]查看纯文本

int len=strlen(argv[0]);

00401000moveax,dwordptr[esp 8]

00401004moveax,dwordptr[eax]

00401006leaedx,[eax 1]

00401009movcl,byteptr[eax] -

0040100 bince ax |///逐字节计数

0040100Ctestcl,cl|

0040100Ejnemain 9(401009h)

00401010subeax,edx

代码更加简洁,也没有函数调用开销。(其实你会惊讶的发现,这几个字的代码就是本文开头的第二个C语言版本strlen的反汇编代码。当然是优化后的代码,节省了这里的调用开销。实际上,本文开头的两个strlen,在开启更高的优化级别时,编译器会对这两个函数进行优化和嵌入,这与固有函数是一致的。为了说明这一点,编译器是人性化的,只要能满足优化条件,就会果断优化)。当打开最小大小(/O1)优化和内部函数(/Oi)优化时,生成的代码与在release下打开最大速度(/O2)或完全优化(/Ox)时的代码相同。当最大速度(/O2)或完全优化(/Ox)在释放下打开时,即使您没有打开内部函数(/Oi)优化,编译器也会去掉strlen,生成上面的代码。它这与优化水平有关。级别越高,优化自然就越全面,不管你是不是强行设置了什么。它这也是人性化的设计。要为内部函数优化打开函数,可以通过代码打开它,如下所示:

[CPP]查看纯文本

#pragmaintrinsic(strlen)

打开,然后自然关闭,如下所示:

[CPP]查看纯文本

#pragmafunction(strlen)

Force strlen s优化关闭,这样即使你最大化速度(/O2)或者完全优化(/Ox),你还是会调用crt的strlen函数。请参考MSDN,或点击这里,为两者的详细说明。

关于这种内在的语用,MSDN有详细准确的解释,或者说英文原文更能理解其原意:

intrinsicpragma告诉编译器一个函数有已知的行为。编译器可能会调用函数,而不会用inl ineinstructions替换函数调用,如果这样会带来更好的性能的话。.

使用内部函数的程序速度更快,因为它们没有函数调用的开销,但由于生成了额外的代码,程序可能会更大。

对了,唐不要试图使用这两个东西来强制打开或关闭一个公共函数的(/Oi)优化。所谓内在当然是编译器预设的一些函数,可以看作是一些细节优化的选择性。如果你不如果你不相信我,你肯定会得到一个警告:

警告C4163:"