GDB如何從Coredump檔案恢復動態庫資訊
[原創]轉載請註明來源於CSDN _xiao。
在Linux生成Coredump檔案時程式並沒有對動態連結庫檔案資訊進行特殊處理,但GDB在載入Coredump檔案時卻能正確載入所有的動態連結庫,包括程式執行中呼叫dlopen動態載入的so檔案,其原理是什麼呢?這裡通過對GDB原始碼的略覽來解開這個過程。
[關於如何設定庫搜尋路徑以及路徑搜尋的優先順序可參考"GDB動態庫搜尋路徑"筆記]
GDB的大略架構:gdb.c中的main函式是程式的入口,它只是簡單地呼叫gdb_main,後者再呼叫captured_main,captured_main是執行的主體函式。它先解析命令列引數選項,再初始化所有變數和所有檔案,最後是一個
載入一個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庫裡的一個函式,動態庫裡的函式操作一個空指標以生成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庫資訊載入過程就完成了。