文档章节

调试器的工作原理(一):基础篇

l
 linux-tao
发布于 2017/05/08 10:22
字数 3264
阅读 2
收藏 0
点赞 0
评论 0

缘由

为了理解我们要做什么,让我们先考虑下调试器为了完成调试都需要什么资源。调试器可以开始一个进程并调试这个进程,又或者将自己同某个已经存在的进程关联起来。调试器能够单步执行代码,设定断点并且将程序执行到断点,检查变量的值并追踪堆栈。许多调试器有着更高级的特性,例如在调试器的地址空间内执行表达式或者调用函数,甚至可以在进程执行过程中改变代码并观察效果。

尽管现代的调试器都十分的复杂(我没有检查,但我确信 gdb 的代码行数至少有六位数),但它们的工作的原理却是十分的简单。调试器的基础是操作系统与编译器 / 链接器提供的一些基础服务,其余的部分只是简单的编程而已。

Linux 的调试 - ptrace

Linux 调试器中的瑞士军刀便是 ptrace 系统调用(使用 man 2 ptrace 命令可以了解更多)。这是一种复杂却强大的工具,可以允许一个进程控制另外一个进程并从内部替换Peek and poke被控制进程的内核镜像的值(Peek and poke 在系统编程中是很知名的叫法,指的是直接读写内存内容)。

接下来会深入分析。

执行进程的代码

我将编写一个示例,实现一个在“跟踪”模式下运行的进程。在这个模式下,我们将单步执行进程的代码,就像机器码(汇编代码)被 CPU 执行时一样。我将分段展示、讲解示例代码,在文章的末尾也有完整 c 文件的下载链接,你可以编译、执行或者随心所欲的更改。

更进一步的计划是实现一段代码,这段代码可以创建可执行用户自定义命令的子进程,同时父进程可以跟踪子进程。首先是主函数:

