ptrace是Linux内核提供的一个非常强大的系统调用。通过ptrace可以实现进程的单步调试和系统调用的收集。比如strace和gdb都是基于ptrace实现的。strace可以显示进程调用了哪些系统调用,gdb可以实现进程的调试。本文介绍了这些工具的底层ptrace是如何实现的。这里选择1.2.13早期版本。原理类似。新版内核的代码太多了,不必赘述太多。1、进程调试ptrace系统调用的实现包含了很多函数。我们先来看一下单步调试的实现。通过ptrace实现单步调试有两种方式。1、父进程执行fork创建子进程,通过ptrace给子进程设置PF_PTRACED标志,然后执行execve加载被调试程序。2、通过ptraceattach到指定pid完成进程的调试(控制)。先看第一个的实现。1.1模式1pid_tpid=fork();//subprocessif(pid==0){ptrace(PTRACE_TRACEME,0,NULL,NULL);//加载调试程序execve(argv[1],NULL,NULL);执行fork创建子进程后,ptrace的PTRACE_TRACEME指示操作系统将子进程设置为被调试(设置PF_PTRACED标志)。让我们看看操作系统在这一步做了什么。asmlinkageintsys_ptrace(longrequest,longpid,longaddr,longdata){if(request==PTRACE_TRACEME){current->flags|=PF_PTRACED;return0;}}这一步很简单,接下来看看execve是如何将程序加载到内存中执行的..intdo_execve(char*filename,char**argv,char**envp,structpt_regs*regs){//loaderfor(fmt=formats;fmt;fmt=fmt->next){int(*fn)(structlinux_binprm*,structpt_regs*)=fmt->load_binary;retval=fn(&bprm,regs);}}do_execve逻辑很复杂,但我们只需要关注需要的部分即可。do_execve通过钩子函数加载程序,我们来看看都有哪些格式。structlinux_binfmt{structlinux_binfmt*next;int*use_count;int(*load_binary)(structlinux_binprm*,structpt_regs*regs);int(*load_shlib)(intfd);int(*core_dump)(longsignr,structpt_regs*regs);};staticstructlinux_binfmt*formats=&aout_format;intregister_binfmt(structlinux_binfmt*fmt){structlinux_binfmt**tmp=&formats;if(!fmt)return-EINVAL;if(fmt->next)return-EBUSY;while(*tmp){if(fmt==*tmp)return-EBUSY;tmp=&(*tmp)->next;}*tmp=fmt;return0;}可以看到formats是一个链表。可以通过register_binfmt函数注册节点。那么这个函数是谁调用的呢?structlinux_binfmtelf_format={NULL,NULL,load_elf_binary,load_elf_library,NULL};intinit_module(void){register_binfmt(&elf_format);return0;}最后调用load_elf_binary函数加载器。同样,我们只关注相关逻辑。if(current->flags&PF_PTRACED)send_sig(SIGTRAP,current,0);load_elf_binary会判断进程是否设置了PF_PTRACED标志,然后会向当前进程发送一个SIGTRAP信号。然后再看信号处理函数的相关逻辑。if((current->flags&PF_PTRACED)&&signr!=SIGKILL){current->exit_code=signr;//修改当前进程(被调试进程)为挂起状态current->state=TASK_STOPPED;//通知父进程notify_parent(current);//调度其他进程执行schedule();}所以程序加载到内存后,没有机会执行,直接修改为挂起状态。接下来我们看看notify_parent通知父进程的内容。voidnotify_parent(structtask_struct*tsk){//发送SIGCHLD信号给父进程if(tsk->p_pptr==task[1])tsk->exit_signal=SIGCHLD;send_sig(tsk->exit_signal,tsk->p_pptr,1);wake_up_interruptible(&tsk->p_pptr->wait_chldexit);}父进程收到信号后,可以通过sys_ptrace控制子进程。sys_ptrace还提供了很多功能,比如读取子进程的数据。//pid为子进程idnum=ptrace(PTRACE_PEEKUSER,pid,ORIG_RAX*8,NULL);这个就不展开了,主要是内存校验和数据读取。这里说一下PTRACE_SINGLESTEP命令,它控制子进程的单步执行。casePTRACE_SINGLESTEP:{/*setthetrapflag.*/longtmp;child->flags&=~PF_TRACESYS;//设置eflags单步调试flaggtmp=get_stack_long(child,sizeof(long)*EFL-MAGICNUMBER)|TRAP_FLAG;put_stack_long(child,sizeof(long)*EFL-MAGICNUMBER,tmp);//修改子进程状态为可执行child->state=TASK_RUNNING;child->exit_code=data;return0;}PTRACE_SINGLESTEP让子进程重新进入运行状态,但是有一个很关键的地方就是设置了单步调试标志。让我们看看陷阱标志是什么。Atrap标志允许处理器以单步模式运行。如果这样的标志可用,调试器可以使用它逐步执行计算机程序。也就是说,子进程执行完一条指令后,会被中断,然后系统会向被调试进程发送一个SIGTRAP信号。同样,被调试进程在信号处理函数中通知父进程,使控制权交还给父进程,等等。1.2方式二除了一开始设置通过ptrace调试进程,还可以动态设置通过ptrace调试进程的能力,具体通过PTRACE_ATTACH命令。if(request==PTRACE_ATTACH){//设置调试标志child->flags|=PF_PTRACED;//设置与父进程的关系if(child->p_pptr!=current){REMOVE_LINKS(child);child->p_pptr=current;SET_LINKS(child);}//向被调试进程发送SIGSTOP信号send_sig(SIGSTOP,child,1);return0;}前面已经分析过,信号处理函数会将进程置为挂起状态,然后通知主进程,主进程可以控制子进程,这和前面的进程是一样的。2跟踪系统调用ptrace除了处理跟踪进程的执行过程外,还可以实现跟踪系统调用。具体来说,通过PTRACE_SYSCALL命令。casePTRACE_SYSCALL:casePTRACE_CONT:{longtmp;//设置PF_TRACESYS标志if(request==PTRACE_SYSCALL)child->flags|=PF_TRACESYS;child->exit_code=data;child->state=TASK_RUNNING;//清除trapflag标志tmp=get_stack_long(child,sizeof(long)*EFL-MAGICNUMBER)&~TRAP_FLAG;put_stack_long(child,sizeof(long)*EFL-MAGICNUMBER,tmp);return0;}看起来很简单,就是设置一个新的flagPF_TRACESYS。看看这个标签的作用。//调用syscall_trace函数1:call_syscall_tracemovlmovlORIG_EAX(%esp),%eax//调用系统调用call_sys_call_table(,%eax,4)movl%eax,EAX(%esp)#savethereturnvaluemovl_current,%eaxmovlerrno(%eax),%edxnegl%edxje1fmovl%edx,EAX(%esp)orl$(CF_MASK),EFLAGS(%esp)#setcarrytoindicateerror//调用syscall_trace函数1:call_syscall_trace可以看到系统调用前后都有syscall_trace逻辑,所以之前系统调用在那之后,我们都可以做一些事情。让我们看看这个函数做了什么。asmlinkagevoidsyscall_trace(void){//挂起子进程,通知父进程,调度其他进程执行current->exit_code=SIGTRAP;current->state=TASK_STOPPED;notify_parent(current);schedule();}这里的逻辑就是把逻辑Switch放到主进程,主进程就可以通过命令获取被调试进程的系统调用信息。下面是一个跟踪一个进程的所有系统调用的例子。/*通过特定进程使用eptracetofindallsystemcallthatcall*/#include
