嵌套函数的实现
我最近发现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);
}
该函数调用一个函数,该函数调用一个从上下文返回计数器并同时递增该计数器的函数。
I recently found out that gcc allows the definition of nested function. In my opinion, this is a cool feature, but I wonder how to implement it.
While it is certainly not difficult to implement direct calls of nested functions by passing a context pointer as a hidden argument, gcc also allows to take a pointer to a nested function and pass this pointer to an arbitrary other function that in turn can call the nested function of the context. Because the function that calls the nested function has only the type of the nested function to call, it obviously can't pass a context pointer.
I know, that other languages like Haskell that have a more convoluted calling convention allow partial application to support such stuff, but I see no way to do that in C. How is it possible to implement this?
Here is a small example of a case that illustrates the problem:
int foo(int x,int(*f)(int,int(*)(void))) {
int counter = 0;
int g(void) { return counter++; }
return f(x,g);
}
This function calls a function that calls a function that returns a counter from the context and increments it at the same time.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
GCC 使用一种称为蹦床的东西。
信息:http://gcc.gnu.org/onlinedocs/gccint/Trampolines.html
蹦床是GCC 在堆栈中创建的一段代码,当您需要指向嵌套函数的指针时使用。在您的代码中,蹦床是必需的,因为您将
g
作为参数传递给函数调用。蹦床初始化一些寄存器,以便嵌套函数可以引用外部函数中的变量,然后跳转到嵌套函数本身。蹦床非常小——您从蹦床“弹跳”到嵌套函数的主体中。以这种方式使用嵌套函数需要一个可执行堆栈,但现在不鼓励这样做。实际上没有任何办法可以解决这个问题。
剖析蹦床:
这是 GCC 扩展 C 中的嵌套函数的示例:
它非常简单,因此我们可以看到它是如何工作的。这是
outer
的结果汇编,减去了一些内容:您会注意到它所做的大部分工作是将寄存器和常量写入堆栈。我们可以继续观察,发现在 SP+4 处它放置了一个 19 字节的对象,其中包含以下数据(采用 GAS 语法):
这很容易通过反汇编程序运行。假设
$nested.1594
为0x01234567
,%rsp
为0x0123456789abcdef
。objdump
提供的反汇编结果为:因此,trampoline 将外部函数的堆栈指针加载到
%r10
中,并跳转到嵌套函数的主体。嵌套函数体如下所示:正如您所看到的,嵌套函数使用
%r10
来访问外部函数的变量。当然,蹦床比嵌套函数本身更大是相当愚蠢的。你可以轻松地做得更好。但使用这个功能的人并不多,这样,无论嵌套函数有多大,trampoline 都可以保持相同的大小(19 字节)。
最后注意事项:在程序集的底部,有一个最终指令:
它指示链接器将堆栈标记为可执行文件。
GCC uses something called a trampoline.
Information: http://gcc.gnu.org/onlinedocs/gccint/Trampolines.html
A trampoline is a piece of code that GCC creates in the stack to use when you need a pointer to a nested function. In your code, the trampoline is necessary because you pass
g
as a parameter to a function call. A trampoline initializes some registers so that the nested function can refer to variables in the outer function, then it jumps to the nested function itself. Trampolines are very small -- you "bounce" off a trampoline and into the body of the nested function.Using nested functions this way requires an executable stack, which is discouraged these days. There is not really any way around it.
Dissection of a trampoline:
Here is an example of a nested function in GCC's extended C:
It's very simple so we can see how it works. Here is the resulting assembly of
outer
, minus some stuff:You'll notice that most of what it does is write registers and constants to the stack. We can follow along, and find that at SP+4 it places a 19 byte object with the following data (in GAS syntax):
This is easy enough to run through a disassembler. Suppose that
$nested.1594
is0x01234567
and%rsp
is0x0123456789abcdef
. The resulting disassembly, provided byobjdump
, is:So, the trampoline loads the outer function's stack pointer into
%r10
and jumps to the nested function's body. The nested function body looks like this:As you can see, the nested function uses
%r10
to access the outer function's variables.Of course, it's fairly silly that the trampoline is larger than the nested function itself. You could easily do better. But not very many people use this feature, and this way, the trampoline can stay the same size (19 bytes) no matter how large the nested function is.
Final note: At the bottom of the assembly, there is a final directive:
This instructs the linker to mark the stack as executable.