日期:2014-05-16  浏览次数:20476 次

gdb调试工作机制 1

gdb调试工作机制 1
2011年01月25日
  Gdb,只要在linux下编过程序的应该都用过,估计大家也多对这个软件有着好奇的念头。其实gdb本身也不是那么强大,它大部分都是使用linux操作系统所带有的东西实现,其实稍加时间,自己编写一个简易的调试器也不是太难的事。今天就讲下内核中有关调试实现的机制,然后谈下gdb如何使用这些已有的东西达到调试效果以及推广到硬件调试器
  。
  首先让我们想一下,什么是调试,如何达到调试效果?调试有两个目的: 1.让程序在运行到我们想要停的地方停下来:
  2.停下来后读取系统的各种信息。 停下来有两种:
  一是断点,一是观察点。
  如果是硬件调试,这两种非常相似,只不过一种是检测内存读写,一种是检测取指令,从流水线角度看就是:断点在取指阶段,观察点在执行阶段。
  如果是软件调试,断点是替换为trap指令,而观察点则不一样的意思了,只是看这个观察点的值,而不是当观察点的值发生变化时停止。
  要让程序运行到指定地址时停止执行(对于硬件是指cpu不执行,对于软件是调试程序进程不执行,cpu还是在执行) ,有两种方式:
  一种是利用cpu带有的调试功能,比如x86的eflags寄存器的TF标志位,arm的观察点寄存器组等等
  。
  二是使用软件陷入中断。
  X86的调试状态是产生中断,这样在中断处理程序就可以控制调试,而arm的调试状态是将时钟更改为由jtag的dclk提供,而这个jtag的dclk是由我们执行jtag相关操作产生,从而我们能够控制调试。上面的两种方式都是需要实现一个功能,那就是当进入调试状态时能够让我们知道,并且由我们控制,arm进入debug状态时,调试器要轮询debug status register来判断是否进入debug模式了。
  事实上对于断点,上面的两种架构都没有采用硬件实现方式,为什么?对于x86,其实只有单步调试机制,没有断点设置机制,这是就得在需要设断点的地方设置int3中断控制程序,对于arm,虽然有断点(观察点)硬件机制,但是进入调试状态后必须由jtag(DCLK)控制,程序无法控制,所以仍然不能使用硬件,使用的是F001 sys_call系统调用。
  具体实现是在断点处加入int 3,int 80等trap指令,然后在这些中断的处理程序中给被调试程序发trap信号,这样在中断返回时处理trap信号时,被调试进程会suspend(只要是ptrace状态,所有信号(除了kill信号外)),并通知调试程序(gdb)。 使用TF标志位实现,这个
  Tflag寄存器
  TF(Trap Flag)--位8,跟踪标志。置1 则开启单步执行调试模式,置0 则关闭。在单步执行模式下,处理器在每条指令后产生一个调试异常,这样在每条指令执行后都可以查看执行程序的状态。如果程序用POPF、POPFD 或者ET 指令设置TF 标志,那么这之后的第一条指令就会产生调试异常。
  这样我们只需在调试异常中断时suspend被调试程序,通知调试程序即可实现单步。 由于arm硬件没有单步的实现机制,故还是得像断点一样,将下一次要执行的指令地址设置为断点,这个断点和一般断点的区别是它是临时的,执行完下一指令后不用再恢复为断点,既然要知道下一条指令的地址,就需要有预测机制,即从当前指令判断出下次会执行哪一条指令,由于预测一是涉及到b xx,ld pc等指令还有是条件项,由于判断条件得不偿失,我们一般设置两个断点,即在条件两个分支都设置断点,这样不管条件是什么,都会进入debug。
  set_system_intr_gate(3, &int3); /* int3-5 can be called from all */              ENTRY(int3)         pushl $-1                     # mark this as an int         SAVE_ALL         xorl %edx,%edx           # zero error code         movl %esp,%eax         # pt_regs pointer        call do_int3         jmp ret_from_exception        也就是说int 3会进入do_int3
  DO_VM86_ERROR( 3, SIGTRAP, "int3", int3)
  //这个宏就会生成do_int3函数。
  #define DO_VM86_ERROR(trapnr, signr, str, name) \
  fastcall void do_##name(struct pt_regs * regs, long error_code) \
  { \
  if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, signr) \
  == NOTIFY_STOP) \
  return; \
  do_trap(trapnr, signr, str, 1, regs, error_code, NULL); \
  }
  do_trap会给被调试进程发SIGTRAP信号:
  static void do_trap(int trapnr, int signr, char *str, int vm86,
  struct pt_regs * regs, long error_code, siginfo_t *info)
  {
  struct task_struct *tsk = current;
  tsk->thread.error_code = error_code;
  tsk->thread.trap_no = trapnr;
  if (regs->eflags & VM_MASK) {
  if (vm86)
  goto vm86_trap;
  goto trap_signal;
  }
  if (!user_mode(regs))
  goto kernel_trap;
  trap_signal: {
  if (info)
  force_sig_info(signr, info, tsk);
  else
  force_sig(signr, tsk);
  return;
  //给被调试进程发sigtrap信号
  }
  kernel_trap: {
  if (!fixup_exception(regs))
  die(str, regs, error_code);
  return;
  }
  vm86_trap: {
  int ret = handle_vm86_trap((struct kernel_vm86_regs *) regs, error_code, trapnr);
  if (ret) goto trap_signal;
  return;
  }
  }
  然后调试中断返回时执行下面这个信号处理函数(每次从内核态到用户态会执行):
  int fastcall do_signal(struct pt_regs *r