本文是一系列探究调试器工作原理的文章的第一篇。我还不确定这个系列需要包括多少篇文章以及它们所涵盖的主题,但我打算从基础知识开始说起。

关于本文

我打算在这篇文章中介绍关于Linux下的调试器实现的主要组成部分——ptrace系统调用。本文中出现的代码都在32位的Ubuntu系统上开发。请注意,这里出现的代码是同平台紧密相关的,但移植到别的平台上应该不会太难。

动机

要想理解我们究竟要做什么,试着想象一下调试器是如何工作的。调试器可以启动某些进程,然后对其进行调试,或者将自己本身关联到一个已存在的进程之上。它可以单步运行代码,设置断点然后运行程序,检查变量的值以及跟踪调用栈。许多调试器已经拥有了一些高级特性,比如执行表达式并在被调试进程的地址空间中调用函数,甚至可以直接修改进程的代码并观察修改后的程序行为。

尽管现代的调试器都是复杂的大型程序,但令人惊讶的是构建调试器的基础确是如此的简单。调试器只用到了几个由操作系统以及编译器/链接器提供的基础服务,剩下的仅仅就是简单的编程问题了。(可查阅维基百科中关于这个词条的解释,作者是在反讽)

Linux下的调试——ptrace

Linux下调试器拥有一个瑞士军刀般的工具,这就是ptrace系统调用。这是一个功能众多且相当复杂的工具,能允许一个进程控制另一个进程的运行,而且可以监视和渗入到进程内部。ptrace本身需要一本中等篇幅的书才能对其进行完整的解释,这就是为什么我只打算通过例子把重点放在它的实际用途上。让我们继续深入探寻。

遍历进程的代码

我现在要写一个在“跟踪”模式下运行的进程的例子,这里我们要单步遍历这个进程的代码——由CPU所执行的机器码(汇编指令)。我会在这里给出例子代码,解释每个部分,本文结尾处你可以通过链接下载一份完整的C程序文件,可以自行编译执行并研究。从高层设计来说,我们要写一个程序,它产生一个子进程用来执行一个用户指定的命令,而父进程跟踪这个子进程。首先,main函数是这样的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

int main(int argc,char**

argv)

