Skip to content

Commit aeb355f

Browse files
author
yangjingjing
committed
init blog
1 parent 264ac85 commit aeb355f

File tree

49 files changed

+22636
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+22636
-0
lines changed

_posts/2015-01-17-JavaIO接口.md

Lines changed: 242 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
---
2+
layout: post
3+
categories: [C]
4+
description: none
5+
keywords: C
6+
---
7+
# GDB调试程序的核心技术-ptrace系统调用与使用示例
8+
在程序出现bug的时候,最好的解决办法就是通过 GDB 调试程序,然后找到程序出现问题的地方。比如程序出现 段错误(内存地址不合法)时,就可以通过 GDB 找到程序哪里访问了不合法的内存地址而导致的。
9+
10+
本文不是介绍GDB不是使用方式,而是大概介绍 GDB 的实现原理,当然是 GDB 是一个庞大而复杂的项目,不可能只通过一篇文章就能解释清楚,所以本文主要是介绍 GDB 使用的核心的技术 - ptrace。
11+
12+
## ptrace系统调用
13+
ptrace() 系统调用是 Linux 提供的一个调试进程的工具,ptrace() 系统调用非常强大,它提供非常多的调试方式让我们去调试某一个进程,下面是 ptrace() 系统调用的定义:
14+
```
15+
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
16+
```
17+
下面解释一下 ptrace() 各个参数的作用:
18+
- request:指定调试的指令,指令的类型很多,如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等等,下面会介绍不同指令的作用。
19+
- pid:进程的ID(这个不用解释了)。
20+
- addr:进程的某个地址空间,可以通过这个参数对进程的某个地址进行读或写操作。
21+
- data:根据不同的指令,有不同的用途,下面会介绍。
22+
23+
## ptrace使用示例
24+
下面通过一个简单例子来说明 ptrace() 系统调用的使用,这个例子主要介绍怎么使用 ptrace() 系统调用获取当前被调试(追踪)进程的各个寄存器的值,代码如下(ptrace.c):
25+
```
26+
#include <sys/ptrace.h>
27+
#include <sys/types.h>
28+
#include <sys/wait.h>
29+
#include <unistd.h>
30+
#include <sys/user.h>
31+
#include <stdio.h>
32+
int main()
33+
{ pid_t child;
34+
struct user_regs_struct regs;
35+
36+
child = fork(); // 创建一个子进程
37+
if(child == 0) { // 子进程
38+
ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态
39+
execl("/bin/ls", "ls", NULL); // 执行 `/bin/ls` 程序
40+
}
41+
else { // 父进程
42+
wait(NULL); // 等待子进程发送一个 SIGCHLD 信号
43+
ptrace(PTRACE_GETREGS, child, NULL, &regs); // 获取子进程的各个寄存器的值
44+
printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",
45+
regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值
46+
ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程
47+
sleep(1);
48+
}
49+
return 0;
50+
}
51+
```
52+
通过命令 gcc ptrace.c -o ptrace 编译并运行上面的程序会输出如下结果:
53+
```
54+
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
55+
ptrace ptrace.c
56+
```
57+
上面结果的第一行是由父进程输出的,主要是打印了子进程执行 /bin/ls 程序后各个寄存器的值。而第二行是由子进程输出的,主要是打印了执行 /bin/ls 程序后面输出的结果。
58+
59+
下面解释一下上面程序的执行流程:
60+
1. 主进程调用 fork() 系统调用创建一个子进程。
61+
2. 的进程调用 ptrace(PTRACE_TRACEME,...) 把自己设置为被追踪状态,并且调用 execl() 执行 /bin/ls 程序。
62+
3. 被设置为追踪(TRACE)状态的子进程执行 execl() 的程序后,会向父进程发送 SIGCHLD 信号,并且暂停自身的执行。
63+
4. 父进程通过调用 wait() 接收子进程发送过来的信号,并且开始追踪子进程。
64+
5. 父进程通过调用 ptrace(PTRACE_GETREGS, child, ...) 来获取到子进程各个寄存器的值,并且打印寄存器的值。
65+
6. 父进程通过调用 ptrace(PTRACE_CONT, child, ...) 让子进程继续执行下去。
66+
67+
从上面的例子可以知道,通过向 ptrace() 函数的 request 参数传入不同的值时,就会有不同的效果。比如传入 PTRACE_TRACEME 就可以让进程进入被追踪状态,而转入 PTRACE_GETREGS 时,就可以获取被追踪的子进程各个寄存器的值等。
68+
69+
## ptrace实现原理
70+
本文使用的 Linux 2.4.16 版本的内核。看懂本文需要的基础:进程调度,内存管理和信号处理等相关知识。
71+
72+
调用 ptrace() 系统函数时会触发调用内核的 sys_ptrace() 函数,由于不同的原因 CPU 架构有着不同的调试方式,所以 Linux 为每种不同的 CPU 架构实现了不同的 sys_ptrace() 函数,而本文主要介绍的是 X86 CPU 的调试方式,所以 sys_ptrace() 函数所在文件是 linux-2.4.16/arch/i386/kernel/ptrace.c。
73+
74+
sys_ptrace() 函数的主体是一个 switch 语句,会传入的 request 参数不同进行不同的操作,如下:
75+
```
76+
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
77+
{
78+
struct task_struct *child;
79+
struct user *dummy = NULL;
80+
int i, ret;
81+
...
82+
83+
read_lock(&tasklist_lock);
84+
child = find_task_by_pid(pid); // 获取 pid 对应的进程 task_struct 对象
85+
if (child)
86+
get_task_struct(child);
87+
read_unlock(&tasklist_lock);
88+
if (!child)
89+
goto out;
90+
91+
if (request == PTRACE_ATTACH) {
92+
ret = ptrace_attach(child);
93+
goto out_tsk;
94+
}
95+
96+
...
97+
switch (request) {
98+
case PTRACE_PEEKTEXT:
99+
case PTRACE_PEEKDATA:
100+
...
101+
case PTRACE_PEEKUSR:
102+
...
103+
case PTRACE_POKETEXT:
104+
case PTRACE_POKEDATA:
105+
...
106+
case PTRACE_POKEUSR:
107+
...
108+
case PTRACE_SYSCALL:
109+
case PTRACE_CONT:
110+
...
111+
case PTRACE_KILL:
112+
...
113+
case PTRACE_SINGLESTEP:
114+
...
115+
case PTRACE_DETACH:
116+
...
117+
}
118+
out_tsk:
119+
free_task_struct(child);
120+
out:
121+
unlock_kernel();
122+
return ret;
123+
}
124+
```
125+
从上面的代码可以看出,sys_ptrace() 函数首先根据进程的 pid 获取到进程的 task_struct 对象。然后根据传入不同的 request 参数在 switch 语句中进行不同的操作。
126+
127+
ptrace() 支持的所有 request 操作定义在 linux-2.4.16/include/linux/ptrace.h 文件中,如下:
128+
```
129+
#define PTRACE_TRACEME 0
130+
#define PTRACE_PEEKTEXT 1
131+
#define PTRACE_PEEKDATA 2
132+
#define PTRACE_PEEKUSR 3
133+
#define PTRACE_POKETEXT 4
134+
#define PTRACE_POKEDATA 5
135+
#define PTRACE_POKEUSR 6
136+
#define PTRACE_CONT 7
137+
#define PTRACE_KILL 8
138+
#define PTRACE_SINGLESTEP 9
139+
#define PTRACE_ATTACH 0x10
140+
#define PTRACE_DETACH 0x11
141+
#define PTRACE_SYSCALL 24
142+
#define PTRACE_GETREGS 12
143+
#define PTRACE_SETREGS 13
144+
#define PTRACE_GETFPREGS 14
145+
#define PTRACE_SETFPREGS 15
146+
#define PTRACE_GETFPXREGS 18
147+
#define PTRACE_SETFPXREGS 19
148+
#define PTRACE_SETOPTIONS 21
149+
```
150+
由于 ptrace() 提供的操作比较多,所以本文只会挑选一些比较有代表性的操作进行解说,比如 PTRACE_TRACEME、PTRACE_SINGLESTEP、PTRACE_PEEKTEXT、PTRACE_PEEKDATA 和 PTRACE_CONT 等,而其他的操作,有兴趣的朋友可以自己去分析其实现原理。
151+
152+
进入被追踪模式(PTRACE_TRACEME操作)
153+
154+
当要调试一个进程时,需要使进程进入被追踪模式,怎么使进程进入被追踪模式呢?有两个方法:
155+
156+
被调试的进程调用 ptrace(PTRACE_TRACEME, ...) 来使自己进入被追踪模式。
157+
调试进程(如GDB)调用 ptrace(PTRACE_ATTACH, pid, ...) 来使指定的进程进入追踪模式。
158+
第一种方式是进程自己主动进入被追踪模式,而第二种是进程被动进入被追踪模式。
159+
160+
被调试的进程必须进入追踪模式才能进行调试,因为 Linux 会对被追踪的进程进行一些特殊的处理。下面我们主要介绍第一种进入追踪模式的实现,就是 PTRACE_TRACEME 操作过程,代码如下:
161+
```
162+
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
163+
{
164+
...
165+
if (request == PTRACE_TRACEME) {
166+
if (current->ptrace & PT_PTRACED)
167+
goto out;
168+
current->ptrace |= PT_PTRACED; // 标志 PTRACE 状态
169+
ret = 0;
170+
goto out;
171+
}
172+
...
173+
}
174+
```
175+
从上面的代码可以发现,ptrace() 对 PTRACE_TRACEME 的处理就是把当前进程标志为 PTRACE 状态。
176+
177+
当然事情不会这么简单,因为当一个进程被标记为 PTRACE 状态后,当调用 exec() 函数去执行一个外部程序时,将会暂停当前进程的运行,并且发送一个 SIGCHLD 给父进程。父进程接收到 SIGCHLD 信号后就可以对被调试的进程进行调试。
178+
179+
我们来看看 exec() 函数是怎样实现上述功能的,exec() 函数的执行过程为 sys_execve() -> do_execve() -> load_elf_binary():
180+
```
181+
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
182+
{
183+
...
184+
if (current->ptrace & PT_PTRACED)
185+
send_sig(SIGTRAP, current, 0);
186+
...
187+
}
188+
```
189+
从上面代码可以看出,当进程被标记为 PTRACE 状态时,执行 exec() 函数后便会发送一个 SIGTRAP 的信号给当前进程。
190+
191+
我们再来看看,进程是怎么处理的 SIGTRAP 信号的。信号是通过 do_signal() 函数进行处理的,而对 SIGTRAP 信号的处理逻辑如下:
192+
```
193+
int do_signal(struct pt_regs *regs, sigset_t *oldset)
194+
{
195+
for (;;) {
196+
unsigned long signr;
197+
198+
spin_lock_irq(&current->sigmask_lock);
199+
signr = dequeue_signal(&current->blocked, &info);
200+
spin_unlock_irq(&current->sigmask_lock);
201+
202+
// 如果进程被标记为 PTRACE 状态
203+
if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
204+
/* 让调试器运行 */
205+
current->exit_code = signr;
206+
current->state = TASK_STOPPED; // 让自己进入停止运行状态
207+
notify_parent(current, SIGCHLD); // 发送 SIGCHLD 信号给父进程
208+
schedule(); // 让出CPU的执行权限
209+
...
210+
}
211+
}
212+
}
213+
```
214+
上面的代码主要做了3件事:
215+
- 如果当前进程被标记为 PTRACE 状态,那么就使自己进入停止运行的状态。
216+
- 发送 SIGCHLD 信号给父进程。
217+
- 让出 CPU 的执行权限,使 CPU 执行其他进程。
218+
219+
执行以上过程后,追踪进程便进入了调试模式
220+
221+
当父进程(调试进程)接收到 SIGCHLD 信号后,表示被调试进程已经标记为被追踪状态并且停止运行,那么调试进程就可以开始进行调试了。
222+
223+
获取被调试进程的内存数据(PTRACE_PEEKTEXT / PTRACE_PEEKDATA)
224+
225+
调试进程(如GDB)可以通过调用 ptrace(PTRACE_PEEKDATA, pid, addr, data) 立即获取被调试进程 addr 处虚拟内存地址的数据,但每次只能读取一个大小为 4字节的数据。
226+
227+
我们来看看 ptrace() 对 PTRACE_PEEKDATA 操作的处理过程,代码如下:
228+
```
229+
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
230+
{
231+
...
232+
switch (request) {
233+
case PTRACE_PEEKTEXT:
234+
case PTRACE_PEEKDATA: {
235+
unsigned long tmp;
236+
int copied;
237+
238+
copied = access_process_vm(child, addr, &tmp, sizeof(tmp), 0);
239+
ret = -EIO;
240+
if (copied != sizeof(tmp))
241+
break;
242+
ret = put_user(tmp, (unsigned long *)data);
243+
break;
244+
}
245+
...
246+
}
247+
```
248+
从上面代码可以看出,对 PTRACE_PEEKTEXT 和 PTRACE_PEEKDATA 的处理是相同的,主要是通过调用 access_process_vm() 函数来读取被调试进程 addr 处的虚拟内存地址的数据。
249+
250+
access_process_vm() 函数的实现主要涉及到 内存管理 相关的知识,可以参考我以前对内存管理分析的文章,这里主要大概说明一下 access_process_vm() 的原理。
251+
252+
我们知道每个进程都有个 mm_struct 的内存管理对象,而 mm_struct 对象有个表示虚拟内存与物理内存映射关系的页目录的指针 pgd。如下:
253+
```
254+
struct mm_struct {
255+
...
256+
pgd_t *pgd; /* 页目录指针 */
257+
...
258+
}
259+
```
260+
而 access_process_vm() 函数就是通过进程的页目录来找到 addr 虚拟内存地址映射的物理内存地址,然后把此物理内存地址处的数据复制到 data 变量中。
261+
262+
access_process_vm() 函数的实现这里就不分析了,有兴趣的读者可以参考我之前对内存管理分析的文章自行进行分析。
263+
264+
## 单步调试模式(PTRACE_SINGLESTEP)
265+
266+
单步调试是一个比较有趣的功能,当把被调试进程设置为单步调试模式后,被调试进程没执行一条CPU指令都会停止执行,并且向父进程(调试进程)发送一个 SIGCHLD 信号。
267+
268+
我们来看看 ptrace() 函数对 PTRACE_SINGLESTEP 操作的处理过程,代码如下:
269+
```
270+
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
271+
{
272+
...
273+
switch (request) {
274+
case PTRACE_SINGLESTEP: { /* set the trap flag. */
275+
long tmp;
276+
...
277+
tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
278+
put_stack_long(child, EFL_OFFSET, tmp);
279+
child->exit_code = data;
280+
/* give it a chance to run. */
281+
wake_up_process(child);
282+
ret = 0;
283+
break;
284+
}
285+
...
286+
}
287+
```
288+
要把被调试的进程设置为单步调试模式,英特尔的 X86 CPU 提供了一个硬件的机制,就是通过把 eflags 寄存器的 Trap Flag 设置为1即可。
289+
290+
当把 eflags 寄存器的 Trap Flag 设置为1后,CPU 每执行一条指令便会产生一个异常,然后会触发 Linux 的异常处理,Linux 便会发送一个 SIGTRAP 信号给被调试的进程。
291+
292+
从上图可知,eflags 寄存器的第8位就是单步调试模式的标志。
293+
294+
所以 ptrace() 函数的以下2行代码就是设置 eflags 进程的单步调试标志:
295+
```
296+
tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
297+
put_stack_long(child, EFL_OFFSET, tmp);
298+
```
299+
而 get_stack_long(proccess, offset) 函数用于获取进程栈 offset 处的值,而 EFL_OFFSET 偏移量就是 eflags 寄存器的值。
300+
301+
所以上面两行代码的意思就是:
302+
- 获取进程的 eflags 寄存器的值,并且设置 Trap Flag 标志。
303+
- 把新的值设置到进程的 eflags 寄存器中。
304+
305+
设置完 eflags 寄存器的值后,就调用 wake_up_process() 函数把被调试的进程唤醒,让其进入运行状态。
306+
307+
处于单步调试模式时,被调试进程每执行一条指令都会触发一次 SIGTRAP 信号,而被调试进程处理 SIGTRAP 信号时会发送一个 SIGCHLD 信号给父进程(调试进程),并且让自己停止执行。
308+
309+
而父进程(调试进程)接收到 SIGCHLD 后面,就可以对被调试的进程进行各种操作,比如读取被调试进程内存的数据和寄存器的数据,或者通过调用 ptrace(PTRACE_CONT, child,...) 来让被调试进程进行运行等。
310+
311+
由于 ptrace() 的功能十分强大,所以本文只能抛砖引玉,没能对其所有功能进行分析。另外断点功能并不是通过 ptrace() 函数实现的,而是通过 int3 指令来实现的,在 Eli Bendersky 大神的文章有所介绍。而对于 ptrace() 的所有功能,只能读者自己慢慢看代码来体会了。
312+
313+
314+
315+
316+
317+
318+
319+
320+
321+
322+
323+
324+
325+
326+
327+
328+
329+
330+
331+
332+
333+
334+
335+
336+
337+
338+
339+
340+
341+
342+
343+

0 commit comments

Comments
 (0)