X86_64上没有写C函数声明导致的BUG

X86_64上没有写C函数声明导致的BUG

我的博客:http://blog.striveforfreedom.net

1 简介

最近修改一个用C写的开源程序,需要加几个函数,因为偷懒没写函数声明,导致程序崩溃,最后花了很多时间才查明原因,原来是没有写函数声明惹的祸。感觉这个BUG在X86_64上还挺有代表性,因此这里把它记录下来。

2 导致崩溃的代码及解决思路

2.1 导致崩溃的代码

导致崩溃的代码简化后只大致这个样子的:

//foo.c
#include <stdlib.h>
#include "bar.h"

static const char* value = NULL;
void set_value(const char* p)
{
    value = p;
}

const char* get_value()
{
    return value;
}

int main(int argc, char* argv[])
{
    char p[] = "abcd";
    set_value(p);
    failed_func();

    return 0;
}

//bar.c
#include "bar.h"  //简单起见,就没给出bar.h了,该文件包含函数failed_func的声明。

char failed_func(void)
{
    const char* p = get_value();
    return *p;  //进程崩溃
}    

程序执行每次执行到failed_func函数,都会在注释的那一行崩溃。

2.2 解决思路

乍一看,这几个函数很简单,根本看不出有什么问题,为什么会导致崩溃呢?用gdb在函数failed_func上下一个断点,再step进函数get_value里,发现返回值就是是当初用set_value设置的值,然而等get_value函数返回再查看指针p的值时,发现指针p的值却不是当初设置的那个值了,这就很奇怪了,一个简单的函数调用却有如此怪异的结果,当时在C语言层面实在看不出有什么不对的地方,于是查看汇编代码,用set disassemble-next-line on,再一次进入函数get_value里,发现该函数设置好寄存器rax的值就直接返回了,rax的值就是当初用set_value设置的值,这个函数显然没有问题(该函数的返回值存在rax里)。回到函数failed_func里,紧接着调用函数get_value的callq指令之后的是一条cltq指令,该指令的作用是对eax的值进行符号扩展(sign-extend),结果存在rax里,这就导致rax的高32位值被设为全1或全0了(取决于eax最高位的值),再之后的指令是访问rax所指内存的指令,这条指令直接导致了崩溃,因为rax的值已经不是get_value所设置的值了(我们这个例子中rax的高32位全被置为1了)。这里的关键是cltq指令,为什么gcc会产生这么一条指令呢?原因在于C89有一个隐式声明规则(implicit declaration),当需要调用一个函数但找不到函数原型时,编译器会提供一个隐式声明,该隐式声明会假定函数返回值类型为int,C99已经去掉了这一规则,要求函数调用必须有函数声明,但gcc可能为了兼容老代码,并没有强制执行C99,只是给出了一个警告。在我们这个例子里,gcc找不到函数get_value的原型,于是假定函数get_value的返回值类型是int,因为X86_64上int是32位的而指针是64位的,于是把函数get_value返回值赋给指针p就相当于把一个32位的有符号数赋值给一个64位的无符号数(指针值是无符号的),C语言规定当赋值表达式两边类型不相同时,等号右边的类型会转成等号左边的类型(当然是在可转的前提下),于是32位的有符号int被转换转成64位的无符号数,于是编译器便生成了符号扩展指令cltq。这段代码在X86上不会崩溃,因为X86上int和指针都是32位的,编译器不会产生符号扩展指令。

设计上面这段示例代码的时候还有一个小小的trick,第一次设计这段代码的时候,在main函数里,传给函数set_value的参数我是这么定义的:

const char* p = "abcd";

但如果这样的话程序是不会崩溃的,原因在于字符串常量通常和代码一起放在代码段,而通常代码段一般会加载在较低的内存地址(通常会小于0x10000000),于是cltq指令执行之前rax值高32位就是0,执行之后rax的高32位还是0,rax值没有改变,程序也就不会崩溃,后来想到栈一般位于较高的内存地址,于是就将代码改成:

char p[] = "abcd";

因为栈的地址通常会大于0x10000000,执行cltq指令之后rax值的高32位全为1,这时的rax值代表着一个很大的虚拟地址,访问便会导致段错误,原因请看我的另一篇文章: Linux & X86上Segmentation fault原因分析

3 小结

其实这个BUG完全可以避免,编译时gcc给出了一条很明显的警告:warning: initialization makes pointer from integer without a cast,这条警告已经说明了问题的实质所在——用一个整数值来初始化指针。加上-Wall选项之后,还会一条函数没有声明的警告:warning: implicit declaration of function ‘get_value’,如果当时能看到这两条警告,问题立马就能得到解决。我平时写程序,编译选项都是加上-Wall, -Werror的,这次修改开源程序,偷懒没写函数声明,再加上这个开源程序本身产生的警告实在太多了,导致编译器给出的找不到函数声明的警告淹没在这一大堆警告里,根本没有察觉,最终花了很多时间才查明原因。这个事情给我的教训就是:无论如何都要坚持写函数声明,一定不能忽视警告,一定要从一开始就消灭警告,否则等警告多起来,就很难有意愿去消除警告了。

原文地址:https://www.cnblogs.com/sheniudou/p/3026421.html