从事编程工做的咱们,总有调试的时刻,不论是经过 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