2009-08-26 41 views
13

我需要一個真正的C古魯的幫助來分析我的代碼崩潰。不是爲了修理墜機;我可以很容易地解決這個問題,但在此之前,我想了解這次崩潰甚至有可能發生,因爲這對我來說似乎完全不可能。如何在C中取消引用NULL指針不會導致程序崩潰?

這個崩潰只發生在一個客戶機上,我不能重現它在本地(所以我無法通過代碼使用調試器步驟),因爲我無法獲得該用戶的數據庫的副本。我的公司也不會允許我在代碼中修改幾行代碼,併爲此客戶定製構建(所以我不能添加一些printf行並讓他再次運行代碼),當然,客戶的構建沒有調試符號。換句話說,我的debbuging能力非常有限。儘管如此,我可以確定崩潰並獲得一些調試信息。然而,當我看到這些信息,然後在代碼中,我無法理解程序流如何能夠到達所討論的線路。在進入該行之前,代碼應該已經崩潰了很久。我完全迷失在這裏。

讓我們開始與相關的代碼。這是非常小的代碼:

// ... code above skipped, not relevant ... 

if (data == NULL) return -1; 

information = parseData(data); 

if (information == NULL) return -1; 

/* Check if name has been correctly \0 terminated */ 
if (information->kind.name->data[information->kind.name->length] != '\0') { 
    freeParsedData(information); 
    return -1; 
} 

/* Copy the name */ 
realLength = information->kind.name->length + 1; 
*result = malloc(realLength); 
if (*result == NULL) { 
    freeParsedData(information); 
    return -1; 
} 
strlcpy(*result, (char *)information->kind.name->data, realLength); 

// ... code below skipped, not relevant ... 

已經是這樣了。它崩潰了。我甚至可以告訴你在運行時調用真的。 strlcpy實際上被稱爲以下參數:

strlcpy (0x341000, 0x0, 0x1); 

瞭解這一點很明顯,爲什麼strlcpy崩潰。它試圖從NULL指針讀取一個字符,這當然會崩潰。而且由於最後一個參數的值爲1,原始長度必須爲0.我的代碼在這裏顯然有一個錯誤,它無法檢查名稱數據是否爲NULL。我可以解決這個問題,沒問題。

我的問題是:
這段代碼如何能夠首先到達strlcpy?
爲什麼這個代碼不能在if語句崩潰?

我嘗試了我的本地機器上:

int main (
    int argc, 
    char ** argv 
) { 
    char * nullString = malloc(10); 
    free(nullString); 
    nullString = NULL; 

    if (nullString[0] != '\0') { 
     printf("Not terminated\n"); 
     exit(1); 
    } 
    printf("Can get past the if-clause\n"); 

    char xxx[10]; 
    strlcpy(xxx, nullString, 1); 
    return 0; 
} 

此代碼永遠不會被通過的if語句。它在if語句中崩潰,這絕對是預期的。

因此,誰能想到的任何理由,第一個代碼可以獲得通過,如果語句沒有如果的名字 - >數據是真的NULL崩潰?這對我來說是完全神祕的。它似乎並不確定。

重要的額外信息:
兩種意見之間的代碼是真的完整,一切都沒有被排除在外。此外,該應用程序是單線程,所以沒有其他線程可能會意外改變背景中的任何記憶。發生這種情況的平臺是PPC CPU(一個G4,以防可能發揮任何作用)。如果有人想知道「kind」,這是因爲「information」包含一個名爲「kind」的「union」,name又是一個結構(kind是一個union,每個可能的union值都是一個不同類型的struct);但這一切都不應該在這裏真正重要。

我在這裏的任何想法感謝。如果這不僅僅是一個理論,我更感激,但是如果有辦法,我可以證實這個理論對於客戶是真的。

解決方案

我接受了正確的答案了,但以防萬一有人發現在谷歌這個問題,這裏到底發生了什麼:

指針均指向記憶,已經被釋放。釋放內存不會使其全部爲零或導致進程一次性將其返回給系統。所以即使內存被錯誤地釋放,它也包含了正確的值。在執行「如果檢查」時,問題指針不爲NULL。

