到目前為止,可能你已經聽到了關于調試信息或者關于除了解析代碼以外的理解源代碼的方法的DWARF的只言片語。今天,我們將介紹源代碼級的調試信息的細節,以備在該系列的余下部分使用它。
ELF和DWARF簡介
ELF和DWARF可能是在程序員日常生活中經常使用但是可能卻沒有聽說過的兩個部件。ELF(Executable and Linkable Format)是Linux世界最廣泛中使用的一種Object File Format;它指定了一種將各部分數據存儲在二進制文件的方式,比如說代碼,靜態數據,調試信息,以及一些字符串等這些數據。同時,也告訴加載器以何種方式對待二進制文件以及準備好執行,這涉及到將二進制文件的不同部分加載到內存中,以及根據其他一些組件的位置來修復(重定位)相關的數據位等等。我不會在文章中包含太多的ELF相關的知識,但是如果感興趣的話你可以看一下這個精彩的圖表或者這個ELF標準文檔。
DWARF是ELF文件通常使用的調試信息格式。通常來講DWARF對ELF來說并不是必須的,但是這兩者是被串聯開發在一起的,并且一起使用非常好。這個格式允許編譯器告訴調試器源代碼是如何與被執行的二進制文件相關的。調試信息被分割在ELF不同的區段中,每一部分都傳達了本區塊的相關信息。一下是一些預定義的一些區段,如果信息過時的話,可以從這里獲取最新信息,DWARF調試信息簡介:
.debug_abbrev在.debug_info中使用的縮寫
.debug_aranges內存地址和匯編間的映射
.debug_frame調用棧幀信息
.debug_info包含DWARF信息入口(DIEs)的核心數據
.debug_line行號信息
.debug_loc?位置描述
.debug_macinfo宏定義描述
.debug_pubnames全局對象和函數查找表
.debug_pubtypes全局類型查找表
.debug_rangesDIEs引用地址范圍
.debug_str在.debug_info中使用的字符串表
.debug_types類型描述信息
我們最感興趣的是.debug_line和.debug_info區段,所以讓我們用一個簡單的程序來看一下一些DWARF信息吧:
int?main()?{????long?a?=?3;????long?b?=?2;????long?c?=?a?+?b;????a?=?4;}
DWARF行號表
如果在編譯程序的時候指定了-g選項,然后通過dwarfdump運行結果,應該類似以下信息的行號區段:
.debug_line:?line?number?info?for?a?single?cuSource?lines?(from?CU-DIE?at?.debug_info?offset?0x0000000b):????????????NS?new?statement,?BB?new?basic?block,?ET?end?of?text?sequence????????????PE?prologue?end,?EB?epilogue?begin????????????IS=val?ISA?number,?DI=val?discriminator?value????????[lno,col]?NS?BB?ET?PE?EB?IS=?DI=?uri:?"filepath"0x00400670??[???1,?0]?NS?uri:?"/home/simon/play/MiniDbg/examples/variable.cpp"0x00400676??[???2,10]?NS?PE0x0040067e??[???3,10]?NS0x00400686??[???4,14]?NS0x0040068a??[???4,16]0x0040068e??[???4,10]0x00400692??[???5,?7]?NS0x0040069a??[???6,?1]?NS0x0040069c??[???6,?1]?NS?ET
開始的一大串信息是關于如何理解dump的一些說明,主行號信息從0x00400770這行開始。本質上,它映射了代碼內存地址和在文件中的行和列信息。NS表示該地址標志著新語句的開始,這通常用于設置斷點或單步。PE標志著函數頭部的結束,這有助于設置函數入口斷點。ET標示該映射塊的結尾。信息實際上并不是像這樣編碼,實際的編碼是一種非常節省空間的程序,由它來建立這些行號信息。
那么,如果我們想在variable.cpp中的第4行下一個斷點,應該怎么做呢? 查找與該文件相對應的條目,然后找到相關的行號,找到相關的地址,然后設置一個斷點就可以了。在我們的小程序中,就是這一條:
0x00400686??[???4,14]?NS
所以我們需要在0x00400686地址處設置一個斷點。如果你想嘗試一下,你可以用你已經寫過的調試器手工完成。
相反的工作也是如此,如果我們有一個內存位置 - 比如一個RIP,并且想要找出它在源代碼中的哪個位置,只需在行號信息表中找到最接近的映射地址,并從中獲取行號即可。
DWARF調試信息
.debug_info是DWARF的核心所在。它給了我們程序中存在的關于類型,功能,變量,希望和夢想的信息。該區段的基本單位是DWARF信息入口,也就是被親切地稱為DIE的東西。DIE包含一個標簽,告訴你代表什么樣的源代碼級的條目,后面是一系列適用于該條目的屬性。以下是之前的那個簡單程序的.debug_info:
.debug_infoCOMPILE_UNIT
:0><0x0000000b>??DW_TAG_compile_unit????????????????????DW_AT_producer??????????????clang?version?3.9.1?(tags/RELEASE_391/final)????????????????????DW_AT_language??????????????DW_LANG_C_plus_plus????????????????????DW_AT_name??????????????????/super/secret/path/MiniDbg/examples/variable.cpp????????????????????DW_AT_stmt_list?????????????0x00000000????????????????????DW_AT_comp_dir??????????????/super/secret/path/MiniDbg/build????????????????????DW_AT_low_pc????????????????0x00400670????????????????????DW_AT_high_pc???????????????0x0040069cLOCAL_SYMBOLS:1><0x0000002e>????DW_TAG_subprogram??????????????????????DW_AT_low_pc????????????????0x00400670??????????????????????DW_AT_high_pc???????????????0x0040069c??????????????????????DW_AT_frame_base????????????DW_OP_reg6??????????????????????DW_AT_name??????????????????main??????????????????????DW_AT_decl_file?????????????0x00000001?/super/secret/path/MiniDbg/examples/variable.cpp??????????????????????DW_AT_decl_line?????????????0x00000001??????????????????????DW_AT_type??????????????????<0x00000077>??????????????????????DW_AT_external??????????????yes(1)2><0x0000004c>??????DW_TAG_variable????????????????????????DW_AT_location??????????????DW_OP_fbreg?-8????????????????????????DW_AT_name??????????????????a????????????????????????DW_AT_decl_file?????????????0x00000001?/super/secret/path/MiniDbg/examples/variable.cpp????????????????????????DW_AT_decl_line?????????????0x00000002????????????????????????DW_AT_type??????????????????<0x0000007e>2><0x0000005a>??????DW_TAG_variable????????????????????????DW_AT_location??????????????DW_OP_fbreg?-16????????????????????????DW_AT_name??????????????????b????????????????????????DW_AT_decl_file?????????????0x00000001?/super/secret/path/MiniDbg/examples/variable.cpp????????????????????????DW_AT_decl_line?????????????0x00000003????????????????????????DW_AT_type??????????????????<0x0000007e>2><0x00000068>??????DW_TAG_variable????????????????????????DW_AT_location??????????????DW_OP_fbreg?-24????????????????????????DW_AT_name??????????????????c????????????????????????DW_AT_decl_file?????????????0x00000001?/super/secret/path/MiniDbg/examples/variable.cpp????????????????????????DW_AT_decl_line?????????????0x00000004????????????????????????DW_AT_type??????????????????<0x0000007e>1><0x00000077>????DW_TAG_base_type??????????????????????DW_AT_name??????????????????int??????????????????????DW_AT_encoding??????????????DW_ATE_signed??????????????????????DW_AT_byte_size?????????????0x000000041><0x0000007e>????DW_TAG_base_type??????????????????????DW_AT_name??????????????????long?int??????????????????????DW_AT_encoding??????????????DW_ATE_signed??????????????????????DW_AT_byte_size?????????????0x00000008
第一個DIE表示一個編譯單元(CU),它本質上是一個源文件,其中包含所有#include并且被解析的包含文件。以下是它們的包含注釋的屬性:
DW_AT_producer???clang?version?3.9.1?(tags/RELEASE_391/final)????<--?The?compiler?which?produced?????????????????????????????????????????????????????????????????????this?binaryDW_AT_language???DW_LANG_C_plus_plus?????????????????????????????<--?The?source?languageDW_AT_name???????/super/secret/path/MiniDbg/examples/variable.cpp??<--?The?name?of?the?file?which?????????????????????????????????????????????????????????????????????this?CU?representsDW_AT_stmt_list??0x00000000??????????????????????????????????????<--?An?offset?into?the?line?table?????????????????????????????????????????????????????????????????????which?tracks?this?CUDW_AT_comp_dir???/super/secret/path/MiniDbg/build??????????????????<--?The?compilation?directoryDW_AT_low_pc?????0x00400670??????????????????????????????????????<--?The?start?of?the?code?for?????????????????????????????????????????????????????????????????????this?CUDW_AT_high_pc????0x0040069c??????????????????????????????????????<--?The?end?of?the?code?for?????????????????????????????????????????????????????????????????????this?CU
其他DIE遵循類似的方案,你可以直觀地看出不同屬性的含義。
現在我們可以嘗試使用我們新發現的DWARF知識來解決一些實際問題。
此刻處于哪個函數中?
比如說我們有一個RIP,并想弄清楚我們處在那個函數中。一個簡單的算法是:
for?each?compile?unit:????if?the?pc?is?between?DW_AT_low_pc?and?DW_AT_high_pc:????????for?each?function?in?the?compile?unit:????????????if?the?pc?is?between?DW_AT_low_pc?and?DW_AT_high_pc:????????????????return?function?information
這可以用于大多數目標,但是在成員函數和內聯存在的情況下,事情會變得更加困難。例如,存在內聯的情況下,一旦我們發現某個函數范圍包含了RIP,需要對該DIE的子條目進行遞歸,以查看是否有任何更匹配的內聯函數。我不會在這個調試器的代碼中處理內聯,但是如果你喜歡,你可以添加對它的支持。
如何在函數上下斷點?
同樣的,這取決于是否要支持成員函數,命名空間等。對于單獨的函數,你可以在不同的編譯單元中的函數中迭代查找,直到找到具有正確名稱的函數。如果你的編譯器足夠友好的填寫了.debug_pubnames部分,則可以更有效地做到這一點。
一旦找到該函數,就可以在給定的內存地址DW_AT_low_pc上設置斷點。但是,這將會在在函數頭部開始時中斷,最好在用戶代碼開始時中斷。由于行表信息可以指定指定函數頭部結束的內存地址,因此可以直接在行表中查找DW_AT_low_pc的值,然后繼續讀取,直到找到標記為函數頭部結尾的條目。有些編譯器不會輸出這個信息,所以另外一個選擇是在該函數的第二行條目給出的地址上設置一個斷點。
假設我們要在示例程序中的main設置一個斷點。我們搜索main函數,并得到這個DIE:
1><0x0000002e>????DW_TAG_subprogram??????????????????????DW_AT_low_pc????????????????0x00400670??????????????????????DW_AT_high_pc???????????????0x0040069c??????????????????????DW_AT_frame_base????????????DW_OP_reg6??????????????????????DW_AT_name??????????????????main??????????????????????DW_AT_decl_file?????????????0x00000001?/super/secret/path/MiniDbg/examples/variable.cpp??????????????????????DW_AT_decl_line?????????????0x00000001??????????????????????DW_AT_type??????????????????<0x00000077>??????????????????????DW_AT_external??????????????yes(1)
這告訴我們,函數從0x00400670開始。如果我們在行號表中查看,我們得到這個條目:
0x00400670??[???1,?0]?NS?uri:?"/super/secret/path/MiniDbg/examples/variable.cpp"
我們想跳過函數頭部,所以我們讀取下一個條目:
0x00400676??[???2,10]?NS?PE
Clang在這個條目中包含了頭部結尾標志,所以我們知道在這里停下來,并在地址0x00400676上設置一個斷點。
如何讀取變量內容?
讀取變量可能非常復雜。它們是可以在整個函數中變化的難以捉摸的東西,存儲在寄存器中,放在內存中,被優化,被隱藏在角落里,等等等等亂七八糟。還好,我們簡單的例子確實很簡單。如果我們想要讀取變量a的內容,則需要查看一下它的DW_AT_location?屬性。
DW_AT_location??????????????DW_OP_fbreg?-8
reg6?在x86架構上是RBP,由System V x86_64 ABI指定。現在我們讀取RBP的內容,從中減去8,就找到了我們的變量。如果我們想實際上的理解這個變量,還需要查看它的類型:
2><0x0000004c>??????DW_TAG_variable????????????????????????DW_AT_name??????????????????a????????????????????????DW_AT_type??????????????????<0x0000007e>
如果在調試信息中查找這種類型,我們得到這個DIE:
1><0x0000007e>????DW_TAG_base_type??????????????????????DW_AT_name??????????????????long?int??????????????????????DW_AT_encoding??????????????DW_ATE_signed??????????????????????DW_AT_byte_size?????????????0x00000008
這告訴我們,該類型是一個8字節(64位)有符號整數類型,因此我們可以直接將這些字節解釋為int64_t并將其顯示給用戶。
當然,這些類型可能會比這更復雜,因為它們必須能夠表達類似于C ++類型的東西,但是這給出了它們如何工作的基本思想。
暫時回到RBP,Clang可以很好地根據RBP來追蹤幀基址。最近版本的GCC更傾向于DW_OP_call_frame_cfa,它涉及解析.eh_frame ELF部分,這是一個完全不同的文章,我并不打算寫。如果你告訴GCC使用DWARF 2而不是更新的版本,它會傾向于輸出位置列表,這更容易閱讀:
DW_AT_frame_base?????????????low-off?:?0x00000000?addr??0x00400696?high-off??0x00000001?addr?0x00400697>DW_OP_breg7+8?low-off?:?0x00000001?addr??0x00400697?high-off??0x00000004?addr?0x0040069a>DW_OP_breg7+16?low-off?:?0x00000004?addr??0x0040069a?high-off??0x00000031?addr?0x004006c7>DW_OP_breg6+16?low-off?:?0x00000031?addr??0x004006c7?high-off??0x00000032?addr?0x004006c8>DW_OP_breg7+8
位置列表根據RIP給出不同的位置。這個例子展示了如果RIP位于距DW_AT_low_pc的0x0偏移的位置,那么幀基址距離寄存器7中存儲的值的偏移量為8,如果它位于0x1和0x4之間,那么它距離寄存器7中存儲的值偏移為16,等等。
休息休息
這么多信息會讓你的頭腦暈暈乎乎,但好消息是,在接下來的幾篇文章中,我們將有一個庫來為我們完成這些艱難的工作。理解實際操作中的內容,特別是在出現問題時,或者你希望支持一些DWARF內容(在使用的任何DWARF庫中未實現)時仍然有用。
如果你想了解有關DWARF的更多信息,那么可以從這里獲取相關標準。在撰寫本文時,DWARF 5剛剛被發布,但是DWARF 4更受歡迎。
Linux平臺下調試器的編寫(五):源碼和信號
在之前的幾部分中我們學習了關于DWARF信息以及這些信息是如何在被執行的機器碼和高級語言之間建立起聯系的。在這部分中,我們將實現一些能夠被調試器使用的DWARF相關原語。我們還將借此機會讓調試器在命中斷點之時輸出當前源代碼的上下文信息。
建立DWAR解析器
正如在再還系列的開始時所提到的,我們將會使用libelfin來處理DWARF信息。希望你在我的第一篇文章時就已經得到了該工具,如果沒有的話,你可使用我從倉庫fork出的fbreg分支。
一旦弄好了libelfin,就是時候把它加入到我們的調試器中了。第一步,解析ELF可執行文件并且從中獲取DWARF信息。使用libelfin來完成這一步是非常簡單的,僅僅需要對調試器做如下的改變:
class?debugger?{public:????debugger?(std::string?prog_name,?pid_t?pid)?????????:?m_prog_name{std::move(prog_name)},?m_pid{pid}?{????????auto?fd?=?open(m_prog_name.c_str(),?O_RDONLY);????????m_elf?=?elf::elf{elf::create_mmap_loader(fd)};????????m_dwarf?=?dwarf::dwarf{dwarf::elf::create_loader(m_elf)};????}????//...private:????//...????dwarf::dwarf?m_dwarf;????elf::elf?m_elf;};
##?調試信息原語接下來我們可以實現根據RIP的值來檢索行條目和函數DIE。先從```get_function_from_pc```開始吧:```c++dwarf::die?debugger::get_function_from_pc(uint64_t?pc)?{????for?(auto?&cu?:?m_dwarf.compilation_units())?{????????if?(die_pc_range(cu.root()).contains(pc))?{????????????for?(const?auto&?die?:?cu.root())?{????????????????if?(die.tag?==?dwarf::DW_TAG::subprogram)?{????????????????????if?(die_pc_range(die).contains(pc))?{????????????????????????return?die;????????????????????}????????????????}????????????}????????}????}????throw?std::out_of_range{"Cannot?find?function"};}
這里我采取了一個比較笨拙的方法,只需遍歷編譯單元,直到知道到包含RIP的代碼,然后一直迭代,直到在子節點中找到相關函數(DW_TAG_subprogram)。正如在上篇提到的,你可以想成員函數一樣來處理這些,如果你想的話你還可以使用內聯。 接下來是get_line_entry_from_pc:
dwarf::line_table::iterator?debugger::get_line_entry_from_pc(uint64_t?pc)?{????for?(auto?&cu?:?m_dwarf.compilation_units())?{????????if?(die_pc_range(cu.root()).contains(pc))?{????????????auto?=?cu.get_line_table();????????????auto?it?=?lt.find_address(pc);????????????if?(it?==?lt.end())?{????????????????throw?std::out_of_range{"Cannot?find?line?entry"};????????????}????????????else?{????????????????return?it;????????????}????????}????}????throw?std::out_of_range{"Cannot?find?line?entry"};}
同樣的,我們只需找到正確的便宜單元,然后請求行列表來獲取相關條目。
輸出源碼
當命中斷點的時候或者在源碼上單步的時候,我們需要知道源代碼被執行到哪里了。
void?debugger::print_source(const?std::string&?file_name,?unsigned?line,?unsigned?n_lines_context)?{????std::ifstream?file?{file_name};????//Work?out?a?window?around?the?desired?line????auto?start_line?=?line?<=?n_lines_context???1?:?line?-?n_lines_context;????auto?end_line?=?line?+?n_lines_context?+?(line??"?:?"??");????//Write?lines?up?until?end_line????while?(current_line?<=?end_line?&&?file.get(c))?{????????std::cout?<?"?:?"??");????????}????}????//Write?newline?and?make?sure?that?the?stream?is?flushed?properly????std::cout?<
現在,可以輸出源碼了,只需要將其掛載到我們的調試器中。當調試器從斷點或者(實際上)但不中獲取信號的時候是顯示源碼的上好時機了。這樣做的話,調試器就需要一個更好的信號處理了。
更好的信號處理
我們希望能夠輸出什么樣的信號被發送給了進程,同時亦希望知道該信號是如何被產生的。例如,我們想知道收到的SIGTRAP信號是由于命中斷點還是一個單步執行完產生的,亦或者是由于新線程建立而產生的,等等。 幸運的是,ptrace再一次支援了我們。ptrace有一個參數PTRACE_GETSIGINFO,該參數將會給出進程之前發出的信號的相關信息。如下:
siginfo_t?debugger::get_signal_info()?{????siginfo_t?info;????ptrace(PTRACE_GETSIGINFO,?m_pid,?nullptr,?&info);????return?info;}
這里出現了一個siginfo_t的對象,它提供了如下的信息:
siginfo_t?{????int??????si_signo;?????/*?Signal?number?*/????int??????si_errno;?????/*?An?errno?value?*/????int??????si_code;??????/*?Signal?code?*/????int??????si_trapno;????/*?Trap?number?that?caused??????????????????????????????hardware-generated?signal??????????????????????????????(unused?on?most?architectures)?*/????pid_t????si_pid;???????/*?Sending?process?ID?*/????uid_t????si_uid;???????/*?Real?user?ID?of?sending?process?*/????int??????si_status;????/*?Exit?value?or?signal?*/????clock_t??si_utime;?????/*?User?time?consumed?*/????clock_t??si_stime;?????/*?System?time?consumed?*/????sigval_t?si_value;?????/*?Signal?value?*/????int??????si_int;???????/*?POSIX.1b?signal?*/????void????*si_ptr;???????/*?POSIX.1b?signal?*/????int??????si_overrun;???/*?Timer?overrun?count;??????????????????????????????POSIX.1b?timers?*/????int??????si_timerid;???/*?Timer?ID;?POSIX.1b?timers?*/????void????*si_addr;??????/*?Memory?location?which?caused?fault?*/????long?????si_band;??????/*?Band?event?(was?int?in??????????????????????????????glibc?2.3.2?and?earlier)?*/????int??????si_fd;????????/*?File?descriptor?*/????short????si_addr_lsb;??/*?Least?significant?bit?of?address??????????????????????????????(since?Linux?2.6.32)?*/????void????*si_lower;?????/*?Lower?bound?when?address?violation??????????????????????????????occurred?(since?Linux?3.19)?*/????void????*si_upper;?????/*?Upper?bound?when?address?violation??????????????????????????????occurred?(since?Linux?3.19)?*/????int??????si_pkey;??????/*?Protection?key?on?PTE?that?caused??????????????????????????????fault?(since?Linux?4.6)?*/????void????*si_call_addr;?/*?Address?of?system?call?instruction??????????????????????????????(since?Linux?3.5)?*/????int??????si_syscall;???/*?Number?of?attempted?system?call??????????????????????????????(since?Linux?3.5)?*/????unsigned?int?si_arch;??/*?Architecture?of?attempted?system?call??????????????????????????????(since?Linux?3.5)?*/}
我將使用si——signo來找出是哪一個信號被發送,然后使用si_code來獲取有關該信號的更多信息。放置該段代碼的最佳地方是在我們的wait_for_signal函數中:
void?debugger::wait_for_signal()?{????int?wait_status;????auto?options?=?0;????waitpid(m_pid,?&wait_status,?options);????auto?siginfo?=?get_signal_info();????switch?(siginfo.si_signo)?{????case?SIGTRAP:????????handle_sigtrap(siginfo);????????break;????case?SIGSEGV:????????std::cout?<"Yay,?segfault.?Reason:?"?<
現在處理SIGTRAP只需知道SI_KERNEL或者TRAP_BPKPT將會在斷點命中時被發送,TRAP_TRACE將會在單步完成的時候被發送:
void?debugger::handle_sigtrap(siginfo_t?info)?{????switch?(info.si_code)?{????//one?of?these?will?be?set?if?a?breakpoint?was?hit????case?SI_KERNEL:????case?TRAP_BRKPT:????{????????set_pc(get_pc()-1);?//put?the?pc?back?where?it?should?be????????std::cout?<"Hit?breakpoint?at?address?0x"?<file->path,?line_entry->line);????????return;????}????//this?will?be?set?if?the?signal?was?sent?by?single?stepping????case?TRAP_TRACE:????????return;????default:????????std::cout?<"Unknown?SIGTRAP?code?"?<
你可以處理一堆不同風格的信號。詳情請參閱man sigaction。 由于我們現在在得到SIGTRAP時修正RIP,所以可以去掉step_over_breakpoint中的部分代碼:
void?debugger::step_over_breakpoint()?{????if?(m_breakpoints.count(get_pc()))?{????????auto&?bp?=?m_breakpoints[get_pc()];????????if?(bp.is_enabled())?{????????????bp.disable();????????????ptrace(PTRACE_SINGLESTEP,?m_pid,?nullptr,?nullptr);????????????wait_for_signal();????????????bp.enable();????????}????}}
測試
現在,你應該可以在某些地址設置斷點,運行程序,查看鼠標標記的正在被執行的代碼的源代碼了。
下一次我們將添加源碼級的斷點。可以在此處獲取源碼
?
評論
查看更多