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