作者: 守望,Linux應用開發者,目前在公衆号【程式設計珠玑】 分享Linux/C/C++/資料結構與算法/工具等原創技術文章和學習資源
前言
最近要将整個項目的代碼從原先的隻支援32位變成同時支援32位和64位,這個過程中遇到一個很不容易定位的挂死問題,花了不少時間才定位解決,是以分享給大家。
32位和64位代碼差別
在分享之前,需要了解一下32位和64位程式代碼有何差別,它的主要差別展現在某些資料類型的占用位元組大小的不同:
資料類型 | 32位 | 64位 |
---|---|---|
long | 4位元組 | 8位元組 |
unsigned long | 4位元組 | 8位元組 |
指針 | 4位元組 | 8位元組 |
size_t | 4位元組 | 8位元組 |
ssize_t | 4位元組 | 8位元組 |
這些是主要的差别。
那麼為什麼要切64位呢?原因也很簡單,32位尋址範圍有限,能使用的最大記憶體也是非常有限的,是以需要使其能夠支援64位,這個過程需要修改編譯工程,編譯第三方庫為64位,修改代碼等等。當然這些都不是本文的重點,本文僅介紹遇到的這個典型的問題。
問題描述
由于項目本身涉及的系統比較複雜,是以簡單分享一下定位過程,下一節将通過簡潔的示例程式來說明。
問題現象:向伺服器發送一條操作指令後直接挂死
分析解決過程簡化為如下步驟:
- 檢視日志以及coredump資訊,初步定位挂死的位置
- 發現挂死在停止定時器的位置
- 32位程式正常,而64位異常,是以和32位與64位的差别有關
- 懷疑傳入定時器資料有問題,編寫小demo,排除傳入資料問題
- 編譯可調試版本,加入-g參數
- 跟蹤調試,發現最終挂在了一個動态庫中
- 設定gdb源碼路徑,以便調試跟蹤動态庫
- 通過gdb觀察傳入指針,在通路指針時,出現錯誤,提示通路非法記憶體
- 列印傳入定時器指針位址,發現異常,位址開頭4位元組為全f,不正常,是以懷疑該指針最開始就已經出問題
- 跟蹤啟動定時器部分,動态庫接口傳回的位址值,就已經異常了。但是跟蹤到動态庫接口内部,發現傳回的結果是正常的8位元組位址值,排除定時器接口的問題
- 最終可以确定,在調用動态庫接口時,雖然傳回的是8位元組位址,但是賦給外部變量時,就被截斷了
- 換項目中的另外一個程序調試demo發現,編譯時出現錯誤,提示函數沒有聲明
- 于是加上聲明之後編譯通過,但并沒有出現挂死的問題
- 随即繼續跟蹤原項目出問題的程序,發現同樣這些接口都沒有外部聲明,再加上另外一個程序的警告資訊,提示有int往指針強轉,是以懷疑和函數的聲明有關。
最終确實如此。
具體是為什麼呢?
簡化示例
示例代碼分别放在main.c和test.c中,main.c内容如下:
test.c的内容如下:
上面兩段代碼再簡單不過,testFun在函數中申請一段記憶體,并傳回。而main函數通過調用testFun,将位址值傳回給p,并列印p的位址值。
編譯運作:
從運作結果中,我們可以發現以下幾個事實:
- 64位程式位址為8位元組
- testFun内部申請到的記憶體位址值是占用8位元組的值
- main函數中的p的位址值為4位元組
- 傳回值被截斷了
也就是和我們預期的結果完全不一樣。我們逐漸分析,到底是為什麼。
特别說明:如果指派那一行改成下面這樣
運作結果中如下:
其實看到8位元組的前面4位元組都是f,就可以判斷這個位址是非法的了。為什麼?(提示:程式位址空間分布)。
為什麼coredump?
這個問題很明顯,因為申請記憶體得到的位址值與釋放記憶體的位址不是同一個,是以導緻coredump(coredump的檢視可參考《linux常用指令-開發調試篇》中的gdb部分)。
為什麼位址值被截斷?
在解釋這個之前,我們先看一個簡單的示例程式:
編譯:
我們在編譯的時候出現了一個警告,提示test函數沒有傳回值,會預設傳回值為int。
也就是說,如果函數實際有傳回值,但是函數傳回值類型卻沒有指明,編譯器會将其預設為int。
實際上前面的示例程式在編譯的時候就有警告:
兩個警告的意思分别為:
- testFun沒有聲明
- 嘗試從整形轉換成指針
第一個警告很容易了解,雖然定義了testFun函數,但是在main函數中并沒有聲明。是以對mian函數來說,它在編譯階段(關于編譯階段,可參考《hello程式是如何變成可執行檔案的》),“看不到”testFun,是以會預設為其傳回值為int。而正因如此,就有了第二個警告,提示從整型轉換成指針。
到此其實也就真相大白了。既然testFun的傳回值被編譯器預設為int,傳回一個8位元組的指針類型,而傳回值卻是int,自然就會被截斷了。
如何解決
既然知道原因所在,那麼如何解決呢?這裡提供兩種方式。
- extern聲明
- 在頭檔案中聲明,調用者包含該頭檔案
按照第一種方式,在main.c中增加一行聲明:
運作結果:
第二種方式,增加test.h,内容為testFun的聲明:
main.c包含test.h頭檔案:
test.c修改如下:
以上兩種方式都可解決前面的問題。
而32位程式為什麼正常?相信你已經有了答案。
總結
由于對出現問題的程式代碼不熟悉,加上其編譯工程充斥着大量的警告而沒有處理,以及涉及動态庫,導緻這個引起挂死問題的罪魁禍首沒有提前暴露處出來。而問題的根本原因我們也清楚了,就是因為調用函數前沒有聲明。本文總結如下:
- 不要忽略任何一個警告,除非你非常清楚地知道自己在做什麼
- 在頭檔案中聲明函數,并提供給調用者
- 函數使用前進行聲明
- 問題長期定位不出來時,休息一下
- 盡量編寫通用性代碼
- 非必要時不強轉
- 使用void *指針格外小心
思考
- 為什麼32位的時候運作正常,而64位程式會挂死
- 32位和64位程式使用者空間位址範圍分别是多少
- 如何在調試中設定程式源碼路徑
- 程式完整編譯經曆那幾個階段
●編号532,輸入編号直達本文
●輸入m擷取文章目錄
C語言與C++程式設計
分享C/C++技術文章