Merge pull request #5425 from ictlyh/master

Translated tech/20110123 How debuggers work Part 1 - Basics.md
This commit is contained in:
Yuanhao Luo 2017-04-09 15:44:06 +08:00 committed by GitHub
commit fdc9794d43
2 changed files with 346 additions and 324 deletions

View File

@ -1,324 +0,0 @@
Translating by YYforymj
[How debuggers work: Part 1 - Basics][21]
============================================================
This is the first part in a series of articles on how debuggers work. I'm still not sure how many articles the series will contain and what topics it will cover, but I'm going to start with the basics.
### In this part
I'm going to present the main building block of a debugger's implementation on Linux - the ptrace system call. All the code in this article is developed on a 32-bit Ubuntu machine. Note that the code is very much platform specific, although porting it to other platforms shouldn't be too difficult.
### Motivation
To understand where we're going, try to imagine what it takes for a debugger to do its work. A debugger can start some process and debug it, or attach itself to an existing process. It can single-step through the code, set breakpoints and run to them, examine variable values and stack traces. Many debuggers have advanced features such as executing expressions and calling functions in the debbugged process's address space, and even changing the process's code on-the-fly and watching the effects.
Although modern debuggers are complex beasts [[1]][13], it's surprising how simple is the foundation on which they are built. Debuggers start with only a few basic services provided by the operating system and the compiler/linker, all the rest is just [a simple matter of programming][14].
### Linux debugging - <tt class="docutils literal" style="font-family: Consolas, monaco, monospace; color: rgb(0, 0, 0); background-color: rgb(247, 247, 247); white-space: nowrap; border-radius: 2px; font-size: 21.6px; padding: 2px;">ptrace
The Swiss army knife of Linux debuggers is the ptrace system call [[2]][15]. It's a versatile and rather complex tool that allows one process to control the execution of another and to peek and poke at its innards [[3]][16]. ptrace can take a mid-sized book to explain fully, which is why I'm just going to focus on some of its practical uses in examples.
Let's dive right in.
### Stepping through the code of a process
I'm now going to develop an example of running a process in "traced" mode in which we're going to single-step through its code - the machine code (assembly instructions) that's executed by the CPU. I'll show the example code in parts, explaining each, and in the end of the article you will find a link to download a complete C file that you can compile, execute and play with.
The high-level plan is to write code that splits into a child process that will execute a user-supplied command, and a parent process that traces the child. First, the main function:
```
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;
}
```
Pretty simple: we start a new child process with fork [[4]][17]. The if branch of the subsequent condition runs the child process (called "target" here), and the else if branch runs the parent process (called "debugger" here).
Here's the target process:
```
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);
}
```
The most interesting line here is the ptrace call. ptrace is declared thus (in sys/ptrace.h):
```
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
```
The first argument is a  _request_ , which may be one of many predefined PTRACE_* constants. The second argument specifies a process ID for some requests. The third and fourth arguments are address and data pointers, for memory manipulation. The ptrace call in the code snippet above makes the PTRACE_TRACEMErequest, which means that this child process asks the OS kernel to let its parent trace it. The request description from the man-page is quite clear:
> Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(). **Also, all subsequent calls to exec() by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution**. A process probably shouldn't make this request if its parent isn't expecting to trace it. (pid, addr, and data are ignored.)
I've highlighted the part that interests us in this example. Note that the very next thing run_targetdoes after ptrace is invoke the program given to it as an argument with execl. This, as the highlighted part explains, causes the OS kernel to stop the process just before it begins executing the program in execl and send a signal to the parent.
Thus, time is ripe to see what the parent does:
```
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);
}
```
Recall from above that once the child starts executing the exec call, it will stop and be sent the SIGTRAP signal. The parent here waits for this to happen with the first wait call. wait will return once something interesting happens, and the parent checks that it was because the child was stopped (WIFSTOPPED returns true if the child process was stopped by delivery of a signal).
What the parent does next is the most interesting part of this article. It invokes ptrace with the PTRACE_SINGLESTEP request giving it the child process ID. What this does is tell the OS -  _please restart the child process, but stop it after it executes the next instruction_ . Again, the parent waits for the child to stop and the loop continues. The loop will terminate when the signal that came out of the wait call wasn't about the child stopping. During a normal run of the tracer, this will be the signal that tells the parent that the child process exited (WIFEXITED would return true on it).
Note that icounter counts the amount of instructions executed by the child process. So our simple example actually does something useful - given a program name on the command line, it executes the program and reports the amount of CPU instructions it took to run from start to finish. Let's see it in action.
### A test run
I compiled the following simple program and ran it under the tracer:
```
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
```
To my surprise, the tracer took quite long to run and reported that there were more than 100,000 instructions executed. For a simple printf call? What gives? The answer is very interesting [[5]][18]. By default, gcc on Linux links programs to the C runtime libraries dynamically. What this means is that one of the first things that runs when any program is executed is the dynamic library loader that looks for the required shared libraries. This is quite a lot of code - and remember that our basic tracer here looks at each and every instruction, not of just the main function, but  _of the whole process_ .
So, when I linked the test program with the -static flag (and verified that the executable gained some 500KB in weight, as is logical for a static link of the C runtime), the tracing reported only 7,000 instructions or so. This is still a lot, but makes perfect sense if you recall that libc initialization still has to run before main, and cleanup has to run after main. Besides, printf is a complex function.
Still not satisfied, I wanted to see something  _testable_  - i.e. a whole run in which I could account for every instruction executed. This, of course, can be done with assembly code. So I took this version of "Hello, world!" and assembled it:
```
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
```
Sure enough. Now the tracer reported that 7 instructions were executed, which is something I can easily verify.
### Deep into the instruction stream
The assembly-written program allows me to introduce you to another powerful use of ptrace - closely examining the state of the traced process. Here's another version of the run_debugger function:
```
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);
}
```
The only difference is in the first few lines of the while loop. There are two new ptrace calls. The first one reads the value of the process's registers into a structure. user_regs_struct is defined in sys/user.h. Now here's the fun part - if you look at this header file, a comment close to the top says:
```
/* The whole purpose of this file is for GDB and GDB only.
Don't read too much into it. Don't use it for
anything other than GDB unless know what you are
doing. */
```
Now, I don't know about you, but it makes  _me_  feel we're on the right track :-) Anyway, back to the example. Once we have all the registers in regs, we can peek at the current instruction of the process by calling ptrace with PTRACE_PEEKTEXT, passing it regs.eip (the extended instruction pointer on x86) as the address. What we get back is the instruction [[6]][19]. Let's see this new tracer run on our assembly-coded snippet:
```
$ 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, so now in addition to icounter we also see the instruction pointer and the instruction it points to at each step. How to verify this is correct? By using objdump -d on the executable:
```
$ 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 80 int $0x80
8048096: b8 01 00 00 00 mov $0x1,%eax
804809b: cd 80 int $0x80
```
The correspondence between this and our tracing output is easily observed.
### Attaching to a running process
As you know, debuggers can also attach to an already-running process. By now you won't be surprised to find out that this is also done with ptrace, which can get the PTRACE_ATTACH request. I won't show a code sample here since it should be very easy to implement given the code we've already gone through. For educational purposes, the approach taken here is more convenient (since we can stop the child process right at its start).
### The code
The complete C source-code of the simple tracer presented in this article (the more advanced, instruction-printing version) is available [here][20]. It compiles cleanly with -Wall -pedantic --std=c99 on version 4.4 of gcc.
### Conclusion and next steps
Admittedly, this part didn't cover much - we're still far from having a real debugger in our hands. However, I hope it has already made the process of debugging at least a little less mysterious. ptrace is truly a versatile system call with many abilities, of which we've sampled only a few so far.
Single-stepping through the code is useful, but only to a certain degree. Take the C "Hello, world!" sample I demonstrated above. To get to main it would probably take a couple of thousands of instructions of C runtime initialization code to step through. This isn't very convenient. What we'd ideally want to have is the ability to place a breakpoint at the entry to main and step from there. Fair enough, and in the next part of the series I intend to show how breakpoints are implemented.
### References
I've found the following resources and articles useful in the preparation of this article:
* [Playing with ptrace, Part I][11]
* [How debugger works][12]
[1] I didn't check but I'm sure the LOC count of gdb is at least in the six-figures range.
[2] Run man 2 ptrace for complete enlightment.
[3] Peek and poke are well-known system programming jargon for directly reading and writing memory contents.
[4] This article assumes some basic level of Unix/Linux programming experience. I assume you know (at least conceptually) about fork, the exec family of functions and Unix signals.
[5] At least if you're as obsessed with low-level details as I am :-)
[6] A word of warning here: as I noted above, a lot of this is highly platform specific. I'm making some simplifying assumptions - for example, x86 instructions don't have to fit into 4 bytes (the size of unsigned on my 32-bit Ubuntu machine). In fact, many won't. Peeking at instructions meaningfully requires us to have a complete disassembler at hand. We don't have one here, but real debuggers do.
--------------------------------------------------------------------------------
via: http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1
作者:[Eli Bendersky ][a]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:http://eli.thegreenplace.net/
[1]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id1
[2]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id2
[3]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id3
[4]:http://www.jargon.net/jargonfile/p/peek.html
[5]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id4
[6]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id5
[7]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id6
[8]:http://eli.thegreenplace.net/tag/articles
[9]:http://eli.thegreenplace.net/tag/debuggers
[10]:http://eli.thegreenplace.net/tag/programming
[11]:http://www.linuxjournal.com/article/6100?page=0,1
[12]:http://www.alexonlinux.com/how-debugger-works
[13]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id7
[14]:http://en.wikipedia.org/wiki/Small_matter_of_programming
[15]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id8
[16]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id9
[17]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id10
[18]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id11
[19]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id12
[20]:https://github.com/eliben/code-for-blog/blob/master/2011/simple_tracer.c
[21]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1

View File

@ -0,0 +1,346 @@
[调试器的工作原理:第一篇-基础][21]
============================================================
这是调试器工作原理系列文章的第一篇,我不确定这个系列会有多少篇文章,会涉及多少话题,但我仍会从这篇基础开始。
### 这一篇会讲什么
我将为大家展示 Linux 中调试器的主要构成模块 - ptrace 系统调用。这篇文章所有代码都是基于 32 位 Ubuntu 操作系统.值得注意的是,尽管这些代码是平台相关的,将他们移植到其他平台应该并不困难。
### 缘由
为了理解我们要做什么,让我们先考虑下调试器为了完成调试都需要什么资源。调试器可以开始一个进程并调试这个进程,又或者将自己同某个已经存在的进程关联起来。调试器能够单步执行代码,设定断点并且将程序执行到断点,检查变量的值并追踪堆栈。许多调试器有着更高级的特性,例如在调试器的地址空间内执行表达式或者调用函数,甚至可以在进程执行过程中改变代码并观察效果。
尽管现代的调试器都十分的复杂 [[1]][13],但他们的工作的原理却是十分的简单。调试器的基础是操作系统与编译器 / 链接器提供的一些基础服务,其余的部分只是[简单的编程][14]。
### Linux 的调试 - ptrace
Linux 调试器中的瑞士军刀便是 ptrace 系统调用 [[2]][15]。这是一种复杂却强大的工具,可以允许一个进程控制另外一个进程并从内部替换被控制进程的内核镜像的值[[3]][16].。
接下来会深入分析。
### 执行进程的代码
我将编写一个示例,实现一个在“跟踪”模式下运行的进程。在这个模式下,我们将单步执行进程的代码,就像机器码(汇编代码)被 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 命令创建了一个新的子进程。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 <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
```
令我惊讶的是,跟踪器花了相当长的时间,并报告整个执行过程共有超过 100,000 条指令执行。仅仅是一条输出语句?什么造成了这种情况?答案很有趣[[5]][18]。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。如果你查看头文件头部的注释这么写到
```
/* The whole purpose of this file is for GDB and GDB only.
Don't read too much into it. Don't use it for
anything other than GDB unless know what you are
doing. */
```
```
/* 这个文件只为了GDB而创建
不用详细的阅读.如果你不知道你在干嘛,
不要在除了 GDB 以外的任何地方使用此文件 */
```
不知道你做何感想,但这让我觉得我们找对地方了。回到例子中,一旦我们在 regs 变量中取得了寄存器的值,我们就可以通过将 PTRACE_PEEKTEXT 作为参数、 regs.eipx86 上的扩展指令指针)作为地址,调用 ptrace ,读取当前进程的当前指令。下面是新跟踪器所展示出的调试效果:
```
$ 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 <.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 80 int $0x80
8048096: b8 01 00 00 00 mov $0x1,%eax
804809b: cd 80 int $0x80
```
这个结果和我们跟踪器的结果就很容易比较了。
### 将跟踪器关联到正在运行的进程
如你所知调试器也能关联到已经运行的进程。现在你应该不会惊讶ptrace 通过 以PTRACE_ATTACH 为参数调用也可以完成这个过程。这里我不会展示示例代码通过上文的示例代码应该很容易实现这个过程。出于学习目的这里使用的方法更简便因为我们在子进程刚开始就可以让它停止
### 代码
上文中的简单的跟踪器更高级的可以打印指令的版本的完整c源代码可以在[这里][20]找到。它是通过 4.4 版本的 gcc 以 -Wall -pedantic --std=c99 编译的。
### 结论与计划
诚然,这篇文章并没有涉及很多内容 - 我们距离亲手完成一个实际的调试器还有很长的路要走。但我希望这篇文章至少可以使得调试这件事少一些神秘感。ptrace 是功能多样的系统调用,我们目前只展示了其中的一小部分。
单步调试代码很有用但也只是在一定程度上有用。上面我通过c的“Hello World!”做了示例。为了执行主函数可能需要上万行代码来初始化c的运行环境。这并不是很方便。最理想的是在main函数入口处放置断点并从断点处开始分步执行。为此在这个系列的下一篇我打算展示怎么实现断点。
### 参考
撰写此文时参考了如下文章
* [Playing with ptrace, Part I][11]
* [How debugger works][12]
[1] 我没有检查,但我确信 gdb 的代码行数至少有六位数。
[2] 使用 man 2 ptrace 命令可以了解更多。
[3] Peek and poke 在系统编程中是很知名的叫法,指的是直接读写内存内容。
[4] 这篇文章假定读者有一定的 Unix/Linux 编程经验。我假定你知道至少了解概念forkexec 族函数与 Unix 信号。
[5] 至少你同我一样痴迷与机器/汇编语言。
[6] 警告:如同我上面所说,文章很大程度上是平台相关的。我简化了一些设定 - 例如x86指令集不需要调整到 4 字节我的32位 Ubuntu unsigned int 是 4 字节)。事实上,许多平台都不需要。从内存中读取指令需要预先安装完整的反汇编器。我们这里没有,但实际的调试器是有的。
--------------------------------------------------------------------------------
via: http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1
作者:[Eli Bendersky ][a]
译者:[译者ID](https://github.com/YYforymj)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:http://eli.thegreenplace.net/
[1]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id1
[2]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id2
[3]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id3
[4]:http://www.jargon.net/jargonfile/p/peek.html
[5]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id4
[6]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id5
[7]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id6
[8]:http://eli.thegreenplace.net/tag/articles
[9]:http://eli.thegreenplace.net/tag/debuggers
[10]:http://eli.thegreenplace.net/tag/programming
[11]:http://www.linuxjournal.com/article/6100?page=0,1
[12]:http://www.alexonlinux.com/how-debugger-works
[13]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id7
[14]:http://en.wikipedia.org/wiki/Small_matter_of_programming
[15]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id8
[16]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id9
[17]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id10
[18]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id11
[19]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1#id12
[20]:https://github.com/eliben/code-for-blog/blob/master/2011/simple_tracer.c
[21]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1