读书笔记(《C陷阱与缺陷》)

《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]覆盖的问题,程序仍然能够执行。
另外,可以看出,优化编译使程序生成的代码更短小精悍,使用寄存器而非内存存储待处理数据,减少了数据存取时间。


综上所述,具体情况要具体分析,关键要看编译器如何处理,才是决定程序行为的关键。当程序行为出乎我们的预想时,最便捷的办法是调试,如果没有效果,只有求助于反汇编查看最最原始的指令执行情况了。

 


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值