《C陷阱与缺陷》45页有如下文字:
==================================================
int i, a[10];
for(i = 1; i <= 10; i++){
a[i] = 0;
}
这段代码本意是要设置数组a中所有元素为0,却产生了一个出人意料的“副效果”。在for语句的比较部分本来是i < 10,却写成了i <= 10,因此实际上并不存在的a[10[被设置为0,也就是内存中在数组a之后的一个字(word)的内存被设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数字a之后的一个字(word)实际上是分配给了整型变量i。此时,本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就陷入了一个死循环。
==================================================
正好我刚学完了汇编,于是把这段代码编译查看其汇编代码,发现了很有意思的事情。
这是程序源码:
#include <stdio.h>
#include <conio.h>
int main()
{
int i , a[10];
printf("Start./n");
for(i = 0; i <= 10; ++i){
a[i] = 0;
}
printf("Over./n");
getch();
return 0;
}
使用VC6.0编译该代码后,反汇编显示为:
1: #include <stdio.h>
2: #include <conio.h>
3:
4: int main()
5: {
00401010 55 push ebp
00401011 8B EC mov ebp,esp
00401013 83 EC 6C sub esp,6Ch // main函数调用,分配栈空间,此时esp指向栈顶
00401016 53 push ebx
00401017 56 push esi
00401018 57 push edi // 保护寄存器值
00401019 8D 7D 94 lea edi,[ebp-6Ch]
0040101C B9 1B 00 00 00 mov ecx,1Bh //
00401021 B8 CC CC CC CC mov eax,0CCCCCCCCh
00401026 F3 AB rep stos dword ptr [edi]
6: int i , a[10];
7:
8: printf("Start./n");
00401028 68 24 20 42 00 push offset string "Start./n" (00422024) // 字符串
0040102D E8 6E 00 00 00 call printf (004010a0) // 输出
00401032 83 C4 04 add esp,4 // esp+4
9: for(i = 0; i <= 10; ++i){
00401035 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0 // i=0,i存储在[ebp-4]处
0040103C EB 09 jmp main+37h (00401047)
0040103E 8B 45 FC mov eax,dword ptr [ebp-4] // 用eax保存i值
00401041 83 C0 01 add eax,1 // i++
00401044 89 45 FC mov dword ptr [ebp-4],eax // 送回i到内存[ebp-4]处
00401047 83 7D FC 0A cmp dword ptr [ebp-4],0Ah // 条件比较:i <= 10
0040104B 7F 0D jg main+4Ah (0040105a) // i > 10时,跳转到0040105a处;i <= 10时,顺序执行
10: a[i] = 0;
0040104D 8B 4D FC mov ecx,dword ptr [ebp-4] // i保存到ecx,目的是fetch the element of array a[]
00401050 C7 44 8D D4 00 00 00 mov dword ptr [ebp+ecx*4-2Ch],0 // a[i] = 0,注意对数组元素的获取是通过计算[ebp+ecx*4-2Ch]而来,
11: } // 用数组元素地址跟i的地址联立方程 ebp-4 = ebp+ecx*4-2Ch 不难算出
00401058 EB E4 jmp main+2Eh (0040103e) // ecx = 10 时,a[10]和i的地址重复。(0x2C = 44 )
12: printf("Over./n");
0040105A 68 1C 20 42 00 push offset string "Over./n" (0042201c)
0040105F E8 3C 00 00 00 call printf (004010a0)
00401064 83 C4 04 add esp,4
13: getch();
00401067 E8 D4 C6 00 00 call _getch (0040d740)
14: return 0;
0040106C 33 C0 xor eax,eax
15: }
内存示意图:
ebp-> ______ 高地址,栈底
ebp-4-> | i |
---------
| . |
| . |
| . |
| . |
ebp-40-> | a[1] |
ebp-44-> | a[0] |
| |
| |
esp-> 低地址,栈顶
由此可见,书上讲“此时,本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就陷入了一个死循环。”的确如此。
正好我这还有GCC编译器,于是也编译了一把,查看其反汇编代码:
先不用优化编译:
$gcc -S 2.c
生成汇编码:
.file "2.c"
gcc2_compiled.:
___gnu_compiled_c:
.text
LC0:
.ascii "Start./12/0"
LC1:
.ascii "Over./12/0"
.p2align 2
.globl _main
_main:
pushl %ebp
movl %esp,%ebp // ebp指向栈底
subl $56,%esp
addl $-12,%esp // 预留空间,esp指向栈顶
pushl $LC0
call _printf
addl $16,%esp //
movl $0,-4(%ebp) // 此处,用[ebp-4]处的内存空间保存i的值0
.p2align 4,,7
L3:
cmpl $10,-4(%ebp) // 条件比较 i<=10
jle L6 // 满足i<=10 ,跳到L6
jmp L4 // 不满足,跳出循环到L4
.p2align 4,,7
L6:
movl -4(%ebp),%eax // i==>eax
movl %eax,%edx // eax==>edx,将i值赋给edx,进行下标操作
leal 0(,%edx,4),%eax // 取偏移地址eax=4*edx,获取a[i]在内存中的存储位置
leal -44(%ebp),%edx // 取基址
movl $0,(%eax,%edx) // ebp-44+4*edx = 0 即实现了 a[i]=0
L5:
incl -4(%ebp) // i++
jmp L3 // 跳转到循环开始处L3
.p2align 4,,7
L4:
addl $-12,%esp
pushl $LC1
call _printf
addl $16,%esp
call _getch
xorl %eax,%eax
jmp L2
.p2align 4,,7
L2:
movl %ebp,%esp
popl %ebp
ret
显然,跟VC6.0的编译结果如出一辙
分析一下i的实现不难发现:
i的作用:
1。循环次数计数,此功能由内存[ebp-4]处实现
2。取得数组元素(下标操作),此功能由edx实现
可见当ebp-4 = ebp-44+edx*4 即 edx = 10时,i在内存中的位置被a[10]覆盖,程序陷入死循环。
再试试2级优化编译:
$gcc -O2 -S 2.c
.file "2.c"
gcc2_compiled.:
___gnu_compiled_c:
.text
LC0:
.ascii "Start./12/0"
LC1:
.ascii "Over./12/0"
.p2align 2
.globl _main
_main:
pushl %ebp
movl %esp,%ebp // ebp指向栈底
subl $56,%esp
addl $-12,%esp // 预留空间,esp指向栈顶
pushl $LC0
call _printf
addl $16,%esp
movl $10,%ecx // 循环次数=10
movl %ebp,%eax // 栈底地址赋给eax,作为数组a[]的起始地址
.p2align 4,,7
L6:
movl $0,(%eax) // a[i] = 0
addl $-4,%eax // i++
decl %ecx // 循环次数-1
jns L6
addl $-12,%esp
pushl $LC1
call _printf
call _getch
xorl %eax,%eax
movl %ebp,%esp
popl %ebp
ret
可见用GCC进行2级优化编译时,跟上面的结果有很大不同:
i的作用:
1。循环次数计数,此功能由ecx实现
2。取得数组元素(下标操作),此功能由eax实现
用寄存器而非内存空间保存i的值,因而不存在i的值被溢出的a[10]覆盖的问题,程序仍然能够执行。
另外,可以看出,优化编译使程序生成的代码更短小精悍,使用寄存器而非内存存储待处理数据,减少了数据存取时间。
综上所述,具体情况要具体分析,关键要看编译器如何处理,才是决定程序行为的关键。当程序行为出乎我们的预想时,最便捷的办法是调试,如果没有效果,只有求助于反汇编查看最最原始的指令执行情况了。