[原創]轉載請註明來源於CSDN _xiao。
在Linux產生Coredump檔案時程式並沒有對動態連結程式庫檔案資訊進行特殊處理,但GDB在載入Coredump檔案時卻能正確載入所有的動態連結程式庫,包括程式運行中調用dlopen動態載入的so檔案,其原理是什麼呢。這裡通過對GDB源碼的略覽來解開這個過程。
[關於如何設定庫搜尋路徑以及路徑搜尋的優先順序可參考"GDB動態庫搜尋路徑"筆記]
GDB的大略架構:gdb.c中的main函數是程式的入口,它只是簡單地調用gdb_main,後者再調用captured_main,captured_main是執行的主體函數。它先解析命令列參數選項,再初始化所有變數和所有檔案,最後是一個while迴圈,這個迴圈裡不斷調用captured_command_loop來擷取輸入的命令並執行命令所指示的動作。其中初始化時gdb_init會調用initialize_all_files函數來初始化所有檔案,這個函數在編譯之前是看不到的,在編譯時間Makefile會掃描所有源檔案,將所有類型為initialize_file_ftype的函數搜集起來,放在gdb目錄下新產生的init.c檔案中,並在此檔案中建立initialize_all_files函數,此函數依次調用每一個_initialize_xxx函數。模組在自己的_initialize_xxx函數中除了初始化自身外,還會調用add_cmd或類似的函數來註冊命令,之後使用者輸入所註冊的命令時就會調用模組的處理函數了。例如corefile.c檔案在_initialize_core函數中註冊了"core-file”命令,命令的處理函數是core_file_command,這樣當使用者敲入"core Coredump”時就會調用core_file_command函數來處理了("core”是"core-file”命令的別名)。同樣exec.c註冊了"file”指令,其處理函數為file_command。通常,命令"xxx”的處理函數名為"xxx_command”,例如"info sharedLibrary”命令的處理函數為info_sharedlibrary_command,根據此規則,可以快速找到命令的處理函數。
載入一個Coredump檔案時,由core_file_command來處理,首先通過find_core_target來尋找有能力處理"CORE”檔案的target,並調用target的open函數處理。corelow.c在初始化時註冊了該類型的target,所以會進入corelow.c的core_open函數。core_open函數調用bfd_fopen函數開啟該檔案(bfd_fopen會識別格式並按ELF格式開啟),然後調用build_section_table讀入Coredump檔案的所有sections資訊(即segments資訊),完成後調用push_target將core的target操作添加到target鏈表中(這樣類似於readmemory等的一些動作就可以在Coredump檔案的地址空間進行了)。最後調用post_create_inferior進行後處理,調用init_thread_list讀取PT_NOTE資訊段中的線程資訊,調用target_fetch_registers讀取寄存器資訊,並根據寄存器恢複調用幀(frame),最後調用print_stack_frame列印幀資訊(即backtrace),整個Coredump檔案的載入就完成了。
對於如何恢複動態連結程式庫資訊,我們需要關注的是post_create_inferior函數。在這個函數裡,如果在core指令之前已執行了file或exec_file命令,即已擁有了主執行程式的資訊,那麼就會調用solib_add來添加所有的so庫。
可見,恢複動態連結程式庫資訊的前提是必須擁有Coredump檔案和原始主執行程式的Binary檔案,如果只有其中一個,是不能恢複動態連結程式庫資訊的。
繼續看solib_add函數,它主要調用update_solib_list來更新所有的so庫列表,在update_solib_list函數裡,關鍵的地方是調用ops->current_sos函數來擷取so庫資訊列表,而current_sos函數總是根據當前資訊重建so庫列表。
在不同的作業系統和體系架構上,會有不同的current_sos實現。對於工程中通常使用的ARM指令和MIPS指令上的Linux系統,會由svr4_current_sos函數來實現重建功能。
進入svr4_current_sos函數,首先調用locate_base擷取調試資訊的基址。它調用elf_locate_base分析主執行程式的ELF檔案得到該資訊。elf_locate_base先調用scan_dyntag尋找類型為DT_MIPS_RLD_MAP(0x70000016)的動態資訊,如果失敗再調用scan_dyntag尋找類型為DT_DEBUG(21)的動態資訊。對於MIPS,編譯器用DT_MIPS_RLD_MAP資訊存放調試資訊,而DT_DEBUG資訊是無意義的,對於其他平台如ARM,則用DT_DEBUG資訊存放調試資訊,沒有DT_MIPS_RLD_MAP資訊。san_dyntag讀取名為".dynamic”的section並逐一掃描,該section的內容由dynamic section structure數組組成,每個structure由兩個整數組成,第一個整數是dynamic的類型(例如DT_DEBUG),第二個整數是dynamic的值,值的意義與類型相關。scan_dyntag逐一掃描,找到類型為DT_MIPS_RLD_MAP的動態資訊,然後返回其值。該值是在編譯時間已經計算好的,實際上其值總是名為".rld_map”的section的地址。elf_locate_base會讀取scan_dyntag返回的值所指向的內容,也就是".rld_map” section的內容。".rld_map” section的長度只有4位元組,其內容是調試資訊的基址,指向dynamic linker structs。在編譯時間,".rld_map”的值為0,在運行時,由載入器填寫其值,載入器會維護一個dynamic linker structs,地址就放在".rld_map”中。在linux中,載入器通常是ld.so或者ld_linux.so。locate_base將elf_locate_base返回的值賦給全域變數debug_base,這樣debug_base就指向了dynamic linker structs。由於這個資訊是運行時才有的,所以GDB只有在同時載入主執行檔案和Coredump檔案後才能恢複這個鏈表。
svr4_current_sos再調用solib_svr4_r_map從dynamic linker structs中擷取link map list鏈表,由於不同平台上資料的組織不同,GDB在讀取資訊時會調用svr4_fetch_link_map_offsets等函數來擷取各變數的位移地址和尺寸,在mips中,它最終會通過svr4_ilp32_fetch_link_map_offsets提供的資訊來解析結構體的資料。在這裡r_map_offset的資訊為4,所以solib_svr4_r_map從debug_base + 4的地方讀取link map list資訊,這樣就得到了整個連結映射表的頭指標。
然後svr4_current_sos開始遍曆link map list,這裡鏈表每個元素為20位元組,其大概的結構如下(在不同平台上其大小和位置是不同的):
U32 l_addr; // 4 bytes
U32 l_name; // 4 bytes
U32 l_ld; // 4 bytes
U32 l_next; // 4 bytes
U32 l_prev; // 4 bytes
GDB從Coredump檔案中讀取鏈表所在記憶體中的值,l_name是模組名稱的地址,從所指地址即可讀出so庫檔案的名稱,l_addr則是模組的載入地址,l_next是下一個模組連結資訊的地址,svr4_current_sos逐一遍曆,將所有so庫檔案的名稱和資訊重建為struct so_list結構的鏈表,最後返回這個鏈表。
之後回到update_solib_list函數,這個函數掃描從current_sos返回的so庫鏈表,檢查哪些so庫已載入,哪些so庫需要重新載入,哪些已載入的so庫需要卸載掉,然後對每一個需要載入的so庫調用solib_map_sections將這些so庫映射到target的記憶體空間。載入so庫時會調用tilde_expand和solib_open來擴充庫檔案名稱,如果設定了正確sys_root路徑和庫搜尋路徑,庫就能正確找到和載入。
在所有庫都載入到target的記憶體空間後,整個進程的記憶體鏡像就恢複到Coredump時的狀況了,然後就可以觀察Coredump時的變數和狀態資訊了。
GDB載入動態庫資訊的過程示意如圖1所示。
圖(1)庫檔案資訊載入過程示意圖
下面用一個測試例子來描述庫資訊的恢複過程。
該樣本程式由兩個檔案組成,一個主程式,一個動態so庫,主程式調用動態so庫裡的一個函數,動態庫裡的函數操作一個null 指標以產生Coredump。
主程式,編譯後產生gdbso:
int main(){int ires = 0;LPFun lpFun = NULL;void *pHandle = dlopen("./libddd.so", RTLD_LAZY);if (NULL == pHandle){printf("open libddd.so failed\n");return 1;}else{printf("open libddd.so success\n");}lpFun = (LPFun)dlsym(pHandle, "fun_dll");if (NULL == lpFun){printf("dlsym failed\n");return 2;}ires = lpFun();dlclose(pHandle);return 0;}
動態庫程式,編譯後產生libddd.so:
int fun_dll(){void *pTmp = NULL;printf("In dll\n");memcpy(pTmp, 0, sizeof(100));return 1;}
編譯主程式和動態庫,在MIPS平台上運行產生Coredump,然後用GDB載入主程式gdbso和Coredump檔案,載入前使用set sys_root和set solib_search_path設定正確的庫搜尋路徑。
在GDB中,使用"set debug target 10”可以開啟載入target時的調試資訊,觀察GDB是如何負載檔案的。
根據CORE載入過程,GDB會讀取主程式gdbso的".dynamic” section內容,我們使用objdump –h指令查看gdbso的section資訊,如圖2所示。
圖(2)gdbso的objdump結果
從objdump的結果看到,.dynamic section在檔案中的位移地址為0x017C,在載入後記憶體中的地址為0x0040017C,這段資料是唯讀,所以在記憶體中的資料與檔案中的資料是相同的。我們在GDB中通過"x /28w 0x0040017c”查看.dynamic section的內容,如圖3所示。
圖(3).dynamic section的內容
從.dynmaic section的內容看到,地址0x004001dc就是DT_MIPS_RLD_MAP資訊,其類型為0x70000016,值為0x00410ac0,這剛好是.rld_map section的地址,與前文所述一致。
再使用"x /w 0x00410ac0”查看.rld_map section的內容,如圖4所示。
圖(4).rld_map section的內容
可以看到,.rld_map section的內容為0x2aad7a10(該section是可寫的,在檔案中的值為0x00000000,在Coredump載入後記憶體中的值為0x2aad7a10),所以dynamic linker structs的基地址為0x2aad7a10。
使用"x /4w 0x2aad7a10”查看dynamic linker structs的內容,如圖5所示。
圖(5)dynamic linker structs的部分內容
根據前文分析,在dynamic linker structs中,位移地址為4的地方就是link map list的地址。所以圖5中連結映射表(link map list)的頭指標為0x2aad7a28。鏈表的每個元素是20個位元組,使用"x /8w 0x2aad7a28”查看第一個鏈表元素的內容,如圖6所示,注意其中只有前20個位元組是有效。
圖(6)link map第一個元素的內容
從圖6看到,第一個鏈表元素的l_addr為0x00000000,l_name為0x2aac47e8,l_ld為0x0040017c,l_next為0x2aac75f8,l_prev為0x00000000。此模組的載入地址為0x00000000,表示是主程式gdbso的模組資訊,所以忽略掉它,看下一個鏈表元素。
使用"x /8w 0x2aac75f8”查看第二個鏈表元素的內容,如圖7所示。
圖(7)link map第二個元素的內容
從圖7看到,第二個鏈表元素的l_addr為0x2aad8000,l_name為0x2aac75e8,l_ld為0x2aad818c,l_next為0x2aac7958,l_prev為0x2aad7a28。該模組的載入地址為0x2aad8000,模組名稱地址為0x2aac75e8。使用"x /4w 0x2aac75e8”和"x /s 0x2aac75e8”查看該地址的內容,如圖8所示,可以看到,該模組的名稱為"/lib/librt.so.1”。
圖(8)link map第二個元素的模組的名稱
按上面的方式,繼續根據l_next瀏覽鏈表中的每一個模組,直到l_next為0x00000000,如圖9所示。
圖(9)link map後續元素的解析
從圖9看到,整個link map list,包含了"/lib/librt.so.1”、"/lib/libm.so.6”、"/lib/libpthread.so.0”、"/lib/libc.so.6”、"/lib/libdl.so.2”、"/lib/ld.so.1”、"./libddd.so”共7個模組的資訊。
從最後一個元素看到,動態庫libddd.so被載入到地址0x2ad1a000處,這是整個模組的載入地址,並不是其程式碼片段的載入地址。我們使用objdump查看libddd.so的.text段的位移資訊,如圖10所示。
圖(10)libddd.so的objdump結果
從圖10看到,libddd.so的.text在記憶體中的位移為0x0590,所以該模組載入到地址0x2ad1a000之後其程式碼片段會被載入到0x2ad1a590處。
我們用"info sharedlibrary”查看GDB解析的結果,和我們的分析是一致的,如圖11所示。
圖(11)GDB的info sharedlibrary結果
至此,整個so庫資訊載入過程就完成了。