dump_stack介绍以及内核符号表的生成和查找过程
內(nèi)核中的dump_stack()
獲得內(nèi)核中當(dāng)前進(jìn)程的棧回溯信息需要用到的最重要的三個(gè)內(nèi)容就是:
棧指針:sp寄存器,用來跟蹤程序執(zhí)行過程。
返回地址:ra寄存器,用來獲取函數(shù)的返回地址。
程序計(jì)數(shù)器:epc,用于定位當(dāng)前指令的位置。
本文的內(nèi)容都是基于mips體系架構(gòu)的,如果你不搞mips,就只看個(gè)大致流程就可以了,不然可能會(huì)被某些內(nèi)容誤導(dǎo)。在ARM中,這三個(gè)寄存器分別為SP、LR和PC寄存器。
dump_stack()用于回溯函數(shù)調(diào)用關(guān)系,他需要做的工作很簡(jiǎn)單:
1.???從進(jìn)程棧中找到當(dāng)前函數(shù)(callee)的返回地址。
2.???根據(jù)函數(shù)返回地址,從代碼段中定位該地址位于哪個(gè)函數(shù)中,找到的函數(shù)即為caller函數(shù)。
3.???打印caller函數(shù)的函數(shù)名。
4.???重復(fù)前3個(gè)步驟。直到返回值為0或不在內(nèi)核記錄的符號(hào)表范圍內(nèi)。
在編譯程序的時(shí)候,所有函數(shù)所需要的棧空間的大小都已經(jīng)計(jì)算出來,如果函數(shù)需要保存返回地址,返回地址在該函數(shù)的棧空間中保存的位置也都計(jì)算出來了。所以,我們想得到返回地址,只需得到每個(gè)函數(shù)棧即可,而所有函數(shù)棧都放在進(jìn)程的棧中,棧頂為sp。
返回地址是caller函數(shù)中將要執(zhí)行的指令,是指向代碼段的,這個(gè)更容易得到,因?yàn)榇a段在編譯時(shí)就確定了。
當(dāng)前函數(shù)的位置通過pc的值可以得到。
例如,現(xiàn)在有func0調(diào)用func1,func1又調(diào)用func2,在func2執(zhí)行過程中,進(jìn)程棧空間大致如下:
左圖為棧空間,棧頂為sp,右圖為程序代碼的部分內(nèi)容。右圖中的實(shí)曲線表示出了函數(shù)之間的調(diào)用和返回關(guān)系。調(diào)用關(guān)系通過跳轉(zhuǎn)指令完成,返回地址通過左圖每個(gè)函數(shù)棧空間中存儲(chǔ)的返回地址指定。這樣我們就可以得到函數(shù)的調(diào)用關(guān)系,并通過每個(gè)函數(shù)的地址打印出函數(shù)名。
那dump_stack的工作流程就很清楚了。我就不帖代碼了,因?yàn)榛旧隙际求w系結(jié)構(gòu)相關(guān)的操作。
需要說明的一個(gè)地方是,通過函數(shù)的地址來打印函數(shù)名是通過格式控制符%pS來打印的:
printk("[<%p>] %pS\n", (void *) ip,(void *) ip);
在內(nèi)核代碼樹的lib/vsprintf.c中的pointer函數(shù)中,說明了printk中的%pS的意思:
[cpp]?view plaincopy
即'S'表示打印符號(hào)名,而這個(gè)符號(hào)名是kallsyms里獲取的。
可以看一下kernel/kallsyms.c中的kallsyms_lookup()函數(shù),它負(fù)責(zé)通過地址找到函數(shù)名,分為兩部分:
1. 如果地址在編譯內(nèi)核時(shí)得到的地址范圍內(nèi),就查找kallsyms_names數(shù)組來獲得函數(shù)名。
2. 如果這個(gè)地址是某個(gè)內(nèi)核模塊中的函數(shù),則在模塊加載后的地址表中查找。
kallsyms_lookup()最終返回字符串“函數(shù)名+offset/size[mod]”,交給printk打印。
關(guān)于內(nèi)核符號(hào)表kallsyms_names可參考我的另一篇文章點(diǎn)擊打開鏈接。
實(shí)現(xiàn)應(yīng)用程序中的dump_stack()
按照如上所述,實(shí)現(xiàn)一個(gè)用戶態(tài)程序的dump_stack好像不是什么難事,因?yàn)樯厦嬲f的步驟在用戶態(tài)都可以完成,程序運(yùn)行的方式也基本上是相同的。
那我們實(shí)現(xiàn)一個(gè)dump_stack需要做的事情只有兩點(diǎn):
1.???獲得程序當(dāng)前運(yùn)行時(shí)間點(diǎn)的pc值和棧指針sp。這樣就可以得到每個(gè)函數(shù)棧中的返回地址。
2.???構(gòu)造和內(nèi)核符號(hào)表相同的應(yīng)用程序符號(hào)表。
需要注意,不同用戶進(jìn)程都擁有自己的虛擬地址空間,所以棧回溯只能在本進(jìn)程中完成。
具體實(shí)現(xiàn)當(dāng)然也是體系結(jié)構(gòu)相關(guān)的。既然原理都知道了,那我就直接給出代碼供參考(mips的)。代碼見https://github.com/castoz/backtrace。
其中backtrace.c實(shí)現(xiàn)了棧回溯,uallsyms.c用于生成符號(hào)表,main.c中為測(cè)試代碼。
backtrace.c中提供了兩個(gè)接口供其他文件調(diào)用:
show_backtrace():打印函數(shù)的回溯信息。
addr_to_name(addr):打印addr對(duì)應(yīng)的函數(shù)名。
uallsyms.c文件直接使用內(nèi)核中的scripts/kallsyms.c,只需要做少量修改,具體的改動(dòng)為:
1. 符號(hào)基準(zhǔn)地址改為__start。
2. 需要記錄的符號(hào)范圍改為在_init到_fini之間或_init到_end之間。
3. 維護(hù)uallsyms_addresses、uallsyms_num_syms和uallsyms_names三個(gè)全局變量,不使用壓縮算法,所以不需要其他三個(gè)全局變量。
4. 在生成的匯編代碼中刪除"#include <asm/types.h>"一行,因?yàn)樵诰幾g時(shí)不需要。
測(cè)試文件main.c的內(nèi)容:
[cpp]?view plaincopy
運(yùn)行結(jié)果:
[plain]?view plaincopy
參考自:http://blog.csdn.net/jasonchen_gbd/article/details/44066815
在內(nèi)核中維護(hù)者一張符號(hào)表,記錄了內(nèi)核中所有的符號(hào)(函數(shù)、全局變量等)的地址以及名字,這個(gè)符號(hào)表被嵌入到內(nèi)核鏡像中,使得內(nèi)核可以在運(yùn)行過程中隨時(shí)獲得一個(gè)符號(hào)地址對(duì)應(yīng)的符號(hào)名。而內(nèi)核代碼中可以通過?printk("%pS\n", addr)?打印符號(hào)名。
本文介紹內(nèi)核符號(hào)表的生成和查找過程。
1. System.map和/proc/kallsyms
System.map文件是編譯內(nèi)核時(shí)生成的,它記錄了內(nèi)核中的符號(hào)列表,以及符號(hào)在內(nèi)存中的虛擬地址。這個(gè)文件通過nm命令生成,具體可參考內(nèi)核目錄下的scripts/mksysmap腳本。System.map中每個(gè)條目由三部分組成,例如:
f0081e80 T alloc_vfsmnt
即“地址? 符號(hào)類型? 符號(hào)名”
其中符號(hào)類型有如下幾種:
- ? A =Absolute
- ? B =Uninitialised data (.bss)
- ? C = Comonsymbol
- ? D =Initialised data
- ? G =Initialised data for small objects
- ? I = Indirectreference to another symbol
- ? N =Debugging symbol
- ? R = Readonly
- ? S =Uninitialised data for small objects
- ? T = Textcode symbol
- ? U =Undefined symbol
- ? V = Weaksymbol
- ? W = Weaksymbol
- ?Corresponding small letters are local symbols
/proc/kallsyms文件是在內(nèi)核啟動(dòng)后生成的,位于文件系統(tǒng)的/proc目錄下,實(shí)現(xiàn)代碼見kernel/kallsyms.c。前提是內(nèi)核必須打開CONFIG_KALLSYMS編譯選項(xiàng)。它和System.map的區(qū)別是它同時(shí)包含了內(nèi)核模塊的符號(hào)列表。
通常情況下我們只需要_stext~_etext和_sinittext~_einittext之間的符號(hào),如果需要將nm命令獲得的所有符號(hào)都記錄下來,則需要開啟內(nèi)核的CONFIG_KALLSYMS_ALL編譯選項(xiàng),不過一般是不需要打開的。
2. 內(nèi)核符號(hào)表
內(nèi)核在執(zhí)行過程中,可能需要獲得一個(gè)地址所在的函數(shù),比如在輸出某些調(diào)試信息的時(shí)候。一個(gè)典型的例子就是使用dump_stack()函數(shù)打印棧回溯信息。
但是內(nèi)核在查找一個(gè)地址對(duì)應(yīng)的函數(shù)名時(shí),沒有求助于上述兩個(gè)文件,而是在編譯內(nèi)核時(shí),向vmlinux嵌入了一個(gè)符號(hào)表,這樣做可能是為了方便快速的查找并避免文件操作帶來的不良影響。
2.1 內(nèi)核符號(hào)表的結(jié)構(gòu)
內(nèi)嵌的符號(hào)表是通過內(nèi)核目錄下的scripts/kallsyms工具生成的,工具的源碼為相同目錄下的kallsyms.c。這個(gè)工具的用法如下:
[plain]?view plaincopy
可見同樣是通過nm命令得到vmlinux的符號(hào)表,并將這些符號(hào)表信息進(jìn)行調(diào)整,最終生成一個(gè)匯編文件。這個(gè)匯編文件中包含了6個(gè)全局變量:kallsyms_addresses,kallsyms_num_syms,kallsyms_names,kallsyms_markers,kallsyms_token_table和kallsyms_token_index,其中:
- kallsyms_addresses:一個(gè)數(shù)組,存放所有符號(hào)的地址列表,按地址升序排列。
- kallsyms_num_syms:符號(hào)的數(shù)量。
- kallsyms_names:一個(gè)數(shù)組,存放所有符號(hào)的名稱,和kallsyms_addresses一一對(duì)應(yīng)。
其他三個(gè)全局變量的含義后續(xù)會(huì)提到。
這些變量被嵌入在vmlinux中,所以在內(nèi)核代碼中直接extern就可以使用。例如dump_stack()就是通過這些變量來查找一個(gè)地址對(duì)應(yīng)的函數(shù)名的。
那由scripts/kallsyms生成的匯編文件是如何嵌入到vmlinux中的呢。在編譯內(nèi)核的后期主要進(jìn)行了一下幾步額外的編譯和鏈接過程:
此時(shí),上面的那6個(gè)全局變量被寫進(jìn)vmlinux中的“.rodata”段(所以還是叫全局常量吧),內(nèi)核代碼就可以使用了,使用前需extern一下:
[cpp]?view plaincopy
weak屬性表示當(dāng)我們不確定外部模塊是否提供了某個(gè)變量或函數(shù)時(shí),可以將這個(gè)變量或函數(shù)定義為弱屬性,如果外部有定義則使用,沒有定義則相當(dāng)于自己定義。
在使用這6個(gè)全局常量之前,我們先要弄清楚他們都是干什么用的。kallsyms_addresses、kallsyms_num_syms和kallsyms_names在前面已經(jīng)講過,實(shí)際上他們已經(jīng)可以提供一個(gè)[地址 : 符號(hào)]的映射關(guān)系了,但是內(nèi)核中幾萬(wàn)個(gè)符號(hào)這樣一條一條的存起來會(huì)占用大量的空間,所以內(nèi)核采用一種壓縮算法,將所有符號(hào)中出現(xiàn)頻率較高的字符串記錄成一個(gè)個(gè)的token,然后將原來的符號(hào)中和token匹配的子串進(jìn)行壓縮,這樣可以實(shí)現(xiàn)使用一個(gè)字符來代替n個(gè)字符,以減小符號(hào)存儲(chǔ)長(zhǎng)度。
因此符號(hào)表維護(hù)了一個(gè)kallsyms_token_table,他有256個(gè)元素,對(duì)應(yīng)一個(gè)字節(jié)的長(zhǎng)度。由于符號(hào)名的只能出現(xiàn)下劃線、數(shù)字和字母,那在kallsyms_token_table[256]數(shù)組中,除了這些字符的ASCII碼對(duì)應(yīng)的位置,還有很多未被使用的位置就可以用來存儲(chǔ)壓縮串。kallsyms_token_table表的內(nèi)容像下面這樣:
[cpp]?view plaincopy
那我們?cè)诒硎疽粋€(gè)函數(shù)名時(shí),就可以用0x00來表示“end”,用0x04來表示“to_”等。沒有被壓縮的如0x61仍然表示“a”。
kallsyms_token_index記錄每個(gè)token首字符在kallsyms_token_table中的偏移。同token table共256條,在打印token時(shí)需要用到。
[cpp]?view plaincopy
至于kallsyms_token_table表是如何生成的,可以閱讀scripts/kallsyms.c的實(shí)現(xiàn),大致就是將所有符號(hào)出現(xiàn)的相鄰的兩個(gè)字符出現(xiàn)的次數(shù)都記錄起來,例如對(duì)于“nf_nat_nf_init”,就記錄下“nf”、“f_”、“_n”、“na”、……,每?jī)蓚€(gè)字符組合出現(xiàn)的次數(shù)記錄在token_profit[0x10000]數(shù)組中(兩個(gè)字符一組,共有2^8 * 2^8 = 0x10000中可能組合),然后挑選出現(xiàn)次數(shù)最多的一個(gè)組合形成一個(gè)token,比如用“g”來表示“nf”,那“nf_nat_nf_init”就被改為“g_nat_g_init”。接下來,再在修改后的所有符號(hào)中計(jì)算每?jī)蓚€(gè)字符的出現(xiàn)次數(shù)來挑選出現(xiàn)次數(shù)最多的組合,例如用“J”來表示“g_”,那“g_nat_g_init”又被改為“Jnat_Jinit”。直到生成最終的token表。
2.2 內(nèi)核查找一個(gè)符號(hào)的過程
這時(shí)還沒講到全局常量kallsyms_markers。我們先來看內(nèi)核如何根據(jù)這六個(gè)全局常量來查找一個(gè)地址對(duì)應(yīng)的函數(shù)名的,實(shí)現(xiàn)函數(shù)為kernel/kallsyms.c中的kallsyms_lookup()。
我不講函數(shù)實(shí)現(xiàn),只是用一個(gè)例子來說明內(nèi)核符號(hào)的查找過程:
比如我在內(nèi)核中想打印出0x80216bf4地址所在的函數(shù)。首先不管內(nèi)核怎么做,我們可以先在System.map文件中看到這個(gè)地址位于為nf_register_hook和nf_register_hooks兩個(gè)符號(hào)之間,那可以確定它屬于nf_register_hook函數(shù)了。
80060000 A?_text
... ...
80216b8c T nf_unregister_hooks
80216be4 T nf_register_hook
80216c8c T nf_register_hooks
... ...
注意,System.map和內(nèi)核啟動(dòng)后的/proc/kallsyms文件中的符號(hào)表只是給我們看的,內(nèi)核不會(huì)使用它們。
在由script/kallsyms工具生成的.tmp_kallsyms2.S文件中,kallsyms_addresses數(shù)組存放著所有符號(hào)的地址,并且是按照地址升序排列的,所以通過二分查找可以定位到0x80216bf4所在函數(shù)的起始地址是下面的這個(gè)條目:
kallsyms_addresses:
?? ... ...
??? PTR?_text?+ 0x1b6be4
?? ... ...
而這一項(xiàng)在kallsyms_addresses中的index為8801,所以現(xiàn)在需要找到kallsyms_names中的第8801個(gè)符號(hào)。
我們這時(shí)實(shí)際上可以在kallsyms_names進(jìn)行查找了,怎么找呢?我們先看一下kallsyms_names大致的樣子:
[cpp]?view plaincopy
其中每一行存儲(chǔ)一個(gè)壓縮后的符號(hào),而index和kallsyms_addresses中的index是一一對(duì)應(yīng)的。每一行的內(nèi)容分為兩部分:第一個(gè)byte指明符號(hào)的長(zhǎng)度,后續(xù)才是符號(hào)自身。雖然我們這里看到的符號(hào)是一行一行分開的,但實(shí)際上kallsyms_names是一個(gè)unsigned char的數(shù)組,所以想要找第8801個(gè)符號(hào),只能這樣來找:
1. 從第一個(gè)字節(jié)開始,獲得第一個(gè)符號(hào)的長(zhǎng)度len;
2. 向后移len+1個(gè)字節(jié),就達(dá)到第二個(gè)符號(hào)的長(zhǎng)度字節(jié),這時(shí)記錄下已經(jīng)走過的總長(zhǎng)度;
3. 重復(fù)前兩步的動(dòng)作,直到走過的總長(zhǎng)度為8801。
這樣找的話,要找到kallsyms_names的第8801個(gè)符號(hào)就要移動(dòng)8801次,那如果要尋找最后一個(gè)符號(hào),就要移動(dòng)更多次,時(shí)間耗費(fèi)較多,所以內(nèi)核通過一個(gè)kallsyms_markers數(shù)組進(jìn)行查找。
將kallsyms_names每256個(gè)符號(hào)分為一組,每一組的第一個(gè)字符的位置記錄在kallsyms_markers中,這樣,我們?cè)谡襨allsyms_names中的某個(gè)條目時(shí),可以快速定義到它位于那個(gè)組,然后再在組內(nèi)尋找,組內(nèi)移動(dòng)次數(shù)最多為255次。
所以我們先通過(8801 >> 8)得到了要找的符號(hào)位于第34組,
我們看到kallsyms_markers的第34項(xiàng)為:
??? PTR 91280
這個(gè)值指明了kallsyms_names中第34組的起始字符的偏移,所以我們直接找到kallsyms_names[91280]位置,即是第34組所有符號(hào)的第一個(gè)字節(jié)。同時(shí)我們可以通過(8801 && 0xFF)得到要找的符號(hào)在第34組組內(nèi)的序號(hào)為97,即第97個(gè)符號(hào)。
接下來尋找第97個(gè)符號(hào)就只能通過上面講到的方法了。
通過上面一系列的查找,我們定位到第34組中第97個(gè)符號(hào)如下:
.byte 0x08, 0x05, 0x66, 0xdc, 0xb6, 0xc8, 0x68, 0x6f,0x0b
這個(gè)是壓縮后的符號(hào),第一個(gè)字節(jié)0x08是符號(hào)長(zhǎng)度,所以我們接下來的任務(wù)就剩下解壓了。
每個(gè)字節(jié)解壓后對(duì)應(yīng)的字符串在kallsyms_token_table中可以找到。于是在kallsyms_token_table表中尋找第5(0x05)項(xiàng)、第5(0x05)項(xiàng)、第102(0x66)項(xiàng)、……、第11(0x0b)項(xiàng),得到的結(jié)果分別為:
"Tn", "f", "_re","gist", "er_", "h", "o", "ok"
由于在壓縮的時(shí)候?qū)⒎?hào)類型“T”也壓進(jìn)去了,所以要去掉第一個(gè)字符,至此就獲得了0x80216bf4地址所在的函數(shù)為nf_register_hook。
參考自:http://blog.csdn.net/jasonchen_gbd/article/details/44025681
3. 內(nèi)核模塊的符號(hào)
內(nèi)核模塊是在內(nèi)核啟動(dòng)過程中動(dòng)態(tài)加載到內(nèi)核中的,所以,不能試圖將模塊中的符號(hào)嵌入到vmlinux中。加載模塊時(shí),模塊的符號(hào)表被存放在該模塊的struct module結(jié)構(gòu)中。所有已加載的模塊的structmodule結(jié)構(gòu)都放在一個(gè)全局鏈表中。
在查找一個(gè)內(nèi)核模塊的符號(hào)時(shí),調(diào)用的函數(shù)依然是kallsyms_lookup(),模塊符號(hào)的實(shí)際查找工作在get_ksymbol()函數(shù)中完成。
附錄:一個(gè).tmp_kallsyms2.S文件
[cpp]?view plaincopy
總結(jié)
以上是生活随笔為你收集整理的dump_stack介绍以及内核符号表的生成和查找过程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新连接?新商业 一场关于商业变革的活动正
- 下一篇: 手机扫码登录实现原理