2010年8月22日 星期日

淺談 GCC 中的 trampoline (1)

最近正好看到前輩 Jserv 的文章 GCC 的 nested function 與 trampoline 以及學長Thinker的 補充說明 後,由於這個領域也算是小弟的守備範圍,又巧逢本格乾旱已久,也好久沒有文章了,就當成是久旱逢甘霖,搭一下順風車淺談一下這個議題。這篇文章會以 GCC 的觀點來切入 trampoline ,並且提及在不同平台中支援 trampoline 的注意事項。不巧的是GCC 4.5 系列 trampoline 的 API 正好有作一些更改,把一些原本是 Macro 的 API 改成使用 Target Hook 的方式,雖然跟功能性沒有什麼太大的關係,由於小弟還沒跟上 GCC 4.5 系列,為了重現臨場感,所以本篇文章如果有使用到 GCC 的原始碼的話,是使用GCC-4.4 系列的原始碼為範例的,請讀者見諒。

Trampoline 字面的意思是體操用的彈翻床,所以就有跳(Jump)的意思存在,其實trampoline這個字在計算機科學領域是有多重意義存在的,不過大多跟跳有關就是了,trampoline詳細的定義和意義可以參考英文維基 Trampoline (computers) 條目中的內容,這邊不再贅述。而 GCC中 Trampoline 主要是用來支援一個以前在程式語言界蠻流行的功能 Nested function,而 Nested function 最主要的目的是被設計來實現 information hiding 的,information hiding 在那個年代被認為是提高軟體工程師生產力的秘密武器,而且也是廣為流行的,就把它想成現在的嵌入式程式阿宅都把灌"吸"當王道這樣就好了。或許讀者就會問說:「好好的 C 語言標準不支援,支援這種奇怪的東西幹麻?」當然除了它可以實現一些很奇特的功能像 Functional  Language 的closure 之類的東西,最重要的原因就是歷史因素,這個歷史因素也很簡單,那就是 Glibc 有用到這個功能,所以 GCC 就支援這個功能了。

如同上一段所說的,傳統上來說 Nested Function 是被設計來實現 information hiding 的,所以變數的Scope (變數的有效範圍) 是 Nested Function 中很重要的議題,就語言學來說,其實變數的Scoping 有分成 Dynamic Scoping 和 Static Scoping,用白話來說,假設每個函式有分成外圈和內圈好了,所謂的 A 比 B 還外圈就是代表 B 被允許存取 A 的資料,而 Static Scoping 的語言代表 A 跟 B 的關係在 Static Time (也就是所謂的 Compiling Time) 決定的,而 Dynamic Scoping 的語言代表 A 跟 B 的關係是在 Dynamic Time (也就是所謂的 Runtime) 決定的。舉個簡單的例子來說明,

procudure A
       procudure B
            XXXX

end

procudure C

    A()
end
   

就 Static Scoping 的語言來說 B 是 A 的內圈,可是就 Dynamic Scoping 的語言來說 A 是 C 的內圈。以實作上來說,內圈函式為了存取外圈函式的變數,內圈函式的 Stack Frame 中就必須要建一條 Link 到外圈函式的 Stack Frame 去,而連結到 Static Scope 外圈函式的 Link 稱為 Static Chain 而連結到 Dynamic  Scope 外圈函式的 Link 稱為 Dynamic  Chain 。

就學理來說,Static Typing 的語言通常會使用 Static Scope ,所以我們偉大的灌"C"哥非常靜態,所以可以想見在C語言上實作 Nested Function 也應該是屬於 Static Scoping 的 ,既然是這樣,所以我們也可以很快的想到 "這樣的系統" 是必須要有 Static Chain 的。所以 GCC 中的 trampoline 需要負責做兩件事情,就是 1.幫將被呼叫的函式產生  Static Chain,然後 2.跳轉到 被呼叫的函式去。GCC 為了減少執行時期的開銷,Static Chain 是存放在一個稱為 Static Chain Register 的暫存器內,想當然爾這個暫存器最好是 Caller-Save Register,而且每個平台會使用不同的Register。以x86 32位元的平台來說,Static Chain Register 是 ECX ,然而 x86-64 ECX已經被當成參數來使用,所以使用R10。最後附帶一提的是,由於 trampoline 的機制必須要在執行在 Stack 上的程式碼,大部分的 RISC Machine ( 因為 instruction 跟 data caches分離) 在trampoline 複製到 Stack 中的時候因為不知道他是指令,所以會產生 Memory 的值跟 Instruction Cache 中的值不一致的狀況,所以在使用這種機制時就必須要 clear instruction cache ,以避免執行到 instruction cache 中的舊指令。