2012年11月26日 星期一

實戰呼叫慣例 - 從 C 語言呼叫外部組合語言副常式

一段時間沒發文了...zzz  來發個文
假如有詳細閱讀前面解說呼叫慣例系列的文章,相信程式語言功力有也一定的提升啦
但是畢竟那些是理論,你看我講很輕鬆,但是其實你沒自己做過還是不會全懂,所以
今天就來玩一玩實戰呼叫慣例 - 讓我們從 C 語言呼叫組合語言寫的副常式


從組合語言寫一個副常式(Subroutine)這是很重要的基本功夫,了解這個比會使用各種炫麗的程式庫來的更重要 (例如 STL, MFC, Qt, GTK, OpenGL, DirectX, WinCurses, ...),程式庫太多了
學不完,基本功打實了,這些自己看文件學習都不是問題。
一個系統程式在誕生的過程是逐漸的從組合語言一路逐漸過渡到 C 語言環境,基本上並沒有甚麼神祕的色彩,在甚麼都還沒有的原始環境中,你就得先用組合語言寫一些 Subroutine,然後給你的 C 程式呼叫,當這些基礎程式庫逐漸完備後,C 程式能夠完全自己支援自己,就可以完全脫離組合語言完全用 C 語言寫程式了。另外一種情況是你想要嘗試 Intel 或 AMD 加入的新指令集,可是 C 語言編譯器的 intrinsic 還不支援,那就得使用組合語言囉。第三種原因就是 微軟在 CL for 64-bit (CL 是微軟的 C 編譯器),已經不支援直接在 C 語言內插組語,所以要了解組合語言的 Subroutine 如何撰寫。假如你是用 GCC 的話,還是可以用內插組語,但是得要熟悉 AT&T 組合語言語法,這個就不解說了。

註解: intrinsic 是甚麼東西阿,我個人喜歡稱為編譯器已經內建的固有函式,也就是說
你不用去 include 甚麼原始檔,或引入甚麼 library,compiler 已經把該函式的組語內建
於編譯程式內,當你使用 intrinsic 的時候 compiler 會自動把對應的組語直接產生出來

本篇要示範 在一個 C 程式內 呼叫用 FASM 寫出來的 Subroutine,呼叫慣例 採用 stdcall

0.0+  假如不清楚 stdcall 慣例,請回頭先閱讀 呼叫慣例系列 文章,不在多做解釋。

假如有一段 C 程式為

/*
關於視窗程式內常用的字串型態 請參考 codeproject 上的經典文章

What are TCHAR, WCHAR, LPSTR, LPWSTR, LPCTSTR (etc.)?

簡單講,這類經過 typedef 的字串,只是要解決 mbcs 與 unicode 字串造成的編碼問題
用巨集的方式丟給編譯器去煩惱~~

*/


#include <tchar.h>   // 因為我有用到 TCHAR 這種 Type
#include <stdio.h> 

extern "C" int __stdcall fasm_stdcall_avg(int a,int b);

int main(int argc,TCHAR argv[])
{
int val;
printf_s("Call assembly procedures from FASM...\n");
val = fasm_stdcall_avg(2, 4); // 這個函式我們將要在 FASM 中實作
printf_s("fasm_stdcall_avg(2, 4)= %d\n", val);
return 0;
}

現在程式裡面有呼叫一條 subroutine 是 fasm_stdcall_avg,它能夠算兩整數相加的平均
現在這條 subroutine 因為在外部,所以可以看到原型宣告有 extern "C",表示跟編譯器
講說,這個函式這邊沒有實作,只需要產生函式符號。

我們的重點在 FASM 程式的撰寫,FASM 是我用過最好用的組譯器,FASM 有非常強大的
巨集展開功能,沒有複雜的參數指令,環境簡潔,要甚麼功能在組譯器裡面用語言來控制,
我喜歡它的原因是現在許多的編譯與組譯工具越搞越複雜你不是經常使用這些工具的專家,你會非常頭疼,建構過嵌入式 Linux 核心下程式的人應該可以體會,不只要了解編譯器怎麼用,另外一種工具也讓人很頭痛就是連結器,連結器會讀取 Linker Script,複雜的鏈結描述檔會告知連結器眾多的目的檔要如何連接在一起,如何定位,是否可重新定位,進入點在哪裡等等。FASM 一反常態這些複雜的東西通通消失了,組譯器的參數只有,-m 與 -p,-m 指組譯時可以占用的記憶體,-p 指組譯器可以輪迴的次數,剩下的東西全部在程式內用語言控制,沒有甚麼隱藏的東西,FASM 最重要的特性 SSSO,甚麼意思,你看到的原始程式,就是將來組譯器輸出的樣子 (順序不會發生變化,只是變成對應的機械語言),其二,FASM 獨立 Link 的觀念,FASM 裡面要自己用語言控制要引入的函式符號,除非像本文是要在 FASM 寫 subroutine 給 C 語言用,所以必須產生目的檔讓 VC 去 link 。

請去 FASM 官網下載 fasmw16924 (也許現在有更新我很久沒換了), 執行 FASMW.EXE 就
可以開始寫 FASM 了。


