〇、前言
最近在學習 debugger 的實現原理,并按照博客實現,是一個很不錯的小項目,這是地址。由于 macOS 的問題,系統調用并不完全相同,因此實現了兩個版本分支,一個是 main 版本分支(macOS M1 silicon),另一個是 linux 版本分支(Ubuntu 20.04 x86),這是倉庫地址。以下以及后都用 linux 版本代碼闡述其原理。
一、斷點創建
這很簡單,主要是由 ptrace()
實現(debug工具都依賴于 ptrace()
):
#ifndef BREAKPOINT_HPP_
#define BREAKPOINT_HPP_
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
class BreakPoint {pid_t m_pid;intptr_t m_addr;bool m_enabled;uint8_t m_saved_data; // 最低位的舊數據(1 字節),之后需要恢復public:BreakPoint() {}BreakPoint(pid_t pid, intptr_t addr): m_pid(pid), m_addr(addr), m_enabled(false), m_saved_data{} {}auto is_enabled() const -> bool { return m_enabled; }auto get_address() const -> intptr_t { return m_addr; }void enable() {auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);m_saved_data = static_cast<uint8_t>(data & 0xff); // save bottom byteuint64_t int3 = 0xcc;uint64_t data_with_int3 = ((data & ~0xff) | int3); // set bottom byte to// 0xccptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);m_enabled = true;}void disable() {auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);auto restored_data = ((data & ~0xff) | m_saved_data);ptrace(PTRACE_POKEDATA, m_pid, m_addr, restored_data);m_enabled = false;}
};
#endif
以上是 BreakPoint 類的定義。重點是關注 enable()
和 disable()
兩個方法,在這兩個方法中,這段代碼及其關鍵:
auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);m_saved_data = static_cast<uint8_t>(data & 0xff); // save bottom byteuint64_t int3 = 0xcc;uint64_t data_with_int3 = ((data & ~0xff) | int3); // set bottom byte to// 0xccptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);
這里先說明一下,int3
是 x86
中的一個中斷指令,只要我們把某個指令修改為 int3
,那么它運行到這里就會停下來。另外,我們只是打個斷點,又不想真正得越過這個指令(這個指令被越過不執行,誰都不知道會發生什么),所以后面得恢復這個執行,并重新執行它,這就是 disable()
,我們先討論 enable()
。
因為 int3 指令的代碼為 0xcc
,這很明顯是一個 1 字節指令,只要我們在我們想打斷的指令處,將操作碼改為 0xcc
,這個指令就會停下來(這里牽扯到字節序,因為指令第一個字節是低地址,因為我們需要將 int3 放在一個指令的最低處)。然后再將這個被篡改的指令放回到原處,就成功的打了一個斷點。
至于 disable()
,其實做的也是這樣的事情,將原來的被替換的一個字節再恢復放回去:
auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);auto restored_data = ((data & ~0xff) | m_saved_data);ptrace(PTRACE_POKEDATA, m_pid, m_addr, restored_data);m_enabled = false;
以上都是很簡單的東西,我們現在就可以檢驗這個事情了,對了以下是 debugger 類的定義:
#ifndef DEBUGGER_HPP_
#define DEBUGGER_HPP_#include "../ext/linenoise/linenoise.h"
#include "breakpoint.hpp"
#include "helpers.hpp"
#include <cstddef>
#include <iostream>
#include <string>
#include <unordered_map>class debugger {std::string m_prog_name;pid_t m_pid;std::unordered_map<std::intptr_t, BreakPoint> m_breakPoints; // 存儲斷點public:// 這里不應該給默認參數,斷言:傳了正確的 prog_name,piddebugger(std::string prog_name, pid_t pid): m_prog_name(prog_name), m_pid(pid) {}void run() {int wait_status;auto options = 0;waitpid(m_pid, &wait_status, options);char *line = nullptr;while ((line = linenoise("minidbg> ")) != nullptr) {handl_command(line);linenoiseHistoryAdd(line);linenoiseFree(line);}}// handlersvoid handl_command(const std::string &line) {auto args = split(line, ' ');auto command = args[0];if (is_prefix(command, "continue")) {continue_execution();} else if (is_prefix(command, "break")) { // break 地址std::string addr{args[1], 2};set_breakPoint(std::stol(addr, 0, 16));} else {std::cerr << "Unkown command\n";}}void continue_execution() {ptrace(PTRACE_CONT, m_pid, nullptr, nullptr);int wait_status;auto options = 0;waitpid(m_pid, &wait_status, options);}void set_breakPoint(std::intptr_t addr) {std::cout << "Set breakpoint at address 0x" << std::hex << addr<< std::endl;BreakPoint bp{m_pid, addr};bp.enable();m_breakPoints[addr] = bp;}~debugger() {}
};#endif
二、檢測
main()
就是 debugger 的 main()
了:
#include "../include/debugger.hpp"
#include <cstddef>
#include <iostream>
#include <unistd.h>
#include <sys/personality.h>
int main(int argc, char *argv[]) {if (argc < 2) {std::cerr << "Program paras are not right.";return -1;}auto proj = argv[1];auto pid = fork();if (pid == 0) {personality(ADDR_NO_RANDOMIZE); // 取消隨機內存// child progress// debugged progressptrace(PTRACE_TRACEME, 0, nullptr, nullptr);execl(proj, proj, nullptr);} else if (pid >= 1) {// parent progress// debugger progressstd::cout << "Start debugging the progress: " << proj << ", pid = " << pid<< ":\n";debugger dbg(proj, pid);dbg.run();}return 0;
}
被 debug
的進程放在子進程中,然后由父進程,也就是我們的 debugger process
,由它進行調試。
我們先寫一個被 debug 的程序,這個程序輸出 hello,world.
:
#include <iostream>
int main() {std::cerr << "hello,world.\n";return 0;
}
編譯后,我們要打斷點進行測試,可以看到目前只能傳入一個地址,這個地址還是 0x
開頭的 16 進制地址,我們對于這個地址絲毫沒有頭緒,因為我們不知道 std::cerr << "hello,world.\n";
這個語句對應的匯編代碼的指令地址是什么。這個程序首先有一個程序結構,對這個不清楚的話,可以看看我之前寫的文章,是關于 elf
的,可以參考 c++ 內存模型或者 c++內存管理的那幾篇博客。
現在看看這個被調試的程序的文件結構:
objdump -d hw hw: file format elf64-x86-64Disassembly of section .init:0000000000001000 <_init>:1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__>100f: 48 85 c0 test %rax,%rax1012: 74 02 je 1016 <_init+0x16>1014: ff d0 callq *%rax1016: 48 83 c4 08 add $0x8,%rsp101a: c3 retq Disassembly of section .plt:0000000000001020 <.plt>:1020: ff 35 82 2f 00 00 pushq 0x2f82(%rip) # 3fa8 <_GLOBAL_OFFSET_TABLE_+0x8>1026: f2 ff 25 83 2f 00 00 bnd jmpq *0x2f83(%rip) # 3fb0 <_GLOBAL_OFFSET_TABLE_+0x10>102d: 0f 1f 00 nopl (%rax)1030: f3 0f 1e fa endbr64 1034: 68 00 00 00 00 pushq $0x01039: f2 e9 e1 ff ff ff bnd jmpq 1020 <.plt>103f: 90 nop1040: f3 0f 1e fa endbr64 1044: 68 01 00 00 00 pushq $0x11049: f2 e9 d1 ff ff ff bnd jmpq 1020 <.plt>104f: 90 nop1050: f3 0f 1e fa endbr64 1054: 68 02 00 00 00 pushq $0x21059: f2 e9 c1 ff ff ff bnd jmpq 1020 <.plt>105f: 90 nopDisassembly of section .plt.got:0000000000001060 <__cxa_finalize@plt>:1060: f3 0f 1e fa endbr64 1064: f2 ff 25 65 2f 00 00 bnd jmpq *0x2f65(%rip) # 3fd0 <__cxa_finalize@GLIBC_2.2.5>106b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .plt.sec:0000000000001070 <__cxa_atexit@plt>:1070: f3 0f 1e fa endbr64 1074: f2 ff 25 3d 2f 00 00 bnd jmpq *0x2f3d(%rip) # 3fb8 <__cxa_atexit@GLIBC_2.2.5>107b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)0000000000001080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>:1080: f3 0f 1e fa endbr64 1084: f2 ff 25 35 2f 00 00 bnd jmpq *0x2f35(%rip) # 3fc0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@GLIBCXX_3.4>108b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)0000000000001090 <_ZNSt8ios_base4InitC1Ev@plt>:1090: f3 0f 1e fa endbr64 1094: f2 ff 25 2d 2f 00 00 bnd jmpq *0x2f2d(%rip) # 3fc8 <_ZNSt8ios_base4InitC1Ev@GLIBCXX_3.4>109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Disassembly of section .text:00000000000010a0 <_start>:10a0: f3 0f 1e fa endbr64 10a4: 31 ed xor %ebp,%ebp10a6: 49 89 d1 mov %rdx,%r910a9: 5e pop %rsi10aa: 48 89 e2 mov %rsp,%rdx10ad: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp10b1: 50 push %rax10b2: 54 push %rsp10b3: 4c 8d 05 d6 01 00 00 lea 0x1d6(%rip),%r8 # 1290 <__libc_csu_fini>10ba: 48 8d 0d 5f 01 00 00 lea 0x15f(%rip),%rcx # 1220 <__libc_csu_init>10c1: 48 8d 3d c1 00 00 00 lea 0xc1(%rip),%rdi # 1189 <main>10c8: ff 15 12 2f 00 00 callq *0x2f12(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5>10ce: f4 hlt 10cf: 90 nop00000000000010d0 <deregister_tm_clones>:10d0: 48 8d 3d 39 2f 00 00 lea 0x2f39(%rip),%rdi # 4010 <__TMC_END__>10d7: 48 8d 05 32 2f 00 00 lea 0x2f32(%rip),%rax # 4010 <__TMC_END__>10de: 48 39 f8 cmp %rdi,%rax10e1: 74 15 je 10f8 <deregister_tm_clones+0x28>10e3: 48 8b 05 ee 2e 00 00 mov 0x2eee(%rip),%rax # 3fd8 <_ITM_deregisterTMCloneTable>10ea: 48 85 c0 test %rax,%rax10ed: 74 09 je 10f8 <deregister_tm_clones+0x28>10ef: ff e0 jmpq *%rax10f1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)10f8: c3 retq 10f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001100 <register_tm_clones>:1100: 48 8d 3d 09 2f 00 00 lea 0x2f09(%rip),%rdi # 4010 <__TMC_END__>1107: 48 8d 35 02 2f 00 00 lea 0x2f02(%rip),%rsi # 4010 <__TMC_END__>110e: 48 29 fe sub %rdi,%rsi1111: 48 89 f0 mov %rsi,%rax1114: 48 c1 ee 3f shr $0x3f,%rsi1118: 48 c1 f8 03 sar $0x3,%rax111c: 48 01 c6 add %rax,%rsi111f: 48 d1 fe sar %rsi1122: 74 14 je 1138 <register_tm_clones+0x38>1124: 48 8b 05 c5 2e 00 00 mov 0x2ec5(%rip),%rax # 3ff0 <_ITM_registerTMCloneTable>112b: 48 85 c0 test %rax,%rax112e: 74 08 je 1138 <register_tm_clones+0x38>1130: ff e0 jmpq *%rax1132: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)1138: c3 retq 1139: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001140 <__do_global_dtors_aux>:1140: f3 0f 1e fa endbr64 1144: 80 3d e5 2f 00 00 00 cmpb $0x0,0x2fe5(%rip) # 4130 <completed.0>114b: 75 2b jne 1178 <__do_global_dtors_aux+0x38>114d: 55 push %rbp114e: 48 83 3d 7a 2e 00 00 cmpq $0x0,0x2e7a(%rip) # 3fd0 <__cxa_finalize@GLIBC_2.2.5>1155: 00 1156: 48 89 e5 mov %rsp,%rbp1159: 74 0c je 1167 <__do_global_dtors_aux+0x27>115b: 48 8b 3d a6 2e 00 00 mov 0x2ea6(%rip),%rdi # 4008 <__dso_handle>1162: e8 f9 fe ff ff callq 1060 <__cxa_finalize@plt>1167: e8 64 ff ff ff callq 10d0 <deregister_tm_clones>116c: c6 05 bd 2f 00 00 01 movb $0x1,0x2fbd(%rip) # 4130 <completed.0>1173: 5d pop %rbp1174: c3 retq 1175: 0f 1f 00 nopl (%rax)1178: c3 retq 1179: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)0000000000001180 <frame_dummy>:1180: f3 0f 1e fa endbr64 1184: e9 77 ff ff ff jmpq 1100 <register_tm_clones>0000000000001189 <main>:1189: f3 0f 1e fa endbr64 118d: 55 push %rbp118e: 48 89 e5 mov %rsp,%rbp1191: 48 8d 35 6d 0e 00 00 lea 0xe6d(%rip),%rsi # 2005 <_ZStL19piecewise_construct+0x1>1198: 48 8d 3d 81 2e 00 00 lea 0x2e81(%rip),%rdi # 4020 <_ZSt4cerr@@GLIBCXX_3.4>119f: e8 dc fe ff ff callq 1080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>11a4: b8 00 00 00 00 mov $0x0,%eax11a9: 5d pop %rbp11aa: c3 retq 00000000000011ab <_Z41__static_initialization_and_destruction_0ii>:11ab: f3 0f 1e fa endbr64 11af: 55 push %rbp11b0: 48 89 e5 mov %rsp,%rbp11b3: 48 83 ec 10 sub $0x10,%rsp11b7: 89 7d fc mov %edi,-0x4(%rbp)11ba: 89 75 f8 mov %esi,-0x8(%rbp)11bd: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)11c1: 75 32 jne 11f5 <_Z41__static_initialization_and_destruction_0ii+0x4a>11c3: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)11ca: 75 29 jne 11f5 <_Z41__static_initialization_and_destruction_0ii+0x4a>11cc: 48 8d 3d 5e 2f 00 00 lea 0x2f5e(%rip),%rdi # 4131 <_ZStL8__ioinit>11d3: e8 b8 fe ff ff callq 1090 <_ZNSt8ios_base4InitC1Ev@plt>11d8: 48 8d 15 29 2e 00 00 lea 0x2e29(%rip),%rdx # 4008 <__dso_handle>11df: 48 8d 35 4b 2f 00 00 lea 0x2f4b(%rip),%rsi # 4131 <_ZStL8__ioinit>11e6: 48 8b 05 0b 2e 00 00 mov 0x2e0b(%rip),%rax # 3ff8 <_ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4>11ed: 48 89 c7 mov %rax,%rdi11f0: e8 7b fe ff ff callq 1070 <__cxa_atexit@plt>11f5: 90 nop11f6: c9 leaveq 11f7: c3 retq 00000000000011f8 <_GLOBAL__sub_I_main>:11f8: f3 0f 1e fa endbr64 11fc: 55 push %rbp11fd: 48 89 e5 mov %rsp,%rbp1200: be ff ff 00 00 mov $0xffff,%esi1205: bf 01 00 00 00 mov $0x1,%edi120a: e8 9c ff ff ff callq 11ab <_Z41__static_initialization_and_destruction_0ii>120f: 5d pop %rbp1210: c3 retq 1211: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)1218: 00 00 00 121b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)0000000000001220 <__libc_csu_init>:1220: f3 0f 1e fa endbr64 1224: 41 57 push %r151226: 4c 8d 3d 5b 2b 00 00 lea 0x2b5b(%rip),%r15 # 3d88 <__frame_dummy_init_array_entry>122d: 41 56 push %r14122f: 49 89 d6 mov %rdx,%r141232: 41 55 push %r131234: 49 89 f5 mov %rsi,%r131237: 41 54 push %r121239: 41 89 fc mov %edi,%r12d123c: 55 push %rbp123d: 48 8d 2d 54 2b 00 00 lea 0x2b54(%rip),%rbp # 3d98 <__do_global_dtors_aux_fini_array_entry>1244: 53 push %rbx1245: 4c 29 fd sub %r15,%rbp1248: 48 83 ec 08 sub $0x8,%rsp124c: e8 af fd ff ff callq 1000 <_init>1251: 48 c1 fd 03 sar $0x3,%rbp1255: 74 1f je 1276 <__libc_csu_init+0x56>1257: 31 db xor %ebx,%ebx1259: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)1260: 4c 89 f2 mov %r14,%rdx1263: 4c 89 ee mov %r13,%rsi1266: 44 89 e7 mov %r12d,%edi1269: 41 ff 14 df callq *(%r15,%rbx,8)126d: 48 83 c3 01 add $0x1,%rbx1271: 48 39 dd cmp %rbx,%rbp1274: 75 ea jne 1260 <__libc_csu_init+0x40>1276: 48 83 c4 08 add $0x8,%rsp127a: 5b pop %rbx127b: 5d pop %rbp127c: 41 5c pop %r12127e: 41 5d pop %r131280: 41 5e pop %r141282: 41 5f pop %r151284: c3 retq 1285: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)128c: 00 00 00 00 0000000000001290 <__libc_csu_fini>:1290: f3 0f 1e fa endbr64 1294: c3 retq Disassembly of section .fini:0000000000001298 <_fini>:1298: f3 0f 1e fa endbr64 129c: 48 83 ec 08 sub $0x8,%rsp12a0: 48 83 c4 08 add $0x8,%rsp12a4: c3 retq
可以看到,這個程序雖然只是輸出 hello,world.
,但依然很復雜,因為它要包含其它很多的基礎資源或者子程序,我們只需要重點關注 main
:
0000000000001189 <main>:1189: f3 0f 1e fa endbr64 118d: 55 push %rbp118e: 48 89 e5 mov %rsp,%rbp1191: 48 8d 35 6d 0e 00 00 lea 0xe6d(%rip),%rsi # 2005 <_ZStL19piecewise_construct+0x1>1198: 48 8d 3d 81 2e 00 00 lea 0x2e81(%rip),%rdi # 4020 <_ZSt4cerr@@GLIBCXX_3.4>119f: e8 dc fe ff ff callq 1080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>11a4: b8 00 00 00 00 mov $0x0,%eax11a9: 5d pop %rbp11aa: c3 retq
可以看到,這個段是從0000000000001189
開始的,需要關注的輸出語句為:
119f: e8 dc fe ff ff callq 1080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
這個地址 119f
就是我們需要打斷點的地方,被我們找出來了,這個地址是定死的,它在運行的時候,需要加載到內存中。問題是,加載到哪里?
我們并不知道加載到哪里,換句話說,我們不知道段地址是什么,它不固定,這主要是為了程序數據安全考慮,采用了內存分布隨機化,我們可以關掉內存分布隨機化:
if (pid == 0) {personality(ADDR_NO_RANDOMIZE); // 取消隨機內存// child progress// debugged progressptrace(PTRACE_TRACEME, 0, nullptr, nullptr);execl(proj, proj, nullptr);...
這樣它就固定了,我們可以這樣查看它在運行的時候的 map
,首先用我們程序進行調試:
./main hw
Start debugging the progress: hw, pid = 260915:
minidbg>
可以看到,pid
為 260915
,另開一個 zsh,直接查看:
cat /proc/260915/maps
555555554000-555555555000 r--p 00000000 fc:01 698165 /root/mydebugger/src/hw
555555555000-555555556000 r-xp 00001000 fc:01 698165 /root/mydebugger/src/hw
555555556000-555555557000 r--p 00002000 fc:01 698165 /root/mydebugger/src/hw
555555557000-555555559000 rw-p 00002000 fc:01 698165 /root/mydebugger/src/hw
7ffff7fcb000-7ffff7fce000 r--p 00000000 00:00 0 [vvar]
7ffff7fce000-7ffff7fcf000 r-xp 00000000 00:00 0 [vdso]
7ffff7fcf000-7ffff7fd0000 r--p 00000000 fc:01 1185709 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7ffff7fd0000-7ffff7ff3000 r-xp 00001000 fc:01 1185709 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7ffff7ff3000-7ffff7ffb000 r--p 00024000 fc:01 1185709 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7ffff7ffc000-7ffff7ffe000 rw-p 0002c000 fc:01 1185709 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0
可以看到我們的可執行代碼也就是 main
段在這里:
555555555000-555555556000 r-xp 00001000 fc:01 698165 /root/mydebugger/src/hw
這以后都是固定的,雖然不安全,但是僅僅是為了演示,就沒關系了。段的偏移地址為555555554000
,因為我們需要打斷點的地址為119f
,因此:
基址 + 指令相對地址
= 555555554000 + 119f
= 55555555519f
可以預見的是,如果 break 0x55555555519f
,之后執行,并不會打印出 hello,world
,但是我們如果打到了下一條地址:0x5555555551a4
,運行之后,就會理解打印出 hello,world
。以下進行檢測:
./main hw
Start debugging the progress: hw, pid = 261169:
minidbg> break 0x55555555519f
Set breakpoint at address 0x55555555519f
minidbg> continue
minidbg>
我們換一個地址:
./main hw
Start debugging the progress: hw, pid = 261407:
minidbg> break 0x0x5555555551a4
Set breakpoint at address 0x5555555551a4
minidbg> continue
hello,world.
minidbg>
可以看到,以下就打印出了hello,world
。以上符合我們的預期,因此實驗是成功的,另外不需要擔心 pid
不一樣,由于我們關閉了地址空間布局隨機化(ASLR, Address Space Layout Randomization),段地址不會變,因此地址也是固定的。