ptrace

本文深入介绍了Linux中的ptrace系统调用,它允许父进程监视和控制子进程,包括在用户层拦截和修改系统调用参数。文章通过实例展示了如何读取系统调用参数、改变进程中的数据、单步调试以及设置断点。通过ptrace,开发者可以在不修改源代码的情况下,实现对程序的高级调试和分析。
摘要由CSDN通过智能技术生成

下面是转帖的内容,写的很详细。但是不同的linux发行版中头文件的路径和名称并不相同。如在某些发行版中<linux/user.h>就不存在,其中定义的变量出现在<asm/ptrace-abi.h>和<sys/user.h>中。

==================================================================================================

by Pradeep Padala

Created 2002-11-01 02:00

翻译: Magic.D E-mail: adamgic@163.com

译者序:在开发Hust Online Judge的过程中,查阅了不少资料,关于调试器技术的资料在网上是很少,即便是UNIX编程巨著《UNIX环境高级编程》中,相关内容也不多,直到我在http://www.linuxjournal.com上找到这篇文章,如获至宝,特翻译之,作为鄙人翻译技术文档的第一次尝试,必定会有不少蹩脚之处,各位就将就一下吧,欢迎大力拍砖。

你想过怎么实现对系统调用的拦截吗?你尝试过通过改变系统调用的参数来愚弄你的系统kernel吗?你想过调试器是如何使运行中的进程暂停并且控制它吗?

你可能会开始考虑怎么使用复杂的kernel编程来达到目的,那么,你错了。实际上Linux提供了一种优雅的机制来完成这些:ptrace系统函数。 ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。

使用ptrace,你可以在用户层拦截和修改系统调用(sys call)

在这篇文章中,我们将学习如何拦截一个系统调用,然后修改它的参数。在本文的第二部分我们将学习更先进的技术:设置断点,插入代码到一个正在运行的程序中;我们将潜入到机器内部,偷窥和纂改进程的寄存器和数据段。

 

基本知识

操作系统提供了一种标准的服务来让程序员实现对底层硬件和服务的控制(比如文件系统),叫做系统调用(system calls)。当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后调用软中断0x80,这个中断就像一个让程序得以接触到内核模式的窗口,程序将参数和系统调用号交给内核,内核来完成系统调用的执行。

 在i386体系中(本文中所有的代码都是面向i386体系),系统调用号将放入%eax,它的参数则依次放入%ebx, %ecx, %edx, %esi 和 %edi。 比如,在以下的调用

 Write(2, “Hello”, 5)的汇编形式大概是这样的

[cpp]  view plain copy
  1. movl   $4, %eax  
  2. movl   $2, %ebx  
  3. movl   $hello, %ecx  
  4. movl   $5, %edx  
  5. int    $0x80  

这里的$hello指向的是标准字符串”Hello”。

 那么,ptrace会在什么时候出现呢?在执行系统调用之前,内核会先检查当前进程是否处于被“跟踪”(traced)的状态。如果是的话,内核暂停当前进程并将控制权交给跟踪进程,使跟踪进程得以察看或者修改被跟踪进程的寄存器。

 

让我们来看一个例子,演示这个跟踪程序的过程

[cpp]  view plain copy
  1.    
  2. #include <sys/ptrace.h>  
  3. #include <sys/types.h>  
  4. #include <sys/wait.h>  
  5. #include <unistd.h>  
  6. #include <linux/user.h>   /* For constants   
  7.                                    ORIG_EAX etc */  
  8. int main()  
  9. {  
  10.     pid_t child;  
  11.     long orig_eax;  
  12.     child = fork();  
  13.     if(child == 0) {  
  14.         ptrace(PTRACE_TRACEME, 0, NULL, NULL);  
  15.         execl("/bin/ls""ls", NULL);  
  16.     }  
  17.     else {  
  18.         wait(NULL);  
  19.         orig_eax = ptrace(PTRACE_PEEKUSER,   
  20.                           child, 4 * ORIG_EAX,   
  21.                           NULL);  
  22.         printf("The child made a "  
  23.                "system call %ld ", orig_eax);  
  24.         ptrace(PTRACE_CONT, child, NULL, NULL);  
  25.     }  
  26.     return 0;  
  27. }  

运行这个程序,将会在输出ls命令的结果的同时,输出:

 The child made a system call 11

 说明:11是execve的系统调用号,这是该程序调用的第一个系统调用。想知道系统调用号的详细内容,察看 /usr/include/asm/unistd.h。

 在以上的示例中,父进程fork出了一个子进程,然后跟踪它。在调用exec函数之前,子进程用PTRACE_TRACEME作为第一个参数调用了ptrace函数,它告诉内核:让别人跟踪我吧!然后,在子进程调用了execve()之后,它将控制权交还给父进程。当时父进程正使用wait()函数来等待来自内核的通知,现在它得到了通知,于是它可以开始察看子进程都作了些什么,比如看看寄存器的值之类。

 出现系统调用之后,内核会将eax中的值(此时存的是系统调用号)保存起来,我们可以使用PTRACE_PEEKUSER作为ptrace的第一个参数来读到这个值。

我们察看完系统调用的信息后,可以使用PTRACE_CONT作为ptrace的第一个参数,调用ptrace使子进程继续系统调用的过程。

 

