GCC 使用一种叫做蹦床的东西。
信息:http://gcc.gnu.org/onlinedocs/gccint/Trampolines.html
trampoline 是 GCC 在堆栈中创建的一段代码,当您需要指向嵌套函数的指针时可以使用该代码。在您的代码中,蹦床是必要的,因为您将 g 作为参数传递给函数调用。蹦床初始化一些寄存器,以便嵌套函数可以引用外部函数中的变量,然后跳转到嵌套函数本身。蹦床非常小——你从蹦床上“弹回”并进入嵌套函数的主体。
以这种方式使用嵌套函数需要一个可执行堆栈,现在不鼓励这样做。真的没有办法解决它。
蹦床的解剖:
这是 GCC 扩展 C 中嵌套函数的示例:
void func(int (*param)(int));
void outer(int x)
{
int nested(int y)
{
// If x is not used somewhere in here,
// then the function will be "lifted" into
// a normal, non-nested function.
return x + y;
}
func(nested);
}
它非常简单,因此我们可以看到它是如何工作的。这是outer 的结果程序集,减去一些内容:
subq $40, %rsp
movl $nested.1594, %edx
movl %edi, (%rsp)
leaq 4(%rsp), %rdi
movw $-17599, 4(%rsp)
movq %rsp, 8(%rdi)
movl %edx, 2(%rdi)
movw $-17847, 6(%rdi)
movw $-183, 16(%rdi)
movb $-29, 18(%rdi)
call func
addq $40, %rsp
ret
您会注意到它所做的大部分工作是将寄存器和常量写入堆栈。我们可以继续往下看,发现在 SP+4 处,它放置了一个 19 字节的对象,其中包含以下数据(在 GAS 语法中):
.word -17599
.int $nested.1594
.word -17847
.quad %rsp
.word -183
.byte -29
这很容易通过反汇编程序运行。假设$nested.1594 是0x01234567 并且%rsp 是0x0123456789abcdef。由objdump 提供的反汇编结果是:
0: 41 bb 67 45 23 01 移动 $0x1234567,%r11d
6: 49 ba ef cd ab 89 67 mov $0x123456789abcdef,%r10
电话:45 23 01
10: 49 ff e3 rex.WB jmpq *%r11
因此,蹦床将外部函数的堆栈指针加载到%r10 并跳转到嵌套函数的主体。嵌套函数体如下所示:
movl (%r10), %eax
addl %edi, %eax
ret
如您所见,嵌套函数使用%r10 来访问外部函数的变量。
当然,蹦床比嵌套函数本身大是相当愚蠢的。你可以轻松地做得更好。但是使用这个功能的人并不多,这样无论嵌套函数有多大,蹦床都可以保持相同的大小(19字节)。
最后说明:在程序集的底部,有一个 final 指令:
.section .note.GNU-stack,"x",@progbits
这指示链接器将堆栈标记为可执行。