在 GCC 上实现嵌套函数的间接调用(无需可执行栈)
Indirect Calling of Nested Functions on GCC Without Executable Stack

原始链接: https://uecker.codeberg.page/2026-08-29.html

本文探讨了一种规避 GCC 嵌套函数“蹦床”(trampoline)安全风险的方法;传统上,这些蹦床需要可执行栈。 当间接调用嵌套函数时,GCC 会在栈上生成一个蹦床——这是一小段可执行代码,用于加载静态链(捕获的变量)并跳转到目标函数。为了避免在栈上执行代码,作者演示了如何通过检查蹦床内存来提取函数的代码地址及其静态链指针。利用 `__builtin_call_with_static_chain` 函数,可以直接调用这些组件,从而无需执行蹦床本身。 通过使用这种提取方法,开发者可以在不牺牲嵌套函数功能的前提下,使用 `patchelf --clear-execstack` 来保护程序安全。作者还提出了一种替代的“解释器”方案,即在调用点识别并解码蹦床,而不是直接执行它们。虽然这些方法依赖于特定实现的蹦床结构,且无法改进编译器优化(如去虚拟化),但它们为在旧版 GCC 中支持嵌套函数提供了一种可行的机制,且无需保持栈的可执行权限。

抱歉。
相关文章

原文

Indirect Calling of Nested Functions on GCC Without Executable Stack

Martin Uecker, 2026-08-29

Introduction

We discussed last time how one can use nested functions on GCC 17 and Clang for callbacks with requiring an executable stack. But what if ones needs to support older versions of GCC? Of course, one can simply accept an executable stack (it is not quite as terrible as some people claim), but it is also possible to avoid this with a hack.

GCC: Nested Functions and Trampolines

Let's discuss first how GCC supports taking the address of a nested function. Our toy example without the use of the new macros is shown below (Godbolt Example).


	typedef int cb_f(int y);

	int baz(cb_f p, int x)
	{
		return p(x);
	}

	int foo(int k)
	{
		int bar(int x) { return k + x; }
    		return baz(bar, 2 * k);
	}
	
On x86_64, the generated assembly is the following.

bar.0:
        movl    %edi, %eax
        addl    (%r10), %eax
        ret
foo:
        subq    $56, %rsp
        leaq    64(%rsp), %rax
        movq    %rax, 32(%rsp)
        movl    %edi, (%rsp)
        leaq    4(%rsp), %rax
        movw    $-17591, 4(%rsp)
        movabsq $bar.0, %rcx
        movq    %rcx, 6(%rsp)
        movw    $-17847, 14(%rsp)
        movq    %rsp, 16(%rsp)
        movl    $-1864106167, 24(%rsp)
        addl    %edi, %edi
        call    *%rax
        addq    $56, %rsp
        ret
	

This code places a trampoline on the stack and immediately invokes it via the inlined baz function. The trampoline is a short code sequence that loads the static frame register to a structure on the stack that contains the captured variables from the parent function and then jumps to the local function. If one translates the constants -17591, -17847, and -1864106167 back to assembly instructions one obtains the following x86_64 code.


        movq	$bar.0, r11
        movq    $frame, r10
	jump	*r11
	

Both, the static chain and the code address are immediate constants used by move instructions in the code of the trampoline. Instead of using the address of the trampoline to call the function, we can extract the code address and the static chain from the trampoline and use them with the __builtin_call_with_static_chain built-in function to call the local function directly. For example, using noplate's peek and array_slice macros, this could be done in the following way for x64_64 (and a large memory model).


	unsigned char (*tramp)[24] = (void*)bar;
	void *code = peek(uint64_t, &array_slice(tramp, 2, 10));
	void *chain = peek(uint64_t, &array_slice(tramp, 12, 20));
	

These pointers are then exactly the same information that can be obtained on the yet to-be-released GCC 17 with the new built-ins __builtin_call_static_chain and __builtin_call_code_adress and can be used to call the local function with as discussed previously.


	__builtin_call_with_static_chain(((typeof(bar)*)code)(arg), chain);
	

Thus, we can use reading of these two pointer values from a trampoline as a fallback mechanism in older versions of GCC. The downsides are that a trampoline is still created, the compiler can still not devirtualize the indirect call, and the stack will still be marked executable. So what was gained? Since we never actually invoke the trampoline, we can make the stack non-executable again with the following command, which at least addresses the security concerns of this feature.


	patchelf --clear-execstack program
	

This idea is implemented in my experimental library, noplate, where a wide pointer is constructed from the code address and the static chain.

Trampolines as Function Descriptors

There is another idea I find worth exploring: One could also use the trampoline itself as a function descriptor. Instead of extracting the static chain and code pointer where the trampoline is created we just pass on the address of the trampoline as usually. But everywhere where we might call the trampoline, we first check whether the pointer points to a trampoline, and then extract code address and static chain to call the nested function directly using __builtin_call_with_static_chain. In some sense we could say that instead of invoking the trampoline, we are interpreting the code of the trampoline at the call site using a super simple interpreter that only can interpret this specific code sequence and that is so simple that it can be inlined (Godbolt Example).

Literature

联系我们 contact @ memedata.com