在 Linux 上如何得到一個段錯誤的核心轉儲

發表於2018-07-14

本週工作中,我花了整整一週的時間來嘗試除錯一個段錯誤。我以前從來沒有這樣做過,我花了很長時間才弄清楚其中涉及的一些基本事情(獲得核心轉儲、找到導致段錯誤的行號)。於是便有了這篇部落格來解釋如何做那些事情!

在看完這篇部落格後,你應該知道如何從“哦,我的程式出現段錯誤,但我不知道正在發生什麼”到“我知道它出現段錯誤時的堆疊、行號了! ”。

什麼是段錯誤?

“段錯誤segmentation fault”是指你的程式嘗試訪問不允許訪問的記憶體地址的情況。這可能是由於:

  • 試圖解引用空指標(你不被允許訪問記憶體地址 0);
  • 試圖解引用其他一些不在你記憶體(LCTT 譯註:指不在合法的記憶體地址區間內)中的指標;
  • 一個已被破壞並且指向錯誤的地方的 C++ 虛表指標C++ vtable pointer,這導致程式嘗試執行沒有執行許可權的記憶體中的指令;
  • 其他一些我不明白的事情,比如我認為訪問未對齊的記憶體地址也可能會導致段錯誤(LCTT 譯註:在要求自然邊界對齊的體系結構,如 MIPS、ARM 中更容易因非對齊訪問產生段錯誤)。

這個“C++ 虛表指標”是我的程式發生段錯誤的情況。我可能會在未來的部落格中解釋這個,因為我最初並不知道任何關於 C++ 的知識,並且這種虛表查詢導致程式段錯誤的情況也是我所不瞭解的。

但是!這篇部落格後不是關於 C++ 問題的。讓我們談論的基本的東西,比如,我們如何得到一個核心轉儲?

步驟1:執行 valgrind

我發現找出為什麼我的程式出現段錯誤的最簡單的方式是使用 valgrind:我執行

這給了我一個故障時的堆疊呼叫序列。 簡潔!

但我想也希望做一個更深入調查,並找出些 valgrind 沒告訴我的資訊! 所以我想獲得一個核心轉儲並探索它。

如何獲得一個核心轉儲

核心轉儲core dump是您的程式記憶體的一個副本,並且當您試圖除錯您的有問題的程式哪裡出錯的時候它非常有用。

當您的程式出現段錯誤,Linux 的核心有時會把一個核心轉儲寫到磁碟。 當我最初試圖獲得一個核心轉儲時,我很長一段時間非常沮喪,因為 – Linux 沒有生成核心轉儲!我的核心轉儲在哪裡?

這就是我最終做的事情:

  1. 在啟動我的程式之前執行 ulimit -c unlimited
  2. 執行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

ulimit:設定核心轉儲的最大尺寸

ulimit -c 設定核心轉儲的最大尺寸。 它往往設定為 0,這意味著核心根本不會寫核心轉儲。 它以千位元組為單位。 ulimit 是按每個程式分別設定的 —— 你可以通過執行 cat /proc/PID/limit 看到一個程式的各種資源限制。

例如這些是我的系統上一個隨便一個 Firefox 程式的資源限制:

核心在決定寫入多大的核心轉儲檔案時使用軟限制soft limit(在這種情況下,max core file size = 0)。 您可以使用 shell 內建命令 ulimitulimit -c unlimited) 將軟限制增加到硬限制hard limit。

kernel.core_pattern:核心轉儲儲存在哪裡

kernel.core_pattern 是一個核心引數,或者叫 “sysctl 設定”,它控制 Linux 核心將核心轉儲檔案寫到磁碟的哪裡。

核心引數是一種設定您的系統全域性設定的方法。您可以通過執行 sysctl -a 得到一個包含每個核心引數的列表,或使用 sysctl kernel.core_pattern 來專門檢視 kernel.core_pattern 設定。

所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 將核心轉儲儲存到目錄 /tmp 下,並以 core 加上一系列能夠標識(出故障的)程式的引數構成的字尾為檔名。

如果你想知道這些形如 %e%p 的引數都表示什麼,請參考 man core

有一點很重要,kernel.core_pattern 是一個全域性設定 —— 修改它的時候最好小心一點,因為有可能其它系統功能依賴於把它被設定為一個特定的方式(才能正常工作)。