{

pid_t child_pid;

if (argc < 2) {

fprintf(stderr,"Expected

a program name as argumentn");

return -1;

}

child_pid = fork();

if (child_pid == 0)

run_target(argv[1]);

else if (child_pid

> 0)

run_debugger(child_pid);

else {

perror("fork");

return -1;

}

return 0;

}

代码相当简单,我们通过fork产生一个新的子进程。随后的if语句块处理子进程(这里称为“目标进程”),而else if语句块处理父进程(这里称为“调试器”)。下面是目标进程:

1

2

3

4

5

6

7

8

9

10

11

12

13

void run_target(const char*

programname)

{

procmsg("target started. will run '%s'n",

programname);

/* Allow tracing of this process */

if (ptrace(PTRACE_TRACEME, 0,

0, 0) < 0) {

perror("ptrace");

return;

}

/* Replace this process's image with the given program */

execl(programname, programname, 0);

}

这部分最有意思的地方在ptrace调用。ptrace的原型是(在sys/ptrace.h):

1

long ptrace(enum __ptrace_request

request, pid_t pid,void *addr,void *data);

第一个参数是request,可以是预定义的以PTRACE_打头的常量值。第二个参数指定了进程id,第三以及第四个参数是地址和指向数据的指针,用来对内存做操作。上面代码段中的ptrace调用使用了PTRACE_TRACEME请求,这表示这个子进程要求操作系统内核允许它的父进程对其跟踪。这个请求在man手册中解释的非常清楚:

“表明这个进程由它的父进程来跟踪。任何发给这个进程的信号(除了SIGKILL)将导致该进程停止运行,而它的父进程会通过wait()获得通知。另外,该进程之后所有对exec()的调用都将使操作系统产生一个SIGTRAP信号发送给它,这让父进程有机会在新程序开始执行之前获得对子进程的控制权。如果不希望由父进程来跟踪的话,那就不应该使用这个请求。(pid、addr、data被忽略)”

我已经把这个例子中我们感兴趣的地方高亮显示了。注意,run_target在ptrace调用之后紧接着做的是通过execl来调用我们指定的程序。这里就会像我们高亮显示的部分所解释的那样,操作系统内核会在子进程开始执行execl中指定的程序之前停止该进程,并发送一个信号给父进程。

因此,是时候看看父进程需要做些什么了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

void run_debugger(pid_t child_pid)

{

int wait_status;

unsigned icounter = 0;

procmsg("debugger startedn");

/* Wait for child to stop on its first instruction */

wait(&wait_status);

while (WIFSTOPPED(wait_status))

{

icounter++;

/* Make the child execute another instruction */

if (ptrace(PTRACE_SINGLESTEP,

child_pid, 0, 0) < 0) {

perror("ptrace");

return;

}

/* Wait for child to stop on its next instruction */

wait(&wait_status);

}

procmsg("the child executed %u instructionsn",

icounter);

}

通过上面的代码我们可以回顾一下,一旦子进程开始执行exec调用,它就会停止然后接收到一个SIGTRAP信号。父进程通过第一个wait调用正在等待这个事件发生。一旦子进程停止(如果子进程由于发送的信号而停止运行,WIFSTOPPED就返回true),父进程就去检查这个事件。

父进程接下来要做的是本文中最有意思的地方。父进程通过PTRACE_SINGLESTEP以及子进程的id号来调用ptrace。这么做是告诉操作系统——请重新启动子进程,但当子进程执行了下一条指令后再将其停止。然后父进程再次等待子进程的停止,整个循环继续得以执行。当从wait中得到的不是关于子进程停止的信号时,循环结束。在正常运行这个跟踪程序时,会得到子进程正常退出(WIFEXITED会返回true)的信号。

icounter会统计子进程执行的指令数量。因此我们这个简单的例子实际上还是做了点有用的事情——通过在命令行上指定一个程序名,我们的例子会执行这个指定的程序,然后统计出从开始到结束该程序执行过的CPU指令总数。让我们看看实际运行的情况。

实际测试

我编译了下面这个简单的程序,然后在我们的跟踪程序下执行:

1

2

3

4

5

6

#include

int main()

{

printf(“Hello, world!n”);

return 0;

}

令我惊讶的是,我们的跟踪程序运行了很长的时间然后报告显示一共有超过100000条指令得到了执行。仅仅只是一个简单的printf调用,为什么会这样?答案非常有意思。默认情况下,Linux中的gcc编译器会动态链接到C运行时库。这意味着任何程序在运行时首先要做的事情是加载动态库。这需要很多代码实现——记住,我们这个简单的跟踪程序会针对每一条被执行的指令计数,不仅仅是main函数,而是整个进程。

因此,当我采用-static标志静态链接这个测试程序时(注意到可执行文件因此增加了500KB的大小,因为它静态链接了C运行时库),我们的跟踪程序报告显示只有7000条左右的指令被执行了。这还是非常多,但如果你了解到libc的初始化工作仍然先于main的执行,而清理工作会在main之后执行,那么这就完全说得通了。而且,printf也是一个复杂的函数。

我们还是不满足于此,我希望能看到一些可检测的东西,例如我可以从整体上看到每一条需要被执行的指令是什么。这一点我们可以通过汇编代码来得到。因此我把这个“Hello,world”程序汇编(gcc -S)为如下的汇编码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

section .text

; The _start symbol must be declaredfor the

linker (ld)

global _start

_start:

; Prepare argumentsfor the

sys_writesystem call:

; - eax:system call

number (sys_write)

; - ebx: file descriptor (stdout)

; - ecx: pointer to string

; - edx: string length

mov edx, len

mov ecx, msg

mov ebx, 1

mov eax, 4

; Execute the sys_writesystem call

int 0x80

; Execute sys_exit

mov eax, 1

int 0x80

section .data

msg db'Hello, world!',

0xa

len equ $ - msg

这就足够了。现在跟踪程序会报告有7条指令得到了执行,我可以很容易地从汇编代码来验证这一点。

深入指令流

汇编码程序得以让我为大家介绍ptrace的另一个强大的功能——详细检查被跟踪进程的状态。下面是run_debugger函数的另一个版本:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

void run_debugger(pid_t child_pid)

{

int wait_status;

unsigned icounter = 0;

procmsg("debugger startedn");

/* Wait for child to stop on its first instruction */

wait(&wait_status);

while (WIFSTOPPED(wait_status))

{

icounter++;

struct user_regs_struct regs;

ptrace(PTRACE_GETREGS, child_pid, 0, ®s);

unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0);

procmsg("icounter = %u. EIP = 0x%08x.

instr = 0x%08xn",

icounter, regs.eip, instr);

/* Make the child execute another instruction */

if (ptrace(PTRACE_SINGLESTEP,

child_pid, 0, 0) < 0) {

perror("ptrace");

return;

}

/* Wait for child to stop on its next instruction */

wait(&wait_status);

}

procmsg("the child executed %u instructionsn",

icounter);

}

