Chinaunix首页 | 论坛 | 博客
  • 博客访问: 700384
  • 博文数量: 31
  • 博客积分: 330
  • 博客等级: 一等列兵
  • 技术积分: 3004
  • 用 户 组: 普通用户
  • 注册时间: 2012-09-05 22:38
个人简介

java开发工程师,专注于内核源码,算法,数据结构。 qq:630501400

文章分类
文章存档

2014年(2)

2013年(22)

2012年(7)

分类: C/C++

2013-01-29 21:39:11


调试断点原理

      调试断点,依赖于父进程和子进程之间的通信,打断点实际是在被调试的程序中,改变断点附近程序的代码,这个断点使得被调试的程序,暂时停止,然后发送信号给父进程(调试器进程),然后父进程能够得到子进程的变量和状态。达到调试的目的。

      修改断点附近程序的指令地址为0xcc,这个地址的指令就是int 3,含义是,是当前用户态程序发生中断,告诉内核当前程序有断点,那么内核中会向当前进程发送SIGTRAP信号,使当前进程暂停。父进程调用wait函数,等待子进程的运行状态发生改变,这时子进程由于int 3中断,子进程暂停,父进程就可以开始调试子进程的程序了。

      自己写的一个小例子:

      被调试的程序:

#include

int main()

{

      printf( "~~~~~~~~~~~~> Before breakpoint\n" );

      printf( "~~~~~~~~~~~~> After breakpoint\n" );

      printf( "~~~~~~~~~~~~> final\n" );

      return 0;

}

我们希望在程序执行第二个printf时,打断点,对break.o执行objdump -S break.o

00000000004004f4

:

#include

int main()

{

   4004f4: 55 push %rbp

   4004f5: 48 89 e5 mov %rsp,%rbp

   printf( "~~~~~~~~~~~~> Before breakpoint\n" );

   4004f8: bf 10 06 40 00 mov $0x400610,%edi

   4004fd: e8 ee fe ff ff callq 4003f0

   printf( "~~~~~~~~~~~~> After breakpoint\n" );

   400502: bf 30 06 40 00 mov $0x400630,%edi

   400507: e8 e4 fe ff ff callq 4003f0

   printf( "~~~~~~~~~~~~> final\n" );

   40050c: bf 4f 06 40 00 mov $0x40064f,%edi

   400511: e8 da fe ff ff callq 4003f0

   return 0;

   400516: b8 00 00 00 00 mov $0x0,%eax

}

     通过上面dump,想在输出 After breakpoint这行代码打断点,可行的做法就是把地址为0x400502这一行代码的保存起来,然后把它修改成0xcc,如果想继续执行的时候,在把原来的代码复写到原来的地方,再通知子进程执行即可。

测试程序代码:

11 int main(void)

12 {

13     int wait_val;

14     int pid;

15     long addr;

16     long data;

17     long orginData;

18     struct user_regs_struct regs;

19     setvbuf(stdout,NULL,_IONBF,0); //printf  stdout 默认行缓冲,setvbuf输出无buf,直接输出

20     switch (pid = fork()) {

21       case -1:

22          perror("fork");

23          break;

24       case 0:

25          ptrace(PTRACE_TRACEME, 0, 0, 0); //子进程设置traceme,使得父进程trace子进程

26          execl("/home/djj/tmp/break.o", NULL, NULL);

27       default:

28          wait(&wait_val); //子进程设置了traceme,在执行exec函数的时候,内核会首先产生SIGTRAP信号,先给父进程trace子进程的一个机会。

29          ptrace(PTRACE_GETREGS,pid, 0, ®s);

30          addr = 0x400502; //需要打断点的程序地址

31          data=ptrace( PTRACE_PEEKTEXT, pid, (void *)addr,NULL); //获得程序代码

32          orginData = (data & ~0xff) | 0xcc; //设置代码为int 3指令,中断指令

33          ptrace( PTRACE_POKETEXT, pid, (void *)addr, orginData ); //把代码写到内存中

34          ptrace(PTRACE_CONT, pid, NULL, NULL); //通知子进程继续执行

35          wait(&wait_val); //等待子进程程序执行到断点,产生SIGTRAP信号

36          if(WIFSTOPPED(wait_val)){

37                 ptrace(PTRACE_GETREGS,pid, 0, ®s); //取出rip的值

38                 regs.rip-=1; //要重新执行被替换的指令,这里rip必须减一。

39                 printf("break\n");

40                 ptrace(PTRACE_SETREGS,pid,0,®s);

41                 ptrace(PTRACE_POKETEXT,pid,(void *)addr,data);

42                 ptrace(PTRACE_CONT, pid,NULL,NULL); //子进程继续执行

43          }

44          wait(NULL);

45      }

46       return 0;

47 }

运行结果:

                     

        这里值得注意的一点就是子进程在调用了traceme后,如果执行exec函数,会产生SIGTRAP信号,首先看traceme做的一些事情:

222 int ptrace_traceme(void)

