coredump 断点_Linux:断点原理与实现

本文介绍了Linux环境下断点的原理,通过GDB的简单示范展示了断点的设置与使用。接着讲解了寄存器RIP、Ptrace系统调用的相关知识,并给出了一个简化版的Ptrace实现断点的Tracer代码,该代码能够暂停进程、修改内存中的指令以触发中断,并在调试后恢复进程的执行。
摘要由CSDN通过智能技术生成

前言

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

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

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

简单 GDB 示范

// test.cpp

#include

#include

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

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

1460000021870754

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

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

前置知识准备

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

寄存器:RIP

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

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

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

Ptrace

在ptrace中有两个角色:

tracee:被追踪者,它是被监控的进程,通过ptrace系统调用的操作作用在它之上 (譬如:上文的 22346 进程);

tracer:追踪者,它负责监视并处理被追踪者传来的信息(譬如:GDB);

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

实现思路

实现的思路非常简单

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

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

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

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

1460000021870753

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

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

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

我们现在可以看到前面的地址就是 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

#include

#include

#include

#include

#include

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;

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值