int main(int argc, char** argv)
{
pid_t child_pid;

if (argc < 2) { fprintf(stderr, "Expected a program name as argument/n"); 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 创建了一个新的子进程(这篇文章假定读者有一定的 Unix/Linux 编程经验。我假定你知道或至少了解 fork、exec 族函数与 Unix 信号)。if 语句的分支执行子进程(这里称之为 “target”),else if 的分支执行父进程(这里称之为 “debugger”)。

下面是 target 进程的代码:

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 调用。在 sys/ptrace.h 中,ptrace 是如下定义的:

long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);

第一个参数是 _request_,这是许多预定义的 PTRACE_* 常量中的一个。第二个参数为请求分配进程 ID。第三个与第四个参数是地址与数据指针,用于操作内存。上面代码段中的 ptrace 调用发起了 PTRACE_TRACEME 请求,这意味着该子进程请求系统内核让其父进程跟踪自己。帮助页面上对于 request 的描述很清楚:

意味着该进程被其父进程跟踪。任何传递给该进程的信号(除了 SIGKILL)都将通过 wait() 方法阻塞该进程并通知其父进程。此外,该进程的之后所有调用 exec() 动作都将导致 SIGTRAP 信号发送到此进程上,使得父进程在新的程序执行前得到取得控制权的机会。如果一个进程并不需要它的的父进程跟踪它,那么这个进程不应该发送这个请求。(pid、addr 与 data 暂且不提)

我高亮了这个例子中我们需要注意的部分。在 ptrace 调用后,run_target 接下来要做的就是通过 execl 传参并调用。如同高亮部分所说明,这将导致系统内核在 execl 创建进程前暂时停止,并向父进程发送信号。

是时候看看父进程做什么了。

void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
procmsg("debugger started/n");

/* 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 instructions/n", icounter);
}

如前文所述,一旦子进程调用了 exec,子进程会停止并被发送 SIGTRAP 信号。父进程会等待该过程的发生并在第一个 wait() 处等待。一旦上述事件发生了,wait() 便会返回,由于子进程停止了父进程便会收到信号(如果子进程由于信号的发送停止了,WIFSTOPPED 就会返回 true)。

父进程接下来的动作就是整篇文章最需要关注的部分了。父进程会将 PTRACE_SINGLESTEP 与子进程 ID 作为参数调用 ptrace 方法。这就会告诉操作系统,“请恢复子进程,但在它执行下一条指令前阻塞”。周而复始地,父进程等待子进程阻塞,循环继续。当 wait() 中传出的信号不再是子进程的停止信号时,循环终止。在跟踪器(父进程)运行期间,这将会是被跟踪进程(子进程)传递给跟踪器的终止信号(如果子进程终止 WIFEXITED 将返回 true)。

icounter 存储了子进程执行指令的次数。这么看来我们小小的例子也完成了些有用的事情 - 在命令行中指定程序,它将执行该程序并记录它从开始到结束所需要的 cpu 指令数量。接下来就让我们这么做吧。

测试

我编译了下面这个简单的程序并利用跟踪器运行它:

#include

int main()
{
printf("Hello, world!/n");
return 0;
}

令我惊讶的是,跟踪器花了相当长的时间,并报告整个执行过程共有超过 100,000 条指令执行。仅仅是一条输出语句?什么造成了这种情况?答案很有趣(至少你同我一样痴迷与机器/汇编语言)。Linux 的 gcc 默认会动态的将程序与 c 的运行时库动态地链接。这就意味着任何程序运行前的第一件事是需要动态库加载器去查找程序运行所需要的共享库。这些代码的数量很大 - 别忘了我们的跟踪器要跟踪每一条指令,不仅仅是主函数的,而是“整个进程中的指令”。

所以当我将测试程序使用静态编译时(通过比较,可执行文件会多出 500 KB 左右的大小,这部分是 C 运行时库的静态链接),跟踪器提示只有大概 7000 条指令被执行。这个数目仍然不小,但是考虑到在主函数执行前 libc 的初始化以及主函数执行后的清除代码,这个数目已经是相当不错了。此外,printf 也是一个复杂的函数。

仍然不满意的话,我需要的是“可以测试”的东西 - 例如可以完整记录每一个指令运行的程序执行过程。这当然可以通过汇编代码完成。所以我找到了这个版本的 “Hello, world!” 并编译了它。

section .text
; The _start symbol must be declared for the linker (ld)
global _start

_start:

; Prepare arguments for the sys_write system 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_write system 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 函数的另一个版本:

void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
procmsg("debugger started/n");

/* 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, &regs);
unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0);

procmsg("icounter = %u. EIP = 0x%08x. instr = 0x%08x/n",
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 instructions/n", icounter);
}

不同仅仅存在于 while 循环的开始几行。这个版本里增加了两个新的 ptrace 调用。第一条将进程的寄存器值读取进了一个结构体中。 sys/user.h 定义有 user_regs_struct。如果你查看头文件,头部的注释这么写到:

/* 这个文件只为了 GDB 而创建

不用详细的阅读.如果你不知道你在干嘛

不要在除了 GDB 以外的任何地方使用此文件 */

不知道你做何感想,但这让我觉得我们找对地方了。回到例子中,一旦我们在 regs 变量中取得了寄存器的值,我们就可以通过将 PTRACE_PEEKTEXT 作为参数、 regs.eip(x86 上的扩展指令指针)作为地址,调用 ptrace ,读取当前进程的当前指令(警告:如同我上面所说,文章很大程度上是平台相关的。我简化了一些设定 - 例如,x86 指令集不需要调整到 4 字节,我的32位 Ubuntu unsigned int 是 4 字节。事实上,许多平台都不需要。从内存中读取指令需要预先安装完整的反汇编器。我们这里没有,但实际的调试器是有的)。下面是新跟踪器所展示出的调试效果:

$ 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
现在,除了 icounter,我们也可以观察到指令指针与它每一步所指向的指令。怎么来判断这个结果对不对呢?使用 objdump -d 处理可执行文件:

$ objdump -d traced_helloworld

traced_helloworld: file format elf32-i386

Disassembly of section .text:

08048080 :
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 80 int $0x80
8048096: b8 01 00 00 00 mov $0x1,%eax
804809b: cd 80 int $0x80

这个结果和我们跟踪器的结果就很容易比较了。

将跟踪器关联到正在运行的进程

如你所知,调试器也能关联到已经运行的进程。现在你应该不会惊讶,ptrace 通过以 PTRACE_ATTACH 为参数调用也可以完成这个过程。这里我不会展示示例代码,通过上文的示例代码应该很容易实现这个过程。出于学习目的,这里使用的方法更简便(因为我们在子进程刚开始就可以让它停止)。

代码

上文中的简单的跟踪器(更高级的,可以打印指令的版本)的完整c源代码可以在这里找到。它是通过 4.4 版本的 gcc 以 -Wall -pedantic --std=c99 编译的。

结论与计划

诚然,这篇文章并没有涉及很多内容 - 我们距离亲手完成一个实际的调试器还有很长的路要走。但我希望这篇文章至少可以使得调试这件事少一些神秘感。ptrace 是功能多样的系统调用,我们目前只展示了其中的一小部分。

单步调试代码很有用,但也只是在一定程度上有用。上面我通过 C 的 “Hello World!” 做了示例。为了执行主函数,可能需要上万行代码来初始化 C 的运行环境。这并不是很方便。最理想的是在 main 函数入口处放置断点并从断点处开始分步执行。为此,在这个系列的下一篇,我打算展示怎么实现断点。

原文来自:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/翻译:周阳

本文地址:http://www.linuxprobe.com/debugger-principles-one.html

© 著作权归作者所有

共有 人打赏支持
l
粉丝 10
博文 600
码字总数 681542
作品 0
深圳
运维
调试器工作原理之(三):研究调试信息

这是调试器的工作原理系列文章的第三篇。阅读这篇文章之前应当先阅读第一篇与第二篇。 调试信息 现代编译器能够将有着各种缩进或嵌套的程序流程、各种数据类型的变量的高级语言代码转换为一大...

linux-tao
2017/10/18
0
0
GPU高级调试与优化(2018SH)

[纠正上一版本小错误,重新发送,如有打扰,请原谅] GPU的历史很短,只有十几年。但它发展迅猛,凭借强大的并行计算能力和高效率的固定硬件单元,在人工智能、虚拟和增强现实(VR/AR)、3D游...

pcb4jr
2017/12/27
0
0
Eclipse 调试器:零距离接触实战技巧

调试的方法虽然千千万万,但归根结底,就是找到引发错误的代码。Eclipse调试器的目标是让程序员能对本地或远程程序进行错误侦测与诊断。 调试的方法虽然千千万万,但归根结底,就是找到引发错...

李长春
2011/12/23
0
0
最全java学习路线图 java学习指南

JavaEE学习路线图 准备篇 适用/适合人群:适合基础小白 目标:掌握JavaSE。 ●技术点小节: 1.开发工具的安装配置的介绍 2.JDK安装 3.DOS环境编程 4.Eclipse的安装使用 ●JAVA基础 1.基本的语...

耳东陈_f1e5
01/14
0
0
学习NodeJS第三天:打造Nodejs的调试环境(上)

自从 NodeJS 问世以来,我们注意到的一个现象是,开源生态围绕 NodeJS 的扩展(Extends)、插件(Plugins)或工具(Tools/Utilis)在不断地增多,可见当中之蓬勃。这一方面固然很好,但这里却...

sp42
2010/09/23
0
0
如何调试Android Framework?

Linus有一句名言广为人知:Read the fucking source code. 但其实,要深入理解某个软件、框架或者系统的工作原理,仅仅「看」代码是远远不够的。就拿Android Framework来说,整个代码量非常大...

weishu
2016/05/31
0
0
python-59: 基础篇完

经历了这漫长的时间,我们的基础篇终于讲完了,来说几个问题 1. 基础篇主要讲了什么 基础篇我们主要是使用urllib,urllib2和cookielib这几个库来讲解爬虫的几个实例,为什么要选择这几个库呢?...

达岭凹老大
2015/12/15
96
0
学机械早一点看到这些动态原理图,说不准哥早就成了大设计师啦!

  一、制造篇      一张图告诉你玻璃瓶刚生产出来是什么样子      小型金属锁链就是这样成型的      食品制作      3D打印      碗花制作   二、机械原理篇     ...

UG数控编程
04/07
0
0
Spring Boot揭秘与实战

Spring Boot 揭秘与实战(九) 应用监控篇 - HTTP 应用监控 Spring Boot 提供运行时的应用监控和管理功能。本文,我们通过 HTTP 实现对应用的监控和管理。 博客地址:http://blog.720ui.com...

掘金官方
2017/12/11
0
0
【cocos2dx进阶】调试篇(1)基础介绍

高级模式其实和vld的原理一样,可以再代码接管内存分配器(重载new和delete),然后自己比对分配和释放的过程。也可以在需要分配的首尾加上自定义的内存块,这样可以很方便的监控内存越界的问...

kingzeus
2014/01/18
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Win10专业版安装GIT后使用Git Bash闪退解决办法

百度后把过程和最终解决办法记录下来: 百度首先出来的解决办法如下: 来自:https://segmentfault.com/q/1010000012722511?sort=created 重启电脑 重新安装 安装到C盘 尝试网上的教程 \Git...

特拉仔
18分钟前
0
0
设计模式

1.装饰器模式 概念 允许向一个现有的对象添加新的功能,同时又不改变其结构。装饰者可以在所委托被装饰者的行为之前或之后加上自己的行为,以达到特定的目的。 实现 增加一个修饰类包裹原来的...

EasyProgramming
33分钟前
1
0
用python2和opencv进行人脸识别

一、安装cv2 sudo apt-get install python-opencv opencv-data 二、 Haar特征分类器 Haar特征分类器就是一个XML文件,该文件中会描述人体各个部位的Haar特征值。包括人脸、眼睛、嘴唇等等。 ...

wangxuwei
33分钟前
0
0
python模板中循环字典

{% for k,v in user.items %} {{ k}} {{ v}} {% endfor %}

南桥北木
今天
0
0
Java8系列之重新认识HashMap

简介 Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示: 下面针对各个实现类...

HOT_POT
今天
0
0
获取调用方的className

/** * 获取调用方的class * @return */private static String getInvoke() { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); S......

iborder
今天
0
0
深入了解一下Redis的内存模型!

一前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分。 我们使用Redis时,会接触Redis的5种对象类型(字符...

Java填坑之路
今天
1
0
从实践出发:微服务布道师告诉你Spring Cloud与Spring Boot他如何选择

背景 随着公司业务量的飞速发展,平台面临的挑战已经远远大于业务,需求量不断增加,技术人员数量增加,面临的复杂度也大大增加。在这个背景下,平台的技术架构也完成了从传统的单体应用到微...

老道士
今天
1
0
大数据学习的各个阶段

第一阶段:Linux课程讲解Linux基础操作,讲的是在命令行下进行文件系统的操作,这是Hadoop学习的基础,后面的所有视频都是基于linux操作的。鉴于很多学员没有linux基础,特增加该内容,保证零linux...

董黎明
今天
0
0
CVE-2013-0077 堆溢出分析

找了很久才发现这个环境比较容易搭建分析... 环境: 系统---Win XP SP3 漏洞程序:QQPlayer 3.7.892.400 出错DLL:quartz.dll 6.5.2600.5512 调试工具:x32db+gflag.exe 过程: 首先gflag设置...

Explorer0
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部