223 {

224      int ret = -EPERM;

225

226      write_lock_irq(&tasklist_lock);

227 /* Are we already being traced? */

228      if (!current->ptrace) {

229            ret = security_ptrace_traceme(current->parent);

230 /*

231 * Check PF_EXITING to ensure ->real_parent has not passed

232 * exit_ptrace(). Otherwise we don't report the error but

233 * pretend ->real_parent untraces us right after return.

234 */

235      if (!ret && !(current->real_parent->flags & PF_EXITING)) {

236            current->ptrace = PT_PTRACED; //设置进程的ptracePT_PTRACED,标志子进程被父进程trace

237            __ptrace_link(current, current->real_parent);

238      }

239  }

240    write_unlock_irq(&tasklist_lock);

241

242    return ret;

243 }


      子进程ptrace标志了 PT_PTRACED,在执行exec函数的时候,会去触发SIGTRAP信号

1315 bprm->recursion_depth = depth;

1316     if (retval >= 0) {

1317          if (depth == 0)

1318                 tracehook_report_exec(fmt, bprm, regs); //产生SIGTRAP信号


      tracehook_report_exec函数的实现:

200 static inline void tracehook_report_exec(struct linux_binfmt *fmt,

201 struct linux_binprm *bprm,

202 struct pt_regs *regs)

203 {

204 if (!ptrace_event(PT_TRACE_EXEC, PTRACE_EVENT_EXEC, 0) &&

205 unlikely(task_ptrace(current) & PT_PTRACED))         //如果标志了 PT_PTRACED

206        send_sig(SIGTRAP, current, 0);                            //那么就向当前进程发送SIGTRAP信号,使得当前进程暂停

207 }

      exec函数没研究过,猜测逻辑应该是,按照object文件中的代码段,数据段设置内存结构,在最后将指向下一条指令的地址指向刚刚代码段的起始地址,那么在程序返回用户态之后,就会按照新加载代码段开始的地方开始执行程序。内核中在加载完内存结构之后,如果当前进程标志了ptrace字段,那么暂停当前进程,通知trace的父进程。

系统在初始化的过程中已经定义了int3的中断门

822 void __init early_trap_init(void)

823 {

824        set_intr_gate_ist(1, &debug, DEBUG_STACK);

825        /* int3 can be called from all */

826        set_system_intr_gate_ist(3, &int3, DEBUG_STACK); //定义了int3的中断门

827        set_intr_gate(14, &page_fault);

828        load_idt(&idt_descr);

829 }

      中断门的int3,联系到了arch/x86/kernel/entry_32.S

1476 ENTRY(int3)

1477 RING0_INT_FRAME

1478 pushl $-1 # mark this as an int

1479 CFI_ADJUST_CFA_OFFSET 4

1480 SAVE_ALL

1481 TRACE_IRQS_OFF

1482 xorl %edx,%edx # zero error code

1483 movl %esp,%eax # pt_regs pointer

1484 call do_int3 //执行do_int3函数

1485 jmp ret_from_exception

1486 CFI_ENDPROC

1487 END(int3)

      可以看到最后用户态进程产生的int3中断,会触发执行do_int3函数,其中的一部分代码:

470 preempt_conditional_sti(regs);

471 do_trap(3, SIGTRAP, "int3", regs, error_code, NULL);

472 preempt_conditional_cli(regs);

      最终看到调用了do_trap函数,这个函数的作用就是给当前进程发送SIGTRAP信号,使得当前进程暂停,同时这个进程的暂停,就会唤醒wait函数。使得父进程调用ptrace函数来获得子进程的相关信息。

      对于一个进程想要去调试一个正在运行的进程,那么会调用ptrace,请求PTRACE_ATTACHattach一个pid,这个原理很简单。就是通过当前namespace根据pid,得到task_struct,这个原理请参考pid Namespace浅分析。把ptrace字段设置成为PT_PTRACED。同时这个子进程向自己发送SIGSTOP信号,个人觉得这个暂停的意义就是给父进程一个机会,去设置断点等信息。

700 child = ptrace_get_task_struct(pid); //根据pidnamespace得到task_struct

701 if (IS_ERR(child)) {

702      ret = PTR_ERR(child);

703      goto out;

704 }

705

706 if (request == PTRACE_ATTACH) { //如果是attach请求

707       ret = ptrace_attach(child); //设置ptrace字段为PT_PTRACED

708 /*

709 * Some architectures need to do book-keeping after

710 * a ptrace attach.

711 */

712 if (!ret)

713       arch_ptrace_attach(child);

714  goto out_put_task_struct;

715 }

      ptrace_attach函数的具体逻辑:

200 task->ptrace = PT_PTRACED; //设置ptrace字段为PT_PTRACED

201 if (capable(CAP_SYS_PTRACE))

202        task->ptrace |= PT_PTRACE_CAP;

203

204  __ptrace_link(task, current);

205 send_sig_info(SIGSTOP, SEND_SIG_FORCED, task); //pid那个进程发送暂停信号SIGSTOP

总结:

      调试的大体原理:通过设置被调试的进程ptrace字段,标志这个进程被trace,断点附近的程序代码被替换成了int 3,中断程序,引发了do_int3函数,导致了被trace进程的暂停,这样父进程就能通过ptrace系统调用获得子进程的运行情况了。以上分析代表个人观点,个人水平有限,不正确的地方希望大家指出,积极讨论。

参考文章:

1.http://blog.csdn.net/dog250/article/details/5303228








阅读(13233) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~