GDB之(7)监控内存和库加载
Author:onceday date:2024年1月25日
漫漫长路,才刚刚开始…
全系列文章请查看专栏: Linux实践记录_Once-Day的博客-****博客
推荐参考文档:
- GDB: The GNU Project Debugger ()
- GDB Documentation ()
文章目录
- GDB之(7)监控内存和库加载
- 1. 概述
- 1.1 使用watch
- 1.2 高级watch用法
- 1.3 mprotect操作
- 2. GDB高级内存监视功能
- 2.1 监视未定义内存访问检查
- 2.2 进程虚拟地址分布情况
- 2.3 实例内存映射情况分析
- 2.4 进程虚拟地址和库代码地址
- 3. Linux动态库加载
- 3.1 延迟绑定符号
- 3.2 C++异常库
- 3.3 C++编译和支持
- 3.4 不同符号的区别
- 3.5 C++主动异常解析throw
- 3.6 libunwind替代c++异常堆栈运行时
- 3.7 aarch64架构函数栈流程
1. 概述
1.1 使用watch
在GDB中监控内存区域并在该区域被读取或写入时触发断点,可以使用watch
命令来实现。下面是一些基本的步骤:
-
启动GDB并加载你的程序:
gdb your_program
-
设置断点以停止程序的执行:
这样你就可以在感兴趣的内存区域被访问之前设置监视点。(gdb) break main
-
运行程序:
(gdb) run
-
找到你想监控的内存地址:
假设你已经知道了内存的具体地址,或者你可以通过程序变量来获取它。 -
设置监视点:
假设你想监控的内存地址是0x12345678
,你可以使用以下命令来监视这个地址:-
监视内存写入:
(gdb) watch *(type *)0x12345678
其中
type
是内存地址处数据的类型,例如int
、char
等。 -
监视内存读取(这需要硬件支持,不是所有平台都支持这种类型的断点):
(gdb) rwatch *(type *)0x12345678
-
监视内存读取或写入:
(gdb) awatch *(type *)0x12345678
-
-
继续程序执行:
(gdb) continue
当程序在运行时试图读取或者写入指定的内存地址时,监视点会触发,并且GDB会暂停程序的执行,让你可以检查程序状态,例如查看变量值,调用栈等。
记得替换type
和0x12345678
为你实际要监视的内存区域的类型和地址。如果你是通过变量名来设置监视点的话,只需用变量名代替地址即可,如下所示:
(gdb) watch variable_name
请注意,监视点依赖于硬件支持,可能会导致程序运行变慢。另外,并不是所有的GDB平台都支持rwatch
和awatch
命令,因为这需要特定的硬件调试功能。
1.2 高级watch用法
如果您需要监视一大片内存区域,您不能直接使用watch
命令,因为watch
命令是为单个变量或内存位置设计的。不过,您可以采取以下几种方法来监视较大的内存区域:
-
使用多个监视点:
如果区域不是特别大,您可以在该区域内的几个关键点设置监视点。不过,这种方法不适用于非常大的内存区域,因为它将消耗大量资源,并可能使程序运行非常缓慢。 -
使用条件断点:
您可以设置一个断点,在程序中的一个逻辑位置(例如,可能访问那块内存的函数调用),并在该断点处添加一个条件来检查内存区域的值是否发生变化。这种方法需要您能够预测哪些代码可能会访问到该内存区域。 -
修改代码来帮助监视:
如果您能够修改程序,可以在代码中添加特定的检查,以记录对感兴趣内存区域的访问。例如,通过添加代码来在每次内存访问时打印日志信息。 -
使用GDB的Python扩展:
GDB具有Python API,您可以编写脚本来检查特定的内存区域是否发生变化。这种方法更为高级,但可以提供非常灵活的监视能力。
例如,使用GDB Python API的简单脚本可能如下所示:
import gdb
class MemoryWatch(gdb.Breakpoint):
def __init__(self, location, length):
super(MemoryWatch, self).__init__(location, gdb.BP_WATCHPOINT, internal=True)
self.length = length
self.old_memory = gdb.selected_inferior().read_memory(self.location, self.length)
def stop(self):
new_memory = gdb.selected_inferior().read_memory(self.location, self.length)
if new_memory != self.old_memory:
print("Memory change detected!")
return True # Break here
return False # Continue running
# 使用示例:监视从地址0x12345678开始的64字节内存区域
MemoryWatch(0x12345678, 64)
-
使用操作系统功能:
如果您是在Linux上工作,可能可以使用mprotect
系统调用来将内存区域标记为不可读/不可写,这样任何访问都会引发SIGSEGV
信号,然后您可以在GDB中捕获这个信号。
请注意,监视大块内存通常会导致性能问题,因为它会大大增加调试器检查内存的次数。如果可能,尝试缩小要监视的内存区域,或者使用一种更高效的机制来检测对该区域的访问。
1.3 mprotect操作
mprotect
是 POSIX 系统上的一个系统调用,它可以改变一个进程地址空间中某一区域的保护属性,包括可读、可写和可执行属性。mprotect
可以用于实现内存页的保护,防止数据被意外或恶意修改,或者用于实现诸如断点调试等功能。
在 C 语言中,mprotect
函数的原型通常如下所示:
#include <sys/>
int mprotect(void *addr, size_t len, int prot);
-
addr: 起始地址。这个地址必须是系统页面大小的倍数。在大多数系统上,您可以通过
getpagesize()
或sysconf(_SC_PAGESIZE)
获得页面大小。 -
len: 需要修改保护属性的内存长度,单位是字节。长度不必是页面大小的倍数;如果超过,
mprotect
会扩展到包含指定地址范围的整个页面。 -
prot: 指定内存区域的新保护属性。可以是以下几个值的组合:
-
PROT_NONE
- 页面不能被访问。 -
PROT_READ
- 页面可以被读取。 -
PROT_WRITE
- 页面可以被写入。 -
PROT_EXEC
- 页面可以执行代码。
-
如果调用成功,mprotect
函数返回 0。如果失败,返回 -1 并设置 errno
以指示错误原因。
下面是一个应用例子
#define _GNU_SOURCE
#include <>
#include <>
#include <sys/>
int main() {
// 分配一个页面的内存
size_t pagesize = sysconf(_SC_PAGESIZE);
void *buffer = aligned_alloc(pagesize, pagesize);
if (!buffer) {
perror("aligned_alloc failed");
return EXIT_FAILURE;
}
// 写入一些数据到分配的内存
snprintf(buffer, pagesize, "Hello, world!");
// 将内存区域标记为只读
if (mprotect(buffer, pagesize, PROT_READ) == -1) {
perror("mprotect failed");
free(buffer);
return EXIT_FAILURE;
}
// 尝试写入只读内存 -- 将导致段错误 (SIGSEGV)
snprintf(buffer, pagesize, "This will fail");
free(buffer);
return EXIT_SUCCESS;
}
在上面的例子中,我们首先分配了一块内存区域,接着使用 mprotect
将其设置为只读。当尝试写入这段只读内存时,程序将触发一个段错误(Segmentation Fault)。
在调试时,您可以捕获段错误信号,利用信号处理函数,或者在 gdb 中设置适当的信号处理来响应这类错误。这可以帮助您监测到哪些代码尝试非法地修改了受保护的内存区域。
2. GDB高级内存监视功能
2.1 监视未定义内存访问检查
GDB 可以设置拒绝访问没有明确描述的内存。如果在某个系统下,访问这些内存区域存在不能预料的效果的话,要预防这种状况,或者要提供一个更好的错误检查,都是很大帮助的。下列命令控制这种行为:
set mem inaccessible-by-default [on|off]
如果设置on,设置GDB将未明确描述范围的内存当作不存在的并拒绝对此内存的访问。 只有在至少有一个已定义的内存范围的情况 下才会进行检查。
如果设置了 off,设置 GDB 将此未明确描述范围的内存作为 RAM。 默认值是 on。
show mem inaccessible-by-default
显示当前对于未知内存访问的设置。
2.2 进程虚拟地址分布情况
在 GDB 中,info proc mappings
命令用于展示进程的虚拟内存映射情况。这个命令提供的输出一般会包含多个字段,其中“Size”和“Offset”字段具有特定含义:
-
Size: 显示内存映射区域的大小。这个值通常以字节为单位,表示从该区域的起始地址开始,这块内存区域的总长度。对于一个给定的内存区域,这个“Size”值表示了从映射的起始地址到结束地址范围内包含的内存量。
-
Offset: 显示文件映射到内存的偏移量。对于从文件映射的区域(例如共享库或者文件映射),这个值表示内存中的起始地址对应于底层文件中的偏移位置。换句话说,它是文件内容被映射到内存中起始点的偏移量。对于匿名映射(不基于文件的映射,比如堆),这个值通常为零。
在输出的映射列表中,每一行都代表一个不同的内存区域映射,包括了映射的虚拟地址范围、权限、偏移量、设备信息、节点以及相关联的文件名(如果有的话)。
例如,下面是一个典型的info proc mappings
命令输出的样例:
(gdb) info proc mappings
Start Addr End Addr Size Offset objfile
0x8048000 0x804a000 0x2000 0x0 /home/example/binary
0x804a000 0x804b000 0x1000 0x2000 /home/example/binary
0xb7fc0000 0xb7fe0000 0x20000 0x0 [heap]
...
在这个例子中,每一行显示了一个映射区域的起始地址(Start Addr)和结束地址(End Addr)、该区域的大小(Size)以及文件偏移(Offset)。例如,第一行表示从虚拟地址0x8048000
到0x804a000
的区域,大小为0x2000
字节,从文件/home/example/binary
的起始位置开始映射(因为偏移量是0x0
)。
这些信息对于理解程序如何在内存中布局,以及如何与其它文件和库交互,是非常有用的。
一个共享库文件的不同部分可能会被映射到进程的虚拟内存中的多个区域,这通常是因为共享库包含了不同类型的数据和代码,它们需要以不同的方式被处理。这些区域可能具有不同的权限和特性,例如,某些部分是可执行的,而其他部分可能是只读的或者可写的。下面是一些常见的映射类型:
-
.text 段: 包含共享库的可执行代码,通常映射为只读和可执行,以防止代码被篡改。
-
.data 段: 包含初始化的全局变量和静态变量,这些数据通常映射为可读写,以便程序运行时可以修改它们的值。
-
.bss 段: 包含未初始化的全局变量和静态变量,它们在程序启动时被初始化为零。这部分数据也通常映射为可读写。
-
动态链接器和加载器: 动态链接器(例如 )的代码和数据也可能被映射进来,它负责处理运行时的符号解析和重定位。
-
页对齐和文件偏移: 由于内存分页和文件对齐的要求,共享库文件的相同内容可能会因为映射到不同的页或者因为不同的文件偏移而被映射多次。
这样的映射允许操作系统利用虚拟内存管理的优势,比如通过写时复制(copy-on-write)来高效地处理多个进程使用相同库的情况。这样,即使多个进程加载了相同的共享库,它们也可以共享相同的物理内存页,直到一个进程尝试写入某个页,这时操作系统会为该进程创建这个页的私有副本。
在 GDB 中使用 info proc mappings
命令可以观察到这种行为,因为这个命令会列出进程的所有内存映射,包括由共享库引起的映射。这对于调试和优化程序的内存使用非常有用。
2.3 实例内存映射情况分析
ffff9d344000-ffff9d361000 r-xp 00000000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
ffff9d361000-ffff9d370000 ---p 0001d000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
ffff9d370000-ffff9d371000 r--p 0001c000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
ffff9d371000-ffff9d372000 rw-p 0001d000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
在给出的映射信息中,我们可以看到 /usr/lib/libnetfpc++.so.2.4.0
这个共享库文件的不同部分被映射到了进程的不同虚拟内存区域。每行都显示了不同的映射属性和对应的文件偏移量。我将逐行解释这些映射:
-
ffff9d344000-ffff9d361000 r-xp 00000000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
-
ffff9d344000-ffff9d361000
:这是映射的虚拟内存地址范围。 -
r-xp
:这表示区域权限为读(r)、执行(x),而不可写(p 表示页面是私有的,即写时复制)。 -
00000000
:这是文件映射的偏移量,表示这段映射从文件的开始位置起。 -
07:00 3418
:设备号和节点号,这对于识别文件系统中的文件很有用。 -
/usr/lib/libnetfpc++.so.2.4.0
:这是被映射文件的路径和名称。
-
-
ffff9d361000-ffff9d370000 ---p 0001d000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
-
ffff9d361000-ffff9d370000
:接下来的虚拟内存地址范围。 -
---p
:这表示区域没有任何权限(既不可读、不可写,也不可执行),通常这样的区域用于保留地址空间或者作为前一段映射的占位符。 -
0001d000
:文件映射的偏移量增加了,这通常标志着上一映射区域的结束。
-
-
ffff9d370000-ffff9d371000 r--p 0001c000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
-
ffff9d370000-ffff9d371000
:再接下来的虚拟内存地址范围。 -
r--p
:这表示区域权限为只读。 -
0001c000
:这里的偏移量稍微后退了一些(相较于上一行的0001d000
),这通常意味着这部分内存映射与前面的映射有重叠,这可以用于数据段(如.data
、.rodata
或.bss
)。
-
-
ffff9d371000-ffff9d372000 rw-p 0001d000 07:00 3418 /usr/lib/libnetfpc++.so.2.4.0
-
ffff9d371000-ffff9d372000
:最后的虚拟内存地址范围。 -
rw-p
:这表示区域权限为可读可写。 -
0001d000
:文件映射的偏移量,与第一行相同,这表明这部分内存是为了文件的某部分内容而被映射的,通常是为了.bss
或者动态链接信息。
-
这些映射反映了共享库在内存中的布局。不同的权限是因为代码段(.text
)需要被执行,所以设置为可执行;数据段需要被读取和修改,所以设置为可读写;某些部分可能需要保护不被随意访问,因此可能没有任何权限。这种布局是由链接器和操作系统的内存管理协同决定的。
2.4 进程虚拟地址和库代码地址
在使用 GDB 调试程序时,它提供了不同的命令来显示内存映射和共享库信息。当你使用 info sharedlibrary
命令时,GDB 会显示当前加载的共享库以及它们在内存中的地址。这个列表通常会包括共享库的文本段(也就是代码段)的起始地址。
相比之下,当你查看 /proc/[pid]/maps
文件(或者在 GDB 中使用类似 info proc mappings
的命令)时,你会得到进程的详细内存映射,这包括所有已映射区域的地址范围和权限。这意味着映射信息不仅包括共享库的文本段,还包括数据段、BSS 段、堆栈、堆和其他可能的映射区域。
由于 info sharedlibrary
命令主要关注共享库的代码段,它显示的地址范围通常对应于 /proc/[pid]/maps
文件中具有执行权限的那一部分地址范围。这是因为:
- GDB 的主要关注点是调试代码,所以它特别关注代码段的地址。
- 代码段通常映射为可执行(
r-xp
)。 - 数据段和其他段对于调试代码来说不如代码段重要,所以它们可能不会显示在
info sharedlibrary
命令的输出中。
因此,当你看到 GDB 输出的共享库信息只显示部分地址时,这是正常的行为,并且它主要集中在对调试最有用的部分。如果你需要获取完整的映射信息,你需要查看 /proc/[pid]/maps
或者使用 GDB 提供的内存映射相关命令。
3. Linux动态库加载
3.1 延迟绑定符号
在 Linux 系统中,动态链接器负责在运行时解析共享库中的符号引用。当一个程序使用动态链接库(Dynamic Shared Object, DSO,即 .so
文件)的时候,它会包含一些未解析的符号,这些符号在编译时还未知道具体的地址,需要在程序运行时由动态链接器来解析。这个过程涉及 _dl_runtime_resolve
和 _dl_fixup
函数。
_dl_runtime_resolve
是动态链接器中的一个函数,它在运行时负责解析动态链接的符号。当一个程序第一次调用一个动态链接库中的函数时,控制流会转到 _dl_runtime_resolve
函数。这个函数会查找函数的实际内存地址,然后更新程序的全局偏移表(Global Offset Table, GOT),以便随后的函数调用能够直接跳转到正确的地址。
这个过程通常涉及以下步骤:
- 保存当前的 CPU 寄存器状态,因为这个过程需要使用寄存器来进行计算。
- 从 GOT 中获取需要解析的符号信息。
- 使用符号信息在动态链接库的符号表中查找目标地址。
- 将解析出的地址写回 GOT,这样后续的函数调用就可以直接跳转到这个地址。
- 恢复 CPU 寄存器状态。
- 跳转到目标函数地址继续执行。
_dl_fixup
是实际进行符号解析的函数。它被 _dl_runtime_resolve
调用,并负责执行符号查找和地址解析的大部分工作。_dl_fixup
可以看作是实现了动态链接符号解析逻辑的核心函数。它会查找符号在动态链接库中的实际地址,并处理可能出现的各种情况,例如处理重定位、解决符号冲突等。
动态符号解析是计算成本较高的操作,因此只在每个符号第一次被引用时执行一次。这是一种名为懒惰绑定(lazy binding)的优化技术,它可以减少程序启动时的加载时间,因为不是所有的符号都需要在启动时立即解析。在 _dl_runtime_resolve
更新了 GOT 之后,程序中的后续调用就可以直接跳转到正确的地址,无需再次经过符号解析的过程。
这些函数是动态链接器的内部实现细节,通常对于应用程序开发者来说是透明的,但对于理解动态链接和运行时符号解析的机制非常重要。
3.2 C++异常库
在 C++ 异常处理实现中,Unwind_RaiseException
函数通常是由底层的异常处理机制调用的,用于执行堆栈展开(stack unwinding)过程。libstdc++
是 GNU 项目提供的标准 C++ 库的实现,它与 GCC(GNU Compiler Collection)紧密集成。在 GCC 的实现中,堆栈展开和异常处理是通过 libgcc 提供的功能来实现的,其中 Unwind_RaiseException
是关键的部分。
当一个 C++ 程序抛出异常时,如下过程将会发生:
-
__cxa_throw
被调用,这是 C++ 中抛出异常的函数。这个函数负责初始化异常对象,并开始异常处理流程。 - 在开始堆栈展开以找到异常处理代码(
catch
块)之前,__cxa_throw
最终会调用Unwind_RaiseException
。 -
Unwind_RaiseException
遍历当前线程的调用栈,查找异常处理代码。 - 当
Unwind_RaiseException
遍历堆栈时,它调用每个堆栈帧上注册的析构函数和其他清理工作。这些是通过__cxa_atexit
和析构函数注册的。 - 如果找到一个匹配的
catch
块,堆栈展开停止,异常被传递到catch
块中处理。
libstdc++
调用 Unwind_RaiseException
是因为它需要依赖底层的堆栈展开机制来实现 C++ 异常标准。异常处理是 C++ 运行时环境的一部分,libstdc++
提供了运行时的高级特性,但实际的堆栈操作和异常传播则依靠更底层的库(如 libgcc
)。通过这种方式,libstdc++
能够将复杂的异常处理和堆栈展开细节委托给专门处理它们的代码,从而保持运行时库的清晰和高效。
3.3 C++编译和支持
您看到的错误信息“:27: undefined reference to __cxa_allocate_exception
”表示链接器无法找到__cxa_allocate_exception
函数的定义。这个函数是 C++ 应用二进制接口(ABI)的一部分,通常在抛出异常时由 C++ 运行时内部使用,用于为异常对象分配内存。
要解决这个问题,请参考以下步骤:
-
检查编译器和链接器命令:
确保您使用的是 C++ 编译器(对于 GNU 编译器集合通常是g++
),而不是 C 编译器(gcc
)。C++ 编译器会自动链接 C++ 标准库,其中包含必要的 ABI 支持。 -
链接到 C++ 标准库:
如果您手动调用链接器或使用自定义构建过程,请确保您链接到 C++ 标准库,通常对于 GCC 是-lstdc++
或对于 Clang 是-lc++
。 -
使用正确的链接顺序:
链接命令中的库顺序很重要。确保 C++ 标准库在您的对象文件或依赖它的库之后。 -
检查编译器兼容性:
如果您在应用程序的不同部分使用不同的编译器或不同版本的编译器,可能会存在不兼容性。确保您的应用程序的所有部分都使用兼容的编译器版本进行编译和链接。 -
验证 C++ 标准库的安装:
确保您的系统上正确安装了 C++ 标准库。如果需要,可使用系统的包管理器重新安装。 -
检查自定义构建系统或 Makefile:
如果您使用的是自定义构建系统或 Makefile,请检查它是否正确配置为对 C++ 项目使用正确的编译器和链接器设置。
下面是一个简单的 g++ 命令示例,用于编译和链接 C++ 源文件,并确保链接了标准库:
g++ -o cplusplus-exception
如果您使用多个源文件或库,可能需要更复杂的命令,但原则保持不变:确保使用 g++ 并且链接了 C++ 标准库。
如果您已经检查了所有这些但问题仍然存在,可能是您的开发环境或传递给编译器或链接器的特定标志出现了更复杂的问题。在这种情况下,您可能需要查看编译器的文档或寻求您正在使用的特定工具链的社区支持。
3.4 不同符号的区别
在链接和加载 ELF (Executable and Linkable Format) 文件时,你可能会遇到各种与符号解析相关的名称,特别是在涉及动态链接的情况下。这些名称(_Unwind_RaiseException
, _Unwind_RaiseException@
, _Unwind_RaiseException@plt
)代表 C++ 异常处理中的一个函数,它们在不同上下文中有不同的含义:
-
_Unwind_RaiseException:
这是_Unwind_RaiseException
函数的通常名称,它是用于启动栈展开过程的函数,通常由编译器在生成异常处理代码时使用。当你在代码中使用throw
关键字时,编译器会生成调用_Unwind_RaiseException
函数的代码,以便开始异常的栈展开过程。 -
_Unwind_RaiseException@:
@
后缀的含义与全局偏移表(Global Offset Table, GOT)和过程链接表(Procedure Linkage Table, PLT)有关,这是动态链接机制的一部分。在 ELF 文件中,GOT
用于存储全局数据的地址,PLT
用于解决动态链接函数的地址。-
@got
:表示这是一个 GOT 条目。 -
@plt
:表示这是一个 PLT 条目。
当一个程序调用动态链接库中的函数时,它实际上首先跳到 PLT 中的对应条目。这个条目负责跳转到 GOT 中存储的实际函数地址。第一次这个函数被调用时,PLT 中的代码会触发动态链接器来解析这个函数的真实地址,并更新 GOT 中的条目。这样,后续的调用就可以直接跳转到真实的地址。
-
-
_Unwind_RaiseException@plt:
这个符号用于间接调用动态链接库中的_Unwind_RaiseException
函数。当链接器生成二进制文件时,它会在 PLT 中创建一个条目,该条目在程序运行时用于查找动态库中函数的实际地址。PLT 条目允许程序在运行时延迟解析函数地址,加快了程序的启动时间,并允许使用位置无关代码(PIC)。
在处理链接问题时,通常不需要直接处理这些符号;它们是由编译器和链接器自动处理的。但是,如果您在链接时遇到与这些符号相关的错误,通常意味着您的程序在尝试使用一个动态链接库中的函数,但链接器无法正确解析或定位该符号。这可能是因为缺少库文件、路径问题、或者链接命令中的错误配置。
3.5 C++主动异常解析throw
_cxa_throw
是 C++ ABI (Application Binary Interface) 中的一个函数,它在抛出 C++ 异常时被调用。当你在 C++ 程序中使用 throw
语句时,编译器生成的代码会调用 _cxa_throw
来启动异常处理过程。
如果你想确定 _cxa_throw
抛出的异常信息,你可以做以下几件事情:
-
查看异常类型:
_cxa_throw
的第一个参数是一个指向异常对象的指针。这个对象的类型就是异常的类型。在调试器中,你可以检查这个指针指向的对象来确定异常的具体信息。 -
设置断点:
在调试器中设置断点于_cxa_throw
函数上,当断点触发时,你可以检查传给_cxa_throw
的参数。通常有三个参数:- 第一个参数是指向抛出的异常对象的指针。
- 第二个参数是指向异常类型信息的指针(
type_info
对象)。 - 第三个参数是指向析构函数的指针,该函数将在异常对象不再需要时被调用。
-
使用 backtrace:
当断点触发时,使用 backtrace 来观察异常被抛出的代码堆栈。这可以帮助你确定哪个函数抛出了异常,以及在哪里。 -
查看异常类型的名称:
第二个参数指向的type_info
对象包含了异常类型的名称,你可以使用typeid
表达式或在调试器中查看这个对象来获取类型的名称。 -
自定义异常类:
如果你有自定义的异常类,它们可能包含了额外的信息,如错误消息或错误代码等。你可以通过对异常对象进行类型转换和检查来获取这些信息。
举个例子,以下是可能的代码片段和在 GDB 调试器中如何处理的例子:
#include <stdexcept>
#include <iostream>
void myFunction() {
throw std::runtime_error("An error occurred");
}
int main() {
try {
myFunction();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
在 GDB 中,你可以这样做:
# 启动 GDB
gdb ./my_program
# 设置断点在 _cxa_throw 上
(gdb) break __cxa_throw
# 运行程序
(gdb) run
# 一旦断点触发,检查异常对象
(gdb) print ((std::exception*)$rdi)->what()
请注意,上面使用 $rdi
是假设你使用的是 x86_64 架构,它是第一个参数传递的寄存器。这可能会因架构而异,所以你需要根据你的具体环境调整这一点。
通常,处理这样的底层细节不是必要的,除非你在进行底层的调试或者实现自己的异常处理逻辑。标准 C++ 的 try
和 catch
语句通常是处理异常的首选方式。
3.6 libunwind替代c++异常堆栈运行时
在编译支持库如 libunwind
时,配置选项通常用于启用或禁用库的特定功能。--enable-cxx-exceptions
是一个配置选项,可能用于控制C++异常支持。
当编译一个库,尤其是涉及底层操作或异常处理的库时,通常需要决定是否支持C++异常。如果库或应用程序需要处理C++异常,那么在编译时启用这个选项是必要的。
正确的配置选项通常应该是 --enable-cxx-exceptions
,但你提供的信息是 --eable-cxx-exceptions
,其中 enable
被拼写错误了。如果你在配置编译 libunwind
或类似库时遇到问题,确保你使用的是正确的配置选项。
例如,如果你想要编译 libunwind
并启用C++异常支持,你应该在配置时加上这个选项:
./configure --enable-cxx-exceptions
如果你不需要C++异常支持,那么你可以忽略这个选项或者显式禁用它:
./configure --disable-cxx-exceptions
配置完成后,你可以继续使用 make
和 make install
来编译和安装库。
请确保你按照 libunwind
的文档或配置脚本中的指示来设置正确的配置选项,以便正确编译库以符合你的需求。如果有疑问,常常可以通过 ./configure --help
命令来获取所有可用的配置选项及其描述。
3.7 aarch64架构函数栈流程
在 aarch64
架构(也称为 ARMv8-A 架构)中,ret
指令用于从函数返回。这条指令会从链接寄存器(LR
,寄存器 x30
)中加载返回地址然后跳转到该地址。因此,ret
指令实际上是从 x30
寄存器中读取返回地址。
当一个函数被调用时,调用指令(通常是 bl
或 blr
)会将下一条指令的地址存入链接寄存器 LR
中,这个地址就是返回地址。当 ret
指令执行时,它会将控制权交还到这个地址,也就是函数调用之后的那条指令。
在大多数情况下,你不需要手动管理 x30
寄存器的内容,因为在函数调用和返回时,编译器和链接器会自动处理好。但是,如果你在编写汇编语言或者需要在C语言中进行底层操作,你可能需要直接操作 x30
或者通过堆栈来保存和恢复它的内容,尤其是在写嵌套函数调用或者异常处理代码时。
在函数入口和出口,链接寄存器的值通常会被保存到堆栈中以防止其被后续的函数调用覆盖。这是因为 x30
是一个被调用者保存(callee-saved)寄存器,按照AAPCS(ARM架构过程调用标准)的约定,被调用者负责保存和恢复这个寄存器的原始值。
stp x29, x30, [sp, #-48]!
是一条 ARM64/AArch64 架构的汇编指令,用于同时存储两个寄存器的内容到栈上。这条指令是在函数的序言(prologue)中使用的,用于保存帧指针(x29)和链接寄存器(x30)的值,同时预留出一定的栈空间供函数使用。
这条指令的具体含义如下:
-
stp
是 “Store Pair” 的缩写,意为存储寄存器对。 -
x29
通常用作帧指针(Frame Pointer),在很多函数调用约定中,它用于指向当前栈帧的开始。 -
x30
是链接寄存器(Link Register),它存储着函数返回后应该跳转到的地址。 -
[sp, #-48]!
指定了目标地址,这是一个带有偏移量的预索引寻址模式:-
sp
是堆栈指针(Stack Pointer)。 -
#-48
指明了一个48字节的负偏移量,意味着目标地址是堆栈指针当前值减去48字节。 -
!
表示这是一个带有写回的地址,即在存储操作之前,先将偏移后的地址写回到sp
寄存器。这实际上减小了栈指针的值,分配了栈空间。
-
所以,整条指令 stp x29, x30, [sp, #-48]!
完成了以下操作:
- 将堆栈指针
sp
的当前值减去48字节。 - 将更新后的
sp
值(新的栈顶地址)写回sp
寄存器。 - 将
x29
和x30
寄存器的内容存储到新的栈顶地址和栈顶地址加8字节处(因为每个寄存器是64位,即8字节)。
简而言之,这条指令用于在函数开始时保存当前函数的帧指针和返回地址,并更新堆栈指针以为局部变量和其他数据分配空间。这是保护调用者环境和设置被调用者环境的关键步骤。ldp x29, x30, [sp], #48
是 ARM64/AArch64 指令集中的一条指令,通常在函数的结尾被用作函数的尾声(epilogue)的一部分。这条指令的作用是从栈上加载寄存器对的值,并更新栈指针(sp
)。这是在函数返回之前恢复之前保存的状态。