嵌套函数的实现

Posted

技术标签:

【中文标题】嵌套函数的实现【英文标题】:Implementation of nested functions 【发布时间】:2012-01-01 01:06:05 【问题描述】:

我最近发现 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);

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

【问题讨论】:

我没有意识到你可以传递指向嵌套函数的指针。这是关于它们如何工作的一个非常好的问题 - 大概在外部函数返回后调用指针会导致不良行为? @Autopulated 这实际上是正确且合乎逻辑的,因为相应的堆栈帧不再存在。 很难称其为“酷”功能。 【参考方案1】:

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.15940x01234567 并且%rsp0x0123456789abcdef。由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

这指示链接器将堆栈标记为可执行。

【讨论】:

难道不能在返回时使用malloc 和最终的free 将蹦床放在堆上吗? 作为避免蹦床的机制,函数序言将access link 压入堆栈应该不会太难吧? @sarnold 如果我理解正确的话,使用访问链接并不能真正解决问题。如果从参数函数f 调用foo,哪个计数器会增加?显然,没有额外的信息是无法追踪函数指针对应的栈帧的…… @FUZxxl:这会随着longjmp 泄漏——你将如何处理从malloc 返回的NULL @DietrichEpp,AFAIK,Pascal 没有过程或指向过程的指针类型,不能将过程作为参数传递。

以上是关于嵌套函数的实现的主要内容,如果未能解决你的问题,请参考以下文章

Java嵌套类的作用、用法和调用机制是怎么样的?

多层嵌套可迭代列表的剥皮函数

怎么利用IF函数多个条件进行嵌套?

嵌套函数内的 cProfile

在类中如何实现类的嵌套

使用基于堆栈计算机的语言添加嵌套函数支持