当你用 GDB 的时候,可以看到它完全控制了应用程序进程。当你在程序运行的时候用 Ctrl + C,程序的运行能够终止,而GDB能展示它的当前地址、堆栈跟踪信息之类的内容。
  但是它是怎么办到的呢?
  但是它们怎么不工作呢?
  开始,让我们先研究它怎样才会不工作。它不能通过阅读和分析程序的二进制信息来模拟程序的运行。它其实能做,而那应该能起作用(Valgrind 内存调试器是这样工作的),但是这样的话会很慢。Valgrind会让程序慢1000倍,但是GDB不会。它的工作机制与Qemu虚拟机一样。
  所以到底是怎么回事?黑魔法?……不,如果那样的话太简单了。
  另一种猜想?……?破解!是的,这里正是这样的。操作系统内核也提供了一些帮助。
  首先,关于Linux的进程机制需要了解一件事:父进程可以获得子进程的附加信息,也能够ptrace它们。并且你可以猜到的是,调试器是被调试的进程的父进程(或者它会变成父进程,在Linux中进程可以将一个进程变为自己子进程:-))
  Linux Ptrace API
  Linux Ptrace API 允许一个(调试器)进程来获取低等级的其他(被调试的)进程的信息。特别的,这个调试器可以:
  读写被调试进程的内存 :PTRACE_PEEKTEXT、PTRACE_PEEKUSER、PTRACE_POKE……
  读写被调试进程的CPU寄存器 PTRACE_GETREGSET、PTRACE_SETREGS
  因系统活动而被提醒:PTRACE_O_TRACEEXEC, PTRACE_O_TRACECLONE, PTRACE_O_EXITKILL, PTRACE_SYSCALL(你可以通过这些标识区分exec syscall、clone、exit以及其他系统调用)
  控制它的执行:PTRACE_SINGLESTEP、PTRACE_KILL、PTRACE_INTERRUPT、PTRACE_CONT (注意,CPU在这里是单步执行)
  修改它的信号处理:PTRACE_GETSIGINFO、PTRACE_SETSIGINFO
  Ptrace是如何实现的?
  Ptrace的实现不在本文讨论的范围内,所以我不想进一步讨论,只是简单地解释它是如何工作的(我不是内核专家,如果我说错了请一定指出来,并原谅我过分简化:-))
  Ptrace 是Linux内核的一部分,所以它能够获取进程所有内核级信息:
  读写数据?Linux有copy_to/from_user。
  获取CPU寄存器?用copy_regset_to/from_user很轻松(这里没有什么复杂的,因为CPU寄存器在进程未被调度时保存在Linux的struct task_struct *调度结构中)。
  修改信号处理?更新域last_siginfo
  单步执行?在处理器出发执行前,设置进程task结构的right flag(ARM、x86)
  Ptrace是在很多计划的操作中被Hooked(搜索 ptrace_event函数),所以它可以在被询问时(PTRACE_O_TRACEEXEC选项和与它相关的),向调试器发出一个SIGTRAP信号。
  没有Ptrace的系统会怎么样呢?
  这个解释超出了特定的Linux本地调试,但是对于大部分其他环境是合理的。要了解GDB在不同目标平台请求的内容,你可以看一下它在目标栈里面的操作。
  在这个目标接口里,你可以看到所有C调试需要的高级操作:
