下面我将尝试解释如何从一个函数中返回两次。
我从一开始就警告你,这一切都是黑客行为。
但是有很多地方使用了这些技巧。
首先假设我们有以下 C 程序。
#include <stdio.h>
uint64_t saved_ret;
int main(int argc, char *argv[])
{
if (saveesp()) {
printf("here! esp = %llX\n", saved_ret);
jmpback();
} else {
printf("there! esp = %llX\n", saved_ret);
}
return 0;
}
现在我们希望 saveesp() 返回两次,这样我们就可以访问两个 printf。
下面是 saveesp() 的实现方式:
#define _ENTRY(x) \
.text; .globl x; .type x,@function; x:
#define NENTRY(y) _ENTRY(y)
NENTRY(saveesp)
movq (%rsp), %rax
movq %rax, saved_ret
movl $1, %eax
ret
NENTRY(jmpback)
xorq %rax, %rax
pushq saved_ret
ret
这绝不是可移植的代码。但是您可以为所有想要支持的平台编写类似的程序集存根。
saveesp() 所做的是,它获取存储在堆栈中的返回地址并将其保存到局部变量中。然后它返回 1。这是一个非零返回,它将我们带到第一个 printf。
在 printf() 之后我们调用 jmpback()。这是真正的黑客。这个函数使得 saveesp() 看起来第二次返回。
它通过将保存的返回地址压入堆栈并执行 ret 来实现。 ret 将从堆栈中弹出地址并跳转到它。这次返回代码设置为零。因此,当我们“到达”我们的 C 例程时,看起来我们刚刚从 saveesp() 返回值为零。这样就到达了第二个 printf。
如果您对此类 hack 感兴趣,您应该阅读更多关于用于实现异常处理的 C 标准中的 setjmp 和 longjmp。
此外,我们实际上在 OpenBSD 内核中的挂起/恢复代码路径上使用了它。
看看第 231 和 250 行的here,它与上面的 C 代码几乎相同。然后看看汇编代码here,第 542 行是第一次在挂起时返回的 savecpu 函数,第 375 行是我们在恢复时第二次返回的地方。