引子: 
1.在Linux系统中,进程状态除了我们所熟知的TASK_RUNNING,TASK_INTERRUPTIBLE,TASK_STOPPED等,还有一个TASK_TRACED。这表明这个进程处于什么状态? 
2.strace可以方便的帮助我们记录进程所执行的系统调用,它是如何跟踪到进程执行的? 
3.gdb是我们调试程序的利器,可以设置断点,单步跟踪程序。它的实现原理又是什么? 

所有这一切的背后都隐藏着Linux所提供的一个强大的系统调用ptrace(). 

1.ptrace系统调用 
ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包 括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被 系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。 

其原型为: 

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); 

ptrace有四个参数: 
1). enum __ptrace_request request:指示了ptrace要执行的命令。 
2). pid_t pid: 指示ptrace要跟踪的进程。 
3). void *addr: 指示要监控的内存地址。 
4). void *data: 存放读取出的或者要写入的数据。 
ptrace是如此的强大,以至于有很多大家所常用的工具都基于ptrace来实现,如strace和gdb。接下来,我们借由对strace和gdb的实现,来看看ptrace是如何使用的。 

2. strace的实现 
strace常常被用来拦截和记录进程所执行的系统调用,以及进程所收到的信号。如有这么一段程序: 
HelloWorld.c: 

#include <stdio.h>
int main(){
    printf("Hello World!\n");
    return 0;
} 

编译后,用strace跟踪: strace ./HelloWorld 
可以看到形如: 
execve("./HelloWorld", ["./HelloWorld"], [/* 67 vars */]) = 0 
brk(0)                                  = 0x804a000 
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f18000 
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory) 
open("/home/supperman/WorkSpace/lib/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) 
... 
的一段输出,这就是在执行HelloWorld中,系统所执行的系统调用,以及他们的返回值。 

下面我们用ptrace来研究一下它是怎么实现的。 
... 

   switch(pid = fork())
    {
    case -1:
        return -1;
    case 0: //子进程
        ptrace(PTRACE_TRACEME,0,NULL,NULL);
        execl("./HelloWorld", "HelloWorld", NULL);
    default: //父进程
        wait(&val); //等待并记录execve
        if(WIFEXITED(val))
            return 0;
        syscallID=ptrace(PTRACE_PEEKUSER, pid, ORIG_EAX*4, NULL);
        printf("Process executed system call ID = %ld\n",syscallID);
        ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
        while(1)
        {
            wait(&val); //等待信号
            if(WIFEXITED(val)) //判断子进程是否退出
                return 0;
            if(flag==0) //第一次(进入系统调用),获取系统调用的参数
            {
                syscallID=ptrace(PTRACE_PEEKUSER, pid, ORIG_EAX*4, NULL);
                printf("Process executed system call ID = %ld ",syscallID);
                flag=1;
            }
            else //第二次(退出系统调用),获取系统调用的返回值
            {
                returnValue=ptrace(PTRACE_PEEKUSER, pid, EAX*4, NULL);
                printf("with return value= %ld\n", returnValue);
                flag=0;
            }
            ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
        }
    }
... 

在上面的程序中,fork出的子进程先调用了ptrace(PTRACE_TRACEME)表示子进程让父进程跟踪自己。然后子进程调用 execl加载执行了HelloWorld。而在父进程中则使用wait系统调用等待子进程的状态改变。子进程因为设置了PTRACE_TRACEME而 在执行系统调用被系统停止(设置为TASK_TRACED),这时父进程被唤醒,使用ptrace(PTRACE_PEEKUSER,pid,...)分 别去读取子进程执行的系统调用ID(放在ORIG_EAX中)以及系统调用返回时的值(放在EAX中)。然后使用ptrace (PTRACE_SYSCALL,pid,...)指示子进程运行到下一次执行系统调用的时候(进入或者退出),直到子进程退出为止。 

程序的执行结果如下: 
Process executed system call ID = 11 
Process executed system call ID = 45 with return value= 134520832 
Process executed system call ID = 192 with return value= -1208934400 
Process executed system call ID = 33 with return value= -2 
Process executed system call ID = 5 with return value= -2 
... 
其中,11号系统调用就是execve,45号是brk,192是mmap2,33是access,5是open...经过比对可以发现,和 strace的输出结果一样。当然strace进行了更详尽和完善的处理,我们这里只是揭示其原理,感兴趣的同学可以去研究一下strace的实现。 

PS: 
1). 在系统调用执行的时候,会执行pushl %eax # 保存系统调用号ORIG_EAX在程序用户栈中。 
2). 在系统调用返回的时候,会执行movl %eax,EAX(%esp)将系统调用的返回值放入寄存器%eax中。 
3). WIFEXITED()宏用来判断子进程是否为正常退出的,如果是,它会返回一个非零值。 
4). 被跟踪的程序在进入或者退出某次系统调用的时候都会触发一个SIGTRAP信号,而被父进程捕获。 
5). execve()系统调用执行成功的时候并没有返回值,因为它开始执行一段新的程序,并没有"返回"的概念。失败的时候会返回-1。 
6). 在父进程进行进行操作的时候,用ps查看,可以看到子进程的状态为T,表示子进程处于TASK_TRACED状态。当然为了更具操作性,你可以在父进程中加入sleep()。

