有趣 | 最近遇到一個狡猾的bug,復盤分享
關(guān)注「Linux大陸」,選擇「星標公眾號」一起進步!
最近遇到一個看似青銅、實則王者的bug。
事情是這樣的,某個進程有數(shù)據(jù)解析處理、算法融合。
數(shù)據(jù)來源是gps模塊,我負責這個程序的開發(fā)維護、與算法對接。
下面看看從這個bug的定位、分析、解決過程,一波三折~
機器之前一直正常在跑,但近兩天做了一些特殊測試,發(fā)現(xiàn)機器走到某個位置之后基本上必會出現(xiàn)段錯誤,因為與位置相關(guān)的就是數(shù)據(jù)了,所以剛開始的時候我懷疑可能是數(shù)據(jù)解析出問題了。
但是之前解析有長時間測試過,沒什么問題,特殊位置也有測過沒什么問題。暫時排除了數(shù)據(jù)解析的問題。
定位問題
遇到死機問題,當然得先定位問題,才能去分析、解決問題。定位段錯誤的方法有很多:
1、log打印定位
可以把所有打印調(diào)試信息打開,一些段錯誤問題可以通過打印的方法就可以大致定位到某一塊代碼出現(xiàn)問題。
打印方式只是定位段錯誤的一個小嘗試而已,不要對其結(jié)果抱有太大希望。有時候確實可以很快就定位到問題的根源,但針對本次的bug,通過打印的方式定位出的結(jié)果反而給我們帶來了一些迷惑。
本次的bug通過打印的方式也鎖定到了出問題的代碼,在某個算法函數(shù)里的某兩個個三角函數(shù)的算式。問題就是屏蔽掉這個算式,程序就沒出現(xiàn)段錯誤,打開這兩句代碼就必出現(xiàn)段錯誤,這讓我的注意力集中在了這個地方。
但反反復復看了好多次沒發(fā)現(xiàn)這兩個算式有什么不妥的地方,而且看了前后兩層函數(shù),也沒發(fā)現(xiàn)有什么不妥。最后定位出了問題,這里確實不是問題的根源,但卻在這浪費了很多時間。
應該有很多小伙伴跟我有同樣的習慣,喜歡通過打印法來查找bug,打印法基本能定位到大多數(shù)問題。但對于一些藏得很深的bug,通過打印法有時候只看到了表象,而我們有時候會被這表象給迷惑了。
所以在分析問題的時候,盡量頭腦清醒些,分析遇到不太合理的地方,要不斷的推敲,不斷地推翻不合理地分析。
當然,有好的定位問題的方法也很重要,下面看第二種定位段錯誤的方法:
2、遠程調(diào)試
遠程GDB是一種適合嵌入式系統(tǒng)的調(diào)試手段。它使用目標機端的gdbserver和主機端的gdb調(diào)試器協(xié)同進行調(diào)試,再搭配vscode可以很方便地進行調(diào)試。vscode+gdb+gdbserver遠程調(diào)試的教程見:干貨 | 遠程gdb調(diào)試
遠程GDB的原理是:
?有一小段駐留在目標機上的代碼,它被稱為調(diào)試樁,也稱為調(diào)試代理。調(diào)試代理負責目標機上實現(xiàn)由主機上的調(diào)試器發(fā)送過來的調(diào)試命令,例如:讀寫內(nèi)存、讀寫寄存器、設置斷點及運行被調(diào)試程序等。調(diào)試代理還要向主機調(diào)試器報告目標機上的異常事件。
?
啟動遠程調(diào)試,全速運行,當程序出現(xiàn)段錯誤時可以很快知道出現(xiàn)段錯誤的代碼行號。本次的這個bug也是使用這種方法來快速定位出來的。
除此之外還有其它很多方法來定位段錯誤,如使用strace工具跟蹤、gdb調(diào)試core文件等方法,后續(xù)有機會再寫使用分享。
分析、解決問題
通過遠程調(diào)試的方法可以快速定位到本次的段錯誤出現(xiàn)在一個串口讀函數(shù)里的下面這一句代碼:
FD_SET(fd,&fs_read);
通過打印發(fā)現(xiàn)在出現(xiàn)段錯誤時這個fd的值是一個很大的數(shù),顯然是不對的。
在Linux中,一個進程默認可以打開的文件數(shù)為1024個,fd的范圍為0~1023??梢酝ㄟ^設置,改變最大值。
我們代碼里的fd是一個靜態(tài)全局變量,fd突然出現(xiàn)一個異常的值,通過分析可能會出現(xiàn)兩種情況:
?1、串口的操作不當,沒有正確打開、關(guān)閉;多個線程、進程操作了同一個串口。
2、fd的值被非法篡改了。
?
代碼中對串口的操作都比較合理,所以第二種情況的可能會大些。所以開始著手確認與fd前后相鄰的變量的操作。這可以通過map文件來查看,在CMakeLists.txt中生成map文件的代碼如:
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=output.map")
但Linux下的map文件默認不會顯示static變量的一些地址信息。
剛開始時,我們?yōu)榱瞬榭磃d的地址,把fd前面的static暫時先去掉,就可以在map文件中找到它的地址信息,但這時候再測試并不會出現(xiàn)段錯誤的情況了,這讓我們更加肯定了fd的是被篡改掉了。
把fd前面的static去掉沒有出現(xiàn)段錯誤的原因是由無static修飾,fd存放的區(qū)域不同,沒有出現(xiàn)段錯誤是因為去掉static之后被篡改的就是不fd的值,所以暫時不會出現(xiàn)段錯誤。
所以,要找到問題的根源必須得把static給加回去,然后找到fd前后相鄰地址的變量。問題是有static修飾時的fd的地址信息并不會存放到map文件中。這可如何是好?
當時沒什么其它的辦法,網(wǎng)上也查了,相關(guān)資料很少。只能把整個工程的static修飾的變量的地址挨個打印出來,這是個體力活。。。
還好這份并不是復雜,核心文件就那么幾個,但查找過程也耗費了很多時間。最后鎖定了某個源文件,然后依次把這個源文件里的static變量的值及地址給打印出來。終于,找到了fd的前一個變量,那是一個int類型的cnt計數(shù)變量:

當查到cnt的地址正好是fd的前一個地址時,別提有多開心
。
當查看代碼發(fā)現(xiàn)cnt除了忘了進行清零操作之外,沒有其它操作異常的地方了。不清零也似乎不會影響到后面的fd,cnt不斷遞增,頂多會發(fā)生上溢。
這時候又陷入了沉思~莫非是思考的方向錯了?
在下班后餓了自己一個多小時,突然想到程序才運行一小會,cnt竟然已經(jīng)計數(shù)到了一個十位數(shù)的數(shù),再往前多打印幾個相鄰變量的值也竟然都是十位數(shù)的數(shù)。終于,一切好像比較明了了,這是被篡改了一塊連續(xù)內(nèi)存,絕對是哪里有數(shù)組越界進行寫操作了。
檢查了一遍代碼,果然,在一個算法函數(shù)里有對一個數(shù)組進行寫操作越界了:

這里有一個含有5個元素的float類型的數(shù)組arr,但其因為邏輯設計不當,導致有對arr[5]、arr[6]、arr[7]、arr[8]進行了寫操作,而arr[8]的地址正好是fd的地址,所以對arr[8]進行寫操作就篡改了fd的值,從而導致段錯誤死機。到了這一步一切都明了了,之前打印調(diào)試屏蔽掉的那兩句算式的與這個算法函數(shù)有一定的聯(lián)系,一層套一層。
但是,后來,我同事魚鷹(公眾號:魚鷹談單片機)鉆研出了Linux下static變量地址輸出到map文件的方法。在CMakeLists.txt文件中設置編譯參數(shù),如:
set(CMAKE_C_FLAGS "-fdata-sections")
set(CMAKE_CXX_FLAGS "-fdata-sections")
如果本次的bug問題定位能使用這種方法,就可以很快地查找fd的相鄰地址變量了。
總結(jié)
這次的bug藏得真夠深的,從定位、分析到根本性耗費了一天半的時間。有時候我們通過屏蔽、打開某些代碼來定位問題有可能只是bug的表象,并沒有看到本質(zhì),所以應多思考問題的根源,從根本性地解決才是真正的解決。
雖然這次“浪費”了不少時間在這,但這些bug的解決不正是經(jīng)驗積累嗎,下次再遇到類似的bug就可以很快的挖出來。
另外,Linux下的開發(fā)坑很多,多掌握一些調(diào)試工具、方法,對于我們快速解決問題有很大的幫助。
平時大家都有遇到哪些狡猾的bug呢?
歡迎留言分享
歷史文章:

一文讀懂a(chǎn)pt、deb與背后的知識
