自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

開(kāi)發(fā)一個(gè)Linux調(diào)試器(二):斷點(diǎn)

系統(tǒng) Linux
在該系列的第一部分,我們寫了一個(gè)小的進(jìn)程啟動(dòng)器,作為我們調(diào)試器的基礎(chǔ)。在這篇博客中,我們會(huì)學(xué)習(xí)在 x86 Linux 上斷點(diǎn)是如何工作的,以及如何給我們工具添加設(shè)置斷點(diǎn)的能力。

[[195217]]

在該系列的***部分,我們寫了一個(gè)小的進(jìn)程啟動(dòng)器,作為我們調(diào)試器的基礎(chǔ)。在這篇博客中,我們會(huì)學(xué)習(xí)在 x86 Linux 上斷點(diǎn)是如何工作的,以及如何給我們工具添加設(shè)置斷點(diǎn)的能力。

系列文章索引

隨著后面文章的發(fā)布,這些鏈接會(huì)逐漸生效。

  • 準(zhǔn)備環(huán)境
  • 斷點(diǎn)
  • 寄存器和內(nèi)存
  • Elves 和 dwarves
  • 源碼和信號(hào)
  • 源碼層逐步執(zhí)行
  • 源碼層斷點(diǎn)
  • 調(diào)用棧
  • 讀取變量 10.之后步驟

斷點(diǎn)是如何形成的?

有兩種類型的斷點(diǎn):硬件和軟件。硬件斷點(diǎn)通常涉及到設(shè)置與體系結(jié)構(gòu)相關(guān)的寄存器來(lái)為你產(chǎn)生斷點(diǎn),而軟件斷點(diǎn)則涉及到修改正在執(zhí)行的代碼。在這篇文章中我們只會(huì)關(guān)注軟件斷點(diǎn),因?yàn)樗鼈儽容^簡(jiǎn)單,而且可以設(shè)置任意多斷點(diǎn)。在 x86 機(jī)器上任一時(shí)刻你最多只能有 4 個(gè)硬件斷點(diǎn),但是它們能讓你在讀取或者寫入給定地址時(shí)觸發(fā),而不是只有當(dāng)代碼執(zhí)行到那里的時(shí)候。

我前面說(shuō)軟件斷點(diǎn)是通過(guò)修改正在執(zhí)行的代碼實(shí)現(xiàn)的,那么問(wèn)題就來(lái)了:

  • 我們?nèi)绾涡薷拇a?
  • 為了設(shè)置斷點(diǎn)我們要做什么修改?
  • 如何告知調(diào)試器?

***個(gè)問(wèn)題的答案顯然是 ptrace。我們之前已經(jīng)用它為我們的程序設(shè)置跟蹤并繼續(xù)程序的執(zhí)行,但我們也可以用它來(lái)讀或者寫內(nèi)存。

當(dāng)執(zhí)行到斷點(diǎn)時(shí),我們的更改要讓處理器暫停并給程序發(fā)送信號(hào)。在 x86 機(jī)器上這是通過(guò) int 3 重寫該地址上的指令實(shí)現(xiàn)的。x86 機(jī)器有個(gè)中斷向量表(interrupt vector table),操作系統(tǒng)能用它來(lái)為多種事件注冊(cè)處理程序,例如頁(yè)故障、保護(hù)故障和無(wú)效操作碼。它就像是注冊(cè)錯(cuò)誤處理回調(diào)函數(shù),但是是在硬件層面的。當(dāng)處理器執(zhí)行 int 3 指令時(shí),控制權(quán)就被傳遞給斷點(diǎn)中斷處理器,對(duì)于 Linux 來(lái)說(shuō),就是給進(jìn)程發(fā)送 SIGTRAP 信號(hào)。你可以在下圖中看到這個(gè)進(jìn)程,我們用 0xcc 覆蓋了 mov 指令的***個(gè)字節(jié),它是 init 3 的指令代碼。

 

斷點(diǎn)

謎題的***一個(gè)部分是調(diào)試器如何被告知中斷的。如果你回顧前面的文章,我們可以用 waitpid 來(lái)監(jiān)聽(tīng)被發(fā)送給被調(diào)試的程序的信號(hào)。這里我們也可以這樣做:設(shè)置斷點(diǎn)、繼續(xù)執(zhí)行程序、調(diào)用 waitpid 并等待直到發(fā)生 SIGTRAP。然后就可以通過(guò)打印已運(yùn)行到的源碼位置、或改變有圖形用戶界面的調(diào)試器中關(guān)注的代碼行,將這個(gè)斷點(diǎn)傳達(dá)給用戶。

實(shí)現(xiàn)軟件斷點(diǎn)

