破獲ARM64位CPU下linux crash要案之神技能:手動恢複函數調用棧(大結局)

引言

終於要大結局了,一個字:爽!

後面維護世界和平,鋤奸懲惡的重任就交給各位碼農君啦。

庖丁解牛之實操手動恢復堆棧

前面我們已經傳授了大家神技能法則,以及一件神兵利器crash,所以是時候檢驗下我們的成果了。

上面是一個空指針crash的實際例子,可以看出內核沒有列印出函數的backtrace。而堆棧現場是完整的,因此我們可以通過手動來恢複函數的調用堆棧。

再次回顧下破案的兩條法則:

1)根據callee的FP找到caller的FP,也即找到調用者的棧幀。這樣通過FP的層層回溯就能把整個函數的調用路徑(棧幀)找到。

2)根據本函數棧幀保存的LR來間接獲取PC,從而根據符號表得到具體的函數名,在調用子函數的時候,LR指向子函數的返回的下一條指令,通過LR指向的地址-4位元組偏移就得到了 被調用函數callee的入口地址,再通過符號表即可得到此入口地址對應的函數名。

程序員的世界當然要用程序語言來代言我們自己,所以讓我們再一次用碼農的語言來描述下我們的結論吧。

假設我們需要恢復的堆棧調用為:func1->func2->fun_die。

提問:

已知:當前現場fun_die的 FP(die)/SP(die)/LR(die)/PC(die)。

求解:調用fun_die發生crash的函數調用路徑?

「狗蛋,看你這節課聽得很認真嘛,能否來解答下這個題目,檢驗下本節課的學習成果呢?老師我需要看下大家的聽課情況,好及時查缺補漏,盡量讓大家能夠聽懂,掌握本節課的要點,在實際的職場工作中做到舉一反三的效果。」

「老師,通過這段時間課堂上的學習和平時私下的溝通中,狗蛋我的技能已經得到了很大的提升,知道你的專業課乾貨十足,這節課那是相當的認真,你看下我的解答怎麼樣吧。」

狗蛋之解答:

「恩,不錯,完全正確,看來大家這節課都聽得很認真嘛,老師甚感欣慰啊。現在就請同學們以狗蛋的代碼來幫助初入職場的小馬同事結束這個要案吧。請二丫同學在黑板上來推導,其他同學在座位上完成。」

二丫胸有成竹的來到黑板前開始了她的碼磚事業。

當前現場:

Crash現場寄存器

使用crash工具載入內核轉儲文件即ramdump文件:

1)根據PC獲取指向當前掛掉的函數代碼現場:

pc : [<ffffffc000343068>]

decon_lpd_block_exit對應的彙編如下:

第一現場已經獲取到:ffffffc000343068 (T)decon_lpd_block_exit+12。

「恩,很不錯,請二丫同學等一等,這裡老師再插播一些知識點」:由於對於這個問題比較簡單,其實我們僅僅通過分析第一現場就能知道大致問題是哪裡導致的了。代碼是死在decon_lpd_block_exit+12,也即上圖紅色箭頭標識的彙編 ldr w3,[x0,#1192] 。這條指令是什麼意思呢? 「什麼,彙編不懂?這節課後趕緊去補習吧。」彙編不懂,那我們先看看對應的源碼吧。

通過對應源碼再結合彙編,我等碼農還是可以猜出個5,6層的彙編含義了。在這裡,老王就先直接給出這段代碼的彙編解釋:

注:arm linux彙編會將內聯函數(inline)直接在主代碼中展開,不會進行子函數調用。所以decon_lpd_block(decon)直接展開為if (!decon->id) atomic_inc(&decon->lpd_block_cnt),不進行bl調用。在看上面的黃色彙編: ldr w3, [x0,#1192],1192是什麼意思呢?其實它就是decon的成員id偏移值。

有時候結構體成員比較多,我們很難數清成員的偏移值是多少,這裡我們使用crash利器就灰常方便,直接使用-o參數,像下面這個結構體總共佔用了3019000個位元組,要是我們人為計算後面幾個變數的偏移值,我想大家跳樓的心都要有了。所以crash這個神兵利器很不錯吧。

從上圖我們也得出[x0,#1192]中的1192就是id的偏移位置,也進一步驗證了這句代碼是在取值(decon->id)。而結合「Unable tohandle kernel NULL pointer dereference at virtual address 000004a8」

我們知道大概可能decon是一個空指針,而decon的值是通過形參傳遞進來的,根據子函數傳參數傳遞規則,X0應該是保存的decon的地址,那我們在看下X0的值: x0 : 0000000000000000,果然不是一個有效的地址,所以結論就是decon_lpd_block_exit(structdecon_device *decon)傳遞了一個空指針。因為代碼裡面有很多函數調用了decon_lpd_block_exit函數,所以進一步的我們需要找到是最終在哪個調用路徑上傳遞了NULL的 decon指針。

根據前面的法則:「在調用子函數的時候,LR是指向子函數返回的下一條指令」,那我們來驗證下這個法則是否正確呢?

通過pc值我們已經找到了第一個棧幀調用。那我們看下lr(0xffffffc000333f4c)是否就是調用decon_lpd_block_exit函數返回的主函數的下一條指令的地址呢?

LR指向dsim_read_data+64,根據法則不出意外的話 LR-4=0xffffffc000333f48 地址處應該對應於decon_lpd_block_exit子函數的調用指令。everybody,it』s show time now!:

怎麼樣?出現這個結果是不是很雞凍啊,有木有一種成就感,有木有想擁抱老師?有木有?到此為止,看來老師講的還算是良心乾貨啊。

我想,後面的故事就很輕鬆了。

好,現在請二丫繼續。

2)根據第一現場的FP來獲取PC。

OK,我們現在在把法則搬出來依葫蘆畫瓢:

FP = x29 =ffffffc0b611b970

根據FP得到caller的PC:

PC= *(unsigned long*)(FP+ 8) - 4 = *(unsigned long *)LR- 4=*(unsigned long *)ffffffc000333f48

第二現場已經獲取到:ffffffc000333f48 (T) dsim_read_data+0x3c

3)根據第一現場的FP來回溯caller的調用棧幀。

FP = x29 = ffffffc0b611b970

根據FP得到caller1的棧幀基指:

FP1 = *FP=ffffffc0b611b980

PC1= *(unsignedlong *)(FP1+ 8) - 4 = *(unsigned long *)ffffffc0b611b988 - 4

PC1=*(unsigned long *)ffffffc0b611b988 - 4 = ffffffc00033432c

第三現場已經獲取到:ffffffc00033432c (t)dsim_read_test+0x20

FP2= *FP1=*(unsigned long *)ffffffc0b611b980

PC2= *(unsignedlong *)(FP2+ 8) - 4 = *(unsigned long *)ffffffc0b611b9f8- 4

PC2=*(unsigned long *)ffffffc00033450c- 4 = ffffffc000334508

第四現場已經獲取到:ffffffc000334508 (t) dsim_runtime_resume+0xac。

以此類推:通過callee棧幀的FP得到caller的棧幀FP』。然後根據AAPCS64的棧幀組織結構得到caller的PC』,然後通過crash工具結合符號表得到caller的函數名。直到FP的值為0,表示沒有更多的調用棧幀而到棧底。

由此得出整個函數的backtrace如下:

到此,我們就完成了整個函數backtrace的恢復調用。在結合強大的crash工具和源碼分析,相信破案已經是手到擒來了,是不是有那麼一絲絲的雞凍呢。

通過本節課多個知識點的回顧和學習,老師有足夠的理由相信,你可以在職場上和你一樣的菜鳥們面前顯擺一下(當然老王還是那句話,低調),因為無疑這個案子將使你在crash debug的知識技能點上超越一般的碼農,通過在實際的工作再運用此中的技能來解決實際的問題,相信你在碼農的道路上又前進了一大步。

「哇塞,老師,這麼說來狗蛋,二丫明年升職加薪看來不是問題了呀,想想都雞凍,我們還要和老師學更多的經驗技能,早日晉陞老司機,請老師帶我們早日走上高大上,白富美的生活呀!」

「好了,就你們丫話多,老師最後再啰嗦一句話:everybody,這次大講堂之

「破獲ARM64位CPU下linux crash要案之神技能:手動恢複函數調用棧」

終於大結局了,感謝大家的聆聽,下課!」

瓶子哥終於可以緩口氣了,畢竟今天答應了某位知友一併更完。。。

PS:另外本文章所涉及的AAPCA過程調用規範文檔我在第一篇文章有分享下載鏈接,需要的可以下載閱覽。另外其他ARM手冊相關文檔我也整理了下,需要的話可以在公眾號「程序員觀世界」回復關鍵字ARM一併下載,感謝大家。


推薦閱讀:

TAG:Linux開發 | Linux內核 | linux驅動 |