strace工具的实现原理

2019-07-13 09:27发布

strace是Linux系统下的一个用来跟踪系统调用的工具,它的实现基础是ptrace系统调用。使用strace工具可以跟踪一个程序执行过程中发生的系统调用。 我这里讲到的内容有一点点和mips体系相关,不过不熟悉mips也不影响阅读。

ptrace系统调用

ptrace系统调用提供了一种方法来跟踪和控制进程的执行,它可以读取和修改进程地址空间中的内容,包括寄存器的值。ptrace主要用于实现断点调试和跟踪系统调用。该系统调用的原型如下: long ptrace(enum __ptrace_request request, pid_t pid, void *addr,void *data); ptrace的四个参数的含义为: 1.   request:用于选择一个操作,见下文。 2.   pid:目标进程即被跟踪进程的pid。 3.   addr和data用于修改和拷贝被跟踪进程的进程地址空间的数据。 下面的内容中将用父进程指代跟踪者,用子进程指代被跟踪者。实际上,在一个进程被跟踪之后,跟踪者进程会在某种意义上充当被跟踪进程的父进程(如使用ps命令就可以看到他们的父子关系),而子进程真正的父进程被保存在其task_struct结构的real_parent成员中。

使用ptrace跟踪进程

父进程跟踪一个进程的方式有两种:1.调用fork(),然后子进程打上PTRACE_TRACEME标记,并执行exec。2.父进程可以给自己打上PTRACE_ATTACH标记来跟踪一个已有进程。 一个进程被跟踪后,他只要接收到一个信号(即使这个信号被设置为忽略)就会停止运行(SIGKILL除外),然后父进程会在每次调用wait()时得到子进程停止运行的通知,这时父进程就可以检测和修改子进程了,随后父进程可以让子进程继续运行。 当父进程不想跟踪了,可以通过设置PTRACE_KILL标记来终止子进程的运行。也可以通过设置PTRACE_DETACH标记让子进程解除被跟踪,继续正常运行。

常用的request

PTRACE_TRACEME 进程设置这个request目的是让自己被父进程跟踪。任何发送到该子进程的信号(除了SIGKILL)都会导致他停下来,并在父进程wait()的时候通知到父进程。另外,子进程后续调用exec会导致子进程自己收到一个SIGTRAP信号,这是为了让父进程有机会在exec的新程序开始执行前获得控制权。 除非一个进程知道父进程要跟踪他,一般不会去设置这个request。设置这个请求时,pid,addr和data三个参数都会被忽略。 这个request只供子进程设置,其他的request都是只供父进程使用的。相应的,下面的request中,参数pid为被跟踪的子进程的pid。 PTRACE_ATTACH 将pid指定的进程作为自己要跟踪的进程,并开始跟踪。这和子进程自己调用PTRACE_TRACEME的效果相同。 设置这个request时,子进程会首先收到一个SIGSTOP信号,但并不会停止,只是导致跟踪者进程第一次被中断,从而开始跟踪,否则只能等到子进程接收到第一个信号时才开始跟踪。之后当子进程有待决信号时,进程总是会暂停,这时父进程通过SIGCHLD信号得到通知。在子进程暂停前,父进程可使用wait函数等待。 参数addr和data会被忽略。 PTRACE_CONT 让被停掉的子进程继续运行,而当子进程再次接收到信号时会暂停。 参数data如果被设置为一个非零值并且不是SIGSTOP,那data就是父进程发送给子进程的信号,否则,不给子进程发送信号。这样一来,父进程可以控制是否向子进程发送一个信号。 参数addr会被忽略。 PTRACE_SYSCALL 让停止的子进程继续运行(同PTRACE_CONT),而当子进程再次接收到信号时会暂停,另外,在子进程中发生系统调用时,在系统调用的入口和结束时子进程也会停止,这时父进程认为子进程是因为收到SIGTRAP信号而停止的。 由于子进程在系统调用的入口和结束时都会停止,父进程就可以在系统调用入口处停止后,获得系统调用的参数信息,而在系统调用结束时停止后,获得系统调用的返回值。 参数addr会被忽略。 PTRACE_DETACH 让停止的子进程继续运行(同PTRACE_CONT),但是在这之前会先与跟踪它的父进程解除PTRACE_ATTACH或PTRACE_TRACEME时的关系。 参数addr会被忽略。 PTRACE_KILL 给子进程发送一个SIGKILL信号来终止子进程。addr和data参数会被忽略。 PTRACE_PEEKTEXT,PTRACE_PEEKDATA 读取进程地址空间中addr地址处的内容,读出的长度为一个word(4字节),作为ptrace()的返回值(long型)返回。Linux中的代码和数据的地址空间并不是分离的,所以这两个request实际上意义相同。 参数data会被忽略。 PTRACE_PEEKUSR 在进程的USER区域读取一个word的长度。参数addr是指相对USER开头的offset,结果作为返回值。参数data会被忽略。 在mips中的进程自身信息和进程地址空间中并没有所谓的USER区域,在内核中通过参数addr的值,判断应该返回什么结果,如下: /* Read the word at location addr in theUSER area. */ case PTRACE_PEEKUSR: { struct pt_regs *regs; unsigned long tmp = 0; /* 获得进程地址空间的pt_regs区域的内容。 */ regs =task_pt_regs(child); ret = 0; /* Default return value. */ switch (addr) { case 0 ... 31: /* 通用寄存器 */ tmp =regs->regs[addr]; break; case FPR_BASE ...FPR_BASE + 31: ...... break; case PC: tmp =regs->cp0_epc; break; case CAUSE: tmp =regs->cp0_cause; break; case BADVADDR: tmp =regs->cp0_badvaddr; break; case MMHI: tmp = regs->hi; break; case MMLO: tmp = regs->lo; break; case FPC_CSR: tmp =child->thread.fpu.fcr31; break; case FPC_EIR: { /* implementation / version register */ ...... break; } case DSP_BASE ...DSP_BASE + 5: { ...... break; } case DSP_CONTROL: ...... break; default: tmp = 0; ret = -EIO; goto out; } ret = put_user(tmp,(unsigned long __user *) data); break; } PTRACE_POKETEXT,PTRACE_POKEDATA 将data的内容拷贝到进程地址空间中addr指向的地址。 PTRACE_POKEUSR 将data的内容拷贝到进程USER区域中偏移为addr的地方,一般addr要求是word-aligned的。由于要修改USER区域,内核为保证完整健全,会禁止某些域被修改。

