对于如下所示的一段代码:
int main()
{
char a[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'};
int len = strlen(a);
printf("%d",len);
return 1;
}
运行结果如下:为 “19”。这个结果显然是错误的,从代码可知我们初始化的a[]元素长度为13,那么错误结果19是如何得出来的呢?为什么结果是“19”而不是别的某个数字呢?带着这样的疑问,我首先从编译器的角度进行分析,对代码进行了逐步调试,其反汇编代码如下:
一、程序初始化过程
{
00401110 push ebp ;保持栈底指针ebp
00401111 mov ebp,esp ;使当前栈底指针位置指向栈顶
00401113 sub esp,58h ;抬高栈顶,开辟栈空间0x58,作为局部变量的存储空间
00401116 push ebx ;保持寄存器ebx
00401117 push esi ;保持寄存器esi
00401118 push edi ;保持寄存器edi
00401119 lea edi,[ebp-58h] ;取出此函数新开辟的栈空间首地址
0040111C mov ecx,16h ;设置ecx为0x16,也就是22
00401121 mov eax,0CCCCCCCCh ;局部变量初始化为0CCCCCCCCh
00401126 rep stos dword ptr [edi] ;将eax中的内容以4字节为单位写到edi指向内存中,循环ecx次edi每次移动4字节
局部变量初始化后的内存区域如下:
其中红色CC为初始化的局部变量空间。初始化后的堆栈存储的内容如下:
二、字符数组初始化过程
char a[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'};
00401128 mov byte ptr [ebp-10h],61h ;将“a”的ASCLL值以1字节为单位存储到[ebp -10h]指向的内存中
0040112C mov byte ptr [ebp-0Fh],62h ;将“b”的ASCLL值以1字节为单位存储到[ebp - 0Fh]指向的内存中
00401130 mov byte ptr [ebp-0Eh],63h ;将“c”的ASCLL值以1字节为单位存储到[ebp - 0Eh]指向的内存中
00401134 mov byte ptr [ebp-0Dh],64h ;将“d”的ASCLL值以1字节为单位存储到[ebp - 0Dh]指向的内存中
00401138 mov byte ptr [ebp-0Ch],65h ;将“e”的ASCLL值以1字节为单位存储到[ebp - 0Ch]指向的内存中
0040113C mov byte ptr [ebp-0Bh],66h ;将“f”的ASCLL值以1字节为单位存储到[ebp - 0Bh]指向的内存中
00401140 mov byte ptr [ebp-0Ah],67h ;将“g”的ASCLL值以1字节为单位存储到[ebp - 0Ah]指向的内存中
00401144 mov byte ptr [ebp-9],68h ;将“h”的ASCLL值以1字节为单位存储到[ebp - 9h]指向的内存中
00401148 mov byte ptr [ebp-8],69h ;将“i”的ASCLL值以1字节为单位存储到[ebp - 8h]指向的内存中
0040114C mov byte ptr [ebp-7],6Ah ;将“j”的ASCLL值以1字节为单位存储到[ebp - 7h]指向的内存中
00401150 mov byte ptr [ebp-6],6Bh ;将“k”的ASCLL值以1字节为单位存储到[ebp - 6h]指向的内存中
00401154 mov byte ptr [ebp-5],6Ch ;将“l”的ASCLL值以1字节为单位存储到[ebp - 5h]指向的内存中
00401158 mov byte ptr [ebp-4],6Dh ;将“m”的ASCLL值以1字节为单位存储到[ebp - 4h]指向的内存中
初始化后的内存区域内容如下:
图中红框围起来的为原局部变量初始化区域,其中红色字体为字符数组初始化后的区域,可以看到以将CC改为ACSLL值,上面反汇编指令中的[ebp-10h]对应的地址正是内存中“61”的地址。你若细心,会不会和我一样产生一个疑问,为什么不将字符数组从[ebp-0h]地址(即上图的“64”所在处)开始初始化呢?这样就能保证“6D”的后头不会有3字节CC存在了。你可以想想看,为什么编译器要这样处理,具体原因文章最后再谈。此时堆栈内容如下图所示。
三、对字符串数组求长度
int len = strlen(a);
0040115C lea eax,[ebp-10h] ;取字符串数组首地址存入eax寄存器
0040115F push eax ;将eax入栈
00401160 call strlen (004013d0) ;调用strlen()函数求字符串数组长度
00401165 add esp,4 ;堆栈平衡
00401168 mov dword ptr [ebp-14h],eax ;将eax寄存器中的strlen()函数返回值写入堆栈
strlen:
004013D0 mov ecx,dword ptr [esp+4] ;取出字符串首地址放入ecx寄存器
004013D4 test ecx,3 ;与3的二进制异或看地址是否为4的整数倍
004013DA je main_loop (004013f0) ;是整数倍代表内存对齐跳到main_loop
str_misaligned: ;若不是整数倍执行如下整数对齐指令
004013DC mov al,byte ptr [ecx] ;将[ecx]的内容以一字节为单位存储到al寄存器中
004013DE inc ecx ;ecx加1指针后移
004013DF test al,al ;判断al内容是否为“\0”
004013E1 je byte_3 (00401423) ;为“\o”则跳到byte_3处执行
004013E3 test ecx,3 ;不为“\0”执行与3二进制异或判断地址是否对齐
004013E9 jne str_misaligned (004013dc) ;若不对齐循环执行str_misaligned
004013EB add eax,0
main_loop:
004013F0 mov eax,dword ptr [ecx] ;将[ecx]的内容以4字节为单位存储到eax寄存器中
004013F2 mov edx,7EFEFEFFh ;7EFEFEFFh存储到edx中
004013F7 add edx,eax ;求和结果存储edx中
004013F9 xor eax,0FFh ;与0FFh异或,也就是对eax内容取反
004013FC xor eax,edx ;异或结果存入eax中
004013FE add ecx,4 ;指针后移4字节
00401401 test eax,81010100h ;求与,寻找为“\0”字节
00401406 je main_loop (004013f0) ;若结果为0,循环执行main_loop
00401408 mov eax,dword ptr [ecx-4] ;若结果非0,将[ecx-4]的内容以4字节为单位存储到eax寄存器中
0040140B test al,al ;判断第一个字节是否为“\0”
0040140D je byte_0 (00401441) ;若是跳转到byte_0
0040140F test ah,ah ;判断第二个字节是否为“\0”
00401411 je byte_1 (00401437) ;若是跳转到byte_1
00401413 test eax,0FF0000h ;判断第三个字节是否为“\0”
00401418 je byte_2 (0040142d) ;若是跳转到byte_2
0040141A test eax,0FF000000h ;判断第四个字节是否为“\0”
0040141F je byte_3 (00401423) ;若是跳转到byte_3
00401421 jmp main_loop (004013f0) ;若四个字节都没有“\0”,代表第31位被设置为1,将在文章末尾解释
byte_3:
00401423 lea eax,[ecx-1] ;将“\0”之前的字符串地址存入eax
00401426 mov ecx,dword ptr [esp+4] ;字符串首地址存入ecx
0040142A sub eax,ecx ;获得字符串长度
0040142C ret
byte_2:
0040142D lea eax,[ecx-2] ;将“\0”之前的字符串地址存入eax
00401430 mov ecx,dword ptr [esp+4] ;字符串首地址存入ecx
00401434 sub eax,ecx ;获得字符串长度
00401436 ret
byte_1:
00401437 lea eax,[ecx-3] ;将“\0”之前的字符串地址存入eax
0040143A mov ecx,dword ptr [esp+4] ;字符串首地址存入ecx
0040143E sub eax,ecx ;获得字符串长度
00401440 ret
byte_0:
00401441 lea eax,[ecx-4] ;将“\0”之前的字符串地址存入eax
00401444 mov ecx,dword ptr [esp+4] ;字符串首地址存入ecx
00401448 sub eax,ecx ;获得字符串长度
0040144A ret
根据反汇编代码,可以获知具体strlen()函数源码如下:
typedef unsigned long ulong;
size_t strlen_c(const char * str) {
const char * char_ptr;
const ulong * longword_ptr;
register ulong longword, magic_bits;
for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0 ;++ char_ptr) {
if (*char_ptr == '/0' )
return char_ptr - str;
}
longword_ptr = (ulong* )char_ptr;
magic_bits = 0x7efefeffL ;
while (1 ) {
longword = *longword_ptr++ ;
if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0 ) {
const char *cp = (const char*)(longword_ptr - 1 );
if (cp[0] == 0 )
return cp - str;
if (cp[1] == 0 )
return cp - str + 1 ;
if (cp[2] == 0 )
return cp - str + 2 ;
if (cp[3] == 0 )
return cp - str + 3 ;
}
}
}
这里还要对strlen()源码具体解释下
(1)
for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0 ;++ char_ptr) {
if (*char_ptr == '/0' )
return char_ptr - str;
}
这里sizeof(ulong)值为4。((ulong)char_ptr & (sizeof(ulong) - 1)) != 0判断指针char_ptr所指地址是否为4的倍数,若不是则指针后移继续判断。这期间要是遇到“\0”则返回char_ptr-str及字符串数组长度。这里for循环实现了数据对齐。
(2)
longword_ptr = (ulong* )char_ptr;
magic_bits = 0x7efefeffL ;
将指针char_ptr转换成ulong*类型longword_ptr指针,为了一次去取出4字节内容赋值给longword变量。magic_bits赋值为0x7efefeffL。
(3)
(((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0
此代码为函数核心代码。功能是求出4字节内容中是否有“\0”字符。
(3.1)
magic_bits=
0x7efefeffL
对应二进制表示如下:
magic_bits中的31,24,16,8这些bits都为0,我们把这几个bits称为“判断位”,注意在每个byte(b4、b3、b2、b1)的左边都有一个“判断位”。
(3.2)
longword + magic_bits
意思是产生进位,是相应的判断位改变,用于检测0字节。 longword是四个字节,分别是b4 b3 b2 b1,对于magic_bits,只要b4~b1有一个不为0,那么必定会产生进位,使判断位为1。举例如下:
意思是产生进位,是相应的判断位改变,用于检测0字节。 longword是四个字节,分别是b4 b3 b2 b1,对于magic_bits,只要b4~b1有一个不为0,那么必定会产生进位,使判断位为1。举例如下:
从例子中可看到。b1不为0,相加后对应“判断位”第8bits值由0变为1了。b2为0,相加后对应的“判断位”第16bits值相加后未变还是1。b3不为0,相加后对应“判断位”第24bits值由0变为1了。b4不为0,相加后对应“判断位”第31bits值由0变为1了。也就是说,如果longword 中有一个字节的所有bit都为0,则进行加法后,从这个字节的右边的字节传递来的进位都会落到这个字节的最低位所在的“判断位”上,而从这个字节的最高位则永远不会产生向左边字节的“判断位”的进位。则这个字节左边的“判断位”在进行加法后不会改变,由此可以检测出0字节;相反,如果longword中所有字节都不为0,则每个字节中至少有1位为1,进行加法后所有的“判断位”都会被改变。
(3.3)
^ ~longword
longword取反,跟(longword + magic_bits)做异或运算。目的是取出加法后longword中所有未改变的bit。接着用3.2中例子说明。
运算结果为1的位置代表未改变bit。
(3.4)
& ~magic_bits
magic_bis取反,跟((longword + magic_bits) ^ ~longword)做与运算。目的是取出longword中未改变的“判断位”,如果有任何“判断位”未改变则说明longword中有为0的字节。接着用3.3中例子说明。
如例子可见,当((longword + magic_bits) ^ ~longword)&~magic_bits的结果不为0时存在某个字节为0,也就是存在字符“\0”。
(4)
if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0 ) {
const char *cp = (const char*)(longword_ptr - 1 );
if (cp[0] == 0 )
return cp - str;
if (cp[1] == 0 )
return cp - str + 1 ;
if (cp[2] == 0 )
return cp - str + 2 ;
if (cp[3] == 0 )
return cp - str + 3 ;
}
当(
((longword + magic_bits) ^ ~longword)&~magic_bits)!=0时,可知longword中的4个字节存在某个字节内容为0。之后就执行if内代码寻找是第几个字节内容为0,若找到则将对应指针地址与字符串首地址相减求得字符串数组长度。
[注] 在这里解释下文章开头反汇编代码中提出的疑问,先回顾下代码片段
00401421 jmp main_loop (004013f0) ;若四个字节都没有“\0”,代表第31位被设置为1,将在文章末尾解释
原因是这样的,如果b4为10000000,则进行加法后第31 bit这个“判断位”不会变,代码就会判断出b1、b2、b3、b4这4个字节中有一个字节为0,但其实b4并不为0,说明我们无法检测出b4为10000000的所有longword,这就会导致编译器执行jmp指令。可喜的是用于strlen()的字符串都是ASCII标准字符,其值在0-127之间,这意味着每一个字节的最高位bit都为0。因此上面的算法是安全的。
四、strlen()运行结果
1、总结下strlen()函数执行过程:
(1)首先把指针移到地址是机器字整数倍的位置,假如已经遇到为0的字节(也就是字符“\0”),则直接返回长度。
(2)从此位置开始,每次比较一个机器字,直到这个机器字中有一个字节为0(也就是字符“\0”)。
(3)找出这个0字节在这个机器字的位置,然后返回长度。
(2)从此位置开始,每次比较一个机器字,直到这个机器字中有一个字节为0(也就是字符“\0”)。
(3)找出这个0字节在这个机器字的位置,然后返回长度。
2、下面我就回答文章开头提到的为什么使用strlen(a)会获得字符数组char a[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'}的长度为19,不是别的值呢?
从strlen()函数执行过程可知,strlen()每次比较一个机器字(4字节)直到机器字中有0内容字节就停止比较。这样,通过下图(strlen()函数执行后的堆栈图)我们可以看到char a[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'}初始化后堆栈内容,strlen()函数从堆栈内容是“61”的字节开始读取,每次比较4字节,依次比较“61 62 63 64”、“65 66 67 68”、“69 6A 6B 6C”、“6D cc cc cc”、“C0 FF 12 00”,当在比较“C0 FF 12 00”4字节内容时会发现有1个字节内容为0,则停止比较,这个“00”字节前头有“61 62 63 64 65 66 67 68 69 6A 6B 6C 6D cc cc cc C0 FF”共19个字节,进而求出字符数组长度为19。可以看出来strlen()函数将ebp寄存器存放于堆栈中的函数返回地址“C0 FF 12 00”误认为成了字符数组,导致判断字符数组长度出错。
strlen()函数返回值存入eax寄存器(eax内值为00000013),将eax入栈。此时栈空间内容如下:
3、下面我再来解答第二个疑问,为什么不将字符数组从[ebp-0h]地址(即上图的“64”所在处)开始初始化呢?这样就能保证“6D”的后头不会有3字节CC存在了。
这3个“CC”的存在是有它的意义的!这是编译器考虑到4字节内存对齐优化的结果。
好了通过以上分析我们可以知道对字符数组char a[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'}用strlen()函数求长度是不正确的,我们可以用sizeof()求字符数组长度。