ptrace函数的参数

 Ptrace有四个参数

long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);

 

第一个参数决定了ptrace的行为与其它参数的使用方法,可取的值有:

PTRACE_ME

PTRACE_PEEKTEXT

PTRACE_PEEKDATA

PTRACE_PEEKUSER

PTRACE_POKETEXT

PTRACE_POKEDATA

PTRACE_POKEUSER

PTRACE_GETREGS

PTRACE_GETFPREGS,

PTRACE_SETREGS

PTRACE_SETFPREGS

PTRACE_CONT

PTRACE_SYSCALL,

PTRACE_SINGLESTEP

PTRACE_DETACH

在下文中将对这些常量的用法进行说明。

 

读取系统调用的参数

 

通过将PTRACE_PEEKUSER作为ptrace 的第一个参数进行调用,可以取得与子进程相关的寄存器值。

 

先看下面这个例子

[cpp]  view plain copy
  1. #include <sys/ptrace.h>  
  2. #include <sys/types.h>  
  3. #include <sys/wait.h>  
  4. #include <unistd.h>  
  5. #include <linux/user.h>  
  6. #include <sys/syscall.h>   /* For SYS_write etc */  
  7.   
  8. int main()  
  9. {     
  10.     pid_t child;  
  11.     long orig_eax, eax;  
  12.     long params[3];  
  13.     int status;  
  14.     int insyscall = 0;  
  15.     child = fork();  
  16.     if(child == 0) {  
  17.         ptrace(PTRACE_TRACEME, 0, NULL, NULL);  
  18.         execl("/bin/ls""ls", NULL);  
  19.     }  
  20.     else {  
  21.        while(1) {  
  22.           wait(&status);  
  23.           if(WIFEXITED(status))  
  24.               break;  
  25.           orig_eax = ptrace(PTRACE_PEEKUSER,   
  26.                      child, 4 * ORIG_EAX, NULL);  
  27.           if(orig_eax == SYS_write) {  
  28.              if(insyscall == 0) {      
  29.                 /* Syscall entry */  
  30.                 insyscall = 1;  
  31.                 params[0] = ptrace(PTRACE_PEEKUSER,  
  32.                                    child, 4 * EBX,   
  33.                                    NULL);  
  34.                 params[1] = ptrace(PTRACE_PEEKUSER,  
  35.                                    child, 4 * ECX,   
  36.                                    NULL);  
  37.                 params[2] = ptrace(PTRACE_PEEKUSER,  
  38.                                    child, 4 * EDX,   
  39.                                    NULL);  
  40.                 printf("Write called with "  
  41.                        "%ld, %ld, %ld ",  
  42.                        params[0], params[1],  
  43.                        params[2]);  
  44.                 }  
  45.           else { /* Syscall exit */  
  46.                 eax = ptrace(PTRACE_PEEKUSER,   
  47.                              child, 4 * EAX, NULL);  
  48.                     printf("Write returned "  
  49.                            "with %ld ", eax);  
  50.                     insyscall = 0;  
  51.                 }  
  52.             }  
  53.             ptrace(PTRACE_SYSCALL,   
  54.                    child, NULL, NULL);  
  55.         }  
  56.     }  
  57.     return 0;  
  58. }  

这个程序的输出是这样的

 ppadala@linux:~/ptrace > ls

a.out        dummy.s      ptrace.txt  
libgpm.html  registers.c  syscallparams.c
dummy        ptrace.html  simple.c
ppadala@linux:~/ptrace > ./a.out
Write called with 1, 1075154944, 48
a.out        dummy.s      ptrace.txt
Write returned with 48
Write called with 1, 1075154944, 59
libgpm.html  registers.c  syscallparams.c
Write returned with 59
Write called with 1, 1075154944, 30
dummy        ptrace.html  simple.c
Write returned with 30

 

以上的例子中我们跟踪了write系统调用,而ls命令的执行将产生三个write系统调用。使用PTRACE_SYSCALL作为ptrace的第一个参数,使内核在子进程做出系统调用或者准备退出的时候暂停它。这种行为与使用PTRACE_CONT,然后在下一个系统调用/进程退出时暂停它是等价的。

 在前一个例子中,我们用PTRACE_PEEKUSER来察看write系统调用的参数。系统调用的返回值会被放入%eax。

 wait函数使用status变量来检查子进程是否已退出。它是用来判断子进程是被ptrace暂停掉还是已经运行结束并退出。有一组宏可以通过status的值来判断进程的状态,比如WIFEXITED等,详情可以察看wait(2) man。

 如果你想在系统调用或者进程终止的时候读取它的寄存器,使用前面那个例子的方法是可以的,但是这是笨拙的方法。使用PRACE_GETREGS作为ptrace的第一个参数来调用,可以只需一次函数调用就取得所有的相关寄存器值。

获得寄存器值得例子如下:

[cpp]  view plain copy
  1. #include <sys/ptrace.h>  
  2. #include <sys/types.h>  
  3. #include <sys/wait.h>  
  4. #include <unistd.h>  
  5. #include <linux/user.h>  
  6. #include <sys/syscall.h>  
  7.   
  8. int main()  
  9. {     
  10.     pid_t child;  
  11.     long orig_eax, eax;  
  12.     long params[3];  
  13.     int status;  
  14.     int
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值