同前个版本相比,唯一的不同之处在于while循环的开始几行。这里有两个新的ptrace调用。第一个读取进程的寄存器值到一个结构体中。结构体user_regs_struct定义在sys/user.h中。这儿有个有趣的地方——如果你打开这个头文件看看,靠近文件顶端的地方有一条这样的注释:

1

/* 本文件的唯一目的是为GDB,且只为GDB所用。对于这个文件,不要看的太多。除了GDB以外不要用于任何其他目的,除非你知道你正在做什么。*/

现在,我不知道你是怎么想的,但我感觉我们正处于正确的跑道上。无论如何,回到我们的例子上来。一旦我们将所有的寄存器值获取到regs中,我们就可以通过PTRACE_PEEKTEXT标志以及将regs.eip(x86架构上的扩展指令指针)做参数传入ptrace来调用。我们所得到的就是指令。让我们在汇编代码上运行这个新版的跟踪程序。

1

2

3

4

5

6

7

8

9

10

11

12

$ simple_tracer traced_helloworld

[5700] debugger started

[5701] target started. will run'traced_helloworld'

[5700] icounter = 1. EIP = 0x08048080. instr = 0x00000eba

[5700] icounter = 2. EIP = 0x08048085. instr = 0x0490a0b9

[5700] icounter = 3. EIP = 0x0804808a. instr = 0x000001bb

[5700] icounter = 4. EIP = 0x0804808f. instr = 0x000004b8

[5700] icounter = 5. EIP = 0x08048094. instr = 0x01b880cd

Hello, world!

[5700] icounter = 6. EIP = 0x08048096. instr = 0x000001b8

[5700] icounter = 7. EIP = 0x0804809b. instr = 0x000080cd

[5700] the child executed 7 instructions

OK,所以现在除了icounter以外,我们还能看到指令指针以及每一步的指令。如何验证这是否正确呢?可以通过在可执行文件上执行objdump –d来实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

$ objdump -d traced_helloworld

traced_helloworld: file format elf32-i386

Disassembly of section .text:

08048080 <.text>:

8048080: ba 0e 00 00 00 mov $0xe,%edx

8048085: b9 a0 90 04 08 mov $0x80490a0,%ecx

804808a: bb 01 00 00 00 mov $0x1,%ebx

804808f: b8 04 00 00 00 mov $0x4,%eax

8048094: cd 80int $0x80

8048096: b8 01 00 00 00 mov $0x1,%eax

