0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文詳解Linux內(nèi)核的?;厮菖c妙用

Linux閱碼場 ? 來源:網(wǎng)絡(luò)整理 ? 作者:工程師陳翠 ? 2018-10-05 10:02 ? 次閱讀

1 前言

說起linux內(nèi)核的?;厮莨δ?,我想這對每個Linux內(nèi)核或驅(qū)動開發(fā)人員來說,太常見了。如下演示的是linux內(nèi)核崩潰的一個?;厮荽蛴。辛诉@個崩潰打印我們能很快定位到在內(nèi)核哪個函數(shù)崩潰,大概在函數(shù)什么位置,大大簡化了問題排查過程。

一文詳解Linux內(nèi)核的?;厮菖c妙用

網(wǎng)上或多或少都能找到?;厮莸囊恍┪恼?,但是講的都并不完整,沒有將內(nèi)核?;厮莸墓δ苡糜趯嶋H的內(nèi)核、應(yīng)用程序調(diào)試,這是本篇文章的核心:盡可能引導(dǎo)讀者將?;厮莸墓δ苡糜趯嶋H項目調(diào)試,?;厮莸墓δ芎軓姶蟆?/p>

本文詳細講解了基于mips、arm架構(gòu)linux內(nèi)核棧回溯原理,通過不少例子,盡可能全面給讀者展示各種?;厮莸脑恚谕x者理解透徹?;厮荨T谶@個基礎(chǔ)上,講解筆者近幾年項目開發(fā)過程中使用linux內(nèi)核?;厮莨δ艿膸滋幹攸c應(yīng)用。

1當(dāng)內(nèi)核某處陷入死循環(huán),有時運行sysrq的內(nèi)核線程棧回溯功能可以排查,但并不適用所用情況,筆者實際項目遇到過。最后是在系統(tǒng)定時鐘中斷函數(shù),對死循環(huán)線程?;厮?0多級終于找到死循環(huán)的函數(shù)。

2當(dāng)應(yīng)用程序段錯誤,內(nèi)核捕捉到崩潰,對崩潰的應(yīng)用空間進程/線程棧回溯,像內(nèi)核?;厮菀粯?,打印應(yīng)用段錯誤進程/線程的層層函數(shù)調(diào)用關(guān)系。雖然運用core文件分析或者gdb也很簡便排查應(yīng)用崩潰問題,但是對于不容易復(fù)現(xiàn)、測試部偶先的、客戶現(xiàn)場偶先的,這二者就很難發(fā)揮作用。還有就是如果崩潰發(fā)生在C庫中,CPU的pc和lr(arm架構(gòu))寄存器指向的函數(shù)指令在C庫的用戶空間,很難找到應(yīng)用的代碼哪里調(diào)用了C庫的函數(shù)。arm架構(gòu)網(wǎng)上能找到應(yīng)用層?;厮莸睦樱蔷幾g較麻煩,代碼并不容易理解,況且mips能在應(yīng)用層實現(xiàn)嗎?還是在內(nèi)核實現(xiàn)應(yīng)用程序棧回溯比較方便。

3應(yīng)用程序發(fā)生double free,運用內(nèi)核的?;厮莨δ?,找到應(yīng)用代碼哪里發(fā)生了double free。double free是C庫層發(fā)現(xiàn)并截獲該事件,然后向當(dāng)前進程/線程發(fā)送SIGABRT進程終止信號,后續(xù)就是內(nèi)核強制清理該進程/線程。double free比應(yīng)用程序段錯誤更麻煩,后者內(nèi)核還會打印出錯進程/線程名字、pid、pc和lr寄存器值,double free這些打印全沒有。筆者做過的一個項目,發(fā)布前,遇到一例double free崩潰問題,極難復(fù)現(xiàn),當(dāng)初要是把double free內(nèi)核對出問題進程/線程?;厮莸墓δ茏鲞M內(nèi)核,就能找到出問題的應(yīng)用函數(shù)了。

4 當(dāng)應(yīng)用程序出現(xiàn)鎖死問題,對應(yīng)用所有線程棧回溯,分析每個線程的函數(shù)執(zhí)行流程,對查找鎖死問題有幫助。

以上幾例應(yīng)用,在筆者所做的項目中,內(nèi)核已經(jīng)合入相關(guān)代碼,功能得到驗證。

2 ?;厮莸脑斫忉?/p>

2.1 基于fp棧幀寄存器形式的棧回溯

筆者最初學(xué)習(xí)?;厮荩紫瓤吹降?a href="http://wenjunhu.com/soft/special/" target="_blank">資料就是arm架構(gòu)基于fp寄存器的?;厮?,這種資料網(wǎng)上比較多,這里按照自己理解再描述一遍。這種形式的?;厮菹鄬碚f并不復(fù)雜,也容易理解,遵循APCS(ARM Procedure Call Standard)規(guī)范, APCS規(guī)范了arm寄存器的使用、函數(shù)調(diào)用過程出棧和入棧的約定。如下圖所示,是一個傳統(tǒng)的arm架構(gòu)下函數(shù)棧數(shù)據(jù)分布,函數(shù)棧由fp和sp寄存器分別指向棧底和棧頂(這里舉的例子函數(shù)無形參,無局部變量,方便理解)。

一文詳解Linux內(nèi)核的棧回溯與妙用

通過fp寄存器就可以找到存儲在棧中l(wèi)r寄存器數(shù)據(jù),這個數(shù)據(jù)就是函數(shù)返回地址。同時也可以找到保存在函數(shù)棧中的上一級函數(shù)fp寄存器數(shù)據(jù),這個數(shù)據(jù)指向了上一級函數(shù)的棧底,如此就可以按照同樣的方法找出上一級函數(shù)棧中存儲的lr和fp數(shù)據(jù),就知道哪個函數(shù)調(diào)用了上一級函數(shù)以及這個函數(shù)的棧底地址。這樣就構(gòu)成了一個?;厮葸^程,整個流程以fp為核心,依次找出每個函數(shù)棧中存儲的lr和fp數(shù)據(jù),計算出函數(shù)返回地址和上一級函數(shù)棧底地址,從而找出每一級函數(shù)調(diào)用關(guān)系。

為了使讀者理解更充分,舉一個簡單的例子。以C函數(shù)調(diào)用了B函數(shù)為例,兩個函數(shù)無形參,無局部變量,此時的入棧情況最簡單。兩個函數(shù)以偽代碼的形式列出,演示入棧過程,寄存器的入棧及賦值,與實際的匯編代碼有偏差。

一文詳解Linux內(nèi)核的?;厮菖c妙用

假設(shè)C函數(shù)的棧底地址是0x7fff001c,C函數(shù)的前5條入棧指令執(zhí)行后,pc等寄存器的值保存到C函數(shù)棧中,此時fp寄存器的值是C函數(shù)棧底地址0x7fff001c。然后C函數(shù)跳轉(zhuǎn)到B函數(shù),B函數(shù)前5條指令執(zhí)行后,pc、lr、fp寄存器的值依次保存到B函數(shù)棧中:B函數(shù)棧的第二片內(nèi)存保存的就是lr值,即B函數(shù)的返回地址;第四片內(nèi)存保存的是fp值,就是C函數(shù)棧底地址0x7fff001c(在開始執(zhí)行B函數(shù)指令前,fp寄存器的值是C函數(shù)的棧底地址,B函數(shù)的第4條指令又是令fp寄存器入棧);B函數(shù)第五條指令執(zhí)行后,fp寄存器已經(jīng)更新,其數(shù)據(jù)是B函數(shù)棧的棧底地址0x7fff000c。當(dāng)B函數(shù)發(fā)生崩潰,根據(jù)fp寄存器找到B函數(shù)棧底地址,從B函數(shù)棧第二片內(nèi)存取出的數(shù)據(jù)就是lr,即B函數(shù)返回地址,第4片內(nèi)存取出的數(shù)據(jù)就是fp,即C函數(shù)棧底地址。有了C函數(shù)棧底地址,就能按照上述方法找出C函數(shù)棧中保存的的lr和fp,實現(xiàn)棧回溯…..

