Linux:断点原理与实现

拓朴苔原
• 阅读 7276

前言

从事编程工作的我们,总有调试的时刻,不管是通过 IDE 调试开发中的代码,还是通过 GDB 排查正在运行的进程。

特别是经常使用 GDB 的童鞋,对它提供的强大功能更加如数家珍,其中就不乏 breakpoint(断点)

刚好最近做到 Ptrace 相关的实验,也顺便撸了这篇小文来分享下 断点 当中的道理。

简单 GDB 示范

// test.cpp

#include<iostream>
#include<unistd.h>

void test1(){
    std::cout << "test" << std::endl;
}

int main() {
    while (true) {
        std::cout << "main: " << getpid() << std::endl;
        test1();
        sleep(1);
    }
    return 0;
}

编译运行

g++ -std=c++11 test.cpp && ./a.out

// 输出
main: 22346
test
main: 22346
test
main: 22346
...

开启 GDB,并且在 test1 函数断点

sudo gdb a.out -p 22346

// 输出
... (省略打印的信息, 直接输入命令)

(gdb) break test1       // 在 test1 函数断点
Breakpoint 1 at 0x40091a

(gdb) c                 // 继续运行
Continuing.

Breakpoint 1, 0x000000000040091a in test1() ()

(gdb) i r rip           // 查看 cpu 下一条指令的内容
rip            0x40091a 0x40091a <test1()+4>

回头看 a.out 的输出,可以看到已经停在 main: 5693 不再打印了,而进程状态也变成了 T:
Linux:断点原理与实现

T 状态意味着:(TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态,接下来就可以通过 GDB 实现各种调试的操作了。

我们这次也要实现类似的效果,不过只是一个超简化版本,只考虑:在指定的位置暂停,获得进程的控制权

前置知识准备

在实现之前,我们需要了解下必要的知识:

寄存器:RIP

如果之前没有了解 寄存器 的童鞋可以先看看:https://www.jianshu.com/p/029...

直接摘抄里面的一段描述:

rip 指令地址寄存器,用来存储 CPU 即将要执行的指令地址。

每次 CPU 执行完相应的汇编指令之后,rip 寄存器的值就会自行累加;

Ptrace

如果之前没有了解 Ptrace 的童鞋可以先看看:http://fancy-blogs.com/2018/0...

在ptrace中有两个角色:

  • tracee:被追踪者,它是被监控的进程,通过ptrace系统调用的操作作用在它之上 (譬如:上文的 22346 进程);
  • tracer:追踪者,它负责监视并处理被追踪者传来的信息(譬如:GDB);

下文会直接引用这两个名词。

实现思路

实现的思路非常简单

1. 先确定我们要断点的地址

在 GDB 中,我们是习惯对 行号 或者 函数名 直接设置断点,行号相对来说比较复杂,我们先展示 函数名 的。

在 Linux 环境下编译出来的可执行文件都是遵循 ELF 格式,如果没有特殊处理,它会保留比较完整的 符号表

就拿开头的程序来当例子,可以通过 readelf -s a.out 查看:

Linux:断点原理与实现

这个符号表记录了进程需要用到的符号分别在什么位置。

如图,第一列就是符号的地址(十六进制),第二列是长度,最后一列是符号名字

我们这里需要在 test1 这个函数打断点,也就是红色圈出来的地方,这里可能会有童鞋想问为啥是:_Z5test1v

这里主要是 cpp 的名字修饰问题:https://blog.csdn.net/u013220...,不碍事。

我们现在可以看到前面的地址就是 0x400916;

2. 通过 Ptrace 获得 tracee 的控制权
 // 建立追踪的关系, 很多童鞋可能会用 PTRACE_ATTACH,它和 PTRACE_SEIZE 的区别就是,它会马上暂停 tracee,而 PTRACE_SEIZE 不会
ptrace(PTRACE_SEIZE, pid, addr, data) 

// 中断 tracee 的行为,将控制权交给 tracer
ptrace(PTRACE_INTERRUPT, pid, addr, data)       

// 感知 tracee 的状态变更,便于下一步操作
waitpid(pid, &status, options)
3. 保留当前 rip 的指令内容,并用 中断指令 替换
// 获取 tracee addr 内存的内容
ptrace(PTRACE_PEEKDATA, pid, addr, data)  

// 修改 tracee 指定内存的内容
ptrace(PTRACE_POKEDATA, pid, addr, data) 

// 获取 tracee 当前的寄存器内容
ptrace(PTRACE_GETREGS, pid, addr, data) 
  
// 设置 tracee 当前的寄存器内容
ptrace(PTRACE_SETREGS, pid, addr, data)   
4. 恢复运行,等待 trap 触发
// 让 tracee 继续运行  
ptrace(PTRACE_CONT, pid, addr, data)  
5. 恢复 rip 指令,结束调试

完整 Tracer 代码

#include <sys/ptrace.h>
#include <iostream>
#include <stdio.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <string>

void dowait(pid_t pid) {
    int status, signum;
    while (true) {
        waitpid(pid, &status, 0);
        if (WIFSTOPPED(status)) {
            signum = WSTOPSIG(status);
            if (signum == SIGTRAP) {
                break;
            } else {
                std::cout << "Other signum, skipping..." << std::endl;
                ptrace(PTRACE_CONT, pid, 0, 0);
            }
        }
    }
}


void break_onece(pid_t pid, long addr) {

    // 保存 addr 旧的指令和寄存器(主要是 rip)
    long old_code = ptrace(PTRACE_PEEKDATA, pid, addr, NULL);
    user_regs_struct old_regs;
    ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);


    long trap_code = old_code;
    unsigned char *p = (unsigned char*) &trap_code;

    // Trap 中断指令的十六进制数值
    p[0] = 0xcc;

    // 用 Trap 覆盖 addr 数值,等 cpu 执行至此就会中断
    if (ptrace(PTRACE_POKEDATA, pid, addr, trap_code)) {
        std::cout << "Break failed" << std::endl;
        return;
    }

    ptrace(PTRACE_CONT, pid, NULL, NULL);
    dowait(pid);

    // 敲入任意字符以继续,可以在此加入其它调试逻辑(海阔凭鱼跃!!!)
    std::cout << "Next ? " << std::endl;
    std::string instruction;
    std::cin >> instruction;

    // 恢复 rip, 否则会因缺乏有效 rip 导致 tracee coredump
    ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);

    // 恢复 addr 原值
    ptrace(PTRACE_POKEDATA, pid, addr, old_code);
    ptrace(PTRACE_CONT, pid, 0, 0);
}

void quit(pid_t pid) {
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    std::cout << "quit!" << std::endl;
    exit(0);
}

int main(int argc, char* argv[]) {
    pid_t pid = std::stoi(argv[1]);

    if (ptrace(PTRACE_SEIZE, pid, NULL, NULL)) {
        perror("ptrace_seize failed");
        return -1;
    }

    if(ptrace(PTRACE_INTERRUPT, pid, 0, 0)) {
        perror("interrupt failed");
        quit(pid);
    }

    dowait(pid);

    // 想断点的地址
    long break_addr = 0x400916;
    break_onece(pid, break_addr);

    quit(pid);
    return 1;
}

编译 & 运行

g++ trace_test.cpp -std=c++11 -o trace_test

./trace_test 22346 # 本文开头的进程 

总结

关于断点的原理网上有很多文章提到,但比较多也是蜻蜓点水一笔带过,意犹未尽,干脆直接用最浅显的例子降低大家练手
成本!

其实在文中提到的例子也有非常多可以优化的点:

  • 比如:函数地址获取的方式,既然提到 ELF 的符号表,那么应该通过解析这个表,将用户传入的用户名,转换成地址;
  • 再比如:应该维护一份全局的断点表,储存任意多的断点,也让每个断点处可以重复利用;
  • 甚至还比如:涉及到 Ptrace 的错误返回都要优雅处理,因为在每个返回值不为 0 的情况下,贸然进行下一步是非常危险的,非常大可能导致 tracee coredump;

每个比如都可以展开研究,所以欢迎期待后续。

欢迎各位大神指点交流, QQ讨论群: 258498217
转载请注明来源: https://segmentfault.com/a/1190000021870750

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
代码还原的技术: Unidbg hook_add_new实现条件断点(二)
一、目标在做代码还原的时候,有时候会分析一组结果,希望在中途下个条件断点,比如在代码行0x1234,R00x5678的时候触发断点。今天我们就来试着搞一下。TIP:Unidbg代码同步到官方最新版,最新版已经支持浮点寄存器的显示了。二、步骤先写个floatdemotwo把祖传算法升个级extern"C"JNIEXPORTjstringJNIC
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
拓朴苔原
拓朴苔原
Lv1
看破是心不颠倒;放下是心不贪恋。
文章
8
粉丝
0
获赞
0