1. 程式人生 > >GDB如何從Coredump檔案恢復動態庫資訊

GDB如何從Coredump檔案恢復動態庫資訊

[原創]轉載請註明來源於CSDN _xiao。

Linux生成Coredump檔案時程式並沒有對動態連結庫檔案資訊進行特殊處理,但GDB在載入Coredump檔案時卻能正確載入所有的動態連結庫,包括程式執行中呼叫dlopen動態載入的so檔案,其原理是什麼呢?這裡通過對GDB原始碼的略覽來解開這個過程。

[關於如何設定庫搜尋路徑以及路徑搜尋的優先順序可參考"GDB動態庫搜尋路徑"筆記]

GDB的大略架構:gdb.c中的main函式是程式的入口,它只是簡單地呼叫gdb_main,後者再呼叫captured_maincaptured_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,並呼叫targetopen函式處理。corelow.c在初始化時註冊了該型別的target,所以會進入corelow.ccore_open函式。core_open函式呼叫bfd_fopen函式開啟該檔案(bfd_fopen會識別格式並按ELF格式開啟),然後呼叫build_section_table讀入Coredump檔案的所有sections資訊(即segments資訊),完成後呼叫push_targetcoretarget操作新增到target連結串列中(這樣類似於readmemory等的一些動作就可以在Coredump檔案的地址空間進行了)。最後呼叫post_create_inferior進行後處理,呼叫init_thread_list讀取PT_NOTE資訊段中的執行緒資訊,呼叫target_fetch_registers讀取暫存器資訊,並根據暫存器恢復呼叫幀(frame),最後呼叫print_stack_frame列印幀資訊(即backtrace),整個Coredump檔案的載入就完成了。

對於如何恢復動態連結庫資訊,我們需要關注的是post_create_inferior函式。在這個函式裡,如果在core指令之前已執行了fileexec_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.solocate_baseelf_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庫裡的一個函式,動態庫裡的函式操作一個空指標以生成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所示。

gdbso的objdump結果

gdbso的objdump結果

圖(2)gdbsoobjdump結果

從objdump的結果看到,.dynamic section在檔案中的偏移地址為0x017C,在載入後記憶體中的地址為0x0040017C,這段資料是隻讀的,所以在記憶體中的資料與檔案中的資料是相同的。我們在GDB中通過"x /28w 0x0040017c”檢視.dynamic section的內容,如圖3所示。

.dynamic section的內容

圖(3).dynamic section的內容

從.dynmaic section的內容看到,地址0x004001dc就是DT_MIPS_RLD_MAP資訊,其型別為0x70000016,值為0x00410ac0,這剛好是.rld_map section的地址,與前文所述一致。

再使用"x /w 0x00410ac0”檢視.rld_map section的內容,如圖4所示。

.rld_map section的內容

圖(4).rld_map section的內容

可以看到,.rld_map section的內容為0x2aad7a10(該section是可寫的,在檔案中的值為0x00000000,在Coredump載入後記憶體中的值為0x2aad7a10),所以dynamic linker structs的基地址為0x2aad7a10。

使用"x /4w 0x2aad7a10”檢視dynamic linker structs的內容,如圖5所示。

dynamic linker structs的部分內容

圖(5)dynamic linker structs的部分內容

根據前文分析,在dynamic linker structs中,偏移地址為4的地方就是link map list的地址。所以圖5中連結對映表(link map list)的頭指標為0x2aad7a28。連結串列的每個元素是20個位元組,使用"x /8w 0x2aad7a28”檢視第一個連結串列元素的內容,如圖6所示,注意其中只有前20個位元組是有效的。

link map第一個元素的內容

圖(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所示。

link map第二個元素的內容

圖(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”。

link map第二個元素的模組的名稱

圖(8)link map第二個元素的模組的名稱

按上面的方式,繼續根據l_next瀏覽連結串列中的每一個模組,直到l_next為0x00000000,如圖9所示。

link map後續元素的解析

圖(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所示。

libddd.so的objdump結果

圖(10)libddd.soobjdump結果

從圖10看到,libddd.so的.text在記憶體中的偏移為0x0590,所以該模組載入到地址0x2ad1a000之後其程式碼段會被載入到0x2ad1a590處。

我們用"info sharedlibrary”檢視GDB解析的結果,和我們的分析是一致的,如圖11所示。

GDB的info sharedlibrary結果

圖(11)GDBinfo sharedlibrary結果

至此,整個so庫資訊載入過程就完成了。