之後檢查我分配一些新的內存,調用malloc。不確定malloc究竟在做什麼,但每次調用malloc或free都會對進程的虛擬地址空間的所有動態內存產生深遠影響。在malloc調用之後,指針實際上是NULL。不知何故,malloc(或某些系統調用malloc使用)將已釋放的指針本身所在的內存(不是指向它的數據,指針本身位於動態內存中)置零。對內存進行調零,指針現在的值爲0x0,在我的系統上等於NULL,當調用strlcpy時,它當然會崩潰。

所以,真正的錯誤造成這種奇怪的行爲是在我的代碼完全不同的位置。永遠不要忘記:釋放記憶保持它的價值,但它超出你的控制能力多長時間。要檢查您的應用程序是否存在訪問已釋放內存的內存錯誤,請確保已釋放內存在釋放之前始終爲零。在OS X中,您可以通過在運行時設置環境變量來完成此操作(無需重新編譯任何內容)。當然,這會讓程序變慢,但是你會很早就發現這些錯誤。

+0

你可以問你的客戶核心轉儲並在調試器中調查它 – qrdl 2009-08-26 14:27:41

+0

@qrdl:我有一個進程的崩潰日誌。這是Mac OS X,崩潰過程總是會創建這樣的崩潰日誌。我有堆棧回溯,這就是爲什麼我知道它崩潰的原因,並且我在崩潰時在所有寄存器中都有值;以及知道這個崩潰是由訪問內存位置0x0(NULL指針)引起的。在這樣的日誌中沒有其他有用的信息。 – Mecki 2009-08-26 14:35:24

+0

請說明你聲明'結果'的地方以及你如何分配它指向的內存?您顯示了'* result'的設置位置,但未顯示'result'的分配位置。 – NVRAM 2009-08-26 14:57:56

回答

11

該結構可能位於內存中,該內存已被free()'d或堆損壞。在這種情況下,malloc()可能會修改內存,認爲它是免費的。

你可以嘗試在內存檢查器下運行你的程序。一個支持Mac OS X的內存檢查器是valgrind,雖然它只支持Intel,而不支持PowerPC。

+0

哇,輝煌的回覆!甚至沒有想過那個。是的,實際上,數據指針可能指向if語句中的某些有用的內容,但本身可能位於已經free'd(尚未重用)的內存中。調用malloc可能會導致對空閒內存的更改,所以現在數據指針突然指向NULL。這絕對有可能! – Mecki 2009-08-26 14:48:57

+0

到目前爲止,這個回覆看起來最有希望;這件事很簡單,但我從來沒有想過這種可能性。當然,如果我不小心釋放了它,malloc可能會更改我的代碼中使用的任何內存。我可以與客戶進行測試。您可以強制OS X加載一個可選的malloc實現(通過設置一個環境變量),以確保立即訪問free'd內存崩潰(用於調試目的)。讓我們看看這將告訴我們什麼:-) – Mecki 2009-08-26 14:58:44

+0

您,先生,是一個天才:-)我沒有考慮過調用malloc可以做的事情,比如更改進程的虛擬機映射,零頁等等。我現在可以在本地重現此問題,實際上,當我跳過該malloc調用時,它不會崩潰。所以接受回覆,恭喜! – Mecki 2009-08-27 17:20:51

3

您可能遇到堆棧損壞。您所引用的代碼行可能根本沒有被執行。

+0

嗯......這是一種可能性。很難檢查,而不能通過代碼。讓事情更復雜一點的是,包含代碼的整個函數被內聯到另一個函數中,而不是因爲我使用inline屬性,而是因爲GCC認爲在使用優化標記時這樣做是個好主意。 – Mecki 2009-08-26 14:20:06

5

取消引用空指針的作用是通過標準,據我所知不確定的。

根據C標準6.5.3.2/4:

如果無效值已被分配給指針時,目*運算符的行爲是理解過程定義網絡。

所以有可能是崩潰或可能不會。

+0

這隻適用於C作爲一種語言,還是這對於UNIX來說也適用於根據POSIX標準?由於在解引用NULL指針時,您在UNIX中看到的最常見行爲是應用程序獲取了通常終止它的信號。至少這就是我期望的開發人員。 – Mecki 2009-08-26 14:18:38

+0

是的,這適用於C作爲一種語言。未定義意味着任何事情都可能發生。根據或期望發生任何特定的事情是一個壞主意 - 因爲它可能無法達到你期望的效果,即使是在同一平臺上運行同一個程序。 – 2009-08-26 14:20:47

+1

在客戶的情況下,最常見的行爲並不意味着什麼。這就是爲什麼你必須堅持標準而不是實現特定的怪癖。 – 2009-08-26 14:28:06

