尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
在看完這篇博客後,你應該知道如何從「哦,我的程序出現段錯誤,但我不知道正在發生什麼」到「我知道它出現段錯誤時的堆棧、行號了! 」。
— Julia Evans
致謝
編譯自 |
https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/
作者 | Julia Evans
譯者 | Stephen (stephenxs) ? ? 共計翻譯:2篇 貢獻時間:26 天
本周工作中,我花了整整一周的時間來嘗試調試一個段錯誤。我以前從來沒有這樣做過,我花了很長時間才弄清楚其中涉及的一些基本事情(獲得核心轉儲、找到導致段錯誤的行號)。於是便有了這篇博客來解釋如何做那些事情!
在看完這篇博客後,你應該知道如何從「哦,我的程序出現段錯誤,但我不知道正在發生什麼」到「我知道它出現段錯誤時的堆棧、行號了! 」。
什麼是段錯誤?
「段錯誤segmentation fault」是指你的程序嘗試訪問不允許訪問的記憶體地址的情況。這可能是由於:
◈ 試圖解引用空指針(你不被允許訪問記憶體地址 0);
◈ 試圖解引用其他一些不在你記憶體(LCTT 譯註:指不在合法的記憶體地址區間內)中的指針;
◈ 一個已被破壞並且指向錯誤的地方的 C++ 虛表指針C++ vtable pointer,這導致程序嘗試執行沒有執行權限的記憶體中的指令;
◈ 其他一些我不明白的事情,比如我認為訪問未對齊的記憶體地址也可能會導致段錯誤(LCTT 譯註:在要求自然邊界對齊的體系結構,如 MIPS、ARM 中更容易因非對齊訪問產生段錯誤)。
這個「C++ 虛表指針」是我的程序發生段錯誤的情況。我可能會在未來的博客中解釋這個,因為我最初並不知道任何關於 C++ 的知識,並且這種虛表查找導致程序段錯誤的情況也是我所不了解的。
但是!這篇博客後不是關於 C++ 問題的。讓我們談論的基本的東西,比如,我們如何得到一個核心轉儲?
步驟1:運行 valgrind
我發現找出為什麼我的程序出現段錯誤的最簡單的方式是使用 valgrind:我運行
- valgrind -v your-program
這給了我一個故障時的堆棧調用序列。 簡潔!
但我想也希望做一個更深入調查,並找出些 valgrind 沒告訴我的信息! 所以我想獲得一個核心轉儲並探索它。
如何獲得一個核心轉儲
核心轉儲core dump是您的程序記憶體的一個副本,並且當您試圖調試您的有問題的程序哪里出錯的時候它非常有用。
當您的程序出現段錯誤,Linux 的內核有時會把一個核心轉儲寫到磁盤。 當我最初試圖獲得一個核心轉儲時,我很長一段時間非常沮喪,因為 – Linux 沒有生成核心轉儲!我的核心轉儲在哪里?
這就是我最終做的事情:
☉ 在啟動我的程序之前運行 ulimit -c unlimited
☉ 運行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t
ulimit:設置核心轉儲的最大尺寸
ulimit -c 設置核心轉儲的最大尺寸。 它往往設置為 0,這意味著內核根本不會寫核心轉儲。 它以千字節為單位。 ulimit 是按每個進程分別設置的 —— 你可以通過運行 cat /proc/PID/limit 看到一個進程的各種資源限制。
例如這些是我的系統上一個隨便一個 Firefox 進程的資源限制:
- $ cat /proc/6309/limits
- Limit Soft Limit Hard Limit Units
- Max cpu time unlimited unlimited seconds
- Max file size unlimited unlimited bytes
- Max data size unlimited unlimited bytes
- Max stack size 8388608 unlimited bytes
- Max core file size 0 unlimited bytes
- Max resident set unlimited unlimited bytes
- Max processes 30571 30571 processes
- Max open files 1024 1048576 files
- Max locked memory 65536 65536 bytes
- Max address space unlimited unlimited bytes
- Max file locks unlimited unlimited locks
- Max pending signals 30571 30571 signals
- Max msgqueue size 819200 819200 bytes
- Max nice priority 0 0
- Max realtime priority 0 0
- Max realtime timeout unlimited unlimited us
內核在決定寫入多大的核心轉儲文件時使用軟限制soft limit(在這種情況下,max core file size = 0)。 您可以使用 shell 內置命令 ulimit(ulimit -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[1]。
有一點很重要,kernel.core_pattern 是一個全局設置 —— 修改它的時候最好小心一點,因為有可能其它系統功能依賴於把它被設置為一個特定的方式(才能正常工作)。
kernel.core_pattern 和 Ubuntu
默認情況下在 ubuntu 系統中,kernel.core_pattern 被設置為下面的值:
- $ sysctl kernel.core_pattern
- kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P
這引起了我的迷惑(這 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 把我的核心轉儲留在磁盤上。
現在你有了核心轉儲,接下來幹什麼?
好的,現在我們了解了 ulimit 和 kernel.core_pattern ,並且實際上在磁盤的 /tmp 目錄中有了一個核心轉儲文件。太好了!接下來幹什麼?我們仍然不知道該程序為什麼會出現段錯誤!
下一步將使用 gdb 打開核心轉儲文件並獲取堆棧調用序列。
從 gdb 中得到堆棧調用序列
你可以像這樣用 gdb 打開一個核心轉儲文件:
- $ gdb -c my_core_file
接下來,我們想知道程序崩潰時的堆棧是什麼樣的。在 gdb 提示符下運行 bt 會給你一個調用序列backtrace。在我的例子里,gdb 沒有為二進制文件加載符號信息,所以這些函數名就像 「??????」。幸運的是,(我們通過)加載符號修復了它。
下面是如何加載調試符號。
- symbol-file /path/to/my/binary
- sharedlibrary
這從二進制文件及其引用的任何共享庫中加載符號。一旦我這樣做了,當我執行 bt 時,gdb 給了我一個帶有行號的漂亮的堆棧跟蹤!
如果你想它能工作,二進制文件應該以帶有調試符號信息的方式被編譯。在試圖找出程序崩潰的原因時,堆棧跟蹤中的行號非常有幫助。:)
查看每個線程的堆棧
通過以下方式在 gdb 中獲取每個線程的調用棧!
- thread apply all bt full
gdb + 核心轉儲 = 驚喜
如果你有一個帶調試符號的核心轉儲以及 gdb,那太棒了!您可以上下查看調用堆棧(LCTT 譯註:指跳進調用序列不同的函數中以便於查看局部變量),列印變量,並查看記憶體來得知發生了什麼。這是最好的。
如果您仍然正在基於 gdb 嚮導來工作上,只列印出棧跟蹤與bt也可以。 ?
ASAN
另一種搞清楚您的段錯誤的方法是使用 AddressSanitizer 選項編譯程序(「ASAN」,即 $CC -fsanitize=address)然後運行它。 本文中我不準備討論那個,因為本文已經相當長了,並且在我的例子中打開 ASAN 後段錯誤消失了,可能是因為 ASAN 使用了一個不同的記憶體分配器(系統記憶體分配器,而不是 tcmalloc)。
在未來如果我能讓 ASAN 工作,我可能會多寫點有關它的東西。(LCTT 譯註:這里指使用 ASAN 也能復現段錯誤)
從一個核心轉儲得到一個堆棧跟蹤真的很親切!
這個博客聽起來很多,當我做這些的時候很困惑,但說真的,從一個段錯誤的程序中獲得一個堆棧調用序列不需要那麼多步驟:
☉ 試試用 valgrind
如果那沒用,或者你想要拿到一個核心轉儲來調查:
☉ 確保二進制文件編譯時帶有調試符號信息;
☉ 正確的設置 ulimit 和 kernel.core_pattern;
☉ 運行程序;
☉ 一旦你用 gdb 調試核心轉儲了,加載符號並運行 bt;
☉ 嘗試找出發生了什麼!
我可以使用 gdb 弄清楚有個 C++ 的虛表條目指向一些被破壞的記憶體,這有點幫助,並且使我感覺好像更懂了 C++ 一點。也許有一天我們會更多地討論如何使用 gdb 來查找問題!
via: https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/
作者:Julia Evans[3]譯者:stephenxs校對:wxy
本文由 LCTT原創編譯,Linux中國