发信人: riffle() 
整理人: wenbobo(2002-12-06 23:19:39), 站内信件
 | 
 
 
    前段时间在DOS平台下开发了一套软件,由于是多人开发,除了
 定义好接口外,联调也是一个不容忽视的问题。在联调过程中遇到
 了这样的问题:
 
     语句printf( "abcdef\n" )输出的结果居然是“ab”?!那么问题
 出在哪呢?查了很久,终于查到有个模块的某个函数有问题,如下:
 
      void func( char *s )
      {
         char sTempBuf[ MAX ];
         strncpy( sTempBuf, s, MAX );
         sTempBuf[ MAX ] = '\0';
      }
 
     问题出在第三句sTempBuf[ MAX ] = '\0';这是一个由于疏忽造成
 的下标越界问题。正确的写法应该是:
 
     void func( char *s )
     {
        char sTempBuf[ MAX + 1 ];
        strncpy( sTempBuf, s, MAX );
        sTempBuf[ MAX ] = '\0';
     }
 
     这样既能保证MAX个有效字符,又不会出现下标越界。
 
     但是,这样的一个下标越界,与其它地方的printf( "abcdef\n" )有
 什么关系呢?怎么会造成输出“ab”呢?
 
     这要从DOS的EXE文件结构谈起,我们知道,DOS的EXE文件包括三部分:
 
     一、文件头。
     二、文件加载部分。
     三、覆盖部分(如果有的话)。
 
     加载到内存后则分为四个部分:
 
     一、代码部分。
     二、数据部分。
     三、栈。
     四、堆。
 
     如果有覆盖部分,覆盖部分被覆盖管理器加载到堆中运行。
 
     下面我们看看编译器做了什么,以及程序是如何执行的。
 
     首先,对于printf( "abcdef\n" )语句,编译器把"abcdef\n"看做
 是一个常量,编译完成后,在EXE文件中的位置是处于“文件加载部分”
 的后半部分,也就是说,在“文件加载部分”中,包括代码部分和数据
 部分。C编译器一般是将数据放在代码的后面,当然也有例外。
 
     调用printf函数时,编译产生的代码是将常量"abcdef\n"的地址入
 栈,然后调用printf函数。
 
     对于函数func中的char sTempBuf[ MAX ];申明,编译器将栈指针减
 去MAX个字节的空间,进行栈内存分配。func函数返回后,调用者进行栈
 平衡。
 
     现在我们假设这样的一次调用:
     char sBuf[] = "12345678";
     func( sBuf );
     sBuf[ 0 ] = '\0';
 
     编译器将"12345678"也是当作常量处理的,如果不幸编译器将常量
 "12345678"放在了"abcdef\n"的前面了,那么在func函数中,由于下标
 越界,导致寄存器BP值被破坏,当执行sBuf[ 0 ] = '\0';时,取到的却
 不是sBuf的地址,而是在sBuf之后的若干位置了。
 
     这位置刚好落在"abcdef\n"中“c”字符的位置,于是“c”字符被
 改成了'\0',结果printf( "abcdef\n" )语句居然输出的是“ab”!
 
     由于加减一些语句对编译后各代码及数据的位置有影响,因此添加一
 些或减去一些不相干的语句,有时候又不会有错。这就使局面变得相当复
 杂,有的人甚至开始怀疑是不是编译器有问题了。
 
     问题最终是排除了,但是从中可见,下标的越界问题是C语言程序员
 胸口永远的痛。不进行严格的下标检查,程序大了,找问题的难度成指数
 级增长。
 
     下面我谈谈保护性编程。
 
     对于认为编写程序可以做到完全没有错误的人来说,在软件中建立检
 查的意义不大。相反,对于相信软件中总会有遗留错误的人来说,进行软
 件内部的错误检查则是一项重要策略,这种策略就是保护性编程(defensive
 programming)。
 
     保护性编程技术可分非主动和被动两种。主动保护技术周期性地或在空
 闲时间对整个程序或数据库进行搜索,用以发现异常情况。被动保护技术是
 在达到检查代码时,对程序的某些部分进行检查。
 
     这里我只谈谈被动式保护技术。
 
     某些反对保护性编程人员提出了许多逃避保护性编程的理由:
     (1)、“我们的程序即使有错误,也是很少的,所以不需要保护性编
        程。”
     (2)、“要求采用他人的程序来检查和发现我的程序错误,这是不公
        正的。”
     (3)、“错误检查降低了计算机系统的速度,而且还要求额外的内存。”
     (4)、“错误检查要占用很多编程时间。”
     (5)、“可以在我的程序中加入错误检查,不过一旦程序通过测试,
        就应该把它撤去。”
 
     对这些反对意见的回答,都直接或间接地与程序中预料的错误及潜在
 后果有关。如果相信错误无所不在,即有相当数量的错误在晚期发现是不
 可避免的,就必须采用保护性编程。如果这种保护在程序测试阶段对我们
 有帮助,则肯定要在程序操作使用时继续将其包括在内。如果承认错误会
 出现这一事实,则比起因出现错误所带来的责难来说,增加一些工作量是
 有意义的。
 
     另一方面,基于运行时间、存贮空间和编程费用等方面的反对意见,
 仅当保护性编程要求设计增加的资源量相当大时才有意义。事实上,只有
 在极少数情况下,保护性编程所要求增加的资源量才会相当大,以致不得
 不放弃考虑这种技术。
 
     有人提出在设计初期就把保护性编程包括进去。在设计过程中,大多
 数设计在填充可用存储空间时,其规模都增加得很快,如果企图在设计过
 程的结尾再引入保护性编程,则肯定已没有剩余空间。而且在近乎完成的
 设计中,移动任何部分挤出空间的任何做法,都会招致强烈的反对。但如
 果在一开始就把保护性编程包括进来,在希望增加新的保护性特征时,就
 能容易地避开设计中的讨价还价。
  -- ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.103.138.55]
  | 
 
 
 |