一文詳解Linux內(nèi)核的?;厮菖c妙用

2.2 unwind 形式的棧回溯

在arm架構(gòu)下,不少32位系統(tǒng)用的是unwind形式的?;厮荩@種?;厮菀獜?fù)雜很多。首先需要程序有一個特殊的段.ARM.unwind_idx 或者.ARM.unwind_tab,linux內(nèi)核本身由多段組成,比如內(nèi)核驅(qū)動初始化函數(shù)的init段。在System.map文件可以搜索到__start_unwind_idx,這就是ARM.unwind_idx段的起始地址。這個unwind段中存儲著跟函數(shù)入棧相關(guān)的關(guān)鍵數(shù)據(jù)。當(dāng)函數(shù)執(zhí)行入棧指令后,在unwind段會保存跟入棧指令一一對應(yīng)的編碼數(shù)據(jù),根據(jù)這些編碼數(shù)據(jù),就能計算出當(dāng)前函數(shù)棧大小和cpu的哪些寄存器入棧了,在棧中什么位置。當(dāng)?;厮輹r,首先根據(jù)當(dāng)前函數(shù)中的指令地址,就可以計算出函數(shù)unwind段的地址,然后從unwind段取出跟入棧有關(guān)的編碼數(shù)據(jù),根據(jù)這些編碼數(shù)據(jù)就能計算出當(dāng)前函數(shù)棧的大小以及入棧時lr寄存器數(shù)據(jù)在棧中的存儲地址。這樣就可以找到lr寄存器數(shù)據(jù),就是當(dāng)前函數(shù)返回地址,也就是上一級函數(shù)的指令地址。此時sp一般指向的函數(shù)棧頂,sp+函數(shù)棧大小就是上一級函數(shù)的棧頂。這樣就完成了一次?;厮荩⑶抑懒松弦患壓瘮?shù)的指令地址和棧頂?shù)刂?,按照同樣的方法就能對上一級函?shù)?;厮荩愅凭湍軐崿F(xiàn)整個?;厮萘鞒?。為了方便理解,下方舉一個實際調(diào)試的示例。該示例中首先列出?;厮葸^程每個函數(shù)unwind段的編碼數(shù)據(jù)和棧數(shù)據(jù)。

一文詳解Linux內(nèi)核的?;厮菖c妙用

假設(shè)函數(shù)調(diào)用過程C->B->A,另外每個函數(shù)中只有一個printk打印。這種情況下函數(shù)的入棧和unwind段的信息就很規(guī)則和簡單,這里就以簡單的來講解,便于理解。此時每個函數(shù)第一條指令一般是push{r4,lr},這表示將lr和r4寄存器入棧,此時系統(tǒng)會將跟push{r4,lr}指令相關(guān)的編碼數(shù)據(jù)0x80a8b0b0存入C函數(shù)的unwind段中,0x7fffff10跟偏移有關(guān),但是實際用處不大。0x80a8b0b0分離成0x80,0xa8 ,0xb0又有不同的意義,最重要的是0xa8,表示出棧指令pop {r4 r14},r14就是lr寄存器,與push{r4,lr}入棧指令正好相反。C函數(shù)跳轉(zhuǎn)到B函數(shù)后,會把B函數(shù)的返回地址0xbf004068存入B函數(shù)棧。B函數(shù)按照同樣的方法執(zhí)行,當(dāng)執(zhí)行到A函數(shù)最后,幾個函數(shù)的棧信息和unwind段信息就如圖所示。假設(shè)在A函數(shù)中崩潰了,會首先根據(jù)崩潰的pc值,找到崩潰A函數(shù)的unwind段(每個函數(shù)的指令地址和unwind段都是對應(yīng)的,內(nèi)核有標準的函數(shù)可以查找)。如圖所示,從地址0xbf00416c的A函數(shù)unwind段中取出數(shù)據(jù)0x80a8b0b0,分析出其中的0xa8,就知道對應(yīng)的pop {r4 r14}出棧指令,相應(yīng)就知道函數(shù)入棧時執(zhí)行的是push{r4,lr}指令,其中有兩個重要信息,一個是函數(shù)入棧時只有l(wèi)r和r4寄存器入棧,并且函數(shù)棧大小是2*4=8個字節(jié),函數(shù)崩潰時棧指針sp指向崩潰函數(shù)A的棧頂,根據(jù)sp就能找到lr寄存器存儲在A函數(shù)棧的數(shù)據(jù)0xbf004038,就是崩潰函數(shù)的返回地址,上一級函數(shù)B的指令地址,而sp+ 2*4就是上一級B函數(shù)的棧頂。知道了B函數(shù)的指令地址和棧頂?shù)刂?,就能根?jù)指令地址找到B函數(shù)的unwind段,分析出B函數(shù)的入棧指令,按照同樣的方法,就能找到C函數(shù)的返回地址和棧頂。這只是幾個很簡單unwind?;厮葸^程的演示,省去了很多細節(jié),讀者想研究清楚的話,可以閱讀內(nèi)核arm架構(gòu)unwind_frame函數(shù)實現(xiàn)流程,其中最核心的是在unwind_exec_insn函數(shù),根據(jù)0xa8,0xb0這些跟函數(shù)入棧過程有關(guān)的編碼數(shù)據(jù),分析入棧過程的詳細信息,計算出函數(shù)lr寄存器保存在棧中的地址和上一級函數(shù)的棧頂?shù)刂贰?/p>

不同的入棧指令在函數(shù)的unwind段對應(yīng)不同的編碼,0x80a8b0b0只是其中比較簡單的的編碼,還有0x80acb0b0,0x80aab0b0等等很多??梢詧?zhí)行 readelf -u .ARM.unwind_idx vmlinux查看內(nèi)核init段函數(shù)的unwind段數(shù)據(jù)。比如:

一文詳解Linux內(nèi)核的?;厮菖c妙用

這就表示match_dev_by_uuid函數(shù)在unwind段編碼數(shù)據(jù)是0x808ab0b0,0xc0008af8是該函數(shù)指令首地址。其中有用的是0xa8 ,表示pop {r4,r14}出棧指令,0xb0表示unwind段結(jié)束。

為了方便讀者分析對應(yīng)的?;厮輧?nèi)核源碼,這里把關(guān)鍵點列出,并添加必要注釋。內(nèi)核版本3.10.104。

arch/arm/kernel/unwind.c

一文詳解Linux內(nèi)核的?;厮菖c妙用

一文詳解Linux內(nèi)核的棧回溯與妙用

2.3 fp和unwind形式?;厮莸谋容^

