精华区 [关闭][返回]

当前位置:网易精华区>>讨论区精华>>编程开发>>C/C++>>C、C++语言基础>>为什么printf语句的输出不对?——谈谈保

主题:为什么printf语句的输出不对?——谈谈保
发信人: 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]

[关闭][返回]