我們會(huì)實(shí)現(xiàn)一個(gè) breakpoint 類來(lái)表示某個(gè)位置的斷點(diǎn),我們可以根據(jù)需要啟用或者停用該斷點(diǎn)。

  1. class breakpoint { 
  2. public
  3.     breakpoint(pid_t pid, std::intptr_t addr) 
  4.         : m_pid{pid}, m_addr{addr}, m_enabled{false}, m_saved_data{} 
  5.     {} 
  6.     void enable(); 
  7.     void disable(); 
  8.     auto is_enabled() const -> bool { return m_enabled; } 
  9.     auto get_address() const -> std::intptr_t { return m_addr; } 
  10. private: 
  11.     pid_t m_pid; 
  12.     std::intptr_t m_addr; 
  13.     bool m_enabled; 
  14.     uint64_t m_saved_data; //data which used to be at the breakpoint address 
  15. }; 

這里的大部分代碼都是跟蹤狀態(tài);真正神奇的地方是 enable 和 disable 函數(shù)。

正如我們上面學(xué)到的,我們要用 int 3 指令 - 編碼為 0xcc - 替換當(dāng)前指定地址的指令。我們還要保存該地址之前的值,以便后面恢復(fù)該代碼;我們不想忘了執(zhí)行用戶(原來(lái))的代碼。

  1. void breakpoint::enable() { 
  2.     m_saved_data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr); 
  3.     uint64_t int3 = 0xcc; 
  4.     uint64_t data_with_int3 = ((m_saved_data & ~0xff) | int3); //set bottom byte to 0xcc 
  5.     ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3); 
  6.     m_enabled = true

PTRACE_PEEKDATA 請(qǐng)求告知 ptrace 如何讀取被跟蹤進(jìn)程的內(nèi)存。我們給它一個(gè)進(jìn)程 ID 和一個(gè)地址,然后它返回給我們?cè)摰刂樊?dāng)前的 64 位內(nèi)容。 (m_saved_data & ~0xff) 把這個(gè)數(shù)據(jù)的低位字節(jié)置零,然后我們用它和我們的 int 3 指令按位或(OR)來(lái)設(shè)置斷點(diǎn)。***我們通過(guò) PTRACE_POKEDATA 用我們的新數(shù)據(jù)覆蓋那部分內(nèi)存來(lái)設(shè)置斷點(diǎn)。

disable 的實(shí)現(xiàn)比較簡(jiǎn)單,我們只需要恢復(fù)用 0xcc 所覆蓋的原始數(shù)據(jù)。

  1. void breakpoint::disable() { 
  2.     ptrace(PTRACE_POKEDATA, m_pid, m_addr, m_saved_data); 
  3.     m_enabled = false

在調(diào)試器中增加斷點(diǎn)

為了支持通過(guò)用戶界面設(shè)置斷點(diǎn),我們要在 debugger 類修改三個(gè)地方:

  1. 給 debugger 添加斷點(diǎn)存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)
  2. 添加 set_breakpoint_at_address 函數(shù)
  3. 給我們的 handle_command 函數(shù)添加 break 命令