上文介紹了兩種常用的?;厮菪问降幕驹恚⑤o助了例子說明?;趂p寄存器的棧回溯和unwind形式的?;厮荩饔袃?yōu)點和缺點。fp形式的?;厮?,基于APCS規(guī)范,入棧過程必須要將pc、lr、fp等4個寄存器入棧(其實沒必要這樣做,只需把lr和fp入棧),并且消耗的入棧指令要多(除了入棧pc、lr、fp等4個寄存器,還得將棧底地址保存到fp),同時還浪費了寄存器,至少fp寄存器是浪費了,不能參與指令數(shù)據(jù)運算,CPU寄存器是很寶貴的,多一個對加快指令數(shù)據(jù)運算是有積極意義的。而unwind形式的?;厮荩蜎]有這些缺點,僅僅只是將入棧相關(guān)的指令的編碼保存到unwind段中,不用把無關(guān)的寄存器保存到棧中,也不用浪費fp寄存器。unwind形式棧回溯是有缺點的,首先?;厮莸乃俣瓤隙ū萬p形式?;厮萋斫怆y度要比fp形式大很多,并且,站在開發(fā)者角度,使用前還得對每個入棧指令編碼,這都是需要工作量的。但是站在使用者角度,這些缺點影響并不大,所以現(xiàn)在有很多arm32系統(tǒng)用的是unwind形式的?;厮荨?/p>

3 linux內(nèi)核?;厮莸脑?/p>

當(dāng)內(nèi)核崩潰,將會執(zhí)行異常處理程序,這里以mips架構(gòu)為例,崩潰函數(shù)執(zhí)行流程是:

do_page_fault()->die()->show_registers()->show_stacktrace()->show_backtrace()

?;厮莸倪^程就是在show_backtrace()函數(shù),arm架構(gòu)最終是在dump_backtrace()函數(shù),內(nèi)核崩潰處理流程與mips不同。arm架構(gòu)?;厮葸^程相對來說更簡單,首先講解arm架構(gòu)的?;厮葸^程。

不同內(nèi)核版本,內(nèi)核代碼有差異,本內(nèi)核版本3.10.104

3.1 arm架構(gòu)內(nèi)核?;厮莸姆治?/p>

內(nèi)核實際的棧回溯代碼還是有點復(fù)雜的,在正式講解代碼前,先通過一個簡單演示,進一步詳細的介紹?;厮莸脑?。這次演示是基于fp形式的棧回溯,與上文介紹傳統(tǒng)的fp形式棧回溯稍有差異,但是原理是一樣的。

下方以偽匯編指令,演示一個完整的函數(shù)指令執(zhí)行與跳轉(zhuǎn)流程:C函數(shù)執(zhí)行B函數(shù),B函數(shù)執(zhí)行A函數(shù),然后A函數(shù)發(fā)生空指針崩潰。

一文詳解Linux內(nèi)核的?;厮菖c妙用

為了幫助讀者理解,做一下解釋,以C函數(shù)的第一條指令為例:

0x00034:C函數(shù)返回地址lr入棧指令; C函數(shù)指令1

0x00034:表示匯編指令的內(nèi)存地址,反匯編的讀者應(yīng)該熟悉

C函數(shù)返回地址lr入棧指令:表示具體指令的意思,不再用實際匯編指令表示,理解簡單

C函數(shù)指令1:表示C函數(shù)第一條指令,為了引用的簡單

其中提到的lr,做過arm內(nèi)核開發(fā)的讀者肯定熟悉,是CPU的一個寄存器,存儲函數(shù)返回地址,當(dāng)C函數(shù)跳轉(zhuǎn)到B函數(shù)時,CPU自動將C函數(shù)的指令地址0x00048存入lr寄存器,這表示B函數(shù)執(zhí)行完返回后,CPU將從0x00048地址取指令繼續(xù)運行(mips架構(gòu)是ra寄存器,先以arm為例)。fp寄存器也是arm架構(gòu)的一個CPU寄存器,英文釋義是frame point,中文有稱為棧幀寄存器,我們這里用來存儲每個函數(shù)棧的第2片內(nèi)存地址(一片內(nèi)存地址4個字節(jié),這樣稱呼是為了敘述方便),下方有詳細講解。為了方便讀者理解,特畫出函數(shù)執(zhí)行過程函數(shù)棧數(shù)據(jù)示意圖。

一文詳解Linux內(nèi)核的棧回溯與妙用

矩形框表示函數(shù)棧,初始化全為0,0x1000、0x1004等表示函數(shù)棧處于內(nèi)存的地址,函數(shù)棧向下增長。每個函數(shù)前兩條指令都是入棧指令,每個函數(shù)指令執(zhí)行后只占用兩片內(nèi)存。由于C函數(shù)是初始函數(shù),?;厮葸^程C函數(shù)棧意義不大,就從C函數(shù)跳轉(zhuǎn)到B函數(shù)指令開始分析。此時fp寄存器保存的數(shù)據(jù)是C函數(shù)棧地址0x1010,原因下文會分析到。當(dāng)執(zhí)行C函數(shù)指令5,跳轉(zhuǎn)到B函數(shù)后,棧指針sp指向地址0x100C(先假設(shè),下文的講解可以驗證),B函數(shù)的返回地址也就是C函數(shù)的指令6的地址0x00048就會自動保存到CPU的lr寄存器,然后執(zhí)行B函數(shù)指令1, 就會將0x00048存入B函數(shù)棧地址0x100C,棧指針sp減一,指向B函數(shù)棧地址0X1008。接著執(zhí)行B函數(shù)的指令2,將fp寄存器中的數(shù)據(jù)0x1010存入棧指針sp指向的內(nèi)存地址0x1008,示意圖已經(jīng)標明。接著執(zhí)行B函數(shù)指令3,將此時棧指針sp指向的地址0x1008(就是B函數(shù)的第二片內(nèi)存)存入fp寄存器。指令接著執(zhí)行,由B函數(shù)跳轉(zhuǎn)到A函數(shù),A函數(shù)前三條指令與B函數(shù)執(zhí)行情況類似,重點就三處,A函數(shù)棧的第一片內(nèi)存存儲A函數(shù)的返回地址,A函數(shù)棧的第二片內(nèi)存存儲B函數(shù)棧的第二片內(nèi)存地址,當(dāng)A函數(shù)執(zhí)行到指令5后,fp寄存器保存的是A函數(shù)棧的第二片內(nèi)存地址,示意圖中全部標出。當(dāng)A函數(shù)執(zhí)行指令6崩潰,怎么?;厮荩?/p>

A函數(shù)崩潰時,按照上文的分析,fp寄存器保存的數(shù)據(jù)是A函數(shù)棧的第二片內(nèi)存首地址0X1000。0X1000地址中存儲的數(shù)據(jù)就是B函數(shù)的棧地址0x1008(就是B函數(shù)的棧的第二片內(nèi)存),0x1000+4=0X1004地址就是A函數(shù)棧的第一片內(nèi)存,存儲的數(shù)據(jù)是A函數(shù)的返回地址0X0030,這個指令地址就是B函數(shù)的指令6地址,這樣就知道了時B函數(shù)調(diào)用了A函數(shù)。因為此時已經(jīng)知道了B函數(shù)棧的第二片內(nèi)存地址,該地址的數(shù)據(jù)就是C函數(shù)棧的第二片內(nèi)存地址,B函數(shù)棧的第一片內(nèi)存地址中的數(shù)據(jù)是B函數(shù)的返回地址0X0048(C函數(shù)的指令6內(nèi)存地址)。這樣就倒著推出函數(shù)調(diào)用關(guān)系:A函數(shù)?B函數(shù)?C函數(shù)。

筆者認為,這種情況棧回溯的核心是:每個函數(shù)棧的第二片內(nèi)存地址存儲的數(shù)據(jù)是上一級函數(shù)棧的第二片內(nèi)存地址,每個函數(shù)棧的第一片內(nèi)存地址存儲的數(shù)據(jù)是函數(shù)返回地址。只要獲取到崩潰函數(shù)棧的第二片內(nèi)存地址(此時就是fp寄存器的數(shù)據(jù)),就能循環(huán)計算出每一級調(diào)用的函數(shù)。

