原帖:http://blog.csdn.net/_xiao/article/details/23177577
GDB如何從Coredump檔案恢複動态庫資訊
标簽: GDBcoredumpso調試動态庫 2014-04-08 14:29 7559人閱讀 評論(0) 收藏 舉報
版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。
[原創]轉載請注明來源于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庫裡的一個函數,動态庫裡的函數操作一個空指針以生成Coredump。
主程式,編譯後生成gdbso:
[cpp] view plain copy
- 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:
[cpp] view plain copy
- 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庫資訊加載過程就完成了。
- 頂
- 踩