在 GCC 與 Clang 中使用巢狀函式而不依賴可執行堆疊
綜合科技

在 GCC 與 Clang 中使用巢狀函式而不依賴可執行堆疊

AI News Bot
2026-08-30
Photo by Daniil Komov on Pexels
預計閱讀 2 分鐘原文來源

在 GCC 與 Clang 中使用巢狀函式而不依賴可執行堆疊

在 C 語言的 巢狀函式(nested function)常被拿來實作回呼(callback)時,GCC 17 與 Clang 會自動在堆疊上產生 trampoline(跳板程式碼),並以 可執行堆疊(executable stack)呼叫。雖然接受可執行堆疊的安全風險已被弱化,但在需要支援較舊版 GCC 時,仍希望避免此需求。本文說明一種不依賴可執行堆疊的「hack」做法,並解析其原理與限制。

1. GCC 產生 trampoline 的機制

以下簡化程式展示了巢狀函式的典型寫法(取自原文的 Godbolt 範例):


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);

}

編譯後的 foo 會在堆疊上配置 24 位元組的 trampoline,內容類似:


movq $bar.0, %r11          ; 函式位址

movq $frame, %r10          ; 靜態鏈(static chain)指向捕獲變數結構

jmp  *%r11                 ; 跳至實際程式碼

trampoline 同時儲存 靜態鏈(父函式的環境指標)與 程式碼位址,然後透過 call *%rax 直接呼叫。這就是為什麼需要可執行堆疊:trampoline 本身必須能被 CPU 執行。

2. 從 trampoline 取出資訊,直接呼叫本地函式

若不想讓 CPU 執行堆疊上的程式碼,我們可以讀取 trampoline 內的兩個常數,分別得到:

  • code :指向巢狀函式本身的程式碼位址
  • chain :指向靜態鏈的指標

原文示範了使用 peek 與 array_slice 巨集取得這兩個指標(以 x86_64 大記憶體模型為例):


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 內建函式 __builtin_call_with_static_chain 即可直接呼叫:


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

此呼叫方式等價於原本的 trampoline,但 不再需要可執行堆疊,因為實際執行的程式碼位於常規的可執行段落。

3. 為何這是舊版 GCC 的備援方案

GCC 17 正式引入了 __builtin_call_static_chain 與 __builtin_call_code_address 兩個新內建,直接提供 code 與 chain。在尚未支援這兩個內建的舊版 GCC(如 GCC 12、13),仍可透過 手動解析 trampoline 的方式取得相同資訊,達到相同效果。

缺點:

  1. 仍會產生 trampoline:編譯器仍會在堆疊上放置 trampoline,只是我們不使用它執行。

  2. 間接呼叫無法被去虛擬化(devirtualize):編譯器無法在編譯期確定呼叫目標,可能影響效能。

因此,此技巧適合作為 兼容性備援,而非長期最佳化手段。

4. 未來展望與讀者建議

  • 隨著 GCC 17 及更新版的普及,開發者可直接改用官方提供的 __builtin_call_static_chain 系列,省去手動解析的繁瑣與潛在錯誤。
  • 若必須支援舊版編譯器,建議將上述 hack 包裝成可重用的宏或函式庫,並在編譯時偵測 GCC 版本自動切換。
  • 安全導向的程式碼基礎仍應盡量避免依賴可執行堆疊,尤其在嵌入式或高安全性環境中。

結論:透過讀取 trampoline 內的程式碼位址與靜態鏈,並搭配 __builtin_call_with_static_chain,開發者可以在不啟用可執行堆疊的前提下,讓 GCC 舊版也能支援巢狀函式的回呼機制。此方法雖有一定限制,但為跨版本相容提供了實用的過渡方案。

分享