ptrace实现原理
本文使用的 Linux 2.4.16 版本的内核
看懂本文需要的基础:进程调度,内存管理和信号处理相关知识。
青铜:
是因为操作系统--内存管理模块 检查异常,然后发出信号signal 11。
中断应用程序正常执行flow。执行信号处理函数。
然后这些东西不会融会贯通
像外行一样思考,像专家一样实践:
小王:遇到core怎么办?
老王:gdb调试呀
小王:gdb 为什么可以非侵入调试进程呀。
老王:这个我没想过。。。平时不考虑这个问题
If you are thinking of using complex kernel programming to accomplish tasks, think again. Linux provides an elegant mechanism to achieve all of these things: the ptrace (Process Trace) system call.
ptrace provides a mechanism by which a parent process may observe and control the execution of another process. It can examine and change its core image and registers and is used primarily to implement breakpoint debugging and system call tracing.
如果您正在考虑使用复杂的内核编程来完成任务, PTRACE_TRACEME 请三思。Linux 提供了一种优雅的机制来实现所有这些功能: ptrace (进程跟踪)系统调用。
Ptrace 提供了一种机制,通过这种机制,父进程可以观察和控制另一个进程的执行。
它可以检查和更改其核心映像和寄存器,主要用于实现断点调试和系统调用跟踪。
他们幕后原理工作其实就是ptrace完成的。
ptrace可以让父进程观察和控制其子进程的检查、执行,改变其寄存器和内存的内容,
可以使程序员在程序运行的时候观察程序在内存/寄存器中的使用情况
主要应用于打断点(也是gdb的主要功能)和打印系统调用轨迹。
GDB常用的使用方法有断点设置和单步跟踪
NAME
ptrace - process trace
SYNOPSIS
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
DESCRIPTION
The ptrace() system call provides a means by which one process (the
"tracer") may observe and control the execution of another process (the
"tracee"), and examine and change the tracee's memory and registers.
It is primarily used to implement breakpoint debugging and system call
tracing.
DESCRIPTION
The ptrace() system call provides a means by which a parent process may observe and control the execution of another process,
and examine and change its core image and registers. It is primarily used to implement breakpoint debugging and system call
tracing.
下面解释一下 ptrace() 各个参数的作用:
PTRACE_TRACEME 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_SINGLESTEP 设置单步执行标志
PTRACE_ATTACH 跟踪指定pid 进程。
/ 在linux/kernel/ptrace.c文件中
SYSCALL_DEFINE4(ptrace, long, request, long, pid, long, addr, long, data)
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
struct task_struct *child;
struct user *dummy = NULL;
int i, ret;
...
read_lock(&tasklist_lock);
child = find_task_by_pid(pid); // 获取 pid 对应的进程 task_struct 对象
if (child)
get_task_struct(child);
read_unlock(&tasklist_lock);
if (!child)
goto out;
if (request == PTRACE_ATTACH) {
ret = ptrace_attach(child);
goto out_tsk;
}
...
switch (request) {
case PTRACE_PEEKTEXT:
case PTRACE_PEEKDATA:
...
case PTRACE_PEEKUSR:
...
case PTRACE_POKETEXT:
case PTRACE_POKEDATA:
...
case PTRACE_POKEUSR:
...
case PTRACE_SYSCALL:
case PTRACE_CONT:
...
case PTRACE_KILL:
...
case PTRACE_SINGLESTEP:
...
case PTRACE_DETACH:
...
}
out_tsk:
free_task_struct(child);
out:
unlock_kernel();
return ret;
}
#define PTRACE_TRACEME 0
进入被追踪模式(PTRACE_TRACEME操作)
当要调试一个进程时,需要使进程进入被追踪模式,怎么使进程进入被追踪模式呢?
有两个方法:
被调试的进程调用 ptrace(PTRACE_TRACEME, ...) 来使自己进入被追踪模式。
调试进程(如GDB)调用 ptrace(PTRACE_ATTACH, pid, ...) 来使指定的进程进入被追踪模式。
#define PTRACE_PEEKTEXT 1
#define PTRACE_PEEKDATA 2
#define PTRACE_PEEKUSR 3
#define PTRACE_POKETEXT 4
#define PTRACE_POKEDATA 5
#define PTRACE_POKEUSR 6
#define PTRACE_CONT 7
#define PTRACE_KILL 8
#define PTRACE_SINGLESTEP 9
#define PTRACE_ATTACH 0x10
#define PTRACE_DETACH 0x11
#define PTRACE_SYSCALL 24
#define PTRACE_GETREGS 12
#define PTRACE_SETREGS 13
#define PTRACE_GETFPREGS 14
#define PTRACE_SETFPREGS 15
#define PTRACE_GETFPXREGS 18
#define PTRACE_SETFPXREGS 19
#define PTRACE_SETOPTIONS 21
二、gdb使用ptrace的基本流程
单步调试模式(PTRACE_SINGLESTEP)
单步调试是一个比较有趣的功能,当把被调试进程设置为单步调试模式后,被调试进程没执行一条CPU指令都会停止执行,并且向父进程(调试进程)发送一个 SIGCHLD 信号。
我们来看看 ptrace() 函数对 PTRACE_SINGLESTEP 操作的处理过程,代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
...
switch (request) {
case PTRACE_SINGLESTEP: { /* set the trap flag. */
long tmp;
...
tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
put_stack_long(child, EFL_OFFSET, tmp);
child->exit_code = data;
/* give it a chance to run. */
wake_up_process(child);
ret = 0;
break;
}
...
}
process.
D Uninterruptible sleep (usually IO)
R Running or runnable (on run queue)
S Interruptible sleep (waiting for an event to complete)
T Stopped, either by a job control signal or because it is being traced.
https://stackoverflow.com/questions/7844569/ptrace-single-step-in-the-kernel-from-process-context
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z Defunct ("zombie") process, terminated but not reaped by its parent.
D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。
R (TASK_RUNNING),进程执行中。
S (TASK_INTERRUPTIBLE),可中断的睡眠状态。
T (TASK_STOPPED),暂停状态。
t (TASK_TRACED),进程被追踪。
w (TASK_PAGING),进程调页中,2.6以上版本的内核中已经被移除。
X (TASK_DEAD – EXIT_DEAD),退出状态,进程即将被销毁。
Z (TASK_DEAD – EXIT_ZOMBIE),退出状态,进程成为僵尸进程。
Linux系统调用:使用int 0x80
系统调用的分类
系统调用大体上可分为5类:
进程控制
加载
执行
结束,中止
创建进程
结束进程
得到/设置进程属性
等待(时间、时间、信号)
内存的分配和去配
文件管理
文件的创建和删除
打开和关闭
读、写和重定位
得到/设置文件属性
设备管理
设备的请求和释放
读、写和重定位
得到/设置设备属性
设备的逻辑关联或去关联
信息维护
得到/设置时间或日期
得到/设置系统数据
得到/设置进程、文件或设备属性
通信
通信连接的创建和删除
发送、接收信息
转换状态信息
远程设备的关联或去关联
Linux系统调用:使用 int 0x80
Linux提供了200多个系统调用,通过汇编指令 int 0x80 实现,用系统调用号来区分入口函数。
Linux实现系统调用的基本过程是:
应用程序准备参数,发出调用请求;
C库封装函数引导。该函数在Linux提供的标准C库,即 glibc 中。对应的封装函数由下列汇编指令实现(以读函数调用为例):
; NASM
; read(int fd, void *buffer, size_t nbytes)
mov eax, 3 ; read系统调用号为3
mov ebx, fd
mov ecx, buffer
mov edx, nbytes
int 0x80 ; 触发系统调用
系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态, 而普通的函数调用由函数库或用户自己提供,运行于用户态。
在 i386体系结构上(本文中的所有代码都是 i386特定的) ,系统调用号码放在寄存器% eax 中。这个系统调用的参数按照这个顺序放入寄存器% ebx、% ecx、% edx、% esi 和% edi 中。例如,调用:
Linux 通过 软中断 实现从 用户态 到 内核态 的切换。用户态 与 内核态 是独立的执行流, 因此在切换时,需要准备 执行栈 并保存 寄存器 。
write(2, "Hello", 5)
roughly would translate into
大致可以理解为
movl $4, %eax
movl $2, %ebx
movl $hello,%ecx
movl $5, %edx
int $0x80
root@money:~/code/c++/temo# cat ptrace.c
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
int main()
{ pid_t child;
struct user_regs_struct regs;
child = fork(); // 创建一个子进程
if(child == 0) { // 子进程
ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态
execl("/bin/ls", "ls", NULL); // 执行 `/bin/ls` 程序
}
else { // 父进程
wait(NULL); // 等待子进程发送一个 SIGCHLD 信号
ptrace(PTRACE_GETREGS, child, NULL, ®s); // 获取子进程的各个寄存器的值
printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",
regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值
ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程
sleep(1);
}
return 0;
}
root@money:~/code/c++/temo# ./ptrace
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
1.cpp ptrace ptrace.c
代码1 ptrace(PTRACE_TRACEME, 0, NULL, NULL); 子进程自己主动进入被追踪模式
下面我们主要介绍第一种进入被追踪模式的实现,就是 PTRACE_TRACEME 的操作过程,代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
...
if (request == PTRACE_TRACEME) {
if (current->ptrace & PT_PTRACED)
goto out;
current->ptrace |= PT_PTRACED; // 标志 PTRACE 状态
ret = 0;
goto out;
}
...
}
从上面的代码可以发现,ptrace() 对 PTRACE_TRACEME 的处理就是把当前进程标志为 PTRACE 状态。
execl("/bin/ls", "ls", NULL); 当然事情不会这么简单,因为当一个进程被标记为 PTRACE 状态后,
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
...
if (current->ptrace & PT_PTRACED)
send_sig(SIGTRAP, current, 0);
...
}
从上面代码可以看出,当进程被标记为 PTRACE 状态时,执行 exec() 函数后便会发送一个 SIGTRAP 的信号给当前进程。
我们再来看看,进程是怎么处理 SIGTRAP 信号的。信号是通过 do_signal() 函数进行处理的,而对 SIGTRAP 信号的处理逻辑如下:
int do_signal(struct pt_regs *regs, sigset_t *oldset)
{
for (;;) {
unsigned long signr;
spin_lock_irq(¤t->sigmask_lock);
signr = dequeue_signal(¤t->blocked, &info);
spin_unlock_irq(¤t->sigmask_lock);
// 如果进程被标记为 PTRACE 状态
if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
/* 让调试器运行 */
current->exit_code = signr;
current->state = TASK_STOPPED; // 让自己进入停止运行状态
notify_parent(current, SIGCHLD); // 发送 SIGCHLD 信号给父进程
schedule(); // 让出CPU的执行权限
...
}
}
}
上面的代码主要做了3件事:
如果当前进程被标记为 PTRACE 状态,那么就使自己进入停止运行状态。发送 SIGCHLD 信号给父进程。让出 CPU 的执行权限,使 CPU 执行其他进程。
proc 放置的数据都是在内存当中, 例如系统内核、进程、外部设备的状态及网络状态等。因为这个目录下的数据都是在内存当中 ,所以本身不占任何硬盘空间。