strace工具的实现原理

strace工具是一个用户态的应用程序,用来追踪进程的系统调用。它的基础就是ptrace系统调用。安装strace之后,就可以使用strace命令了。 最简单的strace命令的用法就是:strace PROG,PROG是要执行的程序。strace命令执行的结果就是按照调用顺序打印出所有的系统调用,包括函数名、参数列表以及返回值。 使用strace跟踪一个进程的系统调用的基本流程如图1所示。 图1 strace实现流程 从图中可以看出strace做了以下几件事情: 1.   设置SIGCHLD 信号的处理函数,这个处理函数只要不是SIG_IGN即可。由于子进程停止后是通过SIGCHLD信号通知父进程的,所以这里要防止SIGCHLD信号被忽略。 2.   创建子进程,在子进程中调用ptrace(PTRACE_TRACEME,0L, 0L, 0L)使其被父进程跟踪,并通过execv函数执行被跟踪的程序。 3.   通过wait()等待子进程停止,并获得子进程停止时的状态status。 4.   通过子进程的状态查看子进程是否已正常退出,如果是,则不再跟踪,随后调用ptrace发送PTRACE_DETACH请求解除跟踪关系。 5.   子进程停止后,打印系统调用的函数名、参数和返回值。具体流程见图2。 6.   通过PTRACE_SYSCALL让子进程继续运行,由于这个请求会让子进程在系统调用的入口处和系统调用完成时都会停止并通知父进程,这样,父进程就可以在系统调用开始之前获得参数,结束之后获得返回值。 在系统调用的入口和结束时子进程停止运行时,这时父进程认为子进程是因为收到SIGTRAP信号而停止的。所以父进程在wait()后可以通过SIGTRAP来与其他信号区分开。 Strace中为每个要跟踪的进程维护了一个TCB(Trace Control Block)结构,定义如下。它保存了当前发生的系统调用的信息。 /* Trace Control Block */ struct tcb { int flags; /* See below for TCB_ values */ int pid; /* Process Id of this entry */ int qual_flg; /* qual_flags[scno] or DEFAULT_QUAL_FLAGS + RAW*/ int u_error; /* Error code */ long scno; /* System call number */ long u_arg[MAX_ARGS]; /* System call arguments */ long u_rval; /* Return value */ int curcol; /* Output column for this process */ FILE *outf; /* Output file for this process */ const char *auxstr;/*Auxiliary info from syscall (see RVAL_STR) */ const struct_sysent *s_ent;/* sysent[scno] or dummy struct for bad scno */ struct timeval stime;/*System time usage as of last process wait */ struct timeval dtime; /* Delta for system time usage */ struct timeval etime; /* Syscall entry time */ /* Support fortracing forked processes: */ long inst[2]; /* Saved clone args (badly named) */ }; 上面已经提到,子进程会在系统调用前后各停止一次,所以打印系统调用信息时分为两个阶段:在系统调用开始时可以获取系统调用号和参数,在系统调用结束时可以获取系统调用的返回结果。通过给tcb结构的flags字段清除和添加TCB_INSYSCALL标志位来区分系统调用的开始和结束。 图2 strace中打印系统调用的实现流程 例如编写一个使用printf打印“Hello world”的程序hello.c,使用strace跟踪该程序的系统调用可以看到如下结果: # ./strace ./hello execve("./hello ", ["./hello "], [/* 7 vars */])= 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1, 0) = 0x2aaad000 stat("/etc/ld.so.cache", 0x7faf4ca8) = -1 ENOENT (No such file or directory) open("/tmp/libgcc_s.so.1", O_RDONLY) = -1 ENOENT (No such file or directory) open("/lib/libgcc_s.so.1", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=1565445, ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1, 0) = 0x2aaae000 read(3,"177ELF121