開發(fā)一個(gè)Linux調(diào)試器(六):源碼級(jí)逐步執(zhí)行
在前幾篇博文中我們學(xué)習(xí)了 DWARF 信息以及它如何使我們將機(jī)器碼和上層源碼聯(lián)系起來。這一次我們通過為我們的調(diào)試器添加源碼級(jí)逐步調(diào)試將該知識(shí)應(yīng)用于實(shí)際。
系列文章索引
隨著后面文章的發(fā)布,這些鏈接會(huì)逐漸生效。
- 準(zhǔn)備環(huán)境
- 斷點(diǎn)
- 寄存器和內(nèi)存
- Elves 和 dwarves
- 源碼和信號(hào)
- 源碼級(jí)逐步執(zhí)行
- 源碼級(jí)斷點(diǎn)
- 調(diào)用棧展開
- 讀取變量
- 下一步
揭秘指令級(jí)逐步執(zhí)行
我們正在超越了自我。首先讓我們通過用戶接口揭秘指令級(jí)單步執(zhí)行。我決定將它切分為能被其它部分代碼利用的 single_step_instruction 和確保是否啟用了某個(gè)斷點(diǎn)的 single_step_instruction_with_breakpoint_check 兩個(gè)函數(shù)。
- void debugger::single_step_instruction() {
- ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);
- wait_for_signal();
- }
- void debugger::single_step_instruction_with_breakpoint_check() {
- //首先,檢查我們是否需要停用或者啟用某個(gè)斷點(diǎn)
- if (m_breakpoints.count(get_pc())) {
- step_over_breakpoint();
- }
- else {
- single_step_instruction();
- }
- }
正如以往,另一個(gè)命令被集成到我們的 handle_command 函數(shù):
- else if(is_prefix(command, "stepi")) {
- single_step_instruction_with_breakpoint_check();
- auto line_entry = get_line_entry_from_pc(get_pc());
- print_source(line_entry->file->path, line_entry->line);
- }
利用新增的這些函數(shù)我們可以開始實(shí)現(xiàn)我們的源碼級(jí)逐步執(zhí)行函數(shù)。
實(shí)現(xiàn)逐步執(zhí)行
我們打算編寫這些函數(shù)非常簡(jiǎn)單的版本,但真正的調(diào)試器有 thread plan 的概念,它封裝了所有的單步信息。例如,調(diào)試器可能有一些復(fù)雜的邏輯去決定斷點(diǎn)的位置,然后有一些回調(diào)函數(shù)用于判斷單步操作是否完成。這其中有非常多的基礎(chǔ)設(shè)施,我們只采用一種樸素的方法。我們可能會(huì)意外地跳過斷點(diǎn),但如果你愿意的話,你可以花一些時(shí)間把所有的細(xì)節(jié)都處理好。
對(duì)于跳出 step_out,我們只是在函數(shù)的返回地址處設(shè)一個(gè)斷點(diǎn)然后繼續(xù)執(zhí)行。我暫時(shí)還不想考慮調(diào)用棧展開的細(xì)節(jié) - 這些都會(huì)在后面的部分介紹 - 但可以說返回地址就保存在棧幀開始的后 8 個(gè)字節(jié)中。因此我們會(huì)讀取棧指針然后在內(nèi)存相對(duì)應(yīng)的地址讀取值:
- void debugger::step_out() {
- auto frame_pointer = get_register_value(m_pid, reg::rbp);
- auto return_address = read_memory(frame_pointer+8);
- bool should_remove_breakpoint = false;
- if (!m_breakpoints.count(return_address)) {
- set_breakpoint_at_address(return_address);
- should_remove_breakpoint = true;
- }
- continue_execution();
- if (should_remove_breakpoint) {
- remove_breakpoint(return_address);
- }
- }
remove_breakpoint 是一個(gè)小的幫助函數(shù):
- void debugger::remove_breakpoint(std::intptr_t addr) {
- if (m_breakpoints.at(addr).is_enabled()) {
- m_breakpoints.at(addr).disable();
- }
- m_breakpoints.erase(addr);
- }
接下來是跳入 step_in。一個(gè)簡(jiǎn)單的算法是繼續(xù)逐步執(zhí)行指令直到新的一行。
- void debugger::step_in() {
- auto line = get_line_entry_from_pc(get_pc())->line;
- while (get_line_entry_from_pc(get_pc())->line == line) {
- single_step_instruction_with_breakpoint_check();
- }
- auto line_entry = get_line_entry_from_pc(get_pc());
- print_source(line_entry->file->path, line_entry->line);
- }
跳過 step_over 對(duì)于我們來說是三個(gè)中最難的。理論上,解決方法就是在下一行源碼中設(shè)置一個(gè)斷點(diǎn),但下一行源碼是什么呢?它可能不是當(dāng)前行后續(xù)的那一行,因?yàn)槲覀兛赡芴幱谝粋€(gè)循環(huán)、或者某種條件結(jié)構(gòu)之中。真正的調(diào)試器一般會(huì)檢查當(dāng)前正在執(zhí)行什么指令然后計(jì)算出所有可能的分支目標(biāo),然后在所有分支目標(biāo)中設(shè)置斷點(diǎn)。對(duì)于一個(gè)小的項(xiàng)目,我不打算實(shí)現(xiàn)或者集成一個(gè) x86 指令模擬器,因此我們要想一個(gè)更簡(jiǎn)單的解決辦法。有幾個(gè)可怕的選擇,一個(gè)是一直逐步執(zhí)行直到當(dāng)前函數(shù)新的一行,或者在當(dāng)前函數(shù)的每一行都設(shè)置一個(gè)斷點(diǎn)。如果我們是要跳過一個(gè)函數(shù)調(diào)用,前者將會(huì)相當(dāng)?shù)牡托?,因?yàn)槲覀冃枰鸩綀?zhí)行那個(gè)調(diào)用圖中的每個(gè)指令,因此我會(huì)采用第二種方法。
- void debugger::step_over() {
- auto func = get_function_from_pc(get_pc());
- auto func_entry = at_low_pc(func);
- auto func_end = at_high_pc(func);
- auto line = get_line_entry_from_pc(func_entry);
- auto start_line = get_line_entry_from_pc(get_pc());
- std::vector<std::intptr_t> to_delete{};
- while (line->address < func_end) {
- if (line->address != start_line->address && !m_breakpoints.count(line->address)) {
- set_breakpoint_at_address(line->address);
- to_delete.push_back(line->address);
- }
- ++line;
- }
- auto frame_pointer = get_register_value(m_pid, reg::rbp);
- auto return_address = read_memory(frame_pointer+8);
- if (!m_breakpoints.count(return_address)) {
- set_breakpoint_at_address(return_address);
- to_delete.push_back(return_address);
- }
- continue_execution();
- for (auto addr : to_delete) {
- remove_breakpoint(addr);
- }
- }
這個(gè)函數(shù)有一點(diǎn)復(fù)雜,我們將它拆開來看。
- auto func = get_function_from_pc(get_pc());
- auto func_entry = at_low_pc(func);
- auto func_end = at_high_pc(func);
at_low_pc 和 at_high_pc 是 libelfin 中的函數(shù),它們能給我們指定函數(shù) DWARF 信息條目的最小和***程序計(jì)數(shù)器值。
- auto line = get_line_entry_from_pc(func_entry);
- auto start_line = get_line_entry_from_pc(get_pc());
- std::vector<std::intptr_t> breakpoints_to_remove{};
- while (line->address < func_end) {
- if (line->address != start_line->address && !m_breakpoints.count(line->address)) {
- set_breakpoint_at_address(line->address);
- breakpoints_to_remove.push_back(line->address);
- }
- ++line;
- }
我們需要移除我們?cè)O(shè)置的所有斷點(diǎn),以便不會(huì)泄露出我們的逐步執(zhí)行函數(shù),為此我們把它們保存到一個(gè) std::vector 中。為了設(shè)置所有斷點(diǎn),我們循環(huán)遍歷行表?xiàng)l目直到找到一個(gè)不在我們函數(shù)范圍內(nèi)的。對(duì)于每一個(gè),我們都要確保它不是我們當(dāng)前所在的行,而且在這個(gè)位置還沒有設(shè)置任何斷點(diǎn)。
- auto frame_pointer = get_register_value(m_pid, reg::rbp);
- auto return_address = read_memory(frame_pointer+8);
- if (!m_breakpoints.count(return_address)) {
- set_breakpoint_at_address(return_address);
- to_delete.push_back(return_address);
- }
這里我們?cè)诤瘮?shù)的返回地址處設(shè)置一個(gè)斷點(diǎn),正如跳出 step_out。
- continue_execution();
- for (auto addr : to_delete) {
- remove_breakpoint(addr);
- }
***,我們繼續(xù)執(zhí)行直到***它們中的其中一個(gè)斷點(diǎn),然后移除所有我們?cè)O(shè)置的臨時(shí)斷點(diǎn)。
它并不美觀,但暫時(shí)先這樣吧。
當(dāng)然,我們還需要將這個(gè)新功能添加到用戶界面:
- else if(is_prefix(command, "step")) {
- step_in();
- }
- else if(is_prefix(command, "next")) {
- step_over();
- }
- else if(is_prefix(command, "finish")) {
- step_out();
- }
測(cè)試
我通過實(shí)現(xiàn)一個(gè)調(diào)用一系列不同函數(shù)的簡(jiǎn)單函數(shù)來進(jìn)行測(cè)試:
- void a() {
- int foo = 1;
- }
- void b() {
- int foo = 2;
- a();
- }
- void c() {
- int foo = 3;
- b();
- }
- void d() {
- int foo = 4;
- c();
- }
- void e() {
- int foo = 5;
- d();
- }
- void f() {
- int foo = 6;
- e();
- }
- int main() {
- f();
- }
你應(yīng)該可以在 main 地址處設(shè)置一個(gè)斷點(diǎn),然后在整個(gè)程序中跳入、跳過、跳出函數(shù)。如果你嘗試跳出 main 函數(shù)或者跳入任何動(dòng)態(tài)鏈接庫,就會(huì)出現(xiàn)意料之外的事情。
你可以在這里找到這篇博文的相關(guān)代碼。下次我們會(huì)利用我們新的 DWARF 技巧來實(shí)現(xiàn)源碼級(jí)斷點(diǎn)。