VS寫彙編程序002:用匯編語言寫函數

       彙編語言在運行效率上有優勢,通過精心設計的彙編程序,其執行效率會比C語言高,但是程序難寫難調試。使用匯編程序編寫大型程序很具有挑戰性,不太可能全部使用匯編。爲兼顧開發成本和程序執行效率,C語言和彙編混合編程爲上好的選擇,即在重視性能的模塊使用匯編,在其他部分使用C語言。那麼能不能用匯編語言編寫函數讓C語言調用呢?答案是肯定能!

      在VS上配置好彙編語言開發環境的基礎上,下面就一個簡單的例子來探討一下如何在VS中使用匯編語言編寫函數,並將函數編譯爲obj或lib文件。

一、問題描述

         使用匯編語言編寫一個Add函數,來實現兩個整數的相加,並將函數編譯爲輸出爲lib文件。該lib文件可以被彙編、C語言和C++引用。Add函數的C語言定義如下:

int Add(int x,int y)
{
    return x+y;
}

二、彙編語言編寫函數要注意的問題

(1)函數名稱

         函數輸出到lib文件時,其名稱並非原來的樣子,而是會增加一些修飾符,比如前下劃線,後綴等。函數名稱修飾符跟程序語言、和調用約定有關。(這裏的程序語言指的是C語言和C++,調用約定指的是cdecl和stdcall)。彙編語言寫的函數必須按照有關規定將函數名稱進行修飾,才能被C/C++調用。下面按照調用約定給Add函數命名。

cdecl調用約定:這是C語言的函數調用約定,函數名會被下劃線修飾:_Add

stdcall調用約定:這個約定有兩種風格,如果是C語言的stdcall約定,那麼函數名前要加上下劃線,後面要加標號@和參數字節數。Add函數有兩個4字節的參數,參數總字節數爲8字節,那麼C的stdcall就是_Add@8;如果是C++語言的stdcall約定,那麼前綴加問號“?”,後綴加“@@YG”以及一些參數標識,詳細可以參考這篇博文,C++的stdcall約定下的函數名爲“?Add@@YGHH@Z”。本文不寫C++調用約定爲stdcall風格的函數。

(2)參數傳遞

         參數傳遞跟調用約定有關,對於C語言來說,不管是cdecl約定還是stdcall約定,其參數都是通過堆棧來傳遞的,且進棧的方式是從右到左。傳遞參數是主調函數要做的事,而編寫被調函數並不用關心參數是如何傳進來的,只需考慮如何訪問傳進來的參數即可。如何訪問堆棧中的數據呢?一般通過ebp寄存器來訪問。爲了快速存取堆棧中的數據,系統通常要進行字節對齊,一次壓入或彈出堆棧的字長應是4字節,即使這個參數是小於4字節的。換句話說,就是1字節的參數,也要佔用4字節的堆棧空間。

(3)堆棧平衡

         函數執行完畢,棧頂指針應該恢復到調用前的值。由於參數和局部變量都保存在棧空間,這會使棧頂指針發生改變,爲了堆棧平衡,就要對堆棧進行清理。如果被調函數存在局部變量,那麼被調函數自身一定要進行處理。對於參數,如果是stdcall調用約定,則由被調函數清理,如果是cdecl調用約定,則由主調函數處理。恢復棧頂指針方法有很多,可以使用add/sub指令對esp進行加減,或者ret XX指令對esp進行操作。

三、例子

(1)編寫cdecl調用約定的函數

.386
.model flat//內存模式爲平坦模式
.code
_Add proc
    push ebp//保存基址指針寄存器的值
    mov ebp,esp//將棧指針賦予ebp,用來訪問棧中的參數
    mov eax,[ebp+8]//取出第一參數x
    mov ebx,[ebp+12]//取出第二個參數y
    add eax,ebx
    mov esp,ebp//恢復棧指針
    pop ebp//恢復基址指針
    ret//函數返回
_Add endp//函數結束
end _Add//將程序入口點設爲_Add
    


        基本上,不管什麼樣的函數,函數的開始前兩行代碼都是【push ebp】和【mov ebp,esp】,函數結束前兩行都是【mov esp,ebp】和【pop ebp】。

       訪問保存在棧中的參數,需要知道它們存放的位置。esp是棧指針寄存器,存放的是棧頂指針,它指示這些參數的存放位置。但是又不能直接通過esp訪問參數,原因有二,一是因爲esp總是指向棧頂,也應該指向棧頂,改變其指向會造成堆棧混亂;二是,push/pop指會使堆棧增長或收縮,從而影響esp的值,這不利於獲取參數。利用ebp作爲中間寄存器訪問參數是很有必要的,ebp可以改變,也可以不變,改變時也不影響esp的指向。

        通過[ebp+8]取得第一個參數,[ebp+12]取得第二個參數。爲什麼要將地址加8才能取得第一個參數呢?可以從函數的調用過程來分析這個問題,下圖是參數在棧中的存儲位置示意圖。

       在call指令執行之前,先要將參數從左到右壓入堆棧。參數傳遞 完了之後,執行call指令,這個call指令完成了兩件事:一是將call指令之後第一條指令的地址壓入堆棧,二是將IP(指令指針)指向被調函數的入口點。進入到函數內部時,第一條指令是【push ebp]】,這引起了堆棧增長,第二條指令【mov ebp,esp】,將棧頂指針賦予ebp。從上圖可知,第一個參數X,距離棧頂8字節,第二個參數Y距離棧頂12字節。由於棧的增長方向與地址增長的方向剛好相反,所以獲取第一個參數X是[ebp+8],第二個參數Y是[ebp+12]。

      函數的收尾工作:因爲調用約定是cdecl,清理堆棧的任務交給主調函數,在被調函數中不需要也不能清理堆棧。在返回之前,僅僅是回覆一下ebp和esp寄存器的值即可。於是執行【mov esp,ebp】【pop ebp】。

(2)編寫stdcall調用約定的函數

.386
.model flat
.code
_Add@8 proc
    push ebp
    mov ebp,esp
    mov eax,[ebp+8]
    mov ebx,[ebp+12]
    add eax,ebx
    mov esp,ebp
    pop ebp
    ret 8//函數清理8個字節的參數,並返回
_Add@8 endp
end _Add@8

      stdcall調用約定的函數與cdecl調用約定的函數大部分都是一樣的,不一樣的地方是函數名稱和堆棧清理部分。由於stdcall調用約定的函數需要自己清理堆棧,所以在函數返回時,需要清理棧中的兩個參數。【ret 8】表示清理8字節的堆棧,然後返回。

四、VS編譯彙編程序

彙編程序寫好後如下圖所示:

在“生成”菜單中的“編譯”,或者右擊彙編源文件選擇“編譯”,即可編譯彙編程序、編譯完成後會生成obj文件。爲了防止調用該函數時出現意想不到的麻煩,最好將“Use Safe  Exception handler”設爲“否”。操作如下圖:

在VS中編譯的彙編程序只能生成obj文件,如果需要生成lib文件,則需要利用VS的lib.exe工具進行操作。在生成obj文件之後,利用命令行啓動lib.exe,來生成lib文件。爲了方便,可以寫一個批處理文件來完成這個工作。批處理文件如下:

批處理文件與obj文件放在一個文件夾裏,點擊bat文件即可生成lib文件。如果有多個obj文件,可以用通配符“*”來代表文件名。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章