プログラミング
GCCのネスト関数をワイドポインタとトランポリンなしで使用する II
Using GCC's Nested Functions with Wide Pointers and No Trampolines II (uecker.codeberg.page)
要約
この記事は、GCCのネスト関数機能におけるセキュリティとパフォーマンスの改善について論じています。GCC 16では、親関数の変数をキャプチャしないネスト関数はトランポリンを必要としなくなり、GCC 17では、__builtin_call_with_static_chainなどの組み込み関数と新しいパッチにより、キャプチャするネスト関数でもトランポリンなしで安全に扱えるようになりました。これにより、スタックの実行可能性の必要性がなくなり、パフォーマンスが向上します。
全文翻訳
ブログ連絡先/インプリント
GCCのネスト関数をワイドポインタとトランポリンなしで使用する II
Martin Uecker, 2026-07-14
はじめに
ネスト関数のアドレスが取得されると、GCCは実行時にトランポリンを作成します。このトランポリンはスタック上に配置されるため、スタックは実行可能である必要があります。これは、実行不可能なスタックが重要なセキュリティ機能であるため問題となります。しばらくの間、GCCにはトランポリンをヒープ上に配置するオプションがありましたが、これはまだ理想的ではありません。なぜなら、割り当てにはコストが高く、longjmpが使用されるとトランポリンがリークする可能性があるからです。以前、この問題を新しいワイドポインタ型で回避する方法と、これに必要なGCCの予備パッチについて述べました。ここでは2つのアップデートを提供します。
GCC 16: キャプチャなしのネスト関数
まず、GCC 16がリリースされました。GCC 16では、親関数の変数をアクセスしないネスト関数はトランポリンを必要としないことが保証されています。実際には、最適化時にはすでに保証されていましたが、最適化しない場合でも保証され、文書化されています。これは、そのような関数を親から安全に返すことができ、ローカルコンテキストをアクセスする関数を返そうとすると警告が表示されることも意味します(少なくともコンパイラが容易に認識できる単純なケースでは)。以下の例を参照してください(Godbolt Example)。
typedef int cb_f(int);
cb_f *foo(int x) {
int worker(int y) {
return x + y; // キャプチャ
}
return worker;
}
cb_f *bar(int _x) {
static int x;
x = _x;
int worker(int y) {
return x + y; // キャプチャなし
}
return worker;
}
親関数の変数をアクセスしないネスト関数は、静的変数、名前付き定数、または型(可変修飾されていない場合)にアクセスできます。これはコールバック関数を記述するのに役立ちます(Godbolt Example)。
typedef int cb_f(void *, int y);
int process(cb_f *cb, void *data) {
return cb(data, 5);
}
int foo(int x) {
struct { int x; } data = { .x = x };
int worker(void *_data, int y) {
typeof(data) *data = _data;
return data->x + y;
}
return process(worker, &data);
}
それでも、ネスト関数の真の力は、親関数の変数に直接アクセスできることです。また、上記のコードは、手動でボイラープレートコードを追加してそのような関数をシミュレートしているように見えます。コンパイラは確かにこれを支援できるはずです。これが私の2番目のアップデートです。
GCC 17: キャプチャ付きネスト関数
最近、GCCの開発ブランチに私のパッチのバージョンがマージされ、既存の組み込み関数__builtin_call_with_static_chainと組み合わせて使用できる2つの新しい組み込み関数が実装されました。これにより、トランポリンを必要とせずに、この例(Godbolt Example)を記述できます。
typedef int cb_f(int y);
static int process(cb_f *cb, void *data) {
return __builtin_call_with_static_chain(cb(5), data);
}
int main() {
int x = 7;
int worker(int y) {
return x + y;
}
return process(__builtin_call_code_address(worker), __builtin_call_static_chain(worker));
}
実際、トランポリンがなくなったことで、GCCが内部で行っていることはすべて、上記の例に自動的に変換されることです。手動で記述されたバージョンとの唯一の違いは、データポインタが特別なレジスタを介して渡されることです。このレジスタは、多くの他のプログラミング言語でスタティックチェーンを渡すために使用されており、ABIによってこの用途に予約されています。適切な言語サポートがなくても、汎用ワイドポインタ型を定義し、組み込み関数をマクロでラップすることで、この例をもう少しきれいにすることができます。(Godbolt Example)
#define wide(T) struct wide_##T { typeof(T) *code; void *chain; }
typedef int cb_f(int y);
int baz(wide(cb_f) p, int x) {
return CALL(p, (x));
}
int foo(int k) {
int bar(int x) {
return k + x;
}
return baz(CLOSURE(cb_f, bar), 2 * k);
}
コンパイラは、これを3倍の乗算を実装する単一のアセンブリ命令に喜んで最適化します。
foo:
lea eax, [rdi+rdi*2]
ret
展望
GCC以外のコンパイラもネスト関数をサポートしていますが、この機能は実際にはポータブルではありません。将来的には、clangとGCCの両方でネスト関数をどのように使用できるか、そして古いバージョンのGCCでもトランポリンの作成を回避する方法について、ハックを用いて議論します。
文献
GCC, GCC 16 Release Series Changes, New Features, and Fixes
GCC, Nested Functions
GCC, Constructing Function Calls