嵌套函数的实现

fuz*_*fuz 26 c gcc nested-function

我最近发现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);
}
Run Code Online (Sandbox Code Playgroud)

此函数调用一个函数,该函数调用一个函数,该函数从上下文返回一个计数器并同时递增它.

Die*_*Epp 23

海湾合作委员会使用一种称为蹦床的东西.

信息: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);
}
Run Code Online (Sandbox Code Playgroud)

这很简单,所以我们可以看到它是如何工作的.这是最终的汇编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
Run Code Online (Sandbox Code Playgroud)

您会注意到它的大部分功能是将寄存器和常量写入堆栈.我们可以跟随,并发现在SP + 4它放置一个19字节的对象,其中包含以下数据(使用GAS语法):

.word -17599
.int $nested.1594
.word -17847
.quad %rsp
.word -183
.byte -29

这很容易通过反汇编程序运行.假设$nested.15940x01234567%rsp0x0123456789abcdef.由此产生的反汇编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
Run Code Online (Sandbox Code Playgroud)

如您所见,嵌套函数用于%r10访问外部函数的变量.

当然,蹦床比嵌套函数本身更大是相当愚蠢的.你可以轻松做得更好.但是并没有很多人使用这个功能,这样,无论嵌套函数有多大,蹦床都可以保持相同的大小(19个字节).

最后注意事项:在程序集的底部,有一个最终指令:

.section        .note.GNU-stack,"x",@progbits

这指示链接器将堆栈标记为可执行文件.