我最近发现gcc允许定义嵌套函数。在我看来,这是一个很酷的功能,但我想知道如何实现它。
虽然通过将上下文指针作为隐藏参数传递来实现嵌套函数的直接调用当然不难,但gcc还允许获取指向嵌套函数的指针并将该指针传递给任意其他函数,而该函数又可以调用上下文的嵌套函数。因为调用嵌套函数的函数只有要调用的嵌套函数的类型,所以它显然无法传递上下文指针。
我知道,像Haskell这样的其他语言有更复杂的调用约定允许部分应用程序支持这些东西,但我认为在C中没有办法做到这一点。如何实现呢?
以下是一个说明问题的案例的小例子:
int foo(int x,int(*f)(int,int(*)(void))) {
int counter = 0;
int g(void) { return counter++; }
return f(x,g);
}
此函数调用一个函数,该函数调用一个函数,该函数从上下文返回一个计数器并同时递增它。
答案 0 :(得分:23)
GCC使用一种叫做蹦床的东西。
信息:http://gcc.gnu.org/onlinedocs/gccint/Trampolines.html
trampoline是GCC在堆栈中创建的一段代码,用于需要指向嵌套函数的指针时。在您的代码中,蹦床是必要的,因为您将g
作为参数传递给函数调用。 trampoline初始化一些寄存器,以便嵌套函数可以引用外部函数中的变量,然后它跳转到嵌套函数本身。蹦床很小 - 你蹦蹦跳跳"离开蹦床并进入嵌套功能的主体。
使用嵌套函数这种方式需要一个可执行堆栈,这些天不鼓励这样做。没有任何办法解决它。
解剖蹦床:
以下是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 mov $0x1234567,%r11d 6: 49 ba ef cd ab 89 67 mov $0x123456789abcdef,%r10 d: 45 23 01 10: 49 ff e3 rex.WB jmpq *%r11
因此,trampoline将外部函数的堆栈指针加载到%r10
并跳转到嵌套函数体。嵌套的函数体看起来像这样:
movl (%r10), %eax
addl %edi, %eax
ret
如您所见,嵌套函数使用%r10
来访问外部函数的变量。
当然,蹦床比嵌套功能本身更大是相当愚蠢的。你可以轻松做得更好。但是并没有很多人使用这个功能,这样,无论嵌套函数有多大,蹦床都可以保持相同的大小(19个字节)。
最后注意事项:在程序集的底部,有一个最终指令:
.section .note.GNU-stack,"x",@progbits
指示链接器将堆栈标记为可执行文件。