linux内存地址断点,开发一个 Linux 调试器(三):寄存器和内存
上一篇博文中我們給調試器添加了一個簡單的地址斷點。這次,我們將添加讀寫寄存器和內存的功能,這將使我們能夠使用我們的程序計數器、觀察狀態和改變程序的行為。
注冊我們的寄存器
在我們真正讀取任何寄存器之前,我們需要告訴調試器一些關于我們的目標平臺的信息,這里是 x86_64 平臺。除了多組通用和專用目的寄存器,x86_64 還提供浮點和向量寄存器。為了簡化,我將跳過后兩種寄存器,但是你如果喜歡的話也可以選擇支持它們。x86_64 也允許你像訪問 32、16 或者 8 位寄存器那樣訪問一些 64 位寄存器,但我只會介紹 64 位寄存器。由于這些簡化,對于每個寄存器我們只需要它的名稱、它的 DWARF 寄存器編號以及 ptrace 返回結構體中的存儲地址。我使用范圍枚舉引用這些寄存器,然后我列出了一個全局寄存器描述符數組,其中元素順序和 ptrace 中寄存器結構體相同。enum?class?reg?{????rax,?rbx,?rcx,?rdx,????rdi,?rsi,?rbp,?rsp,????r8,??r9,??r10,?r11,????r12,?r13,?r14,?r15,????rip,?rflags,????cs,
orig_rax,?fs_base,
gs_base,????fs,?gs,?ss,?ds,?es};constexpr?std::size_t?n_registers?=?27;struct?reg_descriptor?{
reg?r;
int?dwarf_r;????std::string?name;};const?std::array?g_register_descriptors?{{
{?reg::r15,?15,?"r15"?},
{?reg::r14,?14,?"r14"?},
{?reg::r13,?13,?"r13"?},
{?reg::r12,?12,?"r12"?},
{?reg::rbp,?6,?"rbp"?},
{?reg::rbx,?3,?"rbx"?},
{?reg::r11,?11,?"r11"?},
{?reg::r10,?10,?"r10"?},
{?reg::r9,?9,?"r9"?},
{?reg::r8,?8,?"r8"?},
{?reg::rax,?0,?"rax"?},
{?reg::rcx,?2,?"rcx"?},
{?reg::rdx,?1,?"rdx"?},
{?reg::rsi,?4,?"rsi"?},
{?reg::rdi,?5,?"rdi"?},
{?reg::orig_rax,?-1,?"orig_rax"?},
{?reg::rip,?-1,?"rip"?},
{?reg::cs,?51,?"cs"?},
{?reg::rflags,?49,?"eflags"?},
{?reg::rsp,?7,?"rsp"?},
{?reg::ss,?52,?"ss"?},
{?reg::fs_base,?58,?"fs_base"?},
{?reg::gs_base,?59,?"gs_base"?},
{?reg::ds,?53,?"ds"?},
{?reg::es,?50,?"es"?},
{?reg::fs,?54,?"fs"?},
{?reg::gs,?55,?"gs"?},
}};
如果你想自己看看的話,你通常可以在 /usr/include/sys/user.h 找到寄存器數據結構,另外 DWARF 寄存器編號取自 System V x86_64 ABI。
現在我們可以編寫一堆函數來和寄存器交互。我們希望可以讀取寄存器、寫入數據、根據 DWARF 寄存器編號獲取值,以及通過名稱查找寄存器,反之類似。讓我們先從實現 get_register_value 開始:uint64_t?get_register_value(pid_t?pid,?reg?r)?{
user_regs_struct?regs;
ptrace(PTRACE_GETREGS,?pid,?nullptr,?®s);????//...}
ptrace 使得我們可以輕易獲得我們想要的數據。我們只需要構造一個 user_regs_struct 實例并把它和 PTRACE_GETREGS 請求傳遞給 ptrace。
現在根據要請求的寄存器,我們要讀取 regs。我們可以寫一個很大的 switch 語句,但由于我們 g_register_descriptors 表的布局順序和 user_regs_struct 相同,我們只需要搜索寄存器描述符的索引,然后作為 uint64_t 數組訪問 user_regs_struct 就行。(你也可以重新排序 reg 枚舉變量,然后使用索引把它們轉換為底層類型,但第一次我就使用這種方式編寫,它能正常工作,我也就懶得改它了。)auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),
[r](auto&&?rd)?{?return?rd.r?==?r;?});????????return?*(reinterpret_cast(®s)?+?(it?-?begin(g_register_descriptors)));
到 uint64_t 的轉換是安全的,因為 user_regs_struct 是一個標準布局類型,但我認為指針算術技術上是未定義的行為undefined behavior。當前沒有編譯器會對此產生警告,我也懶得修改,但是如果你想保持最嚴格的正確性,那就寫一個大的 switch 語句。
set_register_value 非常類似,我們只是寫入該位置并在最后寫回寄存器:void?set_register_value(pid_t?pid,?reg?r,?uint64_t?value)?{
user_regs_struct?regs;
ptrace(PTRACE_GETREGS,?pid,?nullptr,?®s);????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),
[r](auto&&?rd)?{?return?rd.r?==?r;?});
*(reinterpret_cast(®s)?+?(it?-?begin(g_register_descriptors)))?=?value;
ptrace(PTRACE_SETREGS,?pid,?nullptr,?®s);
}
下一步是通過 DWARF 寄存器編號查找。這次我會真正檢查一個錯誤條件以防我們得到一些奇怪的 DWARF 信息。uint64_t?get_register_value_from_dwarf_register?(pid_t?pid,?unsigned?regnum)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),
[regnum](auto&&?rd)?{?return?rd.dwarf_r?==?regnum;?});????if?(it?==?end(g_register_descriptors))?{????????throw?std::out_of_range{"Unknown?dwarf?register"};
}????return?get_register_value(pid,?it->r);
}
就快完成啦,現在我們已經有了寄存器名稱查找:std::string?get_register_name(reg?r)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),
[r](auto&&?rd)?{?return?rd.r?==?r;?});????return?it->name;
}reg?get_register_from_name(const?std::string&?name)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),
[name](auto&&?rd)?{?return?rd.name?==?name;?});????return?it->r;
}
最后我們會添加一個簡單的幫助函數用于導出所有寄存器的內容:void?debugger::dump_registers()?{????for?(const?auto&?rd?:?g_register_descriptors)?{????????std::cout?<
<
}
}
正如你看到的,iostreams 有非常精確的接口用于美觀地輸出十六進制數據(啊哈哈哈哈哈哈)。如果你喜歡你也可以通過 I/O 操縱器來擺脫這種混亂。
這些已經足夠支持我們在調試器接下來的部分輕松地處理寄存器,所以我們現在可以把這些添加到我們的用戶界面。
顯示我們的寄存器
這里我們要做的就是給 handle_command 函數添加一個命令。通過下面的代碼,用戶可以輸入 register read rax、 register write rax 0x42 以及類似的語句。else?if?(is_prefix(command,?"register"))?{????????if?(is_prefix(args[1],?"dump"))?{
dump_registers();
}????????else?if?(is_prefix(args[1],?"read"))?{????????????std::cout?<
}????????else?if?(is_prefix(args[1],?"write"))?{????????????std::string?val?{args[3],?2};?//assume?0xVAL
set_register_value(m_pid,?get_register_from_name(args[2]),?std::stol(val,?0,?16));
}
}
接下來做什么?
設置斷點的時候我們已經讀取和寫入內存,因此我們只需要添加一些函數用于隱藏 ptrace 調用。uint64_t?debugger::read_memory(uint64_t?address)?{????return?ptrace(PTRACE_PEEKDATA,?m_pid,?address,?nullptr);
}void?debugger::write_memory(uint64_t?address,?uint64_t?value)?{
ptrace(PTRACE_POKEDATA,?m_pid,?address,?value);
}
你可能想要添加支持一次讀取或者寫入多個字節,你可以在每次希望讀取另一個字節時通過遞增地址來實現。如果你需要的話,你也可以使用 ?process_vm_readv 和 process_vm_writev 或 /proc//mem 代替 ptrace。
現在我們會給我們的用戶界面添加命令:else?if(is_prefix(command,?"memory"))?{????????std::string?addr?{args[2],?2};?//assume?0xADDRESS
if?(is_prefix(args[1],?"read"))?{????????????std::cout?<
}????????if?(is_prefix(args[1],?"write"))?{????????????std::string?val?{args[3],?2};?//assume?0xVAL
write_memory(std::stol(addr,?0,?16),?std::stol(val,?0,?16));
}
}
給 ?continue_execution 打補丁
在我們測試我們的更改之前,我們現在可以實現一個更健全的 continue_execution 版本。由于我們可以獲取程序計數器,我們可以檢查我們的斷點映射來判斷我們是否處于一個斷點。如果是的話,我們可以停用斷點并在繼續之前跳過它。
為了清晰和簡潔起見,首先我們要添加一些幫助函數:uint64_t?debugger::get_pc()?{????return?get_register_value(m_pid,?reg::rip);
}void?debugger::set_pc(uint64_t?pc)?{
set_register_value(m_pid,?reg::rip,?pc);
}
然后我們可以編寫函數來跳過斷點:void?debugger::step_over_breakpoint()?{????//?-?1?because?execution?will?go?past?the?breakpoint
auto?possible_breakpoint_location?=?get_pc()?-?1;????if?(m_breakpoints.count(possible_breakpoint_location))?{????????auto&?bp?=?m_breakpoints[possible_breakpoint_location];????????if?(bp.is_enabled())?{????????????auto?previous_instruction_address?=?possible_breakpoint_location;
set_pc(previous_instruction_address);
bp.disable();
ptrace(PTRACE_SINGLESTEP,?m_pid,?nullptr,?nullptr);
wait_for_signal();
bp.enable();
}
}
}
首先我們檢查當前程序計算器的值是否設置了一個斷點。如果有,首先我們把執行返回到斷點之前,停用它,跳過原來的指令,再重新啟用斷點。
wait_for_signal 封裝了我們常用的 waitpid 模式:void?debugger::wait_for_signal()?{
int?wait_status;????auto?options?=?0;
waitpid(m_pid,?&wait_status,?options);}
最后我們像下面這樣重寫 continue_execution:void?debugger::continue_execution()?{
step_over_breakpoint();
ptrace(PTRACE_CONT,?m_pid,?nullptr,?nullptr);
wait_for_signal();
}
測試效果
現在我們可以讀取和修改寄存器了,我們可以對我們的 hello world 程序做一些有意思的更改。類似第一次測試,再次嘗試在 call 指令處設置斷點然后從那里繼續執行。你可以看到輸出了 Hello world。現在是有趣的部分,在輸出調用后設一個斷點、繼續、將 call 參數設置代碼的地址寫入程序計數器(rip)并繼續。由于程序計數器操縱,你應該再次看到輸出了 Hello world。為了以防你不確定在哪里設置斷點,下面是我上一篇博文中的 objdump 輸出:0000000000400936?:??400936:????55???????????????????????push???rbp
400937:????48?89?e5?????????????????mov????rbp,rsp
40093a:????be?35?0a?40?00???????????mov????esi,0x400a35
40093f:????bf?60?10?60?00???????????mov????edi,0x601060
400944:????e8?d7?fe?ff?ff???????????call???400820?<_zstlsist11char_traitsiceerst13basic_ostreamict_es5_pkc>??400949:????b8?00?00?00?00???????????mov????eax,0x0
40094e:????5d???????????????????????pop????rbp
40094f:????c3???????????????????????ret
你要將程序計數器移回 0x40093a 以便正確設置 esi 和 edi 寄存器。
在下一篇博客中,我們會第一次接觸到 DWARF 信息并給我們的調試器添加一系列逐步調試的功能。之后,我們會有一個功能工具,它能逐步執行代碼、在想要的地方設置斷點、修改數據以及其它。一如以往,如果你有任何問題請留下你的評論!
你可以在這里找到這篇博文的代碼。
譯文出處:https://www.zcfy.cc/article/writing-a-linux-debugger-part-3-registers-and-memory
總結
以上是生活随笔為你收集整理的linux内存地址断点,开发一个 Linux 调试器(三):寄存器和内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 有用的命令,linux有用命
- 下一篇: Linux退出sqlplus界面,Lin