1

取消對NULL指針的行爲是由標準未定義的。它不保證會崩潰,而且除非您真正嘗試寫入內存,否則往往不會崩潰。

+0

也想到了這一點,但第一次strlcpy也不會嘗試寫入NULL指針(它只是讀取它,我有它的正確來源),其次是在OS X中,其中NULL指針只是一個指向進程的第一個內存頁面的指針,並且此頁面既沒有讀取權限,也沒有寫入權限,所以當您嘗試從中讀取時,程序也會獲得一個SIGBUS信號。 – Mecki 2009-08-26 14:17:04

0

哇,這很奇怪。有一件事看起來稍微可疑的我,雖然它可能不利於:如果信息和數據是好的指針(非空)

會發生什麼,但information.kind.name爲空。你不要直接引用這個指針直到strlcpy行,所以如果它是空的,它可能不會崩潰,直到那時。當然,早於您將數據解引用[1]將其設置爲\ 0,它應該也會崩潰,但由於僥倖,您的程序可能恰好具有對0x01而不是0x00的寫入訪問權限。

而且,我看到你在另一個使用信息 - > name.length在同一個地方,但信息 - > kind.name.length,如果那是一個錯字或如果那是希望的不知道。

+0

對不起,這曾經是一個錯字。修復它。我將名稱稍微改爲更明顯的名稱(否則這個小代碼片段是不可讀的):-) – Mecki 2009-08-26 14:24:59

+0

並且關於您的答案:我在strlcpy之前對其進行了解引用。 x [0]與(* x)相同,x [1]與*(x + 1)相同,依此類推。每個數組訪問,讀取或寫入訪問,取消引用指針。但是,我不寫信給它,但我只嘗試讀取它(但是strlcpy也只是試圖讀取它;我在這裏有正確的libC代碼,並驗證了這一點)。 – Mecki 2009-08-26 14:30:46

13

首先,解引用空指針是未定義的行爲。它可以崩潰,不會崩潰,或將您的牆紙設置爲海綿寶寶圖片。

這就是說,解引用空指針通常會導致崩潰。所以你的問題可能與內存腐敗有關,例如從寫作過去你的一個字符串的末尾。這可能會導致延遲效果崩潰。我特別懷疑,因爲除非您的程序與可用虛擬內存相抵觸,否則malloc(1)將會失敗,您可能會注意到這種情況。

編輯:OP指出,它不是結果是空的,但information->kind.name->data。這是一個潛在的問題,然後:

沒有檢查information->kind.name->data是否爲空。上唯一的檢查是

