當前位置:
首頁 > 知識 > 開發一個 Linux 調試器(三):寄存器和內存

開發一個 Linux 調試器(三):寄存器和內存

開發一個 Linux 調試器(三):寄存器和內存


上一篇博文中我們給調試器添加了一個簡單的地址斷點。這次,我們將添加讀寫寄存器和內存的功能,這將使我們能夠使用我們的程序計數器、觀察狀態和改變程序的行為。 -- Simon Brand

本文導航

  • -系列文章索引 …… 02%

  • -註冊我們的寄存器 …… 07%

  • -顯示我們的寄存器 …… 54%

  • -接下來做什麼? …… 59%

  • -給 continue_execution 打補丁 …… 69%

  • -測試效果 …… 84%

編譯自: https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/

作者: Simon Brand

譯者: ictlyh

上一篇博文中我們給調試器添加了一個簡單的地址斷點。這次,我們將添加讀寫寄存器和內存的功能,這將使我們能夠使用我們的程序計數器、觀察狀態和改變程序的行為。


系列文章索引

隨著後面文章的發布,這些鏈接會逐漸生效。

  1. 準備環境[1]

  2. 斷點[2]

  3. 寄存器和內存[3]

  4. Elves 和 dwarves[4]

  5. 源碼和信號[5]

  6. 源碼級逐步執行[6]

  7. 源碼級斷點

  8. 調用棧展開

  9. 讀取變數

  10. 下一步

註冊我們的寄存器

在我們真正讀取任何寄存器之前,我們需要告訴調試器一些關於我們的目標平台的信息,這裡是 x8664 平台。除了多組通用和專用目的寄存器,x8664 還提供浮點和向量寄存器。為了簡化,我將跳過後兩種寄存器,但是你如果喜歡的話也可以選擇支持它們。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[7]。

現在我們可以編寫一堆函數來和寄存器交互。我們希望可以讀取寄存器、寫入數據、根據 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 << rd.name << " 0x"

<< std::setfill("0") << std::setw(16) << std::hex << get_register_value(m_pid, rd.r) << std::endl;

}

}

正如你看到的,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 << get_register_value(m_pid, get_register_from_name(args[2])) << std::endl;

}

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[8] 或 /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 << std::hex << read_memory(std::stol(addr, 0, 16)) << std::endl;

}

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@plt>

400949: b8 00 00 00 00 mov eax,0x0

40094e: 5d pop rbp

40094f: c3 ret

你要將程序計數器移回 0x40093a 以便正確設置 esi 和 edi 寄存器。

在下一篇博客中,我們會第一次接觸到 DWARF 信息並給我們的調試器添加一系列逐步調試的功能。之後,我們會有一個功能工具,它能逐步執行代碼、在想要的地方設置斷點、修改數據以及其它。一如以往,如果你有任何問題請留下你的評論!

你可以在這裡[9]找到這篇博文的代碼。



via: https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/

作者:TartanLlama[10] 譯者:ictlyh 校對:jasminepeng

本文由 LCTT 原創編譯,Linux中國 榮譽推出

  • [1]: 準備環境 - https://linux.cn/article-8626-1.html

  • [2]: 斷點 - https://linux.cn/article-8645-1.html

  • [3]: 寄存器和內存 - https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/

  • [4]: Elves 和 dwarves - https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/

  • [5]: 源碼和信號 - https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/

  • [6]: 源碼級逐步執行 - https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/

  • [7]: System V x86_64 ABI - https://www.uclibc.org/docs/psABI-x86_64.pdf

  • [8]: process_vm_readv 和 process_vm_writev - http://man7.org/linux/man-pages/man2/process_vm_readv.2.html

  • [9]: 這裡 - https://github.com/TartanLlama/minidbg/tree/tut_registers

  • [10]: TartanLlama - https://www.twitter.com/TartanLlama

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Linux技術 的精彩文章:

20 萬大獎,這是這個夏天開發者最值得參與的大賽

TAG:Linux技術 |

您可能感興趣

ARM的7種工作模式和37個寄存器
教你輕鬆運用CAN_BTR寄存器控制LBKM與SILM
寄存器在CPU中是怎麼工作的呢?
C語言訪問MCU寄存器,有兩種方式可以採用!
累加器是寄存器嗎?寄存器、累加器、暫存器有什麼區別?
兩種方式實現C語言訪問MCU寄存器
計算機8位、16位、32位通用寄存器有什麼區別?
《刀劍神域:代碼寄存器》8月30日將停運
不正經評測之:《刀劍神域:代碼寄存器》
德奧聯合開發出世界最大20量子比特量子寄存器
不管生辰石,還是長命鎖,都只不過是愛和期望的寄存器