從事編程工做的咱們,總有調試的時刻,不論是經過 IDE 調試開發中的代碼,仍是經過 GDB 排查正在運行的進程。ios
特別是常用 GDB 的童鞋,對它提供的強大功能更加如數家珍,其中就不乏 breakpoint(斷點)。c++
恰好最近作到 Ptrace 相關的實驗,也順便擼了這篇小文來分享下 斷點 當中的道理。編程
// test.cpp #include<iostream> #include<unistd.h> void test1(){ std::cout << "test" << std::endl; } int main() { while (true) { std::cout << "main: " << getpid() << std::endl; test1(); sleep(1); } return 0; }
編譯運行segmentfault
g++ -std=c++11 test.cpp && ./a.out // 輸出 main: 22346 test main: 22346 test main: 22346 ...
開啓 GDB,而且在 test1 函數斷點函數
sudo gdb a.out -p 22346 // 輸出 ... (省略打印的信息, 直接輸入命令) (gdb) break test1 // 在 test1 函數斷點 Breakpoint 1 at 0x40091a (gdb) c // 繼續運行 Continuing. Breakpoint 1, 0x000000000040091a in test1() () (gdb) i r rip // 查看 cpu 下一條指令的內容 rip 0x40091a 0x40091a <test1()+4>
回頭看 a.out 的輸出,能夠看到已經停在 main: 5693 再也不打印了,而進程狀態也變成了 T:優化
T 狀態意味着:(TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態,接下來就能夠經過 GDB 實現各類調試的操做了。ui
咱們此次也要實現相似的效果,不過只是一個超簡化版本,只考慮:在指定的位置暫停,得到進程的控制權。spa
在實現以前,咱們須要瞭解下必要的知識:.net
若是以前沒有了解 寄存器 的童鞋能夠先看看:https://www.jianshu.com/p/029...調試
直接摘抄裏面的一段描述:
rip 指令地址寄存器,用來存儲 CPU 即將要執行的指令地址。 每次 CPU 執行完相應的彙編指令以後,rip 寄存器的值就會自行累加;
若是以前沒有了解 Ptrace 的童鞋能夠先看看:http://fancy-blogs.com/2018/0...
在ptrace中有兩個角色:
下文會直接引用這兩個名詞。
實現的思路很是簡單
在 GDB 中,咱們是習慣對 行號 或者 函數名 直接設置斷點,行號相對來講比較複雜,咱們先展現 函數名 的。
在 Linux 環境下編譯出來的可執行文件都是遵循 ELF 格式,若是沒有特殊處理,它會保留比較完整的 符號表。
就拿開頭的程序來當例子,能夠經過 readelf -s a.out 查看:
這個符號表記錄了進程須要用到的符號分別在什麼位置。
如圖,第一列就是符號的地址(十六進制),第二列是長度,最後一列是符號名字。
咱們這裏須要在 test1 這個函數打斷點,也就是紅色圈出來的地方,這裏可能會有童鞋想問爲啥是:_Z5test1v
這裏主要是 cpp 的名字修飾問題:https://blog.csdn.net/u013220...,不礙事。
咱們如今能夠看到前面的地址就是 0x400916;
// 創建追蹤的關係, 不少童鞋可能會用 PTRACE_ATTACH,它和 PTRACE_SEIZE 的區別就是,它會立刻暫停 tracee,而 PTRACE_SEIZE 不會 ptrace(PTRACE_SEIZE, pid, addr, data) // 中斷 tracee 的行爲,將控制權交給 tracer ptrace(PTRACE_INTERRUPT, pid, addr, data) // 感知 tracee 的狀態變動,便於下一步操做 waitpid(pid, &status, options)
// 獲取 tracee addr 內存的內容 ptrace(PTRACE_PEEKDATA, pid, addr, data) // 修改 tracee 指定內存的內容 ptrace(PTRACE_POKEDATA, pid, addr, data) // 獲取 tracee 當前的寄存器內容 ptrace(PTRACE_GETREGS, pid, addr, data) // 設置 tracee 當前的寄存器內容 ptrace(PTRACE_SETREGS, pid, addr, data)
// 讓 tracee 繼續運行 ptrace(PTRACE_CONT, pid, addr, data)
#include <sys/ptrace.h> #include <iostream> #include <stdio.h> #include <sys/user.h> #include <sys/wait.h> #include <string> void dowait(pid_t pid) { int status, signum; while (true) { waitpid(pid, &status, 0); if (WIFSTOPPED(status)) { signum = WSTOPSIG(status); if (signum == SIGTRAP) { break; } else { std::cout << "Other signum, skipping..." << std::endl; ptrace(PTRACE_CONT, pid, 0, 0); } } } } void break_onece(pid_t pid, long addr) { // 保存 addr 舊的指令和寄存器(主要是 rip) long old_code = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); user_regs_struct old_regs; ptrace(PTRACE_GETREGS, pid, NULL, &old_regs); long trap_code = old_code; unsigned char *p = (unsigned char*) &trap_code; // Trap 中斷指令的十六進制數值 p[0] = 0xcc; // 用 Trap 覆蓋 addr 數值,等 cpu 執行至此就會中斷 if (ptrace(PTRACE_POKEDATA, pid, addr, trap_code)) { std::cout << "Break failed" << std::endl; return; } ptrace(PTRACE_CONT, pid, NULL, NULL); dowait(pid); // 敲入任意字符以繼續,能夠在此加入其它調試邏輯(海闊憑魚躍!!!) std::cout << "Next ? " << std::endl; std::string instruction; std::cin >> instruction; // 恢復 rip, 不然會因缺少有效 rip 致使 tracee coredump ptrace(PTRACE_SETREGS, pid, NULL, &old_regs); // 恢復 addr 原值 ptrace(PTRACE_POKEDATA, pid, addr, old_code); ptrace(PTRACE_CONT, pid, 0, 0); } void quit(pid_t pid) { ptrace(PTRACE_DETACH, pid, NULL, NULL); std::cout << "quit!" << std::endl; exit(0); } int main(int argc, char* argv[]) { pid_t pid = std::stoi(argv[1]); if (ptrace(PTRACE_SEIZE, pid, NULL, NULL)) { perror("ptrace_seize failed"); return -1; } if(ptrace(PTRACE_INTERRUPT, pid, 0, 0)) { perror("interrupt failed"); quit(pid); } dowait(pid); // 想斷點的地址 long break_addr = 0x400916; break_onece(pid, break_addr); quit(pid); return 1; }
編譯 & 運行
g++ trace_test.cpp -std=c++11 -o trace_test ./trace_test 22346 # 本文開頭的進程
關於斷點的原理網上有不少文章提到,但比較多也是走馬觀花一筆帶過,意猶未盡,乾脆直接用最淺顯的例子下降你們練手
成本!
其實在文中提到的例子也有很是多能夠優化的點:
每一個好比均可以展開研究,因此歡迎期待後續。
歡迎各位大神指點交流, QQ討論羣: 258498217
轉載請註明來源: http://www.javashuo.com/article/p-uhokhuyp-em.html