3.1.1內(nèi)核源碼分析

如果讀者對上一節(jié)的演示理解的話,理解下方的源碼就比較容易。

arch/arm64/kerneltraps.c

一文詳解Linux內(nèi)核的棧回溯與妙用

內(nèi)核崩潰時,產(chǎn)生異常,內(nèi)核的異常處理程序自動將崩潰時的CPU寄存器存入struct pt_regs結(jié)構(gòu)體,并傳入該函數(shù),相關(guān)代碼不再列出。這樣棧回溯的關(guān)鍵環(huán)節(jié)就是紅色標注的代碼,先對frame.fp,frame.sp,frame.pc賦值。下方進入while循環(huán),先執(zhí)行unwind_frame(&frame) 找出崩潰過程的每個函數(shù)中的匯編指令地址,存入frame.pc(第一次while循環(huán)是直接where = frame.pc賦值,這就是當(dāng)前崩潰函數(shù)的崩潰指令地址),下次循環(huán)存入where變量,再傳入dump_backtrace_entry函數(shù),在該函數(shù)中打印諸如[] chrdev_open+0x12/0x4B1 的字符串。

一文詳解Linux內(nèi)核的?;厮菖c妙用

這個打印的其實是在print_ip_sym函數(shù)中做的,將ip按照%pS形式打印,就能打印出該函數(shù)指令所在的函數(shù),以及相對函數(shù)首指令的偏移。棧回溯的重點是在unwind_frame函數(shù)。

在正式貼出代碼前,先介紹一下?;厮葸^程的三個核心CPU寄存器:pc、lr、fp。pc指向運行的匯編指令地址;sp指向函數(shù)棧;fp是棧幀指針,不同架構(gòu)情況不同,但筆者認為它是棧回溯過程中,聯(lián)系兩個有調(diào)用關(guān)系函數(shù)的紐帶,下面的分析就能體現(xiàn)出來。

arch/arm64/kernel/stacktrace.c

一文詳解Linux內(nèi)核的棧回溯與妙用

首先說明一下,這是arm64位系統(tǒng),一個long型數(shù)據(jù)8個字節(jié)大小。為了敘述方便,假設(shè)內(nèi)核代碼的崩潰函數(shù)流程還是 C函數(shù)->B函數(shù)->A函數(shù),在A函數(shù)崩潰,最后在unwind_frame函數(shù)中?;厮?。

接著針對代碼介紹?;厮莸脑?。第一次執(zhí)行unwind_frame函數(shù)時,第二行,frame->fp保存的就是崩潰時CPU的fp寄存器的值,就是A函數(shù)棧第二片內(nèi)存地址,frame->sp = fp + 0x10賦值后,frame->sp就是A函數(shù)的棧底地址;frame->fp= *(unsigned long *)(fp)獲取的是存儲在A函數(shù)棧第二片內(nèi)存中的數(shù)據(jù),就是調(diào)用A函數(shù)的B函數(shù)的棧的第二片內(nèi)存地址;frame->pc = *(unsigned long *)(fp + 8)是獲取A函數(shù)棧的第一片內(nèi)存中的數(shù)據(jù),就是A函數(shù)的返回地址(就是B函數(shù)中指令地址),這樣就知道了是B函數(shù)調(diào)用了A函數(shù);經(jīng)過一次unwind_frame函數(shù)調(diào)用,就知道了A函數(shù)的返回地址和B函數(shù)的棧的第二片內(nèi)存地址,有了B函數(shù)棧的第二片內(nèi)存地址,就能按照上述過程推出B函數(shù)的返回地址(C函數(shù)的指令地址)和C函數(shù)棧的第二片內(nèi)存地址,這樣就知道了時C函數(shù)調(diào)用了B函數(shù),如此循環(huán),不管有多少級函數(shù)調(diào)用,都能按照這個規(guī)律找出函數(shù)調(diào)用關(guān)系。當(dāng)然這里的關(guān)系是是A?B?C。

為什么?;厮莸脑硎沁@樣?首先這個原理筆者都是實際驗證過的,細心的讀者應(yīng)該會發(fā)現(xiàn),這個?;厮莸牧鞒谈拔牡?節(jié)演示的簡單?;厮菰硪粯?。是的,第2節(jié)就是筆者按照自己對arm 64位系統(tǒng)棧回溯的理解,用簡單的形式表達出來,還附了演示圖,這里不了解的讀者可以回到第2節(jié)分析一下。

3.1.2 arm架構(gòu)從匯編代碼角度解釋?;厮莸脑?/p>

為了使讀者理解的更充分,下文列出一段應(yīng)用層C語言代碼和反匯編后的代碼

C代碼

一文詳解Linux內(nèi)核的?;厮菖c妙用

匯編代碼

一文詳解Linux內(nèi)核的?;厮菖c妙用

分析test_2函數(shù)的匯編代碼,第一條指令stpx29, x30,[sp,#-16],x29就是fp寄存器,x30就是lr寄存器,指令執(zhí)行過程:將x30(lr)、x29(fp)寄存器的值隨著棧指針sp向下偏移依次入棧,棧指針sp共偏移兩次8+8=16個字節(jié)(arm 64位系統(tǒng)棧指針sp減一偏移8個字節(jié),并且棧是向下增長,所以指令是-16)。mov x29,sp 指令就是將棧指針賦予fp寄存器,此時sp就指向test_2函數(shù)棧的第二片內(nèi)存,因為sp偏移了兩次,fp寄存器的值就是test_2函數(shù)棧的第二片內(nèi)存地址。去除不相關(guān)的指令,直接從test_2函數(shù)跳轉(zhuǎn)到test_1函數(shù)開始分析,看test_1函數(shù)的第一條指令stp x29, x30,[sp,#-16],首先棧指針sp減一,將x30(lr)寄存器的數(shù)據(jù)存入test_1函數(shù)棧的第一片內(nèi)存,這就是test_1函數(shù)的返回地址,接著棧指針sp減一,將x29(fp)寄存器值入棧,存入test_1函數(shù)的第二片內(nèi)存,此時fp寄存器的值正是test_2函數(shù)棧的第二片內(nèi)存地址,本質(zhì)就是將test_2函數(shù)棧的第二片內(nèi)存地址存入test_1函數(shù)棧的第二片內(nèi)存中。接著執(zhí)行mov x29,sp 指令,就是將棧指針sp賦予fp寄存器,此時sp指向test_1函數(shù)棧的第二片內(nèi)存…..

這樣就與上一小結(jié)的分析一致了, 這里就對arm?;厮莸囊话氵^程,做個較為系統(tǒng)的總結(jié):當(dāng)C函數(shù)跳轉(zhuǎn)的B函數(shù)時,先將B函數(shù)的返回地址存入B函數(shù)棧的第一片內(nèi)存,然后將C函數(shù)棧的第二片內(nèi)存地址存入B函數(shù)棧的第二片內(nèi)存,接著將B函數(shù)棧的第二片內(nèi)存地址存入fp寄存器,B函數(shù)跳轉(zhuǎn)到A函數(shù)流程也是這樣。當(dāng)A函數(shù)中崩潰時,先從fp寄存器中獲取A函數(shù)棧的第二片內(nèi)存地址,從中取出B函數(shù)棧的第二片內(nèi)存地址,再從A函數(shù)棧的第一片內(nèi)存取出A函數(shù)的返回地址,也就是B函數(shù)中的指令地址,這樣就推導(dǎo)出B函數(shù)調(diào)用了A函數(shù),同理推導(dǎo)出C函數(shù)調(diào)用了B函數(shù)。

演示的代碼很簡答,但是這個分析是適用于復(fù)雜函數(shù)的,已經(jīng)實際驗證過。

3.1.3 arm 內(nèi)核棧回溯的“bug”

這個不是我危言聳聽,是實際測出來的。比如如下代碼:

一文詳解Linux內(nèi)核的?;厮菖c妙用

這個函數(shù)調(diào)用流程在內(nèi)核崩潰了,內(nèi)核?;厮菔遣粫蛴∩线叺腷函數(shù),有arm 64系統(tǒng)的讀者可以驗證一下,我多次驗證得出的結(jié)論是,如果崩潰的函數(shù)沒有執(zhí)行其他函數(shù),就會打亂?;厮菀?guī)則,為什么呢?請回頭看上一節(jié)的代碼演示

