使用__builtin_extract_return_addr()函数查找ret指令的RSP值

Using __builtin_extract_return_addr() function to find the RSP value of ret instruction

我最近一直在尝试使用此处描述的 __builtin_extract_return_addr 函数 (https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html) 来获取 RSP 指针的编码值。我有意避免使用 __builtin_return_address(0) 函数,而只是尝试使用 RSP 寄存器将 return 地址值提供给调用者。

该功能的说明如下:

void * __builtin_extract_return_addr (void * addr)

The address as returned by __builtin_return_address may have to be fed through this function to get the actual encoded address. If no fixup is needed, this function simply passes through addr.

据我了解,这个函数好像可以取任意地址,得到实际编码后的地址。 (例如,如果 RSP 0x7fffffffe458 —▸ 0x40058e (main+14), 然后使用 __builtin_extract_return_addr(0x7fffffffe458) 应该是 0x40058e )

所以我有这个非常简单的测试代码,我一直在用它来了解一些这方面的知识,但没有得到我想要得到的值所以我想在 Whosebug 中问这个问题:

void print_sp() {
   register void *sp asm ("rsp");
   printf("%p\n", __builtin_extract_return_addr(sp));

   void *addr = 0x7fffffffe458;
   printf("%p\n", __builtin_extract_return_addr((addr)));

   printf("%p\n", __builtin_return_address(0)); // I am trying to avoid using this
}

int main() {
   print_sp();
}

在 print_sp() 函数的前两行中,我正在读取并打印 RSP 寄存器值,然后使用 builtin_extract_return_addr 查看是否可以获取什么的编码地址存储在 RSP 寄存器中。这是我使用 gdb 调试时的失败,我理解它是因为调用此行时的 RSP 寄存器值将没有调用者的 return 地址。

在print_sp()函数的后两行中,我将void *addr硬编码为0x7fffffffe458的值,然后使用该地址值查看是否可以得到解码后的return 地址。原因是在ret指令时,RSP值如图 here 如下:

RSP 0x7fffffffe458 —▸ 0x40058e (main+14) ◂— mov eax, 0

总而言之,我正在尝试使用 __builtin_return_address(0) 值 return 获取 0x400578 没有 的地址值。

我也尝试过使用内联汇编来实现 addq , %%rsp; jmpq -8(%%rsp) 但无济于事。是这样的吗?

uintptr_t result;
asm volatile ( "mov %%rsp, %[value]\n\t"
               "addq ,   %[value]\n\t"
                 : [value]"=a"(result)
                 : ); 
uintptr_t  caller_address = (uintptr_t)__builtin_extract_return_addr(result);

Here是print_sp和main()函数

的反汇编

此外,我在 Whosebug 上看到了一些类似的问题: Retq instruction, where does it return Meaning of 0x8($rsp)

我希望这个问题是有道理的。如果有任何不清楚的地方,请告诉我,我会尽快澄清。

谢谢,

我不确定您为什么要避免使用 __builtin_return_address(0),因为它可能是您可以用来获取 return 地址的最佳选择。如果您可以使用它,请使用它,但我们暂时同意。

你的反汇编表明你在函数序言中使用了帧指针寄存器,这意味着你可能在编译中使用了零优化。在这种情况下,您可以使用 rbp 而不是 rsp,因为它在函数开头采用 rsp 的值并保持不变。但是,您需要将 +1 添加到从 rbp 获得的指针(请注意,将 +1 添加到 long 指针实际上会在 x86_64 中添加 8 个字节)。

这样做是因为在将rsp复制到rbp的mov指令之前有一条push指令将旧的rbp值压入栈中,所以我们存储在rsp中的栈指针已经减少(栈向下增长)了8 个字节用于保存先前存储在 rbp 中的旧帧指针。

将 RBP 设置为帧指针的过程在 x86-64 调用约定中是可选的。 16 位 x86 曾经需要这个,一些 32 位调用约定依赖它来进行回溯,但 x86-64 调用约定不需要。如果您需要了解更多关于 return 地址在堆栈中相对于帧指针和堆栈指针的存储位置,您可以阅读它。

我认为不推荐使用寄存器变量,因此我将修改您的第二种内联汇编方法。

  // works only if compiled without optimization, or -fno-omit-frame-pointer
  // RBP points to the saved-RBP value just below the return address
void *bp;
asm ("movq %%rbp, %0\n"
     : "=r" (bp));
void *ra;
ra = (void *) *((long *)bp + 1);

ra 现在应该是您的 return 地址。不幸的是,这仅在您的编译器使用帧指针寄存器 rbp 时有效,如果您在编译时使用优化,它通常不会成立。例如,如果您使用 GCC,除 -O0 之外的任何级别都会在使用此方法时出错。

以防万一,您应该使用__builtin_extract_return_addr(ra)。我不需要它。另请注意,除非您链接的是非 PIE 可执行文件,否则 return 地址将是您在反汇编中看到的值 + 可执行文件起始地址(这是您的可执行文件在内存中加载的位置)。您可以通过全局声明 extern char __executable_start; 然后打印其地址来获取此地址。