前言从事编程工作的我们,总有调试的时刻,无论是通过IDE调试开发中的代码,还是通过GDB查看运行过程。尤其是经常使用GDB的童鞋,更熟悉它提供的强大功能,其中也不乏断点。刚好最近做了个Ptrace相关的实验,也写了这篇小文分享下断点的原理。简单的GDB演示//test.cpp#include#includevoidtest1(){std::cout<<"test"<回头看a.out的输出,可以看到已经停在了main:5693,不再打印,进程状态也变成了T:Tstatus表示:(TASK_STOPPEDorTASK_TRACED),暂停状态或跟踪状态,然后可以通过GDB实现各种调试操作。这次我们也想实现类似的效果,只是超简化版,只考虑:在指定位置暂停,获得进程控制权。前置知识准备在实施之前,我们需要了解必要的知识:注册:RIP如果之前还不知道注册的童鞋,可以先看看:https://www.jianshu.com/p/029...直接摘录其中的一段描述:rip指令地址寄存器,用于存放CPU即将执行的指令的地址。CPU每执行一次相应的汇编指令,rip寄存器的值就会自行累加;之前不知道ptrace的童鞋可以看看:http://fancy-blogs.com/2018/0...中ptrace中有两个角色:tracee:被跟踪的人,也就是被监控的进程,通过ptrace系统调用作用于它的操作(例如:上面的22346进程);tracer:跟踪器,负责监控和处理来自被跟踪者的信息(eg:GDB);以下两个术语将被直接引用。实现思路很简单。1、首先确定我们要的断点地址在GDB中。我们习惯于直接在行号或函数名上设置断点。行号比较复杂,我们先显示函数名。Linux环境编译的可执行文件遵循ELF格式,如果没有特殊处理,会保留一个比较完整的符号表。以开头的程序为例,可以通过readelf-sa.out查看:这个符号表记录了进程需要使用的符号所在的位置。如图,第一列是符号地址(十六进制),第二列是长度,最后一列是符号名称。这里需要打断test1函数,也就是红圈圈出来的地方。这里可能有童鞋想问为什么是:_Z5test1v这个主要是cpp的名字修改:https://blog.csdn.net/u013220...,没问题。我们现在可以看到之前的地址是0x400916;2.通过Ptrace获取tracee的控制权//建立trace关系,很多童鞋可能会用到PTRACE_ATTACH,它和PTRACE_SEIZE的区别在于会立即挂起tracee,而PTRACE_SEIZE不会将ptrace(PTRACE_SEIZE,pid,addr,data)//中断tracee的行为,将控制权交给tracerptrace(PTRACE_INTERRUPT,pid,addr,data)//感知tracee的状态变化,方便下一步操作waitpid(pid,&status,options)3.保留当前rip指令的内容,替换为中断指令//获取traceeaddr内存的内容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)4.恢复运行,等待traptrigger//让tracee继续运行ptrace(PTRACE_CONT,pid,addr,data)5.恢复rip指令,完成调试完整的Tracer代码#include#include#include#include#include#includevoiddowait(pid_tpid){intstatus,signum;while(true){waitpid(pid,&status,0);如果(WIFSTOPPED(status)){signum=WSTOPSIG(status);如果(signum==SIGTRAP){break;}else{std::cout<<"其他符号,跳过..."<>指令;//恢复rip,否则会因为缺少有效的rip而导致traceecoredumpptrace(PTRACE_SETREGS,pid,NULL,&old_regs);//恢复addr的原始值ptrace(PTRACE_POKEDATA,pid,addr,old_code);ptrace(PTRACE_CONT,pid,0,0);}voidquit(pid_tpid){ptrace(PTRACE_DETACH,pid,NULL,NULL);std::cout<<“退出!”<<标准::结束;退出(0);}intmain(intargc,char*argv[]){pid_tpid=std::stoi(argv[1]);if(ptrace(PTRACE_SEIZE,pid,NULL,NULL)){perror("ptrace_seize失败");返回-1;}if(ptrace(PTRACE_INTERRUPT,pid,0,0)){perror("中断失败");退出(pid);}等待(pid);//断点地址longbreak_addr=0x400916;break_onece(pid,break_addr);退出(pid);return1;}Compile&rung++trace_test.cpp-std=c++11-otrace_test./trace_test22346#本文开头的过程总结,网上有很多关于断点原理的文章,但多半是泛泛而谈,意犹未尽。简单的用最简单的例子来降低大家的实践成本!其实文中提到的例子有很多是可以优化的。转换点:例如:获取函数地址的方法,既然提到了ELF符号表,那么用户传入的用户名要通过解析这张表转换为地址;另一个例子:应该维护一个全局断点表,存储任意数量的断点,并允许每个断点被重复使用;甚至比如:涉及到Ptrace的错误返回,一定要优雅的处理,因为在每次返回值不为0的情况下,贸然进行下一步是非常危险的,极有可能导致traceecoredump;每个例子都可以研究一下,欢迎期待后续,欢迎各位大神指教交流。QQ讨论群:258498217转载请注明出处:https://segmentfault.com/a/1190000021870750