引子: 
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. [4.14校内训练赛by hzwer]

    来自FallDream的博客,未经允许,请勿转载,谢谢. hzwer又出丧题虐人 4道noi....        很奇怪 每次黄学长出题总有一题我做过了. 嗯题目你们自己看看呗 好难解释 ----- ...

  2. CTSC2017 铁牌记

    Day 0: 先到了丽都酒店. 看见北京八十中学生在发胸牌手册和T恤,领完之后开始分房间. 我和ryc一屋,lyd和ysq一屋,yzy和gzz分到了珀利酒店,老师尝试了半天把我们分到一个酒店,失败了. ...

  3. Linux查看日志方法总结(1)

    注:日志文件为:test.log 1.tail -f test.log 查看当前打印的日志(平时就知道这方法!打印出的长度有限制.) 以下为网上搜集的: 2.先必须了解两个最基本的命令: tail  ...

  4. 原生js之canvas时钟组件

    canvas一直是前端开发中不可或缺的一种用来绘制图形的标签元素,比如压缩上传的图片.比如刮刮卡.比如制作海报.图表插件等,很多人在面试的过程中也会被问到有没有接触过canvas图形绘制. 定义 ca ...

  5. 关于 printf scanf getchar

    float默认小数6位 右对齐.-m 左对齐 在调用printf函数输出数据时,当数据的实际位宽大于printf函数中的指定位宽时,将按照数据的实际位宽输出数据. .n表精度 输出%符号 注意点 #i ...

  6. 反射实现java深度克隆

    一.克隆 有时想得到对象的一个复制品,该复制品的实体是原对象实体的克隆.复制品实体的变化不会引起原对象实体发生变化,这样的复制品称为原对象实体的克隆对象或简称克隆. 1.浅复制(浅克隆) 概念:被复制 ...

  7. Async分析

     1:android在新版本中不允许UI线程访问网络,但是如果需要访问网络又改怎么办呐?这里有很多解决方案,比如新开一个线程,在新线程中进行访问,然后访问数据,返回后可能会更新界面也可能不更新界面,这 ...

  8. C语言关闭日志文件时忘了将日志文件全局变量指针置为NULL

    C语言写了一个write_log函数以写日志,写了一个close_log_file函数以关闭日志,声明了一个日志文件全局变量文件指针plogFile. write_log中首先判断plogFile是否 ...

  9. Excel init

    Sub Test() Dim r As Range Dim a As Integer a = For Each r In Range("b1:b6") If r.Font.Bold ...

  10. Scheme call/cc 研究

    目前尚不清楚实质,但已经能够从形式上理解它的某些好处,有个很简单的连乘函数可以说明: 为了展示究竟发生了什么,我包装了下乘法函数,将其变为mul. 我们将比较product和xproduct的区别. ...