在GCC中无需可执行栈间接调用嵌套函数的技术
摘要
本文介绍了一种在GCC中无需可执行栈通过从蹦床读取指针来间接调用嵌套函数的技术,适用于较旧的GCC版本,并在noplate库中实现。
暂无内容
查看缓存全文
缓存时间: 2026/08/29 15:36
# Martin Uecker
来源:https://uecker.codeberg.page/2026-08-29.html
### 在GCC上无须可执行栈间接调用嵌套函数
Martin Uecker, 2026-08-29
#### 引言
我们上次讨论过 (https://uecker.codeberg.page/2026-08-06.html) 如何在 GCC 17 和 Clang 上使用嵌套函数进行回调而无需可执行栈。但如果需要支持旧版本的 GCC 怎么办?当然,可以直接接受使用可执行栈(它并非像某些人声称的那么糟糕),但也可以通过某种技巧避免这一点。
#### GCC:嵌套函数与蹦床机制
首先我们来探讨 GCC 如何支持获取嵌套函数的地址。以下是不使用新宏的简单示例(Godbolt 示例 (https://godbolt.org/z/hv4nM5b1j))。
```
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);
}
```
在 x86_64 架构上,生成的汇编代码如下所示。
```
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
```
这段代码在栈上放置了一个蹦床,并立即通过内联的 `baz` 函数调用它。蹦床是一段简短的代码序列,它将静态帧寄存器加载到栈上的一个结构体中(该结构体包含从父函数捕获的变量),然后跳转到本地函数执行。如果将常量 -17591、-17847 和 -1864106167 转换回汇编指令,会得到以下 x86_64 代码。
```
movq $bar.0, r11
movq $frame, r10
jump *r11
```
静态链和代码地址都是蹦床代码中移动指令使用的立即常量。我们不必使用蹦床的地址来调用函数,而是可以从蹦床中提取代码地址和静态链,并利用 `__builtin_call_with_static_chain` 内置函数直接调用本地函数。例如,在 x64_64 架构(且使用大内存模型)下,可以借助 noplate 的 `peek` 和 `array_slice` (https://uecker.codeberg.page/2025-07-09.html) 宏以如下方式实现。
```
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));
```
这些指针所包含的信息,与在尚未发布的 GCC 17 中使用新内置函数 `__builtin_call_static_chain` 和 `__builtin_call_code_adress` 能获取的信息完全相同,并可用于调用本地函数,具体方法如前所述。
```
__builtin_call_with_static_chain(((typeof(bar)*)code)(arg), chain);
```
因此,我们可以将从蹦床读取这两个指针值作为一种回退机制,用于旧版本的 GCC。缺点在于:仍然会创建蹦床,编译器仍无法去虚拟化间接调用,且栈仍会被标记为可执行。那么这样做获得了什么?由于我们实际上从未调用过蹦床,可以使用以下命令将栈重新设置为不可执行,这至少解决了该功能的安全性担忧。
```
patchelf --clear-execstack program
```
这个想法已在我实验性的库 noplate (https://codeberg.org/uecker/noplate/) 中实现,其中根据代码地址和静态链构造了一个宽指针。
#### 将蹦床用作函数描述符
另一个我认为值得探讨的想法:也可以将蹦床本身用作函数描述符。我们无需在创建蹦床的地方提取静态链和代码指针,而是像通常一样传递蹦床的地址。但在所有可能调用蹦床的地方,我们先检查该指针是否指向一个蹦床,然后提取代码地址和静态链,直接使用 `__builtin_call_with_static_chain` 调用嵌套函数。从某种意义上说,我们并不是在执行蹦床,而是在调用点使用一个超级简单的解释器来解释蹦床的代码——这个解释器非常简单,能够被内联,且仅能解释这种特定的代码序列(Godbolt 示例 (https://godbolt.org/z/YsfWednfr))。
#### 参考文献
- GCC, 嵌套函数 (https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html)
- GCC, 构造函数调用 (https://gcc.gnu.org/onlinedocs/gcc/Constructing-Calls.html)
相似文章
使用GCC的嵌套函数与宽指针及无蹦床 II
GCC 16 确保不捕获变量的嵌套函数无需蹦床,提升安全性。GCC 17 引入了内建函数,通过宽指针和静态链寄存器,即使对于捕获变量的嵌套函数也能避免蹦床。
GCC的嵌套函数实现(对比C++ Lambda表达式)
本文在一篇技术博客文章中解释了GCC嵌套函数的实现,通过创建合成结构来访问父变量,并与C++ Lambda表达式进行了对比。
在 C 中调用一个函数而不给它命名
一场关于在 C 语言中不直接命名就调用函数的技术讨论,涵盖底层语言技巧以及编译器和链接器的行为。
栈回溯技术能实现无泄漏的代码执行
本文探讨了在glibc中使用DWARF字节码进行栈回溯如何被利用来执行代码,并通过CTF挑战和对POSIX系统中线程取消机制的分析加以阐释。
底层Haskell:在Haskell/GHC中模拟内联汇编的邪道方法
本文探讨了在Haskell/GHC中模拟内联汇编的技术,用于调用晦涩的CPU指令以及从外部函数高效返回多个值,并使用了扩宽乘法和无进位乘法等示例。