if (information->kind.name->data[information->kind.name->length] != '\0') { 

讓我們假設information->kind.name->data是空的,但信息 - > kind.name->長度,也就是說,100。然後這種說法是等價於:

if (*(information->kind.name->data + 100) != '\0') { 

它不解引用NULL,而是取消引用地址100.如果這沒有崩潰,並且地址100碰巧包含0,那麼這個測試會通過。

+0

當我的操作系統將我的壁紙設置爲海綿寶寶圖像時,如果我遵循NULL指針,我會將其返回:-P BTW malloc不會失敗,*結果不爲NULL,它在進程地址空間中爲0x341000。這是名稱 - >數據是空的,這是好的,因爲我解析的數據可能不包含名稱...它應該有一個名稱根據客戶,並且可能在解析函數中有一個錯誤另一個問題,我猜。 – Mecki 2009-08-26 14:28:33

+0

感謝您的澄清,請參閱編輯。 – 2009-08-26 14:38:14

+2

但是,正如OP指出的,information-> kind.name-> length必須爲零,因爲他知道realLength是1. – 2009-08-26 14:53:26

2

我的理論是,information->kind.name->length是一個非常大的值,所以information->kind.name->data[information->kind.name->length]實際上是指有效的內存地址。

+0

另一個想法出現在我的腦海。如果長度大於4095,我會在第一個內存頁面之後訪問內存(NULL保護頁面沒有讀/寫權限,通常會導致訪問信號),這確實不會崩潰。與此相反的是,在將長度值加1後,realLength爲1,因此長度值之前必須爲零。 – Mecki 2009-08-26 14:33:14

0

儘管解除引用空指針會導致未定義的行爲,而不一定導致崩潰,但您應該檢查information->kind.name->data的值而不是information->kind.name->data[1]的內容。

1

作爲一個供參考,當我看到這條線:

if (information->kind.name->data[information->kind.name->length] != '\0') { 

我看到最多三個不同指針引用:

  1. 信息
  2. 數據(如果它是指針而不是固定陣列)

您檢查非空的信息,但不是名稱而不是數據。是什麼讓你確信他們是正確的?

我也在這裏迴應其他一些關於別的可能損害你的堆的情緒。如果您在Windows上運行,請考慮使用gflags來執行諸如頁面分配之類的操作,這可用於檢測您或其他人是否正在寫入超過緩衝區末尾並踩到您的堆上。

看到你在Mac上 - 忽略gflags評論 - 它可能會幫助其他人閱讀此內容。如果你運行的是早於OS X的東西,那麼有很多方便的Macsbugs工具來強調堆(比如堆爭奪命令'hs')。

+0

是的,你是絕對正確的,沒有一個被檢查。並回答你的問題:我不知道他們是否正確。他們可能都指向無處。但我知道的是,strlcpy的第二個參數是NULL,因此數據指向NULL,程序中沒有任何東西可以改變它,所以它在if語句中一定是NULL ...但是我解除了它的引用並且沒有出錯(數據[0]與(*數據)相同,所以它意味着解除它,不是嗎?)。 – Mecki 2009-08-26 14:44:22

0
char * p = NULL; 

P [i]爲像

p += i; 

這是一個有效的操作,即使是空指針。它然後指向內存位置0x0000 [...]我

+0

是的,但是「p [i] == 0」就像「*(p + i)== 0」,如果p爲NULL,這是不可能的。好吧,不太正確,如果我在大多數系統上的頁面大小比較大(因爲你沒有在大多數操作系統上訪問NULL保護,因此不會導致崩潰),但在我的情況下,realLength在添加後爲1 1到長度,因此長度必須在0之前。如果我是零以上,我們有「*(p + 0)== 0」,這將崩潰,如果p爲NULL,試試看。如果p爲NULL,那麼「if(p [0] == 0)」會崩潰,請嘗試。 – Mecki 2009-08-26 14:54:13

+0

你檢查了NULL是什麼? NULL通常是C中的#define。它也可以是0xFFFFFFFFF或某些任意值。 :) – StampedeXV 2009-08-26 15:34:04

+0

在Mac OS X上NULL確實定義爲「(void *)0」 - 但你說得對,C沒有說它必須是NULL,它可以是任何值。 NULL只是NULL,你不應該在意開發者的意思。 – Mecki 2009-08-27 17:47:07

1

我感興趣的char *演員在調用strlcpy。

類型數據*的大小是否可能與系統上的char *大小不同?如果字符指針較小,則可以獲得可能爲NULL的數據指針的子集。

實施例:

int a = 0xffff0000; 
short b = (short) a; //b could be 0 if lower bits are used 

編輯:拼寫錯誤糾正。

+0

也想到了這個演員陣容......理論上這可能導致了這個問題,但實際上我發現了這個問題(請參閱問題的更新),那不是它 - 在我的系統中,指針的大小都是相同的。儘管如此,你還是會得到讚賞,因爲你是第一個認爲演員是所有這些的可能原因的演員;如果一個系統對於不同的數據類型有不同的指針大小(我認爲這是完全有效的),這可能會導致這樣的問題。 – Mecki 2009-08-27 17:41:43

1

這裏有一個特殊的方式,你可以得到過去的「數據」指針被空在

if (information->kind.name->data[information->kind.name->length] != '\0') { 

說信息 - > kind.name->長度較大。至少大於 4096,在具有特定編譯器的特定平臺上(也就是說,大多數* nixes具有庫存gcc編譯器),代碼將導致內存讀取「kind.name-> data + information-> kind的地址」。名稱 - >長度]

在較低的級別,讀取的內容是「讀取內存地址(0 + 8653)」(或任何長度) * * nixes標記第一頁地址空間爲「不可訪問」,這意味着取消引用讀取內存地址0至4096的NULL指針將導致硬件陷阱傳播到應用程序並使其崩潰。到有效的映射內存中,例如共享庫或其他什麼東西在那裏被映射 - 並且內存訪問不會失敗。沒關係。解引用NULL指針是未定義的行爲,沒有要求它失敗。

+0

是的,這是完全正確的(你知道你的UNIX VM,不是嗎?;-));如果長度是4096或更大,它確實不會崩潰。唯一的問題是,我們可以肯定長度爲0,否則在發生崩潰時realLength不是1(發生崩潰時實際長度爲1)。不過,好主意。你得到一個指出這一點upvote。 – Mecki 2009-08-27 17:45:56

0

您應經常檢查信息 - > kind.name->數據是否爲空,無論如何,但在這種情況下

if (*result == NULL) 
    freeParsedData(information); 
    return -1; 
} 

你已經錯過

一個{

它應該是

if (*result == NULL) 
{ 
    freeParsedData(information); 
    return -1; 
} 

這是此編碼風格的一個很好的原因,而不是

if (*result == NULL) { 
    freeParsedData(information); 
    return -1; 
} 

在這裏你可能不會發現丟失的大括號,因爲您已經習慣了代碼塊的形狀而沒有使用大括號將它與if子句分開。

+0

對不起,我錯過了那個,它在真正的源代碼中。 – Mecki 2009-08-27 17:38:10

+0

太棒了,但無論如何,我強烈推薦這種編碼風格,因爲它將水平方向的起始和結束括號對齊,使查找對應的花括號更容易,並且通常更容易看到代碼結構。 您可以看到,在使用C編寫動態內存時,最基本的工作之一是跟蹤分配和釋放內存,並確保您已覆蓋每個案例。爲了將指針本身用作指示未分配內存的標誌,在空閒後始終將指針設置爲NULL是標準和明智的。 – 2009-08-28 10:34:29

1

在上次if語句後缺少'{'意味着在上面跳過的「// ...代碼跳過,不相關...」部分控制着對整個代碼段的訪問。在所有粘貼的代碼中只有strlcpy被執行。解決方案:不要使用沒有大括號的if語句來闡明控制。

考慮這個...

if(false) 
{ 
    if(something == stuff) 
    { 
     doStuff(); 

    .. snip .. 

    if(monkey == blah) 
     some->garbage= nothing; 
     return -1; 
    } 
} 
crash(); 

只有 「撞車();」得到執行。

+0

當然,編譯器會捕獲不匹配的{},但你永遠不會知道......;) – Pod 2009-08-26 16:13:16

+0

是的,但那是因爲我沒有直接複製代碼,而是輸入它並忘記添加它。它在真正的源文件中:-P我只是這麼做的,因爲真實源文件中的名稱非常神祕(不要說難看),並且使用更明顯的名稱使得它更易於閱讀。 – Mecki 2009-08-27 17:37:38

0

* result = malloc(realLength); // ???

新分配的內存段的地址存儲在變量「result」中包含的地址所引用的位置。

這是意圖嗎?如果是這樣,則可能需要修改。

+0

是的,是打算。結果是函數的返回值。真正的返回值是int,只能表示成功或失敗(以及失敗的種類),但在成功的情況下也必須返回。發生這種情況的調用者有一個「char * xxx;」變量並用「(...,&xxx,...)」調用函數,所以在成功返回時,xxx現在指向分配的內存。 – Mecki 2009-08-27 17:36:29

1

我會在valgrind下運行你的程序。您已經知道NULL指針有問題,因此需要對該代碼進行配置。

valgrind存在於這裏的好處是,它檢查每一個指針引用,並檢查該內存位置是否先前已被聲明過,它會告訴你行號,結構以及其他你關心的事情記憶。

像其他人提到的那樣,引用0內存位置是一個「que sera,sera」的東西。

我的C色彩蜘蛛俠感覺告訴我,你應該擺脫這些結構走在

if (information->kind.name->data[information->kind.name->length] != '\0') { 

線像

if (information == NULL) { 
     return -1; 
    } 
    if (information->kind == NULL) { 
     return -1; 
    } 

等。

+0

你是對的;在使用它之前檢查每個指向NULL的指針是正確的做法。好的,在這種情況下,它沒有解決任何問題(請參閱接受的解決方案和我對問題的更新),但即使我找到了所有這些原因,我仍將代碼完全更改爲您的示例中的內容:-) – Mecki 2009-08-27 17:34:54

-1

根據我的理解,這個問題的特殊情況是無效訪問,導致嘗試讀取或寫入,使用空指針。這裏的問題的檢測是非常依賴於硬件的。在某些平臺上,使用NULL指針訪問內存以進行讀取或寫入操作將導致異常。