參考:https://blog.csdn.net/jasonchen_gbd/article/details/44025681
在內核中維護者一張符號表,記錄了內核中所有的符號(函數、全局變量等)的地址以及名字,這個符號表被嵌入到內核鏡像中,使得內核可以在運行過程中隨時獲得一個符號地址對應的符號名。而內核代碼中可以通過 printk("%pS\n", addr) 打印符號名。
本文介紹內核符號表的生成和查找過程。
1. System.map和/proc/kallsyms
System.map文件是編譯內核時生成的,它記錄了內核中的符號列表,以及符號在內存中的虛擬地址。這個文件通過nm命令生成,具體可參考內核目錄下的scripts/mksysmap腳本。System.map中每個條目由三部分組成,例如:
f0081e80 T alloc_vfsmnt
即“地址 符號類型 符號名”
其中符號類型有如下幾種:
- 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文件是在內核啟動后生成的,位於文件系統的/proc目錄下,實現代碼見kernel/kallsyms.c。前提是內核必須打開CONFIG_KALLSYMS編譯選項。它和System.map的區別是它同時包含了內核模塊的符號列表。
通常情況下我們只需要_stext~_etext和_sinittext~_einittext之間的符號,如果需要將nm命令獲得的所有符號都記錄下來,則需要開啟內核的CONFIG_KALLSYMS_ALL編譯選項,不過一般是不需要打開的。
2. 內核符號表
內核在執行過程中,可能需要獲得一個地址所在的函數,比如在輸出某些調試信息的時候。一個典型的例子就是使用dump_stack()函數打印棧回溯信息。
但是內核在查找一個地址對應的函數名時,沒有求助於上述兩個文件,而是在編譯內核時,向vmlinux嵌入了一個符號表,這樣做可能是為了方便快速的查找並避免文件操作帶來的不良影響。
2.1 內核符號表的結構
內嵌的符號表是通過內核目錄下的scripts/kallsyms工具生成的,工具的源碼為相同目錄下的kallsyms.c。這個工具的用法如下:
1 nm -n vmlinux | scripts/kallsyms [--all-symbols] > symbols.S
可見同樣是通過nm命令得到 vmlinux 的符號表,並將這些符號表信息進行調整,最終生成一個匯編文件。這個匯編文件中包含了6個全局變量:kallsyms_addresses,kallsyms_num_syms,kallsyms_names,kallsyms_markers,kallsyms_token_table和kallsyms_token_index,其中:
- kallsyms_addresses:一個數組,存放所有符號的地址列表,按地址升序排列。
- kallsyms_num_syms:符號的數量。
- kallsyms_names:一個數組,存放所有符號的名稱,和kallsyms_addresses一一對應。
其他三個全局變量的含義后續會提到。
這些變量被嵌入在vmlinux中,所以在內核代碼中直接extern就可以使用。例如dump_stack()就是通過這些變量來查找一個地址對應的函數名的。
那由scripts/kallsyms生成的匯編文件是如何嵌入到vmlinux中的呢。在編譯內核的后期主要進行了一下幾步額外的編譯和鏈接過程:
鏈接器ld將內核的絕大部分組件鏈接成臨時內核映像.tmp_vmlinux1。
使用nm命令將.tmp_vmlinux1中符號和相對的地址導出來,並使用kallsyms工具生成tmp_kallsyms1.S的文件。
對.tmp_kallsyms1.S文件進行編譯生成.tmp_kallsyms1.o文件。
重復1的鏈接過程,這次將步驟3得到的.tmp_kallsyms1.o文件鏈接進入內核得到臨時內核映像.tmp_vmlinux2文件,其中包含的部分函數和非棧變量的地址發生了變化,但但由於.tmp_kallsyms1.S中的符號表還是舊的,所以.tmp_vmlinux2還不能作為最終的內核映像。
再使用nm命令將.tmp_vmlinux2中符號和相對的地址導出來,並使用kallsyms工具生成tmp_kallsyms2.S的文件。
對.tmp_kallsyms2.S文件進行編譯生成.tmp_kallsyms2.o文件。
.tmp_kallsyms2.o即為最終的kallsyms.o目標,並鏈接進入內核生成vmlinux文件。
此時,上面的那6個全局變量被寫進vmlinux中的“.rodata”段(所以還是叫全局常量吧),內核代碼就可以使用了,使用前需extern一下:
1 extern const unsigned long kallsyms_addresses[] __attribute__((weak));
weak屬性表示當我們不確定外部模塊是否提供了某個變量或函數時,可以將這個變量或函數定義為弱屬性,如果外部有定義則使用,沒有定義則相當於自己定義。
在使用這6個全局常量之前,我們先要弄清楚他們都是干什么用的。kallsyms_addresses、kallsyms_num_syms和kallsyms_names在前面已經講過,實際上他們已經可以提供一個[地址 : 符號]的映射關系了,但是內核中幾萬個符號這樣一條一條的存起來會占用大量的空間,所以內核采用一種壓縮算法,將所有符號中出現頻率較高的字符串記錄成一個個的token,然后將原來的符號中和token匹配的子串進行壓縮,這樣可以實現使用一個字符來代替n個字符,以減小符號存儲長度。
因此符號表維護了一個kallsyms_token_table,他有256個元素,對應一個字節的長度。由於符號名的只能出現下划線、數字和字母,那在kallsyms_token_table[256]數組中,除了這些字符的ASCII碼對應的位置,還有很多未被使用的位置就可以用來存儲壓縮串。kallsyms_token_table表的內容像下面這樣:
1 kallsyms_token_table: 2 .asciz "end" 3 .asciz "Tjffs2" 4 .asciz "map_" 5 .asciz "int" 6 .asciz "to_" 7 .asciz "Tn" 8 .asciz "t__" 9 .asciz "unregist" 10 ... ... 11 .asciz "a" 12 .asciz "b" 13 .asciz "c" 14 .asciz "d" 15 .asciz "e" 16 .asciz "f" 17 .asciz "g" 18 .asciz "h" 19 ... ...
那我們在表示一個函數名時,就可以用0x00來表示“end”,用0x04來表示“to_”等。沒有被壓縮的如0x61仍然表示“a”。
kallsyms_token_index記錄每個token首字符在kallsyms_token_table中的偏移。同token table共256條,在打印token時需要用到。
1 kallsyms_token_index: 2 .short 0 3 .short 4 //Tjffs2第一個字符在kallsyms_token_table中的偏移 4 .short 11 5 .short 16
至於kallsyms_token_table表是如何生成的,可以閱讀scripts/kallsyms.c的實現,大致就是將所有符號出現的相鄰的兩個字符出現的次數都記錄起來,例如對於“nf_nat_nf_init”,就記錄下“nf”、“f_”、“_n”、“na”、……,每兩個字符組合出現的次數記錄在token_profit[0x10000]數組中(兩個字符一組,共有2^8 * 2^8 = 0x10000中可能組合),然后挑選出現次數最多的一個組合形成一個token,比如用“g”來表示“nf”,那“nf_nat_nf_init”就被改為“g_nat_g_init”。接下來,再在修改后的所有符號中計算每兩個字符的出現次數來挑選出現次數最多的組合,例如用“J”來表示“g_”,那“g_nat_g_init”又被改為“Jnat_Jinit”。直到生成最終的token表。
2.2 內核查找一個符號的過程
這時還沒講到全局常量kallsyms_markers。我們先來看內核如何根據這六個全局常量來查找一個地址對應的函數名的,實現函數為kernel/kallsyms.c中的kallsyms_lookup()。
我不講函數實現,只是用一個例子來說明內核符號的查找過程:
比如我在內核中想打印出0x80216bf4地址所在的函數。首先不管內核怎么做,我們可以先在System.map文件中看到這個地址位於為nf_register_hook和nf_register_hooks兩個符號之間,那可以確定它屬於nf_register_hook函數了。
80060000 A _text ... ... 80216b8c T nf_unregister_hooks 80216be4 T nf_register_hook 80216c8c T nf_register_hooks
注意,System.map和內核啟動后的/proc/kallsyms文件中的符號表只是給我們看的,內核不會使用它們。
在由script/kallsyms工具生成的.tmp_kallsyms2.S文件中,kallsyms_addresses數組存放着所有符號的地址,並且是按照地址升序排列的,所以通過二分查找可以定位到0x80216bf4所在函數的起始地址是下面的這個條目:
kallsyms_addresses:
... ... PTR _text + 0x1b6be4 ... ...
而這一項在kallsyms_addresses中的index為8801,所以現在需要找到kallsyms_names中的第8801個符號。
我們這時實際上可以在kallsyms_names進行查找了,怎么找呢?我們先看一下kallsyms_names大致的樣子:
1 kallsyms_names: 2 .byte0x04, 0x54, 0x7e, 0xc3, 0x74 3 .byte0x08, 0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79 4 .byte0x09, 0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65 5 .byte0x09, 0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c 6 .byte0x09, 0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4 7 .byte0x07, 0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74 8 ... ...
其中每一行存儲一個壓縮后的符號,而index和kallsyms_addresses中的index是一一對應的。每一行的內容分為兩部分:第一個byte指明符號的長度,后續才是符號自身。雖然我們這里看到的符號是一行一行分開的,但實際上kallsyms_names是一個unsigned char的數組,所以想要找第8801個符號,只能這樣來找:
1. 從第一個字節開始,獲得第一個符號的長度len;
2. 向后移len+1個字節,就達到第二個符號的長度字節,這時記錄下已經走過的總長度;
3. 重復前兩步的動作,直到走過的總長度為8801。
這樣找的話,要找到kallsyms_names的第8801個符號就要移動8801次,那如果要尋找最后一個符號,就要移動更多次,時間耗費較多,所以內核通過一個kallsyms_markers數組進行查找。
將kallsyms_names每256個符號分為一組,每一組的第一個字符的位置記錄在kallsyms_markers中,這樣,我們在找kallsyms_names中的某個條目時,可以快速定義到它位於那個組,然后再在組內尋找,組內移動次數最多為255次。
所以我們先通過(8801 >> 8)得到了要找的符號位於第34組,
我們看到kallsyms_markers的第34項為:
PTR 91280
這個值指明了kallsyms_names中第34組的起始字符的偏移,所以我們直接找到kallsyms_names[91280]位置,即是第34組所有符號的第一個字節。同時我們可以通過(8801 && 0xFF)得到要找的符號在第34組組內的序號為97,即第97個符號。
接下來尋找第97個符號就只能通過上面講到的方法了。
通過上面一系列的查找,我們定位到第34組中第97個符號如下:
.byte 0x08, 0x05, 0x66, 0xdc, 0xb6, 0xc8, 0x68, 0x6f,0x0b
這個是壓縮后的符號,第一個字節0x08是符號長度,所以我們接下來的任務就剩下解壓了。
每個字節解壓后對應的字符串在kallsyms_token_table中可以找到。於是在kallsyms_token_table表中尋找第5(0x05)項、第5(0x05)項、第102(0x66)項、……、第11(0x0b)項,得到的結果分別為:
"Tn", "f", "_re","gist", "er_", "h", "o", "ok"
由於在壓縮的時候將符號類型“T”也壓進去了,所以要去掉第一個字符,至此就獲得了0x80216bf4地址所在的函數為nf_register_hook。
3. 內核模塊的符號
內核模塊是在內核啟動過程中動態加載到內核中的,所以,不能試圖將模塊中的符號嵌入到vmlinux中。加載模塊時,模塊的符號表被存放在該模塊的struct module結構中。所有已加載的模塊的structmodule結構都放在一個全局鏈表中。
在查找一個內核模塊的符號時,調用的函數依然是kallsyms_lookup(),模塊符號的實際查找工作在get_ksymbol()函數中完成。
附錄:一個.tmp_kallsyms2.S文件
1 #include <asm/types.h> 2 #if BITS_PER_LONG == 64 3 #define PTR .quad 4 #define ALGN .align 8 5 #else 6 #define PTR .long 7 #define ALGN .align 4 8 #endif 9 .section.rodata, "a" 10 .globl kallsyms_addresses 11 ALGN 12 kallsyms_addresses: 13 PTR _text + 0x400 14 PTR _text + 0x400 15 PTR _text + 0x410 16 PTR _text + 0x810 17 PTR _text + 0x9e0 18 PTR _text + 0xa14 19 PTR _text + 0xea0 20 PTR _text + 0xec4 21 PTR _text + 0xf00 22 PTR _text + 0xf10 23 ... ... 24 25 .globl kallsyms_num_syms 26 ALGN 27 kallsyms_num_syms: 28 PTR 11132 29 30 .globl kallsyms_names 31 ALGN 32 kallsyms_names: 33 .byte 0x04,0x54, 0x7e, 0xc3, 0x74 34 .byte 0x08,0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79 35 .byte 0x09,0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65 36 .byte 0x09,0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c 37 .byte 0x09,0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4 38 .byte 0x07,0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74 39 .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0x5f, 0x68, 0xe7, 0x74 40 .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0xdc, 0xf1, 0xee, 0x74 41 .byte 0x0b,0x54, 0xc4, 0x73, 0x79, 0xf1, 0x65, 0xcc, 0x74, 0x79, 0x70, 0x65 42 ... ... 43 44 .globl kallsyms_markers 45 ALGN 46 kallsyms_markers: 47 PTR 0 48 PTR 2831 49 PTR 5578 50 PTR 8289 51 PTR 10855 52 PTR 13684 53 PTR 16544 54 PTR 19519 55 PTR 22294 56 PTR 25225 57 PTR 27761 58 PTR 30097 59 ... ... 60 61 .globl kallsyms_token_table 62 ALGN 63 kallsyms_token_table: 64 .asciz "end" 65 .asciz "Tjffs2" 66 .asciz "map_" 67 .asciz "int" 68 .asciz "to_" 69 .asciz "Tn" 70 .asciz "t__" 71 .asciz "unregist" 72 .asciz "tn" 73 .asciz "yn" 74 .asciz "Tf" 75 ... ... 76 77 .globl kallsyms_token_index 78 ALGN 79 kallsyms_token_index: 80 .short 0 81 .short 4 82 .short 11 83 .short 16 84 .short 20 85 .short 24 86 .short 27 87 .short 31 88 .short 40 89 ... ...