本周工作中,我花了整整一周的時(shí)間來(lái)嘗試調(diào)試一個(gè)段錯(cuò)誤。我以前從來(lái)沒(méi)有這樣做過(guò),我花了很長(zhǎng)時(shí)間才弄清楚其中涉及的一些基本事情(獲得核心轉(zhuǎn)儲(chǔ)、找到導(dǎo)致段錯(cuò)誤的行號(hào))。于是便有了這篇博客來(lái)解釋如何做那些事情!
在看完這篇博客后,你應(yīng)該知道如何從“哦,我的程序出現(xiàn)段錯(cuò)誤,但我不知道正在發(fā)生什么”到“我知道它出現(xiàn)段錯(cuò)誤時(shí)的堆棧、行號(hào)了! ”。
什么是段錯(cuò)誤?
“段錯(cuò)誤segmentation fault”是指你的程序嘗試訪問(wèn)不允許訪問(wèn)的內(nèi)存地址的情況。這可能是由于:
試圖解引用空指針(你不被允許訪問(wèn)內(nèi)存地址0);
試圖解引用其他一些不在你內(nèi)存(LCTT 譯注:指不在合法的內(nèi)存地址區(qū)間內(nèi))中的指針;
一個(gè)已被破壞并且指向錯(cuò)誤的地方的 C++ 虛表指針C++ vtable pointer,這導(dǎo)致程序嘗試執(zhí)行沒(méi)有執(zhí)行權(quán)限的內(nèi)存中的指令;
其他一些我不明白的事情,比如我認(rèn)為訪問(wèn)未對(duì)齊的內(nèi)存地址也可能會(huì)導(dǎo)致段錯(cuò)誤(LCTT 譯注:在要求自然邊界對(duì)齊的體系結(jié)構(gòu),如 MIPS、ARM 中更容易因非對(duì)齊訪問(wèn)產(chǎn)生段錯(cuò)誤)。
這個(gè)“C++ 虛表指針”是我的程序發(fā)生段錯(cuò)誤的情況。我可能會(huì)在未來(lái)的博客中解釋這個(gè),因?yàn)槲易畛醪⒉恢廊魏侮P(guān)于 C++ 的知識(shí),并且這種虛表查找導(dǎo)致程序段錯(cuò)誤的情況也是我所不了解的。
但是!這篇博客后不是關(guān)于 C++ 問(wèn)題的。讓我們談?wù)摰幕镜臇|西,比如,我們?nèi)绾蔚玫揭粋€(gè)核心轉(zhuǎn)儲(chǔ)?
運(yùn)行 valgrind
我發(fā)現(xiàn)找出為什么我的程序出現(xiàn)段錯(cuò)誤的最簡(jiǎn)單的方式是使用valgrind:我運(yùn)行
valgrind -vyour-program
這給了我一個(gè)故障時(shí)的堆棧調(diào)用序列。 簡(jiǎn)潔!
但我想也希望做一個(gè)更深入調(diào)查,并找出些valgrind沒(méi)告訴我的信息! 所以我想獲得一個(gè)核心轉(zhuǎn)儲(chǔ)并探索它。
如何獲得一個(gè)核心轉(zhuǎn)儲(chǔ)
核心轉(zhuǎn)儲(chǔ)core dump是您的程序內(nèi)存的一個(gè)副本,并且當(dāng)您試圖調(diào)試您的有問(wèn)題的程序哪里出錯(cuò)的時(shí)候它非常有用。
當(dāng)您的程序出現(xiàn)段錯(cuò)誤,Linux 的內(nèi)核有時(shí)會(huì)把一個(gè)核心轉(zhuǎn)儲(chǔ)寫到磁盤。 當(dāng)我最初試圖獲得一個(gè)核心轉(zhuǎn)儲(chǔ)時(shí),我很長(zhǎng)一段時(shí)間非常沮喪,因?yàn)?– Linux 沒(méi)有生成核心轉(zhuǎn)儲(chǔ)!我的核心轉(zhuǎn)儲(chǔ)在哪里?
這就是我最終做的事情:
在啟動(dòng)我的程序之前運(yùn)行ulimit -c unlimited
運(yùn)行sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t
ulimit:設(shè)置核心轉(zhuǎn)儲(chǔ)的最大尺寸
ulimit -c設(shè)置核心轉(zhuǎn)儲(chǔ)的最大尺寸。 它往往設(shè)置為 0,這意味著內(nèi)核根本不會(huì)寫核心轉(zhuǎn)儲(chǔ)。 它以千字節(jié)為單位。ulimit是按每個(gè)進(jìn)程分別設(shè)置的 —— 你可以通過(guò)運(yùn)行cat /proc/PID/limit看到一個(gè)進(jìn)程的各種資源限制。
例如這些是我的系統(tǒng)上一個(gè)隨便一個(gè) Firefox 進(jìn)程的資源限制:
內(nèi)核在決定寫入多大的核心轉(zhuǎn)儲(chǔ)文件時(shí)使用軟限制soft limit(在這種情況下,max core file size = 0)。 您可以使用 shell 內(nèi)置命令 ulimit(ulimit -c unlimited) 將軟限制增加到硬限制hard limit。
kernel.core_pattern:核心轉(zhuǎn)儲(chǔ)保存在哪里
kernel.core_pattern 是一個(gè)內(nèi)核參數(shù),或者叫 “sysctl 設(shè)置”,它控制 Linux 內(nèi)核將核心轉(zhuǎn)儲(chǔ)文件寫到磁盤的哪里。
內(nèi)核參數(shù)是一種設(shè)定您的系統(tǒng)全局設(shè)置的方法。您可以通過(guò)運(yùn)行 sysctl -a 得到一個(gè)包含每個(gè)內(nèi)核參數(shù)的列表,或使用 sysctl kernel.core_pattern 來(lái)專門查看 kernel.core_pattern 設(shè)置。
所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 將核心轉(zhuǎn)儲(chǔ)保存到目錄 /tmp 下,并以 core 加上一系列能夠標(biāo)識(shí)(出故障的)進(jìn)程的參數(shù)構(gòu)成的后綴為文件名。
如果你想知道這些形如 %e、%p 的參數(shù)都表示什么,請(qǐng)參考 man core。
有一點(diǎn)很重要,kernel.core_pattern 是一個(gè)全局設(shè)置 —— 修改它的時(shí)候最好小心一點(diǎn),因?yàn)橛锌赡芷渌到y(tǒng)功能依賴于把它被設(shè)置為一個(gè)特定的方式(才能正常工作)。
kernel.core_pattern 和 Ubuntu
默認(rèn)情況下在 ubuntu 系統(tǒng)中,kernel.core_pattern 被設(shè)置為下面的值:
$sysctl kernel.core_pattern
kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P
這引起了我的迷惑(這 apport 是干什么的,它對(duì)我的核心轉(zhuǎn)儲(chǔ)做了什么?)。以下關(guān)于這個(gè)我了解到的:
Ubuntu 使用一種叫做 apport 的系統(tǒng)來(lái)報(bào)告 apt 包有關(guān)的崩潰信息。
設(shè)定kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P意味著核心轉(zhuǎn)儲(chǔ)將被通過(guò)管道送給apport程序。
apport 的日志保存在文件/var/log/apport.log中。
apport 默認(rèn)會(huì)忽略來(lái)自不屬于 Ubuntu 軟件包一部分的二進(jìn)制文件的崩潰信息
我最終只是跳過(guò)了 apport,并把kernel.core_pattern重新設(shè)置為sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因?yàn)槲以谝慌_(tái)開發(fā)機(jī)上,我不在乎 apport 是否工作,我也不想嘗試讓 apport 把我的核心轉(zhuǎn)儲(chǔ)留在磁盤上。
現(xiàn)在你有了核心轉(zhuǎn)儲(chǔ),接下來(lái)干什么?
好的,現(xiàn)在我們了解了ulimit和kernel.core_pattern,并且實(shí)際上在磁盤的/tmp目錄中有了一個(gè)核心轉(zhuǎn)儲(chǔ)文件。太好了!接下來(lái)干什么?我們?nèi)匀徊恢涝摮绦驗(yàn)槭裁磿?huì)出現(xiàn)段錯(cuò)誤!
下一步將使用gdb打開核心轉(zhuǎn)儲(chǔ)文件并獲取堆棧調(diào)用序列。
從 gdb 中得到堆棧調(diào)用序列
你可以像這樣用gdb打開一個(gè)核心轉(zhuǎn)儲(chǔ)文件:
$gdb -cmy_core_file
接下來(lái),我們想知道程序崩潰時(shí)的堆棧是什么樣的。在 gdb 提示符下運(yùn)行 bt 會(huì)給你一個(gè)調(diào)用序列backtrace。在我的例子里,gdb 沒(méi)有為二進(jìn)制文件加載符號(hào)信息,所以這些函數(shù)名就像 “??????”。幸運(yùn)的是,(我們通過(guò))加載符號(hào)修復(fù)了它。
下面是如何加載調(diào)試符號(hào)。
symbol-file /path/to/my/binary
sharedlibrary
這從二進(jìn)制文件及其引用的任何共享庫(kù)中加載符號(hào)。一旦我這樣做了,當(dāng)我執(zhí)行 bt 時(shí),gdb 給了我一個(gè)帶有行號(hào)的漂亮的堆棧跟蹤!
如果你想它能工作,二進(jìn)制文件應(yīng)該以帶有調(diào)試符號(hào)信息的方式被編譯。在試圖找出程序崩潰的原因時(shí),堆棧跟蹤中的行號(hào)非常有幫助。:)
查看每個(gè)線程的堆棧
通過(guò)以下方式在 gdb 中獲取每個(gè)線程的調(diào)用棧!
thread apply all bt full
gdb + 核心轉(zhuǎn)儲(chǔ) = 驚喜
如果你有一個(gè)帶調(diào)試符號(hào)的核心轉(zhuǎn)儲(chǔ)以及gdb,那太棒了!您可以上下查看調(diào)用堆棧(LCTT 譯注:指跳進(jìn)調(diào)用序列不同的函數(shù)中以便于查看局部變量),打印變量,并查看內(nèi)存來(lái)得知發(fā)生了什么。這是最好的。
如果您仍然正在基于 gdb 向?qū)?lái)工作上,只打印出棧跟蹤與bt也可以。 :)
ASAN
另一種搞清楚您的段錯(cuò)誤的方法是使用 AddressSanitizer 選項(xiàng)編譯程序(“ASAN”,即$CC -fsanitize=address)然后運(yùn)行它。 本文中我不準(zhǔn)備討論那個(gè),因?yàn)楸疚囊呀?jīng)相當(dāng)長(zhǎng)了,并且在我的例子中打開 ASAN 后段錯(cuò)誤消失了,可能是因?yàn)?ASAN 使用了一個(gè)不同的內(nèi)存分配器(系統(tǒng)內(nèi)存分配器,而不是 tcmalloc)。
在未來(lái)如果我能讓 ASAN 工作,我可能會(huì)多寫點(diǎn)有關(guān)它的東西。(LCTT 譯注:這里指使用 ASAN 也能復(fù)現(xiàn)段錯(cuò)誤)
從一個(gè)核心轉(zhuǎn)儲(chǔ)得到一個(gè)堆棧跟蹤真的很親切!
這個(gè)博客聽起來(lái)很多,當(dāng)我做這些的時(shí)候很困惑,但說(shuō)真的,從一個(gè)段錯(cuò)誤的程序中獲得一個(gè)堆棧調(diào)用序列不需要那么多步驟:
試試用valgrind
如果那沒(méi)用,或者你想要拿到一個(gè)核心轉(zhuǎn)儲(chǔ)來(lái)調(diào)查:
確保二進(jìn)制文件編譯時(shí)帶有調(diào)試符號(hào)信息;
正確的設(shè)置ulimit和kernel.core_pattern;
運(yùn)行程序;
一旦你用gdb調(diào)試核心轉(zhuǎn)儲(chǔ)了,加載符號(hào)并運(yùn)行bt;
嘗試找出發(fā)生了什么!
我可以使用gdb弄清楚有個(gè) C++ 的虛表?xiàng)l目指向一些被破壞的內(nèi)存,這有點(diǎn)幫助,并且使我感覺(jué)好像更懂了 C++ 一點(diǎn)。也許有一天我們會(huì)更多地討論如何使用gdb來(lái)查找問(wèn)題!
-
Linux
+關(guān)注
關(guān)注
87文章
11227瀏覽量
208925 -
C++
+關(guān)注
關(guān)注
22文章
2104瀏覽量
73494 -
線程
+關(guān)注
關(guān)注
0文章
504瀏覽量
19651
原文標(biāo)題:在 Linux 上如何得到一個(gè)段錯(cuò)誤的核心轉(zhuǎn)儲(chǔ)
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論