假设在一段C代码中,我有一个函数 foo ,它调用 bar 。在栏内,我可以使用程序集来获取栏将返回的地址。如何使用此信息确定 foo 的地址?
一种方法是获取 foo 将返回的返回地址,并从调用指令的操作码中获取调用 foo <的地址/ strong>即可。然而,这需要知道使用哪种调用方法(例如,偏移/绝对),因此不可靠。有没有更简单的方法来确定调用者的地址?
编辑:我忘了提到这个问题是关于32位Intel unix机器上的IA32汇编。
答案 0 :(得分:2)
一种方法是获取foo将返回的返回地址,并从调用foo的调用指令的操作码中获取地址。
嗯?这将为您提供 bar 的地址,而不是 foo。
您所需要的只是低于返回地址的最高过程入口点。
答案 1 :(得分:2)
假设存在常规页面框架并且通过常规调用调用bar
(而不是寄存器间接调用)以获取bar
的地址,那么您将进一步“退出”一级并找到call bar
指令。
在foo
中,您的堆栈看起来像:
.
.
parameters to bar (if any)
return address, i.e. address following 'call bar'
saved base page (ebp register) value
locals to bar
...
parameters to foo (if any)
return address, i.e. address following 'call foo' within bar
saved base page (ebp register) value
locals to foo
所以要从bar
中获取foo
的地址,你会做类似下面的事情(这是我的头脑,所以可能需要进行微调,但你应该得到一般的想法)。
mov eax, [ebp] // load calling scope (bar's) frame pointer
mov eax, [eax+4] // load the return address for bar
mov edx, [eax-4] // load offset from the call instruction that called bar
lea eax, eax+edx // adjust (or something similar) to convert from offset to abs
答案 2 :(得分:2)
在Linux中,您可以使用dladdr()
来解析调用函数,方法是使用:
#define _GNU_SOURCE
#include <dlfcn.h>
...
void *retAddr = __builtin_extract_return_addr(__builtin_return_address(0));
Dl_info d;
(void)dladdr(retAddr, &d);
printf("%s called from %s + 0x%p\n",
__FUNC__,
d.dli_sname,
(retAddr - d.dli_saddr));
有关详细信息,请参阅GCC docs, __builtin_return_address()
和Linux联机帮助页dladdr(3)
。
函数dladdr()
在Solaris / MacOSX / * BSD上也可用,但需要其他预处理器定义而不是_GNU_SOURCE
才能看到;请参阅相应操作系统的联机帮助页...
编辑请注意,由于这依赖于符号表的存在,因此它可能不在已剥离的二进制文件上成功解析。我没有尝试过将错误处理添加到上面;通常,任何类型的自动回溯(具有函数名称解析)支持都不喜欢剥离符号表。
对于一个非常快的,我有时只是使用:
#include <execinfo.h>
...
void *retAddr[10];
backtrace_symbols_fd(retAddr, backtrace(retaddr, 10), STDERR_FILENO);
因为它获得了十个条目的深度堆栈跟踪。再次,依赖于没有删除symtabs。由于您要解决多个地址,因此性能会受到影响。
Edit2: 没有符号表(其中包含起始地址和 size 的功能)在可执行文件/库中),什么是“起始地址”的信息是没有意义的;就CPU本身而言,实际上并没有记录指令指针如何到达特定时刻的位置 - 相当于goto
(jmp
)或其他的汇编。自修改指令的奇怪混合与CPU一样“有效”,正如结构良好的编译器生成的代码一样。 x86指令是可变大小,并且操作码映射密集足以使几乎任何随机字节序列构成“有效”指令流;因此,二进制代码的启发式向后反汇编不是100%安全的事情。
在这个意义上,符号表也为调试器建立“标记”。如果您开始在符号表中记录的函数起始地址进行反汇编,则可以期望找到有效的指令流,并且可以通过验证在回溯中找到的任何返回地址实际上都以call
开头来交叉验证指令。