我會(huì)將我的斷點(diǎn)保存到 std::unordered_map<std::intptr_t, breakpoint> 結(jié)構(gòu),以便能簡(jiǎn)單快速地判斷一個(gè)給定的地址是否有斷點(diǎn),如果有的話,取回該 breakpoint 對(duì)象。

  1. class debugger { 
  2.     //... 
  3.     void set_breakpoint_at_address(std::intptr_t addr); 
  4.     //... 
  5. private: 
  6.     //... 
  7.     std::unordered_map<std::intptr_t,breakpoint> m_breakpoints; 

在 set_breakpoint_at_address 函數(shù)中我們會(huì)新建一個(gè) breakpoint 對(duì)象,啟用它,把它添加到數(shù)據(jù)結(jié)構(gòu)里,并給用戶打印一條信息。如果你喜歡的話,你可以重構(gòu)所有的輸出信息,從而你可以將調(diào)試器作為庫(kù)或者命令行工具使用,為了簡(jiǎn)便,我把它們都整合到了一起。

  1. void debugger::set_breakpoint_at_address(std::intptr_t addr) { 
  2.     std::cout << "Set breakpoint at address 0x" << std::hex << addr << std::endl; 
  3.     breakpoint bp {m_pid, addr}; 
  4.     bp.enable(); 
  5.     m_breakpoints[addr] = bp; 

現(xiàn)在我們會(huì)在我們的命令處理程序中增加對(duì)我們新函數(shù)的調(diào)用。

  1. void debugger::handle_command(const std::string& line) { 
  2.     auto args = split(line,' '); 
  3.     auto command = args[0]; 
  4.     if (is_prefix(command, "cont")) { 
  5.         continue_execution(); 
  6.     } 
  7.     else if(is_prefix(command, "break")) { 
  8.         std::string addr {args[1], 2}; //naively assume that the user has written 0xADDRESS 
  9.         set_breakpoint_at_address(std::stol(addr, 0, 16)); 
  10.     } 
  11.     else { 
  12.         std::cerr << "Unknown command\n"
  13.     } 

我刪除了字符串中的前兩個(gè)字符并對(duì)結(jié)果調(diào)用 std::stol,你也可以讓該解析更健壯一些。std::stol 可以將字符串按照所給基數(shù)轉(zhuǎn)化為整數(shù)。

從斷點(diǎn)繼續(xù)執(zhí)行

如果你嘗試這樣做,你可能會(huì)發(fā)現(xiàn),如果你從斷點(diǎn)處繼續(xù)執(zhí)行,不會(huì)發(fā)生任何事情。這是因?yàn)閿帱c(diǎn)仍然在內(nèi)存中,因此一直被重復(fù)***。簡(jiǎn)單的解決辦法就是停用這個(gè)斷點(diǎn)、運(yùn)行到下一步、再次啟用這個(gè)斷點(diǎn)、然后繼續(xù)執(zhí)行。不過(guò)我們還需要更改程序計(jì)數(shù)器,指回到斷點(diǎn)前面,這部分內(nèi)容會(huì)留到下一篇關(guān)于操作寄存器的文章中介紹。

測(cè)試它

當(dāng)然,如果你不知道要在哪個(gè)地址設(shè)置,那么在某些地址設(shè)置斷點(diǎn)并非很有用。后面我們會(huì)學(xué)習(xí)如何在函數(shù)名或者代碼行設(shè)置斷點(diǎn),但現(xiàn)在我們可以通過(guò)手動(dòng)實(shí)現(xiàn)。

測(cè)試你調(diào)試器的簡(jiǎn)單方法是寫一個(gè) hello world 程序,這個(gè)程序輸出到 std::err(為了避免緩存),并在調(diào)用輸出操作符的地方設(shè)置斷點(diǎn)。如果你繼續(xù)執(zhí)行被調(diào)試的程序,執(zhí)行很可能會(huì)停止而不會(huì)輸出任何東西。然后你可以重啟調(diào)試器并在調(diào)用之后設(shè)置一個(gè)斷點(diǎn),現(xiàn)在你應(yīng)該看到成功地輸出了消息。

查找地址的一個(gè)方法是使用 objdump。如果你打開(kāi)一個(gè)終端并執(zhí)行 objdump -d <your program>,然后你應(yīng)該看到你的程序的反匯編代碼。你就可以找到 main 函數(shù)并定位到你想設(shè)置斷點(diǎn)的 call 指令。例如,我編譯了一個(gè) hello world 程序,反匯編它,然后得到了如下的 main 的反匯編代碼:

  1. 0000000000400936 <main>: 
  2.   400936:   55                      push   %rbp 
  3.   400937:   48 89 e5                mov    %rsp,%rbp 
  4.   40093a:   be 35 0a 40 00          mov    $0x400a35,%esi 
  5.   40093f:   bf 60 10 60 00          mov    $0x601060,%edi 
  6.   400944:   e8 d7 fe ff ff          callq  400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 
  7.   400949:   b8 00 00 00 00          mov    $0x0,%eax 
  8.   40094e:   5d                      pop    %rbp 
  9.   40094f:   c3                      retq 

正如你看到的,要沒(méi)有輸出,我們要在 0x400944 設(shè)置斷點(diǎn),要看到輸出,要在 0x400949 設(shè)置斷點(diǎn)。

總結(jié)

現(xiàn)在你應(yīng)該有了一個(gè)可以啟動(dòng)程序、允許在內(nèi)存地址上設(shè)置斷點(diǎn)的調(diào)試器。后面我們會(huì)添加讀寫內(nèi)存和寄存器的功能。再次說(shuō)明,如果你有任何問(wèn)題請(qǐng)?jiān)谠u(píng)論框中告訴我。

你可以在這里 找到該項(xiàng)目的代碼。

責(zé)任編輯:龐桂玉 來(lái)源: Linux中國(guó)
相關(guān)推薦

2017-09-25 08:04:31

Linux調(diào)試器源碼級(jí)斷點(diǎn)

2017-06-22 10:44:55

Linux調(diào)試器準(zhǔn)備環(huán)境

2017-10-09 10:26:01

Linux調(diào)試器堆棧展開(kāi)

2017-10-09 10:56:49

Linux調(diào)試器處理變量

2017-10-12 18:20:44

Linux調(diào)試器高級(jí)主題

2017-07-25 10:30:32

Linux調(diào)試器Elves和dwarv

2017-08-28 14:40:57

Linux調(diào)試器源碼和信號(hào)

2017-07-05 14:37:07

Linux調(diào)試器寄存器和內(nèi)存

2017-08-28 15:29:19

Linux調(diào)試器源碼級(jí)逐步執(zhí)行

2017-04-19 21:35:38

Linux調(diào)試器工作原理

2011-08-25 16:34:27

Lua調(diào)試器

2010-03-01 11:06:52

Python 調(diào)試器

2020-03-16 10:05:13

EmacsGUDLinux

2009-12-14 10:57:34

Ruby調(diào)試器

2011-08-31 16:51:12

Lua調(diào)試器

2019-12-06 14:30:41

GNU調(diào)試器GDB修復(fù)代碼

2024-03-13 08:00:00

Linux調(diào)試器應(yīng)用程序

2023-02-28 11:39:55

CMake腳本項(xiàng)目

2009-06-23 11:05:05

Mircosoft C

2011-08-31 16:47:07

Lua調(diào)試器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)