1. 准备知识
进程状态
S:Interruptible Sleeping,即可中断睡眠;
D:Uninterruptible Sleeping,即不可中断睡眠;
R:Running or Runnable,即运行状态;
Z:Zombie,即僵尸状态;
T:Stopped or Traced,即中止状态(注意是“中止”而非“终止”)。
execl语句可以将当前进程替换成一个新进程。在本例中,execl(“/bin/ls”, “ls”, “-l”, “-h”, NULL);语句将原本的子进程替换成了一条ls命令。值得注意的是,如果execl系统调用的进程处于PTRACE_TRACEME状态的话,就会发送一个SIGTRAP信号给父进程,并让自身处于Traced状态。
SIGTRAP是一个信号,用于表示调试程序中的断点(breakpoint)。它是由程序中的断点触发或者由调试器发送给正在运行的程序的。它的含义是停止执行程序,以便进行调试操作。
#include <unistd.h>
#include <sys/ptrace.h>
#include <stdio.h>
int main(void){
pid_t child;
long orig_rax;
child = fork();
if (child == 0){
printf("Child process id: %d\n", getpid());
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
sleep(10);
execl("/bin/ls", "ls", "-l", "-h", NULL);
} else {
sleep(30);
}
return 0;
}
运行结果:
[root@localhost ~]# ps -aux | grep 30926
root 30926 0.0 0.0 4216 88 pts/1 S+ 15:32 0:00 ./execl_example
# after 10 seconds...
[root@localhost ~]# ps -aux | grep 30926
root 30926 0.0 0.0 404 4 pts/1 t+ 15:32 0:00 ls -l -h
子进程从./execl_example
变成了ls -l -h
,且复用一个PID。而且新的进程进入了t+(Traced stopped)
状态,在等待tracer
进程对其进行控制。
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t child;
int status;
struct user_regs_struct regs;
int orig_rax;
child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "/bin/ls", NULL);
exit(0);
} else {
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
ptrace(PTRACE_GETREGS, child, 0, ®s); // 获取被跟踪进程寄存器的值
orig_rax = regs.orig_rax; // 获取rax寄存器的值
printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值
// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
}
return 0;
}
- 调用了两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL),这是因为跟踪系统调用时,需要跟踪系统调用前的环境(比如获取系统调用的参数)和系统调用后的环境(比如获取系统调用的返回值),所以就需要调用两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL)
- inux系统调用是通过 CPU寄存器 来传递参数的,所以要想获取调用了哪个系统调用,必须获取进程寄存器的值。获取进程寄存器的值,可以通过 ptrace() 系统调用的 PTRACE_GETREGS 命令来实现,
- PTRACE_GETREGS 命令需要在 data 参数传入类型为 user_regs_struct 结构的指针,user_regs_struct 结构定义如下(在文件 sys/user.h 中)
struct user_regs_struct {
unsigned long r15,r14,r13,r12,rbp,rbx,r11,r10;
unsigned long r9,r8,rax,rcx,rdx,rsi,rdi,orig_rax;
unsigned long rip,cs,eflags;
unsigned long rsp,ss;
unsigned long fs_base, gs_base;
unsigned long ds,es,fs,gs;
};
上面的程序只跟踪了一个系统调用,那么怎么跟踪所有的系统调用呢?很简单,只需要把跟踪的代码放到一个无限循环中即可
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
struct syscall {
int code;
char *name;
} syscall_table[] = {
{0, "read"},
{1, "write"},
{2, "open"},
{3, "close"},
{4, "stat"},
{5, "fstat"},
{6, "lstat"},
{7, "poll"},
{8, "lseek"},
...
{-1, NULL},
}
char *find_syscall_symbol(int code) {
struct syscall *sc;
for (sc = syscall_table; sc->code >= 0; sc++) {
if (sc->code == code) {
return sc->name;
}
}
return NULL;
}
int main(int argc, char *argv[])
{
pid_t child;
int status;
struct user_regs_struct regs;
int orig_rax;
child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "/bin/ls", NULL);
exit(0);
} else {
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
while (1) {
// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
break;
}
ptrace(PTRACE_GETREGS, child, 0, ®s); // 获取被跟踪进程寄存器的值
orig_rax = regs.orig_rax; // 获取rax寄存器的值
printf("syscall: %s()\n", find_syscall_symbol(orig_rax)); // 打印系统调用
// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
break;
}
}
}
return 0;
}
输出结果
[root@localhost liexusong]$ ./strace
syscall: brk()
syscall: mmap()
syscall: access()
syscall: open()
syscall: fstat()
syscall: mmap()
syscall: close()
syscall: open()
syscall: read()
syscall: fstat()
syscall: mmap()
syscall: mprotect()
syscall: mmap()
syscall: mmap()
syscall: close()
...
2 一个使用示例
#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;
}
输出结果
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace ptrace.c
第一行是由父进程输出的,主要是打印了子进程执行 /bin/ls 程序后各个寄存器的值。而第二行是由子进程输出的,主要是打印了执行 /bin/ls 程序后输出的结果
执行顺序
- 主进程调用 fork() 系统调用创建一个子进程。
- 子进程调用 ptrace(PTRACE_TRACEME,…) 把自己设置为被追踪状态,并且调用 execl() 执行 /bin/ls 程序。
- 被设置为追踪(TRACE)状态的子进程执行 execl() 的程序后,会向父进程发送 SIGCHLD 信号,并且暂停自身的执行。
- 父进程通过调用 wait() 接收子进程发送过来的信号,并且开始追踪子进程。
- 父进程通过调用 ptrace(PTRACE_GETREGS, child, …) 来获取到子进程各个寄存器的值,并且打印寄存器的值。
- 父进程通过调用 ptrace(PTRACE_CONT, child, …) 让子进程继续执行下去。
3 爸爸查看儿子的信息 PTRACE_PEEKUSER
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_RAX etc */
int main(){
pid_t child;
long orig_rax;
child=fork();
// 0是儿子 非0是爸爸(当前进程到id)
// 儿子直接执行,而爸爸会调用wait等待儿子执行完毕
if(child==0){
printf("1.1 child:%d\n",child);
//trace me:允许爸爸查看我到信息(告诉内核,我是被父进程附加的)
ptrace(PTRACE_TRACEME,0,NULL,NULL);
printf("2. /bin/ls\n");
//如果execl系统调用的进程处于PTRACE_TRACEME状态的话,就会发送一个SIGTRAP信号给父进程,并让自身处于Traced状态。
//子进程运行到这里暂停,由父进程到PTRACE_CONT恢复,这个时候寄存器已经保存了子进程到信息
//execl系统调用给父进程发送SIGTRAP信号后,父进程怎样处理这个信号?
execl("/bin/ls","ls",NULL);
}else{
printf("1.0 child:%d\n",child);
//等待子进程停下来(execl那里),参数为null,不在乎子进程是什么状态
//这里说一下他们到生死关系,首先是执行fork分出了一个爸爸,然后儿子执行了traceme告诉内核
//我是可以被爸爸控制到,然后运行到execl方法企图执行但还未执行,这个时候儿子停止了,
//同时爸爸被激活.执行wait后到语句
//wait系统调用是一个用来进行进程控制的系统调用,它可以用来阻塞父进程,当父进程接收到子进程传来信号或者子进程退出时,父进程才会继续运行。
//所以这里的wait系统调用很显然用来接收子进程调用execl时产生的SIGTRAP信号。
//父进程接收到SIGTRAP信号,就意味着子进程执行execl系统调用已成功。也就意味着现在子进程已经进入了Traced状态,在等待父进程对其进行控制。
wait(NULL);
//peek user:查看儿子到信息(查看子进程中寄存器到内容,ORIG_RAX寄存器值的保存地址)
//PTRACE_PEEKUSER作为操作类型,这个操作类型的作用官方是这样描述的:读取tracee进程的USER字段中相关偏移量位置的值
orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("3. The child made a system call %ld\n",orig_rax);
//cont: 让子进程恢复运行
ptrace(PTRACE_CONT,child,NULL,NULL);
}
}
4查看儿子所有到信息 PTRACE_GETREGS
/*
ptrace之读取目标进程寄存器到值到user_regs_struct
*/
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h> //user_regs_struct regs寄存器的头部
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
pid_t child;
long orig_rax;
int status;
int iscalling=0;
struct user_regs_struct regs;
//fork出两个进程
child=fork();
if(child==0){
//子进程 child为0
printf("1.0 child:%d\n",child);
// 告诉内核,本子进程可以被父进程查看/修改
ptrace(PTRACE_TRACEME,0,NULL,NULL);
//运行ls -l -h
execl("/bin/ls","ls","-l","-h",NULL);
}else{
//父进程 child为子进程pid
printf("1.1 child:%d\n",child);
while(1){
// 如果status不为null,status将返回子进程到是否结束到状态
// 如果其所有子进程都还在运行,则阻塞
// 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回
// 如果没有任何子进程,则立即出错返回
wait(&status);// 接收被子进程发送过来的 SIGCHLD 信号
// WIFEXITED宏可以用来检测接收到的信号是否标志着子进程退出
//检查子进程是暂停还准备退出
// 如若正常结束子进程返回的状态,则为真
if(WIFEXITED(status))
break;
//读取ORIG_RAX的内容,peek user:查看儿子到信息(查看子进程中寄存器的内容,ORIG_RAX:保存了系统调用号)
orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
//printf("1.2 orig_rax:%ld\n",orig_rax);
if(orig_rax==SYS_write){
//GET REGS:获取child进程寄存器到结构体user_regs_struct中
ptrace(PTRACE_GETREGS,child,NULL,®s);
if(!iscalling){
iscalling=1;
// 打印子进程寄存器的值
printf("0. SYS_write call with %lld, addr:%lld, len:%lld\n",regs.rdi,regs.rsi,regs.rdx);
}else{
// 打印系统调用号的值
printf("1. SYS_write call return %lld\n",regs.rax);
iscalling=0;
}
}
// PTRACE_SYSCALL:和PTRACE_CONT一样使暂停的子进程继续执行,唯一不同到是syscall会在child进程下一次执行系统掉调用到时候再次让child进程暂停(SINTRAP信号)。
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
//ptrace(PTRACE_CONT,child,NULL,NULL);
}
}
return 0;
}