July 29, 2010
GCC 的 nested function 與 trampoline
GCC 提供特有的 C 語言 extension,允許像 Pascal 一般,定義 nested function,詳情可參考 GCC 手冊 [Nested Functions],注意,由於程式語言設計的一致性考量,此機制不在 GNU C++ 支援。nested function 的形式很明顯,就是在定義於另一個函式內部的函式,以下引用 GCC 手冊的範例:由上可見,函式 bar() 的程式碼實做中有個 nested functon -- accss(),後者可存取到前者的變數 offset,符合 lexical scoping 的規範。乍看 nested function 只是一種語法上的 syntax sugar?非也,事實上,可在 nested funtion 的 scope 外,將其位址傳遞給其他函式來呼叫。我們可以撰寫以下的程式碼: (nested.c)
嘗試編譯並執行:
$ gcc -g -o nested{,.c} && ./nested
447
427
447
筆者定義了一個名為 f 的函式,其返回值是一個指向 nested function -- nested 的位址,需要留意到,這裡的 nested 函式實做,相當於每次執行時,都動態產生一個實體。於是,一旦搭配 typedef,就可用 "func_t g = f(0x1ab)" 這類的語法,去從 f 函式「合成」一個新函式 g,而呼叫 g(20) 的輸出,就是 427 (即 0x1ab 的十進位表示) + 20 = 447。同理,寫成 "f(f(400)(27))(20)",可理解為,先產生函式 "(*f(400))(int)",再用數值 27 帶入,這時得到一個數值 400 + 27,之後再帶入函式 f 中,得到「合成」的函式,再將數值 20 帶入,這時候就是 400 + 27 + 20。由上可見,引入 nested function 後,讓我們進行 C 語言程式開發時,有了形式上的轉變,這表示可實現 closure in C 一類的高階技巧。可是,函式 nested 既然存在於函式 f 中,到底如何在執行時期取得位址?又,這個動態「生成」的函式,顯然有獨立的 context,否則上述一連串的函式呼叫組合,會使運算結果不同。
筆者的環境是 Ubuntu Linux 10.10 (pre-release) 32-bit + lenovo X200 + gcc-4.5.0,咱們用 GDB 來觀察剛剛的 nested.c:
$ gdb ./nested GNU gdb (GDB) 7.1-ubuntu ... (gdb) b main Breakpoint 1 at 0x8048484: file nested.c, line 13. (gdb) r Starting program: /home/jserv/tmp/nested Breakpoint 1, main () at nested.c:13 13 func_t g = f(0x1ab);將中斷點設定在 main() 函式後,很快就看到取得 nested function 位址並「合成」新函式的操作,我們先印列該位址並將機械碼寫到檔案,以利後續分析:
(gdb) n
14 printf("%d\n", (*g)(20));
(gdb) print g
$1 = (func_t) 0xbfffec40
(gdb) dump memory out.bin 0xbfffec40 0xbfffec4a
(gdb) quit
A debugging session is active.
Inferior 1 [process 31464] will be killed.
Quit anyway? (y or n) y
我們從位址 0xbfffec40 寫入長度為 0xa,也就是 10 bytes 的機械碼到檔案 out.bin,那直接反組譯看看:$ objdump -m i386 -b binary -D out.bin ... Disassembly of section .data: 00000000 <.data>: 0: b9 3c ec ff bf mov $0xbfffec3c,%ecx 5: e9 ca 97 04 48 jmp 0x480497d4等等,這跟我們預期的函式實做有很大的差異,看起來不像是對應的機械碼,至少對應於 "return (arg + nested_arg)" 的加法指令才是。這段程式碼到底作什麼?依據 GCC 手冊 [Trampolines for Nested Functions]:
-
"A trampoline is a small piece of code that is created at run time when the address of a nested function is taken. It normally resides on the stack, in the stack frame of the containing function. These macros tell GCC how to generate code to allocate and initialize a trampoline."
-
"On CISC machines such as the m68k, this requires two instructions, a move immediate and a jump. Then the two addresses exist in the trampoline as word-long immediate operands."
-
"The code generated to initialize the trampoline must store the variable parts--the static chain value and the function address--into the immediate operands of the instructions. On a CISC machine, this is simply a matter of copying each address to a memory reference at the proper offset from the start of the trampoline."
有了這些背景知識後,繼續研究對位址 0xbfffec40 的反組譯結果:(之所以要先寫入檔案,再用 objdump 反組譯,也是考量到這段程式碼是動態生成的,GDB 欠缺該 stack 的資訊)
0: b9 3c ec ff bf mov $0xbfffec3c,%ecx 5: e9 ca 97 04 48 jmp 0x480497d4對應到記憶體的布局,就類似以下的示意圖:
| | +--------+ | local | <- mov ???, %ecx (欲放入 %ecx 的值) +--------+ | local | +--------+ | local | +--------+ | ... | . . +--------+ ---------------------------\ | mov | <- mov $0xbfffec3c, %ecx | + + | | 0x3c | | + + | | 0xec | | + + | | 0xff | | + + (trampoline 實做) | 0xbf | | +--------+ | | jmp | <- jmp 0x480497d4 | + + | | 0xca | | + + | | 0x97 | | + + | | 0x04 | | + + | | 0x48 | | +--------+ ---------------------------/ . ... . | ... | <- %ebp +--------+為了驗證這個想法,筆者做了一個小實驗,嘗試從 nested function 去複製 trampoline,進而生成新的 function,這就有幾分 closure 的意味,請參考以下程式碼: (closure.c)
首先,筆者在結構體 trampoline_code 中,定義 trampoline code 的呈現,而在函式 create_closure() 中寫入 mov ecx 與 jmp 的 IA32 opcode,接著逐一帶入參數去驗證。
另外,GCC 的 nested function 還允許使用 goto 敘述,也就是說,若「父函式」定義一個 label,那麼「子函式」(也就是 nested function) 就能透過 goto,跳躍到從「父函式」所繼承來的 label,需要留意到,一旦使用 goto,nested function 將立刻返回到「父函式」。以下引用 GCC 手冊的範例:
想必看倌仍意猶未竟,不妨參考 Vidar Hokstad 的文章 [How to implement closures],同樣也採用 C 語言搭配 nested function 的技巧,額外還提到 thunk 的技術,用以實現 closure。
後記,感謝 Thinker 指正若干觀念錯誤,已修正於本文,特此感謝。
Posted by jserv at 5:54 PM
| Comments (0)
July 26, 2010
爸,我想寫軟體程式
之前身體狀況不佳,回苗栗老家休息一段時間,除了家務事外,談了台灣的資訊產業,本應興致勃勃,但家母道出的一席話後,頓時陷入無議無論的噤聲:「寫程式這麼累,就別做了吧」。捫心自問,從業數年,除了偶爾聽聞他人基於禮貌給予的讚揚外,的確是一事無成的狀態,儘管我的工作內容之一,就是說服他人採用敝單位的資訊系統,但我卻沒辦法說服家人,同意我繼續寫程式。因緣際會,能在國小三、四年級時,就動手寫電腦程式,其動機僅是怕放在客廳的 80386 個人電腦受潮而無法再開機 (靠海的通霄,常吹濕氣頗重的南風,電腦購入不消四年,扣除硬碟的週邊,幾乎都更換過)。至今回憶,那 MS-DOS 命令列提示與 BASIC 直譯器的畫面,仍是最美麗的景緻之一,得以透過螢光幕,瞥見當時工藝的極致,何其幸運?直到高中畢業前夕,從未想過以資訊技術作為己志,畢竟僅是興趣,雖然中學時代,頗受 Bill Gates 的激勵,但家人總冷冷地說,軟體隨便 copy 就拿走了,怎麼賣錢?而且,將興趣轉換為工作,註定要大打折扣的。中學時,自小即相當照顧我的姑姑,因急性白血症病逝,走的那日正是 [六月六日斷腸時],對此,下了心願要考醫科,試著為醫療科技貢獻一份力量... 但最後,我的意志動搖,雖然成績不惡的我,分數能考上醫科,但始終沒有就讀的勇氣。在通霄國小就學時期,有幸作為知名雕塑家朱銘的學弟,在台灣最有藝術氣息的小學,觀賞著這些由朱銘捐贈的大作,無論是太極系統,抑或人間造型,都反映從自然生命中所領悟出的創作態度,一股自由無拘束的壯闊氣概,最愛徜徉在虎頭山腰,靜靜凝視雕塑與遠方的鐵路,那時,曾不只一次的幻想自己是融合藝術與工程的土木技師。小時過年跟隨母親回娘家,常見到抱著厚重設計圖、不時潛心作設計的舅舅,雖不甚理解土木工程,但那娟麗的字跡穿越著密密麻麻的線條,深深吸引著我,後來我才知道,北二高有一大段工程就是出於舅舅之手,曾有一度想追隨任教於台大土木系的舅舅,用雙手牽起人與人的關聯。
可惜,浪漫的幻想,總是極易戳破,高一的工藝課,無論如何嘗試,就是無法徒手繪出筆直的線條,而三視圖總是充滿瑕疵,「我可能不是作土木建築的料」,默默地承認這個事實。高二選擇第三類組後,則在生物解剖課,徹底粉碎擔任醫療人員的念頭。我還記得,暑假為了提高膽量,逼迫自己切魚的往事,只見雙手的血跡與淚水融為一體,「我真的不是唸醫科的料」,再次接受了真相。當這些選擇一一從志願表刪去時,「資訊工程系」這個倒數第二個選項就浮現了 (最後一個志願是電機系,因為與家父賭氣,不想唸電子電機科系),在沒有選擇餘地的情況下,懵懵懂懂做出選擇。記得三年前應邀去中正大學演講時,資訊系的老師說:「這裡的學生不像你很早就決定自己的志業,請你就業界從業人員的角度,跟學生分享經驗...」,真不知該如何回應,老實說,我的處境跟該校的學生沒太大的分野,也是先考進資訊工程學系,才開始規劃人生的,只是遇到貴人的時間早了些,挑了些較易有成效的項目來作。
魯迅說過:「哪裡有天才,我是把別人喝咖啡的工夫都用在了工作上了」,堅信此道的我,每每保持同樣是魯迅的名言,人生的旅途上遲緩地踏出步伐:「人類總不會寂寞,以為生命是進步的,是天生的」,雖然,至今仍打零工度日,有什麼立場說自己在實踐志業呢?記得 Romain Rolland 說過:
-
「一個人的性格決定他的際遇。如果你喜歡保持你的性格,那麼,你就無權拒絕你的際遇」
-
「也許你不相信,我時常跟現在的學生提到你,一年至少一次, 我回憶起你的程式功力與執著,當然還有比賽前被送進醫院那件事,我一直相信走自己的路與願意走人煙稀少的路的人,成就的機會比較大」
-
「寫作對於我而言,是在漫長旅程上有一個溫暖的春夢做著,路寬夢窄,並且一直大夢未醒。一個人在世上,總得找一個屬於自己的夢做著,不然,這黑夜就顯得太長。」
Posted by jserv at 9:06 PM
| Comments (0)