Linux上程序调试的基石(1)--ptrace的更多相关文章

  1. Linux上程序调试的基石(2)--GDB

    3. GDB的实现 GDB是GNU发布的一个强大的程序调试工具,用以调试C/C++程序.可以使程序员在程序运行的时候观察程序在内存/寄存器中的使用情况.它的实现也是基于ptrace系统调用来完成的.  ...

  2. 嵌入式linux应用程序调试方法

    嵌入式linux应用程序调试方法 四 内存工具 五 C/C++代码覆盖.性能profiling工具 四 内存工具 您肯定不想陷入类似在几千次调用之后发生分配溢出这样的情形. 许多小组花了许许多多时间来 ...

  3. Linux Shell 程序调试

    Linux Shell 程序调试 Shell程序的调试是通过运行程序时加入相关调试选项或在脚本程序中加入相关语句,让shell程序在执行过程中显示出一些可供参考的“调试信息”.当然,用户也可以在she ...

  4. Linux上程序执行的入口--Main

    main()函数,想必大家都不陌生了,从刚开始写程序的时候,大家便开始写main(),我们都知道main是程序的入口.那main作为一个函数,又是谁调用的它,它是怎么被调用的,返回给谁,返回的又是什么 ...

  5. [转]Linux上程序执行的入口--Main

    main()函数,想必大家都不陌生了,从刚开始写程序的时候,大家便开始写main(),我们都知道main是程序的入口.那main作为一个函数,又是谁调用的它,它是怎么被调用的,返回给谁,返回的又是什么 ...

  6. linux文件或文件夹常见操作,排查部署在linux上程序问题常用操作

    创建文件夹 mkdir [-p] DirName [ 在工作目录下,建立一个名为 A 新的子目录 : mkdir A 在工作目录下的 B目录中,建立一个名为 T 的子目录:   若 B 目录不存在,则 ...

  7. Linux Bash命令关于程序调试详解

    转载:http://os.51cto.com/art/201006/207230.htm 参考:<Linux shell 脚本攻略>Page22-23 Linux bash程序在程序员的使 ...

  8. C#在Linux上的开发指南

    本人才疏学浅,在此记录自己用C#在Linux上开发的一点经验,写下这篇指南.(给想要在Linux上开发C#程序的朋友提供建议) 目前在Linux上跑的网站:http://douxiubar.com | ...

  9. pycharm远程linux开发和调试代码

    pycharm是一个非常强大的python开发工具,现在很多代码最终在线上跑的环境都是linux,而开发环境可能还是windows下开发,这就需要经常在linux上进行调试,或者在linux对代码进行 ...

随机推荐

  1. WebStorm配置node.js调试

    最近因为工作关系,一直在做node.js的开发,学习了koa框架,orm框架sequelize,以及swagger文档的配置.但是,最近因为swagger文档使用了es6的修饰器那么个东西(在java ...

  2. WiFi安全网桥探讨

    1 WiFi网桥现状 近年来,随着视频监控产品不断普及,无线网桥,特别是WiFi网桥,也越来越受到市场青睐.主要原因大概归属如下:1)同有线视频传输相比,无线视频传输无需布线,故安装及其方便,施工周期 ...

  3. CentOS7 下安装 Java 8 [wget]

    1. 创建一个文件夹 sudo mkdir /usr/local/services/java8 2. 使用 wget 来下载 wget --no-cookies --no-check-certific ...

  4. PyChram简单使用教程

    一.PyChram下载官网:http://www.jetbrains.com/pycharm Windows:http://www.jetbrains.com/pycharm/download/#se ...

  5. 剑指架构师系列-持续集成之Maven+Nexus+Jenkins+git+Spring boot

    1.Nexus与Maven 先说一下这个Maven是什么呢?大家都知道,Java社区发展的非常强大,封装各种功能的Jar包满天飞,那么如何才能方便的引入我们项目,为我所用呢?答案就是Maven,只需要 ...

  6. 转:禁止360随身wifi驱动自动安装

    from:http://bbs.360safe.com/thread-2643500-1-1.html 有曾经数次卸载随身wifi后,插上又会自动安装随身wifi驱动的,也可以看看下面的解决办法. 卸 ...

  7. DOS/Windows 文本格式与 Unix 文本各式转换

    命令简介: dos2unix是将Windows格式文件转换为Unix.Linux格式的实用命令.Windows格式文件的换行符为\r\n ,而Unix&Linux文件的换行符为\n. dos2 ...

  8. ACM pie

    我的生日快到了,传统上我正在做馅饼.不只是一个馅饼,不,我有N个,各种口味和各种尺寸. 数量为F我的朋友会来到我的聚会,每个人都得到一个馅饼. 这应该是一块馅饼,而不是几个小块,因为看起来很乱.这一块 ...

  9. JavaScript 注释

    JavaScript 注释可用于提高代码的可读性. JavaScript 注释 JavaScript 不会执行注释. 我们可以添加注释来对 JavaScript 进行解释,或者提高代码的可读性. 单行 ...

  10. OpenResty和Resis一些基本的性能配置

    Basics: 1. Ensure that you have not disabled Lua code cache: https://github.com/openresty/lua-nginx- ...