一文詳解Linux內(nèi)核的棧回溯與妙用

匯編代碼是

一文詳解Linux內(nèi)核的?;厮菖c妙用

可以發(fā)現(xiàn),test_a_函數(shù)前兩條指令不是stpx29, x30,[sp,#-16]和mov x29,sp,這兩條指令可是?;厮莸年P(guān)鍵環(huán)節(jié)。怎么解決呢?仔細分析的話,是可以解決的。一般情況,函數(shù)崩潰,fp寄存器保存的數(shù)據(jù)是當(dāng)前函數(shù)棧的第二片內(nèi)存地址,當(dāng)前函數(shù)棧的第一片內(nèi)存地址保存的是函數(shù)返回地址,從該地址取出的數(shù)據(jù)與lr寄存器的數(shù)據(jù)應(yīng)是一致的,因為lr寄存器保存的也是函數(shù)返回地址,如果不相同,說明該函數(shù)中沒有執(zhí)行stp x29, x30,[sp,#-16]指令,此時應(yīng)使用lr寄存器的值作為函數(shù)返回地址,并且此時fp寄存器本身就是上一級函數(shù)棧的第二片內(nèi)存地址,有了這個數(shù)據(jù)就能按照前文的方法棧回溯了。解決方法就是這樣,讀者可以仔細體會一下我的分析。

3.2 mips ?;厮葸^程

前文說過,mips內(nèi)核崩潰處理流程是

do_page_fault()->die()->show_registers()->show_stacktrace()->show_backtrace()

打印崩潰函數(shù)流程是在show_backtrace()函數(shù)。

3.2.1 mips 架構(gòu)內(nèi)核?;厮菰矸治?/p>

arch/mips/kernel/ traps.c

一文詳解Linux內(nèi)核的?;厮菖c妙用

可以發(fā)現(xiàn),與arm架構(gòu)?;厮萘鞒袒疽恢?。函數(shù)開頭是對sp、ra、pc寄存器器賦值,sp和pc與arm架構(gòu)一致,ra相當(dāng)于arm架構(gòu)的lr寄存器,沒有arm架構(gòu)的fp寄存器。print_ip_sym函數(shù)就是根據(jù)pc值打印形如[] chrdev_open+0x12/0x4B1的字符串,不再介紹。關(guān)鍵還是unwind_stack_by_address函數(shù)。mips架構(gòu)由于沒有像arm架構(gòu)的fp寄存器,導(dǎo)致?;厮莸倪^程比arm架構(gòu)復(fù)雜很多,為了讀者理解方便,決定先從mips架構(gòu)匯編代碼分析,指出與?;厮萦嘘P(guān)的指令,推出?;厮莸牧鞒蹋詈笾v解內(nèi)核代碼。

如下是mips架構(gòu)內(nèi)核驅(qū)動ko文件的 C代碼和匯編代碼。

C代碼

一文詳解Linux內(nèi)核的棧回溯與妙用

匯編代碼

這里說明一下,驅(qū)動ko反匯編出來的指令是從0地址開始的,為了敘述方便,筆者加了0x80000000,實際的匯編代碼不是這樣的。

一文詳解Linux內(nèi)核的?;厮菖c妙用

這里直接介紹根據(jù)筆者的分析,總結(jié)mips架構(gòu)內(nèi)核?;厮莸脑?,分析完后再結(jié)合源碼驗證。mips架構(gòu)沒有fp寄存器,假設(shè)在test_c函數(shù)中0X80000048地址處指令崩潰了,首先利用內(nèi)核的kallsyms模塊,根據(jù)崩潰時的指令地址找出該指令是哪個函數(shù)的指令,并且找出該指令地址相對函數(shù)指令首地址的偏移ofs,在本案例中ofs = 0X10(0X80000048 – 0X80000038 =0X10),這樣就能算出test_c函數(shù)的指令首地址是 0X80000048 - 0X10 = 0X80000038。然后就從地址0X80000038開始,依次取出每條指令,找到addiu sp,sp,-24 和sw ra,20(sp),內(nèi)核有標準函數(shù)可以判斷出這兩條指令,下文可以看到。addiu sp,sp,-24是test_c函數(shù)的第一條指令,棧指針向下偏移24個字節(jié),筆者認為是為test_c函數(shù)分配棧大小( 24個字節(jié));sw ra,20(sp)指令將test_c函數(shù)返回地址存入sp +20 內(nèi)存地址處,此時sp指向的是test_c函數(shù)的棧頂,sp+20就是test_c函數(shù)棧的第二片內(nèi)存,該函數(shù)棧大小24字節(jié),一共24/4=6片內(nèi)存。

根據(jù)sw ra,20(sp)指令知道test_c函數(shù)返回地址在test_c函數(shù)棧的存儲位置,取出該地址的數(shù)據(jù),就知道是test_a函數(shù)的指令地址,當(dāng)然就知道是test_a函數(shù)調(diào)用了test_c函數(shù)。并根據(jù)addiu sp,sp,-24指令知道test_c函數(shù)??傆?4字節(jié),因為test_c函數(shù)崩潰時,棧指針sp指向test_c函數(shù)棧頂,sp+24就是test_a函數(shù)的棧頂,因為test_a函數(shù)調(diào)用了test_c函數(shù),兩個函數(shù)的棧必是緊挨著的。按照上述推斷,首先知道了test_a函數(shù)中的指令地址了,使用內(nèi)核kallsyms功能就推算出test_a函數(shù)的指令首地址,同時也計算出test_a函數(shù)的棧頂,就能按照上述規(guī)律找出誰調(diào)用了test_a函數(shù),以及該函數(shù)的棧頂。依次就能找出所有函數(shù)調(diào)用關(guān)系。

關(guān)于內(nèi)核的kallsyms,筆者的理解是:執(zhí)行過cat /proc/kallsyms命令的讀者,應(yīng)該了解過,該命令會打印內(nèi)核所有的函數(shù)的首地址和函數(shù)名稱,還有內(nèi)核編譯后生成的System.map文件,記錄內(nèi)核函數(shù)、變量的名稱與內(nèi)存地址等等,kallsyms也是記錄了這些內(nèi)容,當(dāng)執(zhí)行kallsyms_lookup_size_offset(0X80000048, &size,&ofs)函數(shù),就能根據(jù)0X80000048指令地址計算出處于test_c函數(shù),并將相對于test_c函數(shù)指令首地址的偏移0X10存入ofs,test_c函數(shù)指令總字節(jié)數(shù)存入size。筆者沒有研究過kallsyms模塊,但是可以理解到,內(nèi)核的所有函數(shù)都是按照分配的地址,順序排布。如果記錄了每個函數(shù)的首地址和名稱,當(dāng)知道函數(shù)的任何一條指令地址,就能在其中搜索比對,找到該指令處于按個函數(shù),計算出函數(shù)首地址,該指令的偏移。

3.2.2 mips 架構(gòu)內(nèi)核?;厮莺诵脑创a分析

3.2.1詳細講述了mips棧回溯的原理,接著講解?;厮莸暮诵暮瘮?shù)unwind_stack_by_address。