因為 FASM 常用的巨集都放在 win32a.inc 裡面,通常這是得要引入的組合語言標頭檔。
format MS COFF 可以產生 .obj 形式的目的檔這是微軟的通用目的檔格式
public ... as ... 這個語法有沒有很清楚阿,沒錯,就是要自己處理符號名稱,你要跟
組譯器講,這個 label 實際組譯時要用的符號名稱,這邊你就看到了,函式名稱其實
只是一個標籤,因為要使用 stdcall慣例 與 函式帶有兩個參數,你在寫符號名稱時自己
就得打上 stdcall慣例形式的符號名稱,_fasm_stdcall_avg@8,這就是 FASM 的好處,沒有
任何隱藏的東西,你得自己輸入正確的名稱,在 C 語言裡面你看不到,編譯器幫你處理而隱藏掉這個過程,接下來就是最重要的部分 code section。

為什麼我用貼圖的呢?0.0 自己練習,不要剪下貼上。

有沒有非常清楚的看到,section '.text' code readable executable,這句話很清楚的講說
從下面開始的部分是 程式碼 而且 標籤名稱為 .text,這個就牽涉到要對目的檔或執行檔的格式稍微有一些了解,section 常用的名稱還有很多種,這邊你先了解 .text 一般指的就是這個
section 代表這個程式的本文在這裡,也就是程式碼,所以才會看到後面的屬性是 code,
表示這個區段會存放程式,一樣你在 C 語言裡面看不到被隱藏起來,由編譯器產生。
code 後面還有接兩個屬性 可讀(readable)  與 可執行(executable),這樣下面這個程式區段
當系統的執行檔載入器讀入後才能夠讀取然後被執行

section 指令會讓你看到原來執行檔內原始區段的面貌 (因為你要自己決定)
5 種是 section 名稱類型      : code data resource import export
4 種是 section 權限類型      : executable readable writeable shareable
3 種是 section 記憶體類型  : fixups discardable notpageable

code readable executable 是很常用的組合

注意到這兩段

label .num1 dword at ebp+8   ;num1 argument is now located at ebp+8
label .num2 dword at ebp+12 ;num2 argument is now located at ebp+12

沒錯 這就是 FASM 很強大的地方,我們已經知道第一個參數其實就是放在 [ebp+8]
可是用這種名稱沒有可讀性阿,所以利用 label macro,我們把 ebp+8 化名為 .num1
這樣就可以寫成 [.num1],而且明確指向大小是 dword ,可讀性可以大大提升,因為 FASM 有提供大量巨集,在加上不用連結器,其實寫起來跟 C 語言差不多,有時候甚至更簡單,
這個得要講一個 FASM 呼叫 COM DLL 的例子,這邊就不提了,有這個概念就好。

在來看看在組合語言中的區域變數


    ;allocate space for local variables in stack
    sub  esp, 8
label .loc1 dword at ebp-4
label .loc2 dword at ebp-8


有沒有看到靈活的地方,還記得之前的文章中,有 sub esp, 40h,表示
編譯器預設會為你的函式保留 16 個區域邊數,即使你沒用到還是會有,
在組合語言裡面就靈活了,假如只用到兩個區域變數,esp 減去 8 就好,
這段是故意示範用,因為我根本沒用到區域變數,刪去也無所謂。

push ebx esi edi 我想這不用說明了,以前就講過了,這邊要注意的是語法
直接用 FASM 寫組語的好處其實比用 VC 那種內插組語來的更強大,
你可以使用 FASM 特殊的短語句,這邊就是一個例子,否則你得要打三次 push
自己慢慢推,參數多的話一直打 push 會覺得程式冗長。

至於中間的程式就沒有甚麼,我用 rcr 對 eax 移動一次,有邏輯設計基本常識
應該就會知道,二進位數向右移動一格就是除以2,放在 eax 是因為 return value 必須
用 eax 表達。

後面 Epilogue Code 就不解說了,以前講過了,要注意的重點是 FASM 的語法
可以看到直接使用 retn 2*4,這樣可讀性好,兩個參數,在 32-bit 環境
所以大小為 4-byte,FASM 在組譯時會自己換算成 8。

假如你真的有看懂前面講的 呼叫慣例系列 文章,就會知道我為什麼只講
stdcall,cdecl 其實 就是把 FASM 程式碼裡面 retn 2*4 換成 retn 0
符號名稱改為 _fasm_stdcall_avg,然後 C 程式裡面 fasm_stdcall_avg 
前面的 __stdcall 刪掉就好了。

用 FASM 組譯程式有夠簡單,因為我的程式檔名是 asm.asm ,所以 Ctrl+F9 你就會得到 asm.obj,這樣我們就獲得了帶有這個函式程式碼的目的檔 (Object file)。

白話說其實 目的檔 就是 已經在外部你先 預編譯 或 預組譯 好的程式碼,
所以你在 VC 裡面當然就把它當類似原始檔的 cpp 直接引入阿,這樣就可以編譯程式囉。
所謂半開放程式碼的方式就是這樣做到的啦,把一些重要的程式預編譯讓你不能看
我的習慣是會在方案總管理面建立一個 已編譯程式檔 來做管理,如圖


然後按下 F7 建置你的方案吧,執行結果應該會像下圖


可以看到成功在 C 語言中呼叫 組合語言寫的 subroutine。






沒有留言:

張貼留言