从gdb原理学习ptrace调用

Linux的ptrace系统调用,是Android二进制hook框架adbi的核心。因此学习adbi之前,先学习一下ptrace()函数。

ptrace介绍

ptrace可以拆开来,看作Process Trace,也就是进程跟踪,它提供了父进程观察并修改子进程的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)等。

它的基本原理是:调用ptrace后,所有发给子进程的信号(SIGKILL除外),都会先发送到父进程,子进程被阻塞,此时子进程处于TASK_TRACED状态。父进程接收信号后,即可对子进程进程观察或修改后运行之。

ptrace函数原型为:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
1). enum __ptrace_request request:ptrace要执行的命令。
2). pid_t pid: ptrace要跟踪的进程id。
3). void *addr: 要监控的内存地址。
4). void *data: 存放读取出的或者要写入的数据。

gdb原理

ptrace调用的功能强大,包括gdb等工具都依靠它来实现,下面通过gdb原理介绍。

gdb原理是利用ptrace系统调用,使被调试程序成为gdb子进程,发送给调试程序的信号(SIGKILL除外)被gdb先截获。gdb根据截获的信号,查看/修改程序相应内存、寄存器内容,并控制被调试程序继续执行。调试程序的核心是断点和单步,下面分开讲如何实现这两个机制。

gdb建立调试关系

使用gdb调试程序,可以直接gdb ./test,也可以gdb <pid>的方式。这对应ptrace建立跟踪关系的两种方法。

1)通过fork+execve执行被调试程序。子进程在执行execve之前调用ptrace(PTRACE_TRACEME)。

2)通过attach的方式。gdb通过调用ptrace(PTRACE_ATTACH,pid,…),建立也被调试进程间的父子关系,使自己成为被调试程序的父进程,通过attach建立的调试关系可以通过ptrace (PTRACE_DETACH,pid,…)解除。

断点原理

程序调试时,常用的一个断点功能是“break <行号>”,当执行到断点行时被调试程序会停止,等待gdb操作。

断点的实现原理,就是在指定位置插入断点指令,当执行到断点时cpu产生SIGTRAP信号。该信号被gdb捕获并进行断点命中判断,当判断此信号是由断点产生时等待用户输入,并执行下一步操作。否则继续。

在程序中设置断点,就是先将原来的指令保存,然后向该位置写入int 3中断指令。断点执行完后,恢复int 3处指令并将cpu的IP寄存器指向前移。这在前面写IA-32处理器的调试支持时有讲过。

单步执行原理

单步执行指在运行调试程序时,让程序运行一条指令后停下。gdb中常用的命令有next/step(语句单步)和nexti/stepi(指令单步)。在Linux上,指令单步是通过ptrace来实现。调用ptrace(PTRACE_SINGLESTEP,pid,…)可以使被调试的进程在每执行完一条指令后,产生一个SIGTRAP信号,gdb捕获后运行。下面看一个例子:

child = fork(); 
if(child == 0) { 
    execl("./HelloWorld", "HelloWorld", NULL); 
} else { 
    ptrace(PTRACE_ATTACH,child,NULL,NULL); 
    while(1) { 
        wait(&amp;val); 
        if(WIFEXITED(val)) 
            break; 
        count++; 
        ptrace(PTRACE_SINGLESTEP,child,NULL,NULL); 
    } 
    printf("Total Instruction number= %d\n",count); 
}

这个程序中,子进程调用execl执行HelloWorld程序,父进程通过ptrace(PTRACE_ATTACH,child,NULL,NULL);使子进程一步一停,以统计子进程一共执行了多少条指令。当然这时你也可以查看EIP寄存器存放的指令,或是某个变量的值。当然前提是要知识这个变量在子进程内存映像中的地址。

指令单步可以通过硬件实现,如x86架构处理器通过设置EFLAGS寄存器的TF标志位实现,每执行一条指令,产生一个异常。也可以通过软件实现,即在每条指令后面加入一条断点指令。这与上面讲的断点原理类似。而语句单步的实现基于指令单步,在《软件调试》里面有相当精彩的表述。

当然gdb的实现远比上面讲的要复杂。它能使我们方便的观察、修改被调试进程,比如通过行号、函数名、变量名等。而要实现这些,一是要在编译时提供足够信息,如使用gcc的-g调试选项,gcc会将一些程序信息放到ELF文件中,包括函数符号表、行号、变量信息、宏定义等,以给gdb提供调试资料。二是要对ELF文件格式,进程的内存映像布局以及程序指令码十分熟悉。这样才能保证在正确的时机,找到正确的地址并链接回正确的代码片段。这些可以通过阅读gdb源码分析了解。

小结

ptrace可以实时观察与修改另一个进程的运行。掌握了它的使用,就能开发出很多用户态下不可能实现的应用,当然这可能需要我们掌握编译、文件格式、程序内存布局等相当多的底层知识。

回顾一下ptrace的使用:

  • 用PTRACE_ATTACH或者PTRACE_TRACEME 建立进程间的跟踪关系。

  • PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSR等读取子进程内存/寄存器中保留的值。

  • PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSR等把值写入到被跟踪进程的内存/寄存器中。

  • 用PTRACE_CONT,PTRACE_SYSCALL, PTRACE_SINGLESTEP控制被跟踪进程以何种方式继续运行。

  • PTRACE_DETACH, PTRACE_KILL 脱离进程间的跟踪关系。

提示

  • 进程状态TASK_TRACED用以表示当前进程因为被父进程跟踪而被系统停止。

  • 如在子进程结束前,父进程结束,则trace关系解除。

  • 利用attach建立起来的跟踪关系,虽然ps看到双方为父子关系,但在”子进程”中调用getppid()仍会返回原来的父进程id。

  • 已经被trace的进程,不能再次被attach。

  • 即使是用PTRACE_TRACEME建立起来的跟踪关系,也可以用DETACH的方式予以解除。

  • 因为进入、退出系统调用都会触发一次SIGTRAP,所以通常的做法是在进入的时候读取系统调用的参数,在退出的时候读取系统调用的返回值。


你可能感兴趣的:(从gdb原理学习ptrace调用)