一文詳解Linux內(nèi)核的?;厮菖c妙用

一文詳解Linux內(nèi)核的?;厮菖c妙用

一文詳解Linux內(nèi)核的棧回溯與妙用

一文詳解Linux內(nèi)核的?;厮菖c妙用

上述源碼已經(jīng)在關(guān)鍵點做了詳細注釋,其實就是對3.2.1節(jié)?;厮菰淼耐晟?,請讀者自己分析,這里不再贅述。但是有一點請注意,就是藍色注釋,這是針對崩潰的函數(shù)沒有執(zhí)行其他函數(shù)的情況,此時該函數(shù)沒有類似匯編指令swra,20(sp) 將函數(shù)返回地址保存到棧中,計算方法就變了,要直接使用ra寄存器的值作為函數(shù)返回地址,計算上一級函數(shù)棧頂?shù)姆椒ㄟ€是一致的,后續(xù)?;厮莸姆椒ㄅc前文相同。

4 linux內(nèi)核?;厮莸膽?yīng)用

文章最開頭說過,筆者在實際項目開發(fā)過程,已經(jīng)總結(jié)出了3個內(nèi)核?;厮莸膽?yīng)用:

1 應(yīng)用程序崩潰,像內(nèi)核?;厮菀粯哟蛴≌麄€崩潰過程,應(yīng)用函數(shù)的調(diào)用關(guān)系

2 應(yīng)用程序發(fā)生double free,像內(nèi)核?;厮菀粯哟蛴ouble free過程,應(yīng)用函數(shù)的調(diào)用關(guān)系

3 內(nèi)核陷入死循環(huán),sysrq的內(nèi)核線程?;厮莨δ軣o法發(fā)揮作用時,在系統(tǒng)定時鐘中斷函數(shù)中對卡死線程?;厮?,找出卡死位置

下文逐一講解。

4.1 應(yīng)用程序崩潰?;厮?/p>

筆者在研究過內(nèi)核棧回溯功能后,不禁發(fā)問,為什么不能用同樣的方法對應(yīng)用程序的崩潰?;厮菽兀坎还苁莾?nèi)核空間,應(yīng)用空間,程序的指令是一樣的,無非是地址有差異,函數(shù)入棧出棧原理是一樣的。?;厮莸娜肟?,arm架構(gòu)是獲取崩潰線程/進程的pc、fp、lr寄存器值,mips架構(gòu)是獲取pc、ra、sp寄存器值,有了這些值就能按照各自的回溯規(guī)律,實現(xiàn)?;厮荨睦碚撋蟻碚f,完全是可以實現(xiàn)的。

4.1 .1 arm架構(gòu)應(yīng)用程序?;厮莸膶崿F(xiàn)

當(dāng)應(yīng)用程序發(fā)生崩潰,與內(nèi)核一樣,系統(tǒng)自動將崩潰時所有的CPU寄存器存入struct pt_regs結(jié)構(gòu),一般崩潰入口函數(shù)是do_page_fault,又因為是應(yīng)用程序崩潰,所以是__do_user_fault函數(shù),這里直接分析__do_user_fault。

一文詳解Linux內(nèi)核的?;厮菖c妙用

在該函數(shù)中,tsk就是崩潰的線程,struct pt_regs *regs就指向線程/進程崩潰時的CPU寄存器結(jié)構(gòu)。regs->[29]就是fp寄存器,regs->[30]是lr寄存器, regs->pc的意義很直觀?,F(xiàn)在有了崩潰應(yīng)用線程/進程當(dāng)時的fp、sp、lr寄存器,就能?;厮萘?,完全仿照內(nèi)核dump_backtrace的方法,請看筆者寫在user_thread_ dump_backtrace函數(shù)中的演示代碼。

一文詳解Linux內(nèi)核的?;厮菖c妙用

一文詳解Linux內(nèi)核的棧回溯與妙用

與內(nèi)核?;厮菰硪恢?,打印崩潰過程每個函數(shù)的指令地址,然后在應(yīng)用程序的反匯編文件中查找,就能找到該指令處于的函數(shù),如果不理解,請看文章前方講解的內(nèi)核?;厮荽a與原理。請注意,這不是筆者項目實際用的?;厮荽a,實際的改動完善了很多,這只是演示原理的示例代碼。

還有一點就是,筆者在3.1.3節(jié)提到的,假如崩潰的函數(shù)中沒有調(diào)用其他函數(shù),那上述?;厮菥蜁袉栴},就不會打印第二級函數(shù),解決方法講的也有,解決的代碼這里就不再列出了。

4.1 .2 mips架構(gòu)應(yīng)用程序?;厮莸膶崿F(xiàn)

mips 架構(gòu)不僅內(nèi)核?;厮莸拇a比arm復(fù)雜,應(yīng)用程序的?;厮莞鼜?fù)雜,還有未知bug,即便這樣,還是講解一下具體的解決思路,最后講一下存在的問題。

先簡單回顧一下內(nèi)核棧回溯的原理,首先根據(jù)崩潰函數(shù)的pc值,運用內(nèi)核kallsyms模塊,計算出該函數(shù)的指令首地址,然后從指令首地址開始分析,找出類似addiu sp,sp,-24和sw ra,20(sp)指令,前者可以找到該函數(shù)的棧大小,棧指針sp加上這個數(shù)值,就知道上一級函數(shù)的棧頂?shù)刂?崩潰時sp指向崩潰函數(shù)的棧頂);后者知道函數(shù)返回地址在該函數(shù)棧中存儲的地址,從該地址就能獲取該函數(shù)的返回地址,就是上一級函數(shù)的指令地址,也就知道了上一級函數(shù)是哪個(同樣使用內(nèi)核kallsyms模塊)。知道了上一級函數(shù)的指令地址和棧頂?shù)刂?,按照同樣方法,就能知道再上一級的函?shù)…….

問題來了,內(nèi)核有kallsyms模塊記錄了每個函數(shù)的首地址和函數(shù)名字,函數(shù)還是順序排布。應(yīng)用程序并沒有kallsyms模塊,即便知道了崩潰函數(shù)的pc值,也無法按照同樣的方法找到崩潰函數(shù)的指令首地址,真的沒有方法?其實還有一個最簡單的方法。先列出一段一個應(yīng)用程序函數(shù)的匯編代碼,如下所示,與內(nèi)核態(tài)的有小的差別。

一文詳解Linux內(nèi)核的棧回溯與妙用

現(xiàn)在假如從0X4006a4地址處取指,運行后崩潰了。崩潰發(fā)生時,能像arm架構(gòu)一樣獲取崩潰前的CPU寄存器值,最重要就是pc、sp、ra值。pc值就是0X4006a4,然后令一個unsigned long型指針指向該內(nèi)存地址0X4006a4,每次減一,并取出該地址的指令數(shù)據(jù)分析,這樣肯定能分析到addiu sp,sp,-32 和sw ra,28(sp)指令,我想看到這里,讀者應(yīng)該可以清楚方法了。沒錯,就是以崩潰時pc值作為基地址,每次減1并從對應(yīng)地址取出指令分析,直到分析出久違的addiu sp,sp,-32 和sw ra,28(sp)類似指令,再結(jié)合崩潰時的棧指針值sp,就能計算出該函數(shù)的返回地址和上一級函數(shù)的棧頂?shù)刂?。后續(xù)的方法,就與內(nèi)核棧回溯的過程一致了。下方列出演示的代碼。

