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的,這次修改開源程序,偷懶沒寫函數聲明,再加上這個開源程序本身產生的警告實在太多了,導致編譯器給出的找不到函數聲明的警告淹沒在這一大堆警告里,根本沒有察覺,最終花了很多時間才查明原因。這個事情給我的教訓就是:無論如何都要堅持寫函數聲明,一定不能忽視警告,一定要從一開始就消滅警告,否則等警告多起來,就很難有意願去消除警告了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM