tangptr@126.com 发表于 2025-1-14 10:39:12

【汇编/调试】简介x64上MSVC系ABI的栈回溯

本帖最后由 tangptr@126.com 于 2025-1-14 10:42 编辑


# 前言
栈回溯称得上是调试时的神器。调试器若是没有栈回溯的能力,那么接到异常时就很难得知这个调用源是从哪里来的。

# x86的原始栈回溯方案
在32位x86上,MSVC编译出来的程序通常会用`ebp`寄存器来保存栈帧,从而以链表的形式实现栈回溯。于是乎,就有了最经典的`55 8B EC`三字节序列(即`push ebp`-`mov ebp,esp`两条指令)。
当调试器接到异常(如断点)时,会读取线程`ebp`的值。这个值就是该栈帧的位置。其中:
- 读取``可以获得调用者`call`指令的下一条指令的地址。
- 读取``可以获得上一个栈帧的地址。
以此循环,就可以获得整个调用链。如果每个指令地址都能查询到符号,那就能很直观的看明白调用链了。

# x64的栈回溯
而到了64位时代,MSVC的ABI选择弃用`rbp`寄存器进行栈帧的保存,而是在PE映像中存放栈回溯的信息。这个操作叫做FPO(Frame Pointer Omission)。其实在32位时代也有这个操作,给编译器加上`/Oy`即可,但是64位上这个参数就没了。
这么做虽然会增加程序的大小,而且还会使得shellcode中的函数调用无法回溯,但是可以同时去掉保存与删除栈帧所带来的时间与栈空间的开销,为了性能其实还是值得的。

## `.pdata`节
在`.pdata`节中保存着一个`RUNTIME_FUNCTION`结构体数组。如果链接时把`.pdata`节合并到其他节里,则可以通过PE可选头中的`IMAGE_DIRECTORY_ENTRY_EXCEPTION`数据目录定位。这个结构体的定义是:
```C
typedef struct _FPO_DATA
{
    ULONG StartAddress;
    ULONG EndAddress;
    ULONG UnwindInfo;
}RUNTIME_FUNCTION;
```
以上值皆为相对于映像基址的32位RVA。其中`StartAddress`表示函数的起始地址,`EndAddress`表示函数的终止地址,`UnwindInfo`表示回溯信息。该数组按函数地址排序,因此可以直接用二分查找的方式快速定位回溯信息。
`UnwindInfo`指向的是`UNWIND_INFO`结构体,限于篇幅不做介绍,[详情见MSDN](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170#struct-unwind_info)。
不做介绍的更根本的原因是:`dbghelp.dll`导出的`StackWalk`函数可以帮你实现栈回溯,[详见MSDN](https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-stackwalkex)。

## 回溯纯汇编写的函数
在OS开发时,往往会用纯汇编实现一些特殊操作(如切换栈)。但是,汇编器可没有高级语言编译器那么聪明,并不能自动推断汇编所写的函数如何回溯。
因此,MASM汇编器提供了一系列的特殊语句。这些语句不会生成可执行的机器指令,但是可以帮助汇编器构造回溯信息。
- `.allocstack`语句用于标记分配了多少空间的栈。这个语句通常搭配`sub rsp,xxx`指令使用。
- `.pushreg`语句用于标记保存了哪些寄存器。这个语句通常搭配`push reg`指令使用。
- `.pushframe`语句用于标记这个函数被中断/异常调用,栈上有栈帧。这个语句用于标记这个函数栈上有中断/异常栈帧。
- `.setframe`语句用于标记这个函数有保存栈帧的寄存器。
- `.endprolog`语句用于标记函数的`prologue`行为(即所有的分配栈空间的行为)结束。
因此当使用纯汇编的时候,强烈建议正确结合上述语句构造栈回溯信息从而方便调试。
以上除了`.pushframe`以外,使用方式应该都是比较直观的。

### 回溯被切换的栈

**** Hidden Message *****


# 总结
本文简短的讲了一下32位时代的栈回溯方案,然后对比一下64位的栈回溯特点,最后重点讲解写汇编时如何生成栈回溯的信息,并以切换栈进行举例。

完玩 发表于 2025-1-14 11:21:54

x64时代,趋势是以占用空间来换取其他便利性。SEH和栈信息密不可分,理应同时保存在文件中。再则vc++的异常信息在pe32时就保存在文件中了,那么和其很相关的信息保存在文件中就更合理了。
页: [1]
查看完整版本: 【汇编/调试】简介x64上MSVC系ABI的栈回溯