X86_64上沒有寫C函式宣告導致的BUG

來源:互聯網
上載者:User

X86_64上沒有寫C函式宣告導致的BUG

我的部落格:http://blog.striveforfreedom.net

Table of Contents
  • 1 簡介
  • 2 導致崩潰的代碼及解決思路
    • 2.1 導致崩潰的代碼
    • 2.2 解決思路
  • 3 小結
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的,這次修改開來源程式,偷懶沒寫函式宣告,再加上這個開來源程式本身產生的警告實在太多了,導致編譯器給出的找不到函式宣告的警告淹沒在這一大堆警告裡,根本沒有察覺,最終花了很多時間才查明原因。這個事情給我的教訓就是:無論如何都要堅持寫函式宣告,一定不能忽視警告,一定要從一開始就消滅警告,否則等警告多起來,就很難有意願去消除警告了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.