804809b: cd 80int $0x80

用这份输出对比我们的跟踪程序输出,应该很容易观察到相同的地方。

关联到运行中的进程上

你已经知道了调试器也可以关联到已经处于运行状态的进程上。看到这里,你应该不会感到惊讶,这也是通过ptrace来实现的。这需要通过PTRACE_ATTACH请求。这里我不会给出一段样例代码,因为通过我们已经看到的代码,这应该很容易实现。基于教学的目的,这里采用的方法更为便捷(因为我们可以在子进程刚启动时立刻将它停止)。

代码

本文给出的这个简单的跟踪程序的完整代码(更高级一点,可以将具体指令打印出来)可以在这里找到。程序通过-Wall

–pedantic –std=c99编译选项在4.4版的gcc上编译。

结论及下一步要做的

诚然,本文并没有涵盖太多的内容——我们离一个真正可用的调试器还差的很远。但是,我希望这篇文章至少已经揭开了调试过程的神秘面纱。ptrace是一个拥有许多功能的系统调用,目前我们只展示了其中少数几种功能。

能够单步执行代码是很有用处的,但作用有限。以“Hello, world”为例,要到达main函数,需要先遍历好几千条初始化C运行时库的指令。这就不太方便了。我们所希望的理想方案是可以在main函数入口处设置一个断点,从断点处开始单步执行。下一篇文章中我将向您展示该如何实现断点机制。

参考文献

写作本文时我发现下面这些文章很有帮助:

关于译(作)者:陈舸:程序员,关注网络协议、开源软件、Linux、C/C++、Python。目前从事网络通信设备的开发。在看过《DOOM启示录》后,真正为书中描述的程序员生活和黑客精神所震动。我无法达到Carmark的水准,但可以有和他一样的分享精神。(新浪微博:@bigsh1p)

python调试器原理_调试器工作原理——基础篇相关推荐

  1. 简述ospf的工作原理_物联网水表工作原理简述

    近年来,物联网技术得到了各行各业的推广支持,水表行业也是如此.物联网水表到底有着数据采集,远程控制,线上缴费等功能方便用户的缴费及自来水公司的管理运营工作那么本文为您解析物联网水表的工作原理,看看这些 ...

  2. 小车自动往返工作原理_自动气象站的工作原理介绍

    FT-ZDQX自动气象站的工作原理*自动气象站的工作原理*自动气象站的工作原理*自动气象站的工作原理FT - ZDQX 자동 기상 관측소 의 작 동 원리 * 자동 기상 관측소 의 작 동 원리 ...

  3. 数字调制系统工作原理_空间光调制器工作原理是什么 空间光调制器工作原理...

    空间光调制器(SLM), 空间光调制器(SLM)工作原理是什么? 实时空间光调制器 使得相干处理系统能输入非相干光图像和随时间变化的图像的器件.相干光处理系统的最大优点是二维平行处理.信息容量大,运算 ...

  4. bmp180气压传感器工作原理_各种传感器工作原理汇总

    压阻式传感器测量液位的工作原理 ▼ MQN型气敏电阻结构及测量电路 ▼ 气泡式水平仪的工作原理 ▼ 扩散硅式压力传感器 ▼ 应变加速度感应器 ▼ 称重式料位计 ▼ 电子皮带秤重示意图 ▼ 电子吊车秤 ...

  5. 火焰传感器工作原理_不同接近传感器工作原理

    接近传感器被广泛用于各种自动化生产线,机电一体化设备上,也常常出现在采购清单上,那到底什么是接近传感器呢? 接近传感器是一种具有感知物体接近能力的器件,它利用位移传感器对接近的物体具有敏感特性来识别物 ...

  6. ice通信原理_变频开关电源工作原理,开关电源自我检修

    一.变频开关电源工作原理 开关电源的电路比较复杂,但其基本工作原理并不难理解,下图5-1说明了开关电源的基本工作原理. 在图5-1(a)中,当开关S闭合时,电源E通过S给C充电,在C上得到正向上和负向 ...

  7. 运放电路的工作原理_陶瓷气体放电管工作原理全业电子

    点击上方蓝字关注我们 陶瓷气体放电管工作原理-全业电子 我们都知道,陶瓷气体放电管(GasTube)是防雷保护设备中应用最广泛的一种开关器件,无论是交直流电源的防雷还是各种信号电路的防雷,都可以用它来 ...

  8. st188脉搏传感器工作原理_各种传感器工作原理动态图,拿走不谢~

    AMT:今天和大家分享一波传感器的工作原理图,非常全面. 布料张力测量及控制原理 ▼ 直滑式电位器控制气缸活塞行程 ▼ 压阻式传感器测量液位的工作原理 ▼ MQN型气敏电阻结构及测量电路 ▼ 气泡式水 ...

  9. 相片打印机原理_喷墨打印机工作原理 喷墨打印机优缺点介绍【详解】

    现在打印机的价格在不断的降低,所以很多消费者都曾想买一台打印机放置在家中使用,目前打印机的种类有很多,喷墨打印机就是其中的一种,可能很多人对于选购喷墨打印机都有自己的一套方法,可是你了解喷墨打印机的优 ...

  10. mysql连接池的工作原理_连接池工作原理

    连接池工作原理 连接池技术的核心思想是连接复用,通过建立一个数据库连接池以及一套连接使用.分配和管理策略,使得该连接池中的连接可以得到高效.安全的复用,避免了数据库连接频繁建立.关闭的开销. 连接池的 ...

