【问题标题】:Getting the caller's Return Address获取调用者的返回地址
【发布时间】:2019-08-23 07:48:19
【问题描述】:

我试图弄清楚如何在 MSVC 中获取调用者的返回地址。我可以使用 _ReturnAddress() 来获取函数的返回地址,但我似乎找不到获取调用者的方法。

我尝试过使用 CaptureStackBackTrace,但由于某种原因,它在多次调用后崩溃。我也更喜欢通过内联汇编的解决方案。

void my_function(){
    cout << "return address of caller_function: " << [GET CALLER'S RETURN VALUE];
} // imaginary return address: 0x15AF7C0

void caller_function(){
     my_function();
}// imaginary return address: 0x15AFA70

输出: return address of caller_function: 0x15AFA70

【问题讨论】:

  • 你在使用 /clr 吗?您也可能应该将函数声明为 noinline。而且似乎该功能对调用约定有些敏感。
  • 你为什么需要那个?你想解决什么问题?这有点像XY problem...
  • 我不知道是不是这种情况,但这是修补二进制文件时使用的基本且广泛使用的概念。
  • CaptureStackBackTrace 仅在您错误地调用它时才会崩溃。真正使用它是解决方案

标签: c++ assembly visual-c++ x86 backtrace


【解决方案1】:

在 Windows 中,您可以使用RtlCaptureStackBackTraceRtlWalkFrameChain 安全地执行此操作,而无需依赖调试模式代码生成。见RbMn's answer in comments

在 GNU C / C++ (docs) 中,等价于
void * __builtin_return_address (unsigned int level)。所以__builtin_return_address(0) 得到你自己的,__builtin_return_address(1) 得到你父母的。该手册警告说,使用 0 的 arg 只能 100% 安全,并且可能会因更高的值而崩溃,但许多平台确实具有可以使用的堆栈展开元数据。


仅限 MSVC 32 位调试/未优化构建

如果有保留的调用堆栈(即在调试版本或不存在优化时)并考虑将 MSVC x86 作为目标 PE,您可以执行以下操作:

void *__cdecl get_own_retaddr_debugmode()
{
   // consider you can put this asm inline snippet inside the function you want to get its return address
   __asm
   {
       MOV EAX, DWORD PTR SS:[EBP + 4]
   }
   // fall off the end of a non-void function after asm writes EAX:
   // supported by MSVC but not clang's -fasm-blocks option
}

在调试版本中,当在编译器上禁用优化(MSVC 编译器参数:/Od)并且没有省略帧指针(MSVC 编译器参数:/Oy-)时,对cdecl 函数的函数调用将始终保存返回被调用者堆栈帧的偏移量+4 处的地址。寄存器EBP 存储正在运行的函数堆栈帧的头部。所以在上面的代码中foo会返回调用者的返回地址。

启用优化后,即使这样也会中断:它可以内联到调用者中,并且 MSVC 甚至没有将 EBP 设置为此函数 (Godbolt compiler explorer) 的帧指针,因为 asm 没有'不引用任何 C 局部变量。使用 mov eax, [esp]naked 函数; ret 可以可靠地工作。


通过再次阅读您的问题,我认为您可能想要调用者的调用者的返回地址。您可以通过访问直接调用者的堆栈帧然后获取其返回地址来做到这一点。像这样的:

// only works if *the caller* was compiled in debug mode
// as well as this function
void *__cdecl get_caller_retaddr_unsafe_debug_mode_only()
{
   __asm
   {
       MOV ECX, DWORD PTR SS:[EBP + 0] // [EBP+0] points to caller stack frame pointer
       MOV EAX, DWORD PTR SS:[ECX + 4] // get return address of the caller of the caller
   }
}

需要注意的是,这需要调用者将 EBP 设置为具有传统堆栈帧布局的帧指针。这不是现代操作系统中调用约定或 ABI 的一部分。异常的堆栈展开使用不同的元数据。但是如果对调用者禁用优化就会出现这种情况。

正如 Michael Petch 所指出的,MSVC 不允许在 x86-64 C/C++ 代码上使用 asm inline 构造。尽管编译器允许一整套 intrinsic functions 来处理它。

【讨论】:

  • Arrgh – 疏忽了...抱歉。布局一样的,只是清理在不同的位置...
  • IA-64 不是 x86-64。
  • 在顶部你说这个 (同样适用于 x64 更改其 r64 对应的相应 r32 寄存器并将堆栈帧的偏移量 +4 更改为 +8) 。这意味着您需要做的就是调整偏移量并使用 r64 寄存器使内联汇编在 64 位下工作。问题是内联汇编是 no longer supported when building x86-64 C/C++ code. 。从那个链接的文档中,它说 ARM 和 x64 处理器不支持内联汇编。
  • foo 的优化不是问题。问题是保存的 EBP 值(您从 [ebp+0] 加载的值可能完全没有意义,因为调用 foo 的任何函数都没有将其设置为堆栈指针。例如 godbolt.org/z/86GsAe 表明 MSVC -O2 编译为x86 和 x64 不会在调用另一个函数的函数中设置堆栈框架(仅给出该函数的原型)。即使定义可用,它也不会改变任何事情。所以基本上 第二个版本,追逐 EBP 链,仅适用于调试版本。
  • 确实存在windows api RtlCaptureStackBackTraceRtlWalkFrameChain 可以完成这项工作,同时支持x86/x64,所以不需要编写自定义代码。如果正确的话,它们当然不会崩溃。如果编写自己的代码需要检查下一个可能的帧 eax: mov eax,[ebp] 高于 ebp 并且低于堆栈顶部 - fs:[4] (fs:[NT_TIB.StackBase] )
【解决方案2】:

从上面给出的示例中,这里使用的调用约定__cdecl 的顺序不正确。这就是当前 MVSC++ 编译器代码规范中的样子。

// getting our own return address is easy, and should always work
// using inline asm at all forces MSVC to set up EBP as a frame pointer even with optimization enabled
// But this function might still inline into its caller

void __cdecl *get_own_retaddr()
{
   // consider you can put this asm inline snippet inside the function you want to get its return address
   __asm
   {
       MOV EAX, DWORD PTR SS:[EBP + 4]
   }
   // fall off the end of a non-void function after asm writes EAX:
   // supported by MSVC but not clang's -fasm-blocks option
}

同样适用于上面提供的其他示例。

【讨论】:

  • 这应该是对其他答案的建议编辑,以修复该语法错误。我已经更新了另一个答案,所以现在可以编译了。
猜你喜欢
  • 1970-01-01
  • 2021-01-17
  • 2018-09-26
  • 2011-04-19
  • 1970-01-01
  • 2010-09-12
  • 2019-09-07
  • 2020-02-07
  • 1970-01-01
相关资源
最近更新 更多