1. 程式人生 > >一個小bug:calloc中出現的segment fault

一個小bug:calloc中出現的segment fault

其實也就雞毛蒜皮的小事,本來不想記錄在部落格上的,不過這個bug背後隱藏的東西確實比較有記錄的價值,如果說解bug就像是解初高中數學題,那麼有的bug就像一道出得很漂亮的題,短小精幹但背後隱藏的資訊量卻很大,一下子就讓你記住了背後的那些定理概念。

事情是這樣的,segment fault,程式被謀殺,現場在libc的calloc裡。發生在libc中的segment fault其實不少見,碰得最多的怕就是memcpy,一般都是過大的資料拷貝導致程式的stack corruption,這種情況通過檢查程式的backtrace可以看出(被破壞的棧很容易被發現)。但這次的棧呼叫樹卻很完整;想想calloc要幹什麼事,分配由size指定的記憶體然後填0而已,於是檢查calloc的唯一的外部引數size,不大的一個數,可以說是再平凡不過的一次呼叫,話說回來,就算是詭異的size出現,也應該只會導致calloc失敗而已(比如請求的size太大),也不該是segment fault這種‘不可捕捉’的執行期錯誤。每次遇到這種程式的生命停留在系統級library的情況的時候,一定要抑制自己懷疑‘是不是xx庫的bug’之類的衝動,畢竟這是每天都會被億萬人使用的libc中的calloc,讓我發現它的bug估計和中頭彩的概率差不多。真正的凶手肯定在其他地方。

怎麼解決的過程就不提了,主要是不值得提。總之感謝google大神。

出錯程式大概是這樣一個模式,看過或寫過一些driver的人應該熟悉這種用法,即將data load直接分配在用來管理data load的struct後面:

進入run函式後,第一個函式func1()的執行沒有問題,第二個函式func2()就會死在其中的calloc呼叫裡。

calloc/malloc等的原理其實是通過核心的brk系統服務申請虛擬記憶體,申請的單位以4k/頁為粒度。然後自己再維護申請回來的virtual memory,畢竟不是所有程式都會每次都向calloc/malloc請求大於一個頁的memory的,所以calloc/malloc通過核心申請的virtual memory總是會比使用者需要的更多(除了brk是以頁來滿足calloc/malloc請求這個原因以外,calloc/malloc也需要將使用者申請的記憶體對齊或多申請一些空間做管理用的meta data),然後分成block的形式,再按需分配給需要的應用程式。

跑第一個函式func1()的時候其實已經發生‘記憶體訪問越界’了,之所以在那裡沒有發生‘命案’的原因就是如前所述:calloc真正分配的虛擬記憶體是比使用者請求的大的。如果像func1()中那樣在後面多寫了5個位元組是不會導致mmu的頁異常的。也就是說,在上面那個程式裡,這多寫的5個位元組除了程式設計師自己小心以外,編譯期和執行期都是不能幫助你發現它們的,這種錯誤其實最好是讓編譯器幫忙識別,但像上面那麼做編譯器是沒法發現的,這種允許程式設計師隨心所欲操作memory的做法正是c語言這種貼近彙編和硬體的‘高階’語言的強大之處,當然,對於經驗不夠豐富的人來說,這也為可能出現的各種segment fault埋下了禍根。

那為什麼第二個函式func2()的calloc卻遭‘報應’了呢?前面提到calloc/malloc以block的形式來管理已經分配的虛擬記憶體,這些blocks被劃分為‘allocated’和‘free’兩種狀態,對使用者的分配請求,所要做的自然就是找到一個能滿足大小的free的block,對釋放請求也是將對應的'allocate'的block和鄰近的'free'的block合併。對這些blocks的管理自然少不了一些維護它們的‘元資料’本身(如‘下一個free block的地址’等),為了更有效的處理,這些元資料本身也和block放在了一起,比如放在每個block的頭或尾。說到這裡,真相就很明白了。為了方便說明,見下圖:

假設func1()中呼叫calloc分配到的block就是Allocate(1),後面的P表示meta data,比如裡面有指向最近的free塊‘Free(1)’的指標。那麼,當func1()中的越界訪問發生後,那多寫的5個0就把P的指標破壞掉了!這個破壞當然在func1()中不會導致出錯,但是,當到func2()中用calloc分配記憶體時,calloc試圖從Allocated(1)塊後面的P找到最近的free block時,這個指標已經被corruption了,這個track操作本身導致了calloc中的segment fault。

c語言就是這樣,你用得到,那麼它威力無窮;你用不好,它隨時會製造隱藏在你程式裡的定時炸彈。