最新文章

  1. vpwm的控制变频_变频V/F和矢量控制你知道区别吗?据说这四种控制没有几人能说清...
  2. c# mvc5 view 多层_MVC5+EF6 入门完整教程13 -- 动态生成多级菜单
  3. 使用备用访问映射改变站点访问路径
  4. xcode7.1 安装不了Alcatraz怎么办.看这里
  5. C++ using namespace 命名空间的定义与使用
  6. suse linux 查看cpu,Suse Linux zmd 耗用100% CPU
  7. yarn临时目录 没有jar包_复习之yarn
  8. Spring整合Activiti工作流
  9. 极棒开启AI挑战 全球寻找顶级语音合成“机械师”
  10. Atitit 项目管理优化体系图 第4章 项目整合管理 开始 计划 执行 监控 变更 结束 第5章 项目范围管理  SOW工作说明书 成员通讯录 wbs大概模块级别 第6章 项目时间
  11. Linux系统U盘怎么格式化,u盘怎么格式化各系统教程
  12. java json.stringify_浅谈 JSON.stringify 方法
  13. java显示一个钟表_java实现时钟效果
  14. Excel Pearson相关系数
  15. ps去水印教程_新手必会的PS去水印方法,绝对简单!
  16. 【python与数据分析】实验八——图像批量添加数字水印及实现模拟转盘抽奖游戏
  17. 换行和禁止换行及超出省略号
  18. 创客平台靠什么盈利?
  19. 理解 Linux 中的 关机命令
  20. Android学习——Adapter适配器

热门文章

  1. 2020哈工程计算机考研总结--复试篇!(祝看到的小伙伴都上岸吖!!!)
  2. [zoj4058] [2018ACM青岛站·A] Sequence and Sequence - 高精度 - 数学
  3. win10停止更新的方法以及更新后老是连接不上网的情况
  4. 【240期】面试官问:说说基于 Redis 实现延时队列服务?
  5. 行业应用|工业AI视觉系统,助力物流行业智慧分拣加速升级
  6. Linux 系统安全与优化配置
  7. NSLayoutConstraint约束
  8. --del--() 方法
  9. 如何在Windows10系统中修改.jar文件的默认应用
  10. 中国KAB创业教育网_需求分析说明书