kernel.core_pattern 和 Ubuntu

預設情況下在 ubuntu 系統中,kernel.core_pattern 被設定為下面的值:

這引起了我的迷惑(這 apport 是幹什麼的,它對我的核心轉儲做了什麼?)。以下關於這個我瞭解到的:

  • Ubuntu 使用一種叫做 apport 的系統來報告 apt 包有關的崩潰資訊。
  • 設定 kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P 意味著核心轉儲將被通過管道送給 apport 程式。
  • apport 的日誌儲存在檔案 /var/log/apport.log 中。
  • apport 預設會忽略來自不屬於 Ubuntu 軟體包一部分的二進位制檔案的崩潰資訊

我最終只是跳過了 apport,並把 kernel.core_pattern 重新設定為 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因為我在一臺開發機上,我不在乎 apport 是否工作,我也不想嘗試讓 apport 把我的核心轉儲留在磁碟上。

現在你有了核心轉儲,接下來幹什麼?

好的,現在我們瞭解了 ulimitkernel.core_pattern ,並且實際上在磁碟的 /tmp 目錄中有了一個核心轉儲檔案。太好了!接下來幹什麼?我們仍然不知道該程式為什麼會出現段錯誤!

下一步將使用 gdb 開啟核心轉儲檔案並獲取堆疊呼叫序列。

從 gdb 中得到堆疊呼叫序列

你可以像這樣用 gdb 開啟一個核心轉儲檔案:

接下來,我們想知道程式崩潰時的堆疊是什麼樣的。在 gdb 提示符下執行 bt 會給你一個呼叫序列backtrace。在我的例子裡,gdb 沒有為二進位制檔案載入符號資訊,所以這些函式名就像 “??????”。幸運的是,(我們通過)載入符號修復了它。

下面是如何載入除錯符號。

這從二進位制檔案及其引用的任何共享庫中載入符號。一旦我這樣做了,當我執行 bt 時,gdb 給了我一個帶有行號的漂亮的堆疊跟蹤!

如果你想它能工作,二進位制檔案應該以帶有除錯符號資訊的方式被編譯。在試圖找出程式崩潰的原因時,堆疊跟蹤中的行號非常有幫助。:)

檢視每個執行緒的堆疊

通過以下方式在 gdb 中獲取每個執行緒的呼叫棧!

gdb + 核心轉儲 = 驚喜

如果你有一個帶除錯符號的核心轉儲以及 gdb,那太棒了!您可以上下檢視呼叫堆疊(LCTT 譯註:指跳進呼叫序列不同的函式中以便於檢視區域性變數),列印變數,並檢視記憶體來得知發生了什麼。這是最好的。

如果您仍然正在基於 gdb 嚮導來工作上,只列印出棧跟蹤與bt也可以。 :)

ASAN

另一種搞清楚您的段錯誤的方法是使用 AddressSanitizer 選項編譯程式(“ASAN”,即 $CC -fsanitize=address)然後執行它。 本文中我不準備討論那個,因為本文已經相當長了,並且在我的例子中開啟 ASAN 後段錯誤消失了,可能是因為 ASAN 使用了一個不同的記憶體分配器(系統記憶體分配器,而不是 tcmalloc)。

在未來如果我能讓 ASAN 工作,我可能會多寫點有關它的東西。(LCTT 譯註:這裡指使用 ASAN 也能復現段錯誤)

從一個核心轉儲得到一個堆疊跟蹤真的很親切!

這個部落格聽起來很多,當我做這些的時候很困惑,但說真的,從一個段錯誤的程式中獲得一個堆疊呼叫序列不需要那麼多步驟:

  1. 試試用 valgrind

如果那沒用,或者你想要拿到一個核心轉儲來調查:

  1. 確保二進位制檔案編譯時帶有除錯符號資訊;
  2. 正確的設定 ulimitkernel.core_pattern
  3. 執行程式;
  4. 一旦你用 gdb 除錯核心轉儲了,載入符號並執行 bt
  5. 嘗試找出發生了什麼!

我可以使用 gdb 弄清楚有個 C++ 的虛表條目指向一些被破壞的記憶體,這有點幫助,並且使我感覺好像更懂了 C++ 一點。也許有一天我們會更多地討論如何使用 gdb 來查詢問題!

 

相關文章