一文詳解Linux內(nèi)核的?;厮菖c妙用

為了一致性,應(yīng)用程序棧回溯的函數(shù)還是采用名字user_thread_ dump_backtrace。

一文詳解Linux內(nèi)核的棧回溯與妙用

一文詳解Linux內(nèi)核的?;厮菖c妙用

一文詳解Linux內(nèi)核的?;厮菖c妙用

如上就是mips應(yīng)用程序棧回溯的示例代碼,只是一個演示,筆者實際使用的代碼要復(fù)雜太多。讀者使用時,要基于這個基本原理,多調(diào)試,才能應(yīng)對各種情況,筆者前后調(diào)試幾周才穩(wěn)定。由于這個方法并不是標準的,實際使用時還是會出現(xiàn)誤報函數(shù)現(xiàn)象,分析了發(fā)生誤報的匯編代碼及C代碼,發(fā)現(xiàn)當(dāng)函數(shù)代碼復(fù)雜時,函數(shù)的匯編指令會變得非常復(fù)雜,會出現(xiàn)相似指令等等,讀者實際調(diào)試時就會發(fā)現(xiàn)。這個mips應(yīng)用程序棧回溯的方法,可以應(yīng)對大部分崩潰情況,但是有誤報的可能,優(yōu)化的空間非常大,這點請讀者注意。

4.2 應(yīng)用程序double free 內(nèi)核棧回溯

double free是在C庫層發(fā)生的,正常情況內(nèi)核無能為力,但是筆者研究過后,發(fā)現(xiàn)照樣可以實現(xiàn)對發(fā)生double free應(yīng)用進程的棧回溯。

以arm架構(gòu)為例,doublefree C庫層的代碼,大體原理是,當(dāng)檢測到double free(本人實驗時,一片malloc分配的內(nèi)存free兩次就會發(fā)生),就會執(zhí)行kill系統(tǒng)調(diào)用函數(shù),向出問題的進程發(fā)送SIGABRT信號,既然是系統(tǒng)調(diào)用,從用戶空間進入內(nèi)核空間時,就會將應(yīng)用進程用戶空間運行時的CPU寄存器pc、sp、lr等保存到進程的內(nèi)核棧中,發(fā)送信號內(nèi)核必然執(zhí)行send_signal函數(shù)。在該函數(shù)中,使用struct pt_regs *regs = task_pt_regs(current)方法就能從當(dāng)前進程內(nèi)核棧中獲取進入內(nèi)核空間前,用戶空間運行指令的pc、sp、fp等CPU寄存器值,有了這些值,就能按照用戶空間進程崩潰棧回溯方法一樣,對double free的進程棧回溯了。比如,A函數(shù)double free,A函數(shù)->C庫函數(shù)1-> C庫函數(shù)2->C庫函數(shù)3(檢測到double free并發(fā)送SIGABRT信號,執(zhí)行系統(tǒng)調(diào)用進入內(nèi)核空間發(fā)送信號)?;厮莸慕Y(jié)果是:C庫函數(shù)3 ? C庫函數(shù)2 ? C庫函數(shù)1? A函數(shù)。

源碼不再列出,相信讀者理解的話是可以自己開發(fā)的。其中task_pt_regs函數(shù)的使用,需要讀者對進程內(nèi)核棧有一定的了解。

筆者有個理解,當(dāng)獲取某個進程運行指令某一時間點的CPU寄存器pc、lr、fp的值,就能對該進程進行?;厮?。

4.3 內(nèi)核發(fā)生死循環(huán)sysrq無效時棧回溯的應(yīng)用

內(nèi)核的sysrq中有一個方法,執(zhí)行后可以對所有線程進行內(nèi)核空間函數(shù)棧回溯,但是本人遇到過一次因某個外設(shè)導(dǎo)致的死循環(huán),該方法打印的?;厮菪畔⒍际莾?nèi)核級的函數(shù),沒有頭緒。于是,嘗試在系統(tǒng)定時鐘中斷函數(shù)中實現(xiàn)卡死線程的?;厮?也可以在account_process_tick內(nèi)核標準函數(shù)中,系統(tǒng)定時鐘中斷函數(shù)會執(zhí)行到)。原理是,當(dāng)一個內(nèi)核線程卡死時,首先考慮在某個函數(shù)陷入死循環(huán),系統(tǒng)定時鐘中斷是不斷產(chǎn)生的,此時current線程很大概率就是卡死線程(要考慮內(nèi)核搶占,內(nèi)核支持搶占時,內(nèi)核某處陷入死循環(huán)照樣可以調(diào)度出去),然后使用struct pt_regs *regs = get_irq_regs()方法,就能獲取中斷前線程的pc、sp、fp等寄存器值,有了這些值,就能按照內(nèi)核線程崩潰棧回溯原理,對卡死線程函數(shù)調(diào)用過程?;厮?,找到卡死函數(shù)。mips架構(gòu)?;厮莸暮诵暮瘮?shù)show_backtrace()定義如下,只要傳入內(nèi)核線程的struct task_struct和structpt_regs結(jié)構(gòu),就能對內(nèi)核線程當(dāng)時指令的執(zhí)行進行?;厮?。

static void show_backtrace(struct task_struct *task, const struct pt_regs *regs)

4.4 應(yīng)用程序鎖死時對所有應(yīng)用線程的棧回溯

以arm架構(gòu)為例。當(dāng)應(yīng)用鎖死,尤其是偶現(xiàn)的鎖死卡死問題,可以使用?;厮莸乃悸方鉀Q。以單核CPU為例,應(yīng)用程序的所有線程,正常情況,兩種狀態(tài):正在運行和其他狀態(tài)(大部分情況是休眠)。休眠的應(yīng)用線程,一般要先進入內(nèi)核空間,將應(yīng)用層運行時的pc、lr、fp等寄存器存入內(nèi)核棧,執(zhí)行schdule函數(shù)讓出CPU使用權(quán),最后線程休眠。此時可以通過tesk_pt_regs函數(shù)從線程內(nèi)核棧中獲取線程進入內(nèi)核空間前的pc、lr、fp等寄存器的數(shù)據(jù)。正在運行的應(yīng)用線程,系統(tǒng)定時鐘中斷產(chǎn)生后,系統(tǒng)要執(zhí)行硬件定時器中斷,此時可以通過get_irq_regs函數(shù)獲取中斷前的pc、lr、fp等寄存器的值。不管應(yīng)用線程是否正在運行,都可以獲取線程當(dāng)時用戶空間運行指令的pc、lr、fp等寄存器數(shù)據(jù)。當(dāng)應(yīng)用某個線程,不管是使用鎖異常而長時間休眠,還是陷入死循環(huán),從內(nèi)核的進程運行隊列中,依次獲取到所有應(yīng)用線程的pc、lr、fp等寄存器的數(shù)據(jù)后(可以考慮在account_process_tick函數(shù)實現(xiàn)),就可以按照前文思路對應(yīng)用線程?;厮荩页鰬岩牲c。

實際使用時,要防止內(nèi)核線程的干擾,task->mm可以用來判斷,內(nèi)核線程為NULL。當(dāng)然也可以通過線程名字加限制,對疑似的幾個線程棧回溯。應(yīng)用線程正在內(nèi)核空間運行時,這種情況用這個方法就有問題,這時需加限制,比如通過get_irq_regs函數(shù)獲取到 pc值后,判斷是在內(nèi)核空間還是用戶空間。讀者實現(xiàn)該功能時,有不少其他細節(jié)要注意,這里不再一一列出。

