Linux 调试器之堆栈展开!
导读 | 有时你需要知道的最重要的信息是什么,你当前的程序状态是如何到达那里的。有一个 backtrace 命令,它给你提供了程序当前的函数调用链。这篇文章将向你展示如何在 x86_64 上实现堆栈展开以生成这样的回溯。 |
这些链接将会随着其他帖子的发布而上线。
- 准备环境
- 断点
- 寄存器和内存
- ELF 和 DWARF
- 源码和信号
- 源码级逐步执行
- 源码级断点
- 堆栈展开
- 读取变量
- 之后步骤
用下面的程序作为例子:
void a() { //stopped here } void b() { a(); } void c() { a(); } int main() { b(); c(); }
如果调试器停在 //stopped here' 这行,那么有两种方法可以达到:main->b->a或main->c->a`。如果我们用 LLDB 设置一个断点,继续执行并请求一个回溯,那么我们将得到以下内容:
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3 frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6 frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14 frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291 frame #4: 0x0000000000400409 a.out`_start + 41
这说明我们目前在函数 a 中,a 从函数 b 中跳转,b 从 main 中跳转等等。最后两个帧是编译器如何引导 main 函数的。
现在的问题是我们如何在 x86_64 上实现。最稳健的方法是解析 ELF 文件的 .eh_frame 部分,并解决如何从那里展开堆栈,但这会很痛苦。你可以使用 libunwind 或类似的来做,但这很无聊。相反,我们假设编译器以某种方式设置了堆栈,我们将手动遍历它。为了做到这一点,我们首先需要了解堆栈的布局。
High | ... | +---------+ +24| Arg 1 | +---------+ +16| Arg 2 | +---------+ + 8| Return | +---------+ EBP+--> |Saved EBP| +---------+ - 8| Var 1 | +---------+ ESP+--> | Var 2 | +---------+ | ... | Low
如你所见,最后一个堆栈帧的帧指针存储在当前堆栈帧的开始处,创建一个链接的指针列表。堆栈依据这个链表解开。我们可以通过查找 DWARF 信息中的返回地址来找出列表中下一帧的函数。一些编译器将忽略跟踪 EBP 的帧基址,因为这可以表示为 ESP 的偏移量,并可以释放一个额外的寄存器。即使启用了优化,传递 -fno-omit-frame-pointer 到 GCC 或 Clang 会强制它遵循我们依赖的约定。
我们将在 print_backtrace 函数中完成所有的工作:
void debugger::print_backtrace() {
首先要决定的是使用什么格式打印出帧信息。我用了一个 lambda 来推出这个方法:
auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; };
打印输出的第一帧是当前正在执行的帧。我们可以通过查找 DWARF 中的当前程序计数器来获取此帧的信息:
auto current_func = get_function_from_pc(get_pc()); output_frame(current_func);
接下来我们需要获取当前函数的帧指针和返回地址。帧指针存储在 rbp 寄存器中,返回地址是从帧指针堆栈起的 8 字节。
auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8);
现在我们拥有了展开堆栈所需的所有信息。我只需要继续展开,直到调试器命中 main,但是当帧指针为 0x0 时,你也可以选择停止,这些是你在调用 main 函数之前调用的函数。我们将从每帧抓取帧指针和返回地址,并打印出信息。
while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
就是这样!以下是整个函数:
void debugger::print_backtrace() { auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; }; auto current_func = get_function_from_pc(get_pc()); output_frame(current_func); auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8); while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
当然,我们必须向用户公开这个命令。
else if(is_prefix(command, "backtrace")) { print_backtrace(); }
测试此功能的一个方法是通过编写一个测试程序与一堆互相调用的小函数。设置几个断点,跳到代码附近,并确保你的回溯是准确的。
我们已经从一个只能产生并附加到其他程序的程序走了很长的路。本系列的倒数第二篇文章将通过支持读写变量来完成调试器的实现。在此之前,你可以在这里找到这个帖子的代码。
以上是Linux 调试器之堆栈展开!的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

无法以 root 身份登录 MySQL 的原因主要在于权限问题、配置文件错误、密码不符、socket 文件问题或防火墙拦截。解决方法包括:检查配置文件中 bind-address 参数是否正确配置。查看 root 用户权限是否被修改或删除,并进行重置。验证密码是否准确无误,包括大小写和特殊字符。检查 socket 文件权限设置和路径。检查防火墙是否阻止了 MySQL 服务器的连接。

C语言条件编译是一种根据编译时条件选择性编译代码块的机制,入门方法有:使用#if和#else指令根据条件选择代码块。常用条件表达式包括STDC、_WIN32和linux。实战案例:根据操作系统打印不同消息。根据系统位数使用不同的数据类型。根据编译器支持不同的头文件。条件编译增强了代码的可移植性和灵活性,使其适应编译器、操作系统和CPU架构变化。

1.0.1前言这个项目(包括代码和注释)是在我自学Rust的过程中记录的。可能有不准确或表述不清的地方,还请大家谅解。如果您从中受益,那就更好了。1.0.2为什么使用RustRust可靠且高效。Rust可以取代C和C,性能相似但安全性更高,并且不需要像C和C那样频繁重新编译来检查错误。主要优点包括:内存安全(防止空指针取消引用、悬空指针和数据争用)。线程安全(确保多线程代码在执行前是安全的)。避免未定义的行为(例如,数组越界、未初始化的变量或访问已释放的内存)。Rust提供现代语言功能(例如泛型

Linux的五个基本组件是:1.内核,管理硬件资源;2.系统库,提供函数和服务;3.Shell,用户与系统交互的接口;4.文件系统,存储和组织数据;5.应用程序,利用系统资源实现功能。

MySQL启动失败的原因有多种,可以通过检查错误日志进行诊断。常见原因包括端口冲突(检查端口占用情况并修改配置)、权限问题(检查服务运行用户权限)、配置文件错误(检查参数设置)、数据目录损坏(恢复数据或重建表空间)、InnoDB表空间问题(检查ibdata1文件)、插件加载失败(检查错误日志)。解决问题时应根据错误日志进行分析,找到问题的根源,并养成定期备份数据的习惯,以预防和解决问题。

C语言函数库是一个包含各种函数的工具箱,这些函数被组织在不同的库文件中。添加函数库需要通过编译器的命令行选项来指定,例如 GCC 编译器使用 -l 选项,后跟库名的缩写。如果库文件不在默认搜索路径下,则需要使用 -L 选项指定库文件路径。库有静态库和动态库之分,静态库在编译时直接链接到程序中,而动态库在运行时被加载。

MySQL无法直接在Android上运行,但可以通过以下方法间接实现:使用轻量级数据库SQLite,由Android系统自带,无需单独服务器,资源占用小,非常适合移动设备应用。远程连接MySQL服务器,通过网络连接到远程服务器上的MySQL数据库进行数据读写,但存在网络依赖性强、安全性问题和服务器成本等缺点。

MySQL安装报错的解决方法是:1.仔细检查系统环境,确保满足MySQL的依赖库要求,不同操作系统和版本需求不同;2.认真阅读报错信息,根据提示(例如缺少库文件或权限不足)采取对应措施,例如安装依赖或使用sudo命令;3.必要时,可尝试源码安装并仔细检查编译日志,但这需要一定的Linux知识和经验。最终解决问题的关键在于仔细检查系统环境和报错信息,并参考官方文档。
