x86_64平台SingleStep单步调试原理和示例

我就是我 2023-02-14 15:47 57阅读 0赞

先看一个程序:

  1. // simple.c
  2. int value = 0;
  3. int main(int argc, char **argv)
  4. {
  5. value ++;
  6. value ++;
  7. value ++;
  8. value ++;
  9. value ++;
  10. value ++;
  11. }

现在想单步调试它,跟踪value的变化,如何来做?

用gdb即可。但是如果想要理解背后发生了什么,还是要手工来一遍效果才更佳。

这就需要理解单步跟踪的本质(Linux为例):

  • x86_64体系结构,FLAGS寄存器有一个TF标志位来使能和禁用单步。
  • 每条指令执行完毕,处理器会检查TF标志,在使能单步时会将执行流陷入内核。
  • 操作系统内核会发送SIGTRAP信号通知进程,进程收到信号处理单步流程。

来来来,我们为上面的simple.c添加一些逻辑实现单步跟踪:

  1. // ssdebug.c
  2. #include <stdio.h>
  3. #include <sys/mman.h>
  4. #include <signal.h>
  5. #include <asm/processor-flags.h>
  6. // RIP的偏移,距离stack top有192字节,详情参见rt_sigframe
  7. #define PC_OFFSET 192
  8. // CFLAGS的偏移,距离stack top有200字节,详情参见rt_sigframe
  9. #define F_OFFSET 200
  10. int value = 0;
  11. void trap(int unused);
  12. // force inline,以避开ret指令
  13. void __attribute__((always_inline)) inline breakpoint()
  14. {
  15. unsigned long rip = 0, f = 0;
  16. unsigned char *page;
  17. signal(SIGTRAP, trap);
  18. pass: // 获取自身RIP,打断点
  19. asm volatile("mov $., %0" : "=r"(rip));
  20. if (f == 0) {
  21. page = (unsigned char *)((unsigned long)rip & 0xffffffffffff1000);
  22. mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
  23. #define I_BRK 0xcc
  24. *(unsigned char *)(rip) = I_BRK;
  25. f ++;
  26. goto pass;
  27. }
  28. }
  29. void trap(int unused)
  30. {
  31. unsigned long *p;
  32. static int fbrk = 0;
  33. p = (unsigned long*)((unsigned char *)&p + F_OFFSET);
  34. if (!fbrk) { // 断点处理,开启单步
  35. *p = *p | X86_EFLAGS_TF;
  36. p = (unsigned long*)((unsigned char *)&p + PC_OFFSET);
  37. printf("Break at [RIP:0x%lx]\n", *p);
  38. fbrk ++;
  39. }
  40. p = (unsigned long*)((unsigned char *)&p + PC_OFFSET);
  41. printf("current RIP: 0x%lx value:%d\n", *p, value);
  42. #define I_RET 0xc3
  43. if (*(unsigned char *)*p == I_RET) { // 函数返回,单步结束
  44. p = (unsigned long*)((unsigned char *)&p + F_OFFSET);
  45. *p &= ~X86_EFLAGS_TF;
  46. }
  47. }
  48. int main(int argc, char **argv)
  49. {
  50. breakpoint(); // 打断点,开单步
  51. value ++;
  52. value ++;
  53. value ++;
  54. value ++;
  55. value ++;
  56. value ++;
  57. }

OK,看看效果:

  1. [root@localhost probe]# ./a.out
  2. Break at [RIP:0x40070f]
  3. current RIP: 0x40070f value:0
  4. current RIP: 0x400715 value:0
  5. current RIP: 0x400719 value:0
  6. current RIP: 0x40071e value:0
  7. current RIP: 0x400752 value:0
  8. current RIP: 0x400758 value:0
  9. current RIP: 0x40075b value:0
  10. current RIP: 0x400761 value:1
  11. current RIP: 0x400767 value:1
  12. current RIP: 0x40076a value:1
  13. current RIP: 0x400770 value:2
  14. current RIP: 0x400776 value:2
  15. current RIP: 0x400779 value:2
  16. current RIP: 0x40077f value:3
  17. current RIP: 0x400785 value:3
  18. current RIP: 0x400788 value:3
  19. current RIP: 0x40078e value:4
  20. current RIP: 0x400794 value:4
  21. current RIP: 0x400797 value:4
  22. current RIP: 0x40079d value:5
  23. current RIP: 0x4007a3 value:5
  24. current RIP: 0x4007a6 value:5
  25. current RIP: 0x4007ac value:6
  26. current RIP: 0x4007ad value:6

确实,单步跟踪了value的变化过程,要是能把汇编指令打出来就好了,不过这也不难。


浙江温州皮鞋湿,下雨进水不会胖。

发表评论

表情:
评论列表 (有 0 条评论,57人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Ecplise代码调试

    1、设置断点 在程序里面放置一个断点,也就是双击需要放置断点的程序左边的栏目上。 2、调试 (1)点击”打开透视图”按钮,选择调试透视图,则打开调试透视图界面,然后先

    相关 GDB 调试

    1、首先需要用gcc(g++) 对源文件进行编译生成可执行文件,并且在编译时加上选项-g,把调试信息加到目标文件中。 2、假设生成的可执行文件为test,那么gdb tes

    相关 JavaScript调试

    JavaScript单步调试 作为前端开发人员,单步调试这一项技能往往被忽略,但是我依然觉得单步调试是一个非常实用并且基本的技能。当然,前端开发人员主要与JavaScri

    相关 MyEclipse调试

    1, 首先在一个java文件中设断点,然后运行,当程序走到断点处就会转到debug视图下, 2, F5键与F6键均为单步调试,F5是step into,也就是进入本行代...