5 應(yīng)用程序?;厮莸恼雇?/p>

關(guān)于應(yīng)用程序的?;厮?,筆者正在考慮一個方法,使應(yīng)用程序的?;厮菽苷嬲駜?nèi)核一樣打印出函數(shù)的符號及偏移,比如

現(xiàn)有的方法只能實現(xiàn)如下效果:

之后還得對應(yīng)用程序反匯編才能找到崩潰的函數(shù)。

筆者的分析是,理論上是可以實現(xiàn)的,只要仿照內(nèi)核的kallsyms方法,按照順序記錄每個應(yīng)用函數(shù)的函數(shù)首地址和函數(shù)名字到一個文件中,當(dāng)應(yīng)用程序崩潰時,內(nèi)核中讀取這個文件,根據(jù)崩潰的指令地址在這個文件中搜索,就能找到該指令處于哪個函數(shù)中,本質(zhì)還是實現(xiàn)了與內(nèi)核kallsyms類似的方法。有了這個功能,不僅應(yīng)用程序棧回溯能打印函數(shù)的名稱與偏移,還能讓mips架構(gòu)應(yīng)用程序崩潰的?;厮莅凑諆?nèi)核崩潰?;厮莸脑韥韺崿F(xiàn),不會再出現(xiàn)函數(shù)誤報現(xiàn)象,不知讀者是否理解我的思路?后續(xù)有機會,會嘗試開發(fā)這個功能并分享出來。

6總結(jié)

實際項目調(diào)試時,發(fā)現(xiàn)?;厮莸膽?yīng)用價值非常大,掌握?;厮莸脑恚粌H對內(nèi)核調(diào)試有很大幫助,對加深內(nèi)核的理解也是有不少益處。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11304

    瀏覽量

    209476
  • LINUX內(nèi)核
    +關(guān)注

    關(guān)注

    1

    文章

    316

    瀏覽量

    21650

原文標題:(重磅原創(chuàng))冬之焱: 談?wù)凩inux內(nèi)核的棧回溯與妙用

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    詳解Linux內(nèi)核源碼組織結(jié)構(gòu)

    概要:本文內(nèi)容包含Linux源碼樹結(jié)構(gòu)分析、Linux Makefile分析、Kconfig文件分析、Linux內(nèi)核配置選項分析。這些知識是為了理解
    的頭像 發(fā)表于 05-10 19:28 ?5764次閱讀

    詳解Linux中的各種

    首先, (stack) 是種串列形式的數(shù)據(jù)結(jié)構(gòu)。這種數(shù)據(jù)結(jié)構(gòu)的特點是后入先出 (LIFO, Last In First Out),數(shù)據(jù)只能在串列的端 (稱為:頂 top) 進行
    發(fā)表于 09-28 14:51 ?1312次閱讀

    詳解Linux內(nèi)核搶占實現(xiàn)機制

    本文詳解Linux內(nèi)核搶占實現(xiàn)機制。首先介紹了內(nèi)核搶占和用戶搶占的概念和區(qū)別,接著分析了不可搶占內(nèi)核的特點及實時系統(tǒng)中實現(xiàn)
    發(fā)表于 08-06 06:16

    Linux設(shè)備驅(qū)動開發(fā)詳解:基于最新的Linux 4.0內(nèi)核

    Linux設(shè)備驅(qū)動開發(fā)詳解:基于最新的Linux 4.0內(nèi)核
    發(fā)表于 08-31 12:29

    個實例展示Linux內(nèi)核幀的入和退過程

    1、Linux內(nèi)核調(diào)試方法總結(jié)之幀  幀  幀和指針可以說是C語言的精髓。幀是
    發(fā)表于 11-04 15:47

    C語言之詳解_ifdef等宏及妙用

    C語言之詳解_ifdef等宏及妙用的教程
    發(fā)表于 11-16 19:03 ?0次下載

    linux2.6內(nèi)核設(shè)備驅(qū)動模型精華

    linux 內(nèi)核驅(qū)動部分詳解
    發(fā)表于 04-27 10:43 ?20次下載

    Linux設(shè)備驅(qū)動開發(fā)詳解》第4章、Linux內(nèi)核模塊

    Linux設(shè)備驅(qū)動開發(fā)詳解》第4章、Linux內(nèi)核模塊
    發(fā)表于 10-27 14:15 ?0次下載
    《<b class='flag-5'>Linux</b>設(shè)備驅(qū)動開發(fā)<b class='flag-5'>詳解</b>》第4章、<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>模塊

    linux內(nèi)核rcu機制詳解

    Linux內(nèi)核源碼當(dāng)中,關(guān)于RCU的文檔比較齊全,你可以在 /Documentation/RCU/ 目錄下找到這些文件。Paul E. McKenney 是內(nèi)核中RCU源碼的主要實現(xiàn)者,他也寫了很多RCU方面的文章。今天我們而主
    發(fā)表于 11-13 16:47 ?8779次閱讀
    <b class='flag-5'>linux</b><b class='flag-5'>內(nèi)核</b>rcu機制<b class='flag-5'>詳解</b>

    詳解Linux內(nèi)核測試現(xiàn)狀

    is going on there? Is stable kernel really stable? 剛好今年9月在洛杉磯舉辦的《Linux Plumbers Conference》有個BOF(birds
    的頭像 發(fā)表于 01-01 09:06 ?3201次閱讀

    Linux的進程內(nèi)核的認識

    在每個進程的生命周期中,必然會通過到系統(tǒng)調(diào)用陷入內(nèi)核。在執(zhí)行系統(tǒng)調(diào)用陷入內(nèi)核之后,這些內(nèi)核代碼所使用的并不是原先用戶空間中的
    發(fā)表于 05-12 08:53 ?627次閱讀
    對<b class='flag-5'>Linux</b>的進程<b class='flag-5'>內(nèi)核</b><b class='flag-5'>棧</b>的認識

    你了解Linux內(nèi)核中的常見符號?

    內(nèi)核調(diào)用可以用來方便標記bug,提供斷言并輸出信息。最常用的兩個是BUG()和BUG_ON()。當(dāng)被調(diào)用的時候,它們會引發(fā)oops,導(dǎo)致回溯和錯誤信息的打印。
    發(fā)表于 05-15 15:47 ?589次閱讀
    你了解<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>中的常見符號?

    Linux內(nèi)核GPIO操作函數(shù)的詳解分析

    本文檔的主要內(nèi)容詳細介紹的是Linux內(nèi)核GPIO操作函數(shù)的詳解分析免費下載。
    發(fā)表于 01-22 16:58 ?28次下載

    Arm64回溯 結(jié)構(gòu)介紹

    ,F(xiàn)A)、滿減(Full Descendant Stack,F(xiàn)D)。常用的是滿減,Linux內(nèi)核也使用滿減。 下圖是
    的頭像 發(fā)表于 07-28 11:25 ?830次閱讀
    Arm64<b class='flag-5'>棧</b><b class='flag-5'>回溯</b> 結(jié)構(gòu)介紹

    RVBacktrace RISC-V極簡回溯組件

    RVBacktrace組件簡介個極簡的RISC-V回溯組件。功能在需要的地方調(diào)用組件提供的唯API,開始當(dāng)前環(huán)境的
    的頭像 發(fā)表于 09-15 08:12 ?381次閱讀
    RVBacktrace RISC-V極簡<b class='flag-5'>棧</b><b class='flag-5'>回溯</b>組件