struct target_ops
{
struct target_ops *beneath;
/* To the target under this one.  */
const char *to_shortname;
/* Name this target type */
const char *to_longname;
/* Name for printing */
const char *to_doc;
/* Documentation.  Does not include trailing
newline, and starts with a one-line descrip-
tion (probably similar to to_longname).  */
void (*to_attach) (struct target_ops *ops, const char *, int);
void (*to_fetch_registers) (struct target_ops *, struct regcache *, int);
void (*to_store_registers) (struct target_ops *, struct regcache *, int);
int (*to_insert_breakpoint) (struct target_ops *, struct gdbarch *,
struct bp_target_info *);
int (*to_insert_watchpoint) (struct target_ops *,
CORE_ADDR, int, int, struct expression *);
...
}
  普通的GDB调用这些函数,然后目标相关的组件再实现它们。(概念上)这是一个栈,或者一个金字塔:栈顶的是非常通用的,比如:
  系统特定的Linux
  本地或远程调试
  调试方式特定的(ptrace、ttrace)
  指令集特定的(Linux ARM、Linux x86)
  那个远程目标很有趣,因为它通过一个连接协议(TCP/IP、串行端口)把两台“电脑”间的执行栈分离开来。
  那个远程的部分可以是运行在另一台Linux机器上的gdbserver。但是它也可以是一个硬件调试端口的界面(JTAG) 或者一个虚拟的机器管理程序(比如 Qemu),并能够代替内核和ptrace的功能。那个远程根调试器会查询管理程序的结构,或者直接地查询处理器硬件寄存器来代替对OS内核结构的查询。
  想要深层次学习这个远程协议,Embecosm 写了一篇一个关于不同信息的详细指南。Gdbserver的事件处理循环在这,而也可以在这里找到Qemu gdb-server stub 。
  总结一下
  我们能看到ptrace的API提供了这里所有底层机制被要求实现的调试器:
  获取exec系统调用并从调用的地方阻止它执行
  查询CPU的寄存器来获得处理器当前指令以及栈的地址
  获取clone或fork事件来检测新线程
  查看并改变数据地址读取并改变内存的变量
  但是这是一个调试器的全部工作吗?不,这只是那些非常低级的部分……它还会处理符号。这是,链接源程序和二进制文件。被忽视可能也是重要的的一件事:断点!我会首先解释一下断点是如何工作的,因为这部分内容非常有趣且需要技巧,然后回到符号处理。
  断点不是Ptrace API的一部分
  像我们之前看到的那样,断点不是ptrace API的一部分。但是我们可以改动内存并获取被调试的程序信号。你看不到其中的相关之处?这是因为断点的实现比较需要技巧并且还要一点hack!让我们来检验一下如何在一个指定的地址设置一个断点。
  1、这个调试器读取(ptrace追踪)存在地址里的二进制指令,并保存在它自己的数据结构中。
  2、它在这个位置写入一个不合法的指令。不管这个指令是啥,只要它是不合法的。
  3、当被调试的程序运行到这个不合法的指令时(或者更准确地说,处理器将内存中的内容设置好时)它不会继续运行(因为它是不合法的)。
  4、在现代多任务系统中,一个不合法的指令不会使整个系统崩溃掉,但是会通过引发一个中断(或错误)把控制权交回给系统内核。
  5、这个中断被Linux翻译成一个SIGTRAP信号,然后被发送到处理器……或者发给它的父进程,像调试器希望的那样。
  6、调试器获得信号并查看被调试的程序指令指针的值(换言之,是陷入 trap发生的地方)。如果这个IP地址是在断点列表中,那么是一个调试器的断点(否则是一个进程中的错误,只需要传过信号并让它崩溃)。
  7、现在,那个被调试的程序已经停在了断点,调试器可以让用户来做任何他/她想要做的事,等待时机合适继续执行。
  8、为了要继续执行,这个调试器需要 1、写入正确的指令来回到被调试的程序的内存; 2、单步执行(继续执行单个CPU指令,伴随着ptrace 单步执行); 3、把非法指令写回去(使得这个执行过程下一次可以再次停止) ;4、让这个执行正常运行
  很整洁,是不是?作为一个旁观的评论,你可以注意到,如果不是所有线程同时停止的话这个算法是不会工作的(因为运行的线程可能会在合法的指令出现时传出断点)。我不会详细讨论GDB是如何解决这个问题的,但在这篇论文里已经说得很详细了:使用GDB不间断调试多线程程序。简要地说,他们把指令写到内存中的其他地方,然后把那个指令的指针指向那个地址并单步执行处理器。但是问题在于一些指令是和地址相关的,比如跳转和条件跳转……