警惕C说话的定义与声明

    添加时间:2013-7-29 点击量:

    警惕C说话的定义与声明




    注:为便于申明题目,文中说起的变量和函数都被简化。


    一、来源


    DBProxy在测试过程中,发明对其履行某步经管操纵后,法度有时会溃散,但不是每次都呈现。


    二、GDB跟踪


    反复多次测试,然后用GDB打开core dump文件,查见地度溃散时的客栈,发明可能的溃散只有两处,这两处的共同点是前面都调用了一个函数get_pointer获得一个指针,如下图所示:


    然后在应用该指针进行下步操纵时法度溃散。


    查看该指针的值,发明其指向一个无效地址,所以操纵该地址产生了段错误,如下图所示:


    三、无效地址的产生原因


    函数get_pointer的原型是char get_pointer(void),函数体内只是经由过程简单的malloc操纵获取一个char类型的指针,然后返回给调用者。
    malloc()成果只有两种:



    1. 成功,返回一个指向合法地址的指针

    2. 失败,返回NULL


    为什么会返回一个无效地址呢?
    在get_pointer函数返回前参加一个printf语句,打印即将返回给调用者的指针值:

    再在调用处之后也参加一个printf语句,打印调用者接管到的指针值:

    从头编译,反复进行多次测试,发明打印出的两个值有时雷同有时不合,雷同时法度正常运行,不应时法度必定溃散。
    更首要的是,这些值看上去很有规律:


    p=1c34abd0
    pointer=1c34abd0
    正常运行


    p=38ac7bda1690
    pointer=7bda1690
    接下来溃散


    p=a5ef6824
    pointer=ffffffffa5ef6824
    接下来溃散


    ……


    调查到一个现象:若是p值是4字节(即高4字节为0)且低4字节的高位为0,则二者雷同,不然二者必然不合。而pointer值的高4字节只有两种景象,一种是0×00000000,一种是0 xFFFFFFFF,再连络低4字节的首位,可以看出pointer值的高4字节是由p值的低4字节补全了高4字节形成的。


    四、漏掉的头文件


    重视到编译时有如下信息输出“警告:初始化时将整数赋给指针,未作类型转换”。当


    初调查该函数,发明返回的是指针,认为是编译器的误报,未作处理惩罚。从头核阅该警告信息,连络调用处的源文件,发明该文件没有包含声明get_pointer函数的头文件。在源文件头部添加#include语句,包含该头文件后,从头编译,警告消散,反复测试,法度不再溃散,很是稳定。看来题目产生的原因是因为漏包含了头文件,那么为什么不包含也可以编译经由过程呢?


    五、一个简单的实验


    /a.c/


    #include <stdio.h>


    int main()


    {


    void p = func();


    printf(“p=%lx\n”, p);


    return 0;


    }


    /b.c/


    void func()


    {


    void p = (void)0 x1234567890ABCDEF;


    return p;


    }


    gcc –c a.c b.c,体系提示:

    固然有警告,然则编译却成功了。


    我们再来链接一下,gcc –o all a.o b.o,链接也成功了。


    运行法度,./all,输出成果是p=FFFFFFFF90ABCDEF,而我们等待的值是1234567890ABCDEF,题目重现了。


    加上-Wall选项后从头编译,发明体系多了一行输出“警告:隐式声明函数 ‘func’”,


    查阅了隐式声明的相干材料得知,本来,编译器会将所有隐式声明的函数的返回值类型都认定为int。


    如此一来原因就斗劲清楚了,在b.c里定义的func函数返回的确切是指针类型,而在a.c里认为func返回的是int类型,法度运行时会将func返回的指针类型值强迫转换为int,然后再强迫转换为void,赋给变量p。在64位机上,指针为8字节,int为4字节,在由指针转换为int时,高4字节被丢弃,值由0 x1234567890ABCDEF变为0 x90ABCDEF,然后在由int转换为指针的过程中,按照有符号数的补齐原则,遵守int高位是0还是1,将高4字节每一位全部补全为0或1。0 x90ABCDEF的高位是1,所以高4字节每一位都补全为1,终极形成了成果0 xFFFFFFFF90ABCDEF。


    那为什么实际运行时不是每次都溃散呢?这是因为被调用的函数所返回的指针是动态分派的,其值事先不固定,若是被初始化的指针地址的高4字节和低4字节的高位底本就是0,如0×0000000012345678,那么在将强迫转换为int时丢弃高4字节对其就没有任何影响了,值还是0×12345678,然后再由int转为指针,高4字节补0,值为0×0000000012345678,所以法度可以正常运行下去。


    六、编译与链接、定义与声明


    在编译阶段,各个源文件自力编译,所以a.c和b.c是分隔编译的,a.c里调用了func


    函数而没有包含其声明(声明一般应用#include “b.h”,也可以应用extern函数原型的情势),编译器会认为func函数为隐式声明,将其返回值类型定为int。所以编译固然有警告,但却成功了。


    编译阶段是不须要函数的定义的。我们把b.c里的func函数注释掉,gcc –c a.c b.c一样可以履行成功。


    在链接阶段,链接器将所有源文件编译获得的二进制文件以及调用的库链接到一个可履行文件中,此时链接器会去找func函数的具体定义,以供main函数调用。因为func函数确切有定义,所以链接也会成功。


    链接阶段必须有函数的定义,不然链接器会报错。我们还是注释掉func,编译后再履行gcc –o all a.o b.o,体系输出如下:

    在运行阶段,因为a.c在编译时认为func返回int类型,所以func的返回值(8字节指针)被截断为4字节的int,然后再进行高4字节的扩大,最后赋给了main函数里的变量p。在这两次类型转换中,p的值就有可能与func的返回值不雷同了,p实际上已经成为一个野指针。


    我们换用g++来编译看下结果:



    看来g++对语法请求更严格,不容许隐式声明func函数。


    返回值有可能因为隐式声明而不合适我们的期望,那么函数的参数呢?我们再来测验测验一下。


    /c.c/


    extern void func(long);


    int main()


    {


    func(0 x1234567890ABCDEF);


    return 0;


    }


    /d.c/


    #include <stdio.h>


    void func(int a)


    {


    printf(“a=%x\n”, a);


    }


    gcc -c c.c d.c -Wall,成功。


    gcc -o all c.o d.o -Wall,成功。


    运行法度./all,输出a=90abcdef,这显然不是我们想要的成果,但在编译和链接时却没有任何错误或警告报出。


    应用g++编译,无法经由过程。


    若是我们新建一个头文件d.h,将func函数的原型在d.h里声明,然后在c.c和d.c里都包含d.h,就可以避免参数或返回值可能的不一致了。


    七、总结



    1. 在开辟过程中,应当严格遵守先声明后定义、先声明后应用的原则,一方面对峙杰出的编码风格,另一方面也能避免很多潜伏的错误;

    2. 从参数不一致造成的题目来看,好不要应用extern声明函数,而应当应用包含头文件的情势;

    3. 编译时打开-Wall选项,对于编译过程中输出的每个WARNING都要细心搜检,防止呈现各类匪夷所思的bug;

    4. 在某些场合,应用g++庖代gcc可以获得更好的安然性。

    我所有的自负皆来自我的自卑,所有的英雄气概都来自于我的软弱。嘴里振振有词是因为心里满是怀疑,深情是因为痛恨自己无情。这世界没有一件事情是虚空而生的,站在光里,背后就会有阴影,这深夜里一片寂静,是因为你还没有听见声音。—— 马良《坦白书》
    分享到: