Linux 调试器之堆栈展开!

雨晨吖_2476

雨晨吖_2476

2024-01-06

1113人浏览

转载

导读 有时你需要知道的最重要的信息是什么,你当前的程序状态是如何到达那里的。有一个 backtrace 命令,它给你提供了程序当前的函数调用链。这篇文章将向你展示如何在 x86_64 上实现堆栈展开以生成这样的回溯。
系列索引

这些链接将会随着其他帖子的发布而上线。

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. ELF 和 DWARF
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 堆栈展开
  9. 读取变量
  10. 之后步骤

用下面的程序作为例子:

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 <p>打印输出的第一帧是当前正在执行的帧。我们可以通过查找 DWARF 中的当前程序计数器来获取此帧的信息:</p><div class="aritcle_card flexRow artxards">
											<div class="artcardd flexRow">
												<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8828" title="Obsidian (Linux)"><img
														src="https://img.php.cn/upload/skill/000/000/081/179228531040284.jpg" alt="Obsidian (Linux)" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
												<div class="aritcle_card_info flexColumn">
													<a rel="nofollow" href="/xiazai/skill8828" title="Obsidian (Linux)" class="overflowclass">Obsidian (Linux)</a>
													<p class="overflowclass">操作 Obsidian 库(纯 Markdown 笔记),并通过 notesmd-cli 实现自动化。</p>
												</div>
												<a rel="nofollow" href="/xiazai/skill8828" title="Obsidian (Linux)" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
												</a>
											</div>
										</div><pre class="brush:php;toolbar:false;">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 <strong>添加命令</strong><p>当然,我们必须向用户公开这个命令。</p><pre class="brush:php;toolbar:false;">else if(is_prefix(command, "backtrace")) {
print_backtrace();
}
测试

测试此功能的一个方法是通过编写一个测试程序与一堆互相调用的小函数。设置几个断点,跳到代码附近,并确保你的回溯是准确的。

我们已经从一个只能产生并附加到其他程序的程序走了很长的路。本系列的倒数第二篇文章将通过支持读写变量来完成调试器的实现。在此之前,你可以在这里找到这个帖子的代码。

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

2023.09.15

1091

5

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

2025.11.08

345

7

Python lambda详解
Python lambda详解

本专题整合了Python lambda函数相关教程,阅读下面的文章了解更多详细内容。

2026.01.05

614

7

C++ Lambda 表达式与函数式编程
C++ Lambda 表达式与函数式编程

深入讲解 C++ Lambda 表达式与函数式编程范式,涵盖 Lambda 基础语法与返回值推导、值捕获与引用捕获的区别与陷阱、初始化捕获(C++14 广义捕获)、泛型 Lambda(auto 参数)、mutable 关键字修改捕获变量、std::function 类型擦除与函数包装、std::bind 参数绑定(及其被 Lambda 替代的趋势)、Lambda 在 STL 算法(sort/transform/for_each/remo

2026.05.06

769

22

Java 函数式接口与 Lambda 进阶技巧
Java 函数式接口与 Lambda 进阶技巧

在 Java Stream 基础之上进一步深入函数式编程,涵盖 @FunctionalInterface 注解与自定义函数式接口设计、四种方法引用(静态/实例/对象/构造器)的使用场景、Lambda 中的 effectively final 变量捕获规则、函数组合(andThen/compose)与柯里化技巧、Comparator 链式比较器构建、CompletableFuture 异步函数式编排、Reactor / RxJava 响应

2026.05.08

428

28

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

2026.04.23

379

31

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

2023.07.18

6427

5

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

2023.08.10

2688

6

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

2023.07.18

6427

5

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Python uv官方安装与入门文档
Python uv官方安装与入门文档

共0课时 | 0人学习

Buffalo框架快速入门指南
Buffalo框架快速入门指南

共0课时 | 0人学习

Valgrind支持平台说明
Valgrind支持平台说明

共0课时 | 0人学习