學生:莊晟梃
學號:
B85506056Last Update Date:98/1/5
DOS
的母語是組合語言,換句話說,DOS常常要求程式設計師同時也得是一個系統程式師-目前的PC一般應用程式執行平台已經從DOS轉移到Windows了。在Windows環境下,已經不像以前在DOS下,即使是在一般的應用程式上,要能夠較有效率地利用系統來完成我們的需求,仍常得利用組合語言來處理較低階牽涉系統的控制。在那個時候,就算這些功能都已經有現成的C函式庫包裝好了,我們仍得有對系統的處理機制(比如說中斷)有一定的認識,才能夠完成一些基本的功能。比如說在
DOS下要使用滑鼠。exec別的程式。緊急錯誤情形的處理方式(Divide by 0,檔案錯誤等)。或是想要提供應用程式常駐的公用程式庫,以節省每個執行檔的大小(如Netware)。或是在記憶體限制下,又沒有保護模式前,所發展出的一些諸如program overlays之類噁心的技巧。甚至若要使程式方便好用,所要加入的花招(比如說DOS5提供的swap),也都得用組合來好好實驗才行。這使得DOS在10幾年來,吞沒了不少程式設計師的心血在研究瑣碎的技巧上,也使很多人誤認所謂高手就是了解系統的人……在
Windows下,OS多做了很多事,同時你也「不能」夠做些如將中斷都換掉,或是東修西補整個系統之類的事,以滿足你個人程式的需求,要寫一個應用程式,所必須了解的只是大量的API(使用C的函式方式)、或是廠商提供的程式庫,並且對系統告知的訊息(message)做出回應,而不需要像DOS下得做些如計算byte之類的瑣事。因此在
Windows下,通常假如不是要寫VxD等十分低階的程式,沒有必要為了處理系統的問題而使用到組合語言,要使用滑鼠,只需寫好對應的訊息函式;要做出公用的動態函式庫,也只需compile成DLL。以一個系統所應該提供的服務來講,Win32 API已可以說是應有盡有了(雖然很難用)。不過在很多情形下,1.比如說對一些可能會佔程式執行時間
95%以上的內部迴圈,組合語言仍是有存在的必要的。2.真的得做到硬體的控制時,則組合語言就變成絕對必要了。3.此外假如你得使用MMX來加速特殊運算的話,目前的compiler似乎還沒有automatic MMX的功能(即遇到向量運算等能自動判斷而產生MMX指令)在這種情況下,你也得使用組合語言。此外你可以在此網頁中找到一些解決方案。不過呢,因為
windows上的compiler通常不像DOS上的那麼「老舊」,也就是說,在DOS上用組語狂電TC之類的經驗,也許在VC 5.0上面並不會這麼容易發生。所以除非是真的對速度要求十分嚴苛的部分,否則不值得為了效率的理由用組語改寫。在下面我們會分別介紹
1.直接利用VC的inline assembler,及2.使用MASM產生COFF格式obj檔這兩種方法。ps:
此外下面會提到了Intel的C/C++ Compiler Plug-in,能針對各級x86CPU(包括PII,PPro)幫助compiler產生最佳化的碼,支援MMX,並且能夠整合進VC4/5的IDE中,也許你會有興趣。因為
Visual C++是在Windows上寫C++使用最廣泛的工具(遙遙領先對手),所以在此介紹關於VC使用組合語言的方式。以下大致參考Visual C++ Online Manual of VC4.0。因此,我也只能夠確定這些適用於VC 4.0,而不敢保證在舊版的VC上仍可適用。此外因為在大多數的情況下,我們寧可使用inline assembly而不願意分開用另一個assembler編譯。所以底下用較多篇幅講解VC的inline assembly。Visual C++
的compiler內部支援inline assembler。所以在VC中使用組合語言,你不需要一個類似MASM的assembler,也不需要那些麻煩的link步驟。同時最大的好處是,inline assemby可以使用同一個scope中C的函式及變數。不過inline assembly並不支援所有MASM提供的macro及data directive。所以有時候使用MASM反而會比較方便些。
只要在你能夠放入C或C++敘述的地方,你就可以放入inline assembly。
Inline assmbly
的表示法是1.
__asm
{
mov al, 2
mov dx, 0xD007
out al, dx
}
2.
或是__asm mov al, 2
__asm mov dx, 0xD007
__asm out al, dx
3.
此外像下面寫法也是可以的:__asm mov al, 2 __asm mov dx, 0xD007 __asm out al, dx
在此,以
1.的用法最好。在兩個大括號中的區塊中,組合語言的語法是和MASM一樣的,你可以直接將為MASM所寫的組合程式copy/paste到此。再此你第一個問題可能是:暫存器會不會被
__asm區塊中的指令所改變呢?答案是會的。你可以在有關暫存器的地方看到討論。VC4
支援Intel 486的指令集。其他新增的指令我們可以使用_emit來設定。 在VC 4.1,則直接支援了PPro及MMX指令的inline assembly。此外倘若我們使用VC 4.0(及以後版本),我們可以藉由安裝Intel的C/C++ compiler plug-in來使得VC 4.0支援MMX及較新CPU的inline assembly。並且也可以獲得較高的執行效率。很不幸的是,這東西得花錢買。此外我想news上可能也可以找得到支援MMX的巨集吧。區塊中的組合語法是和MASM一模一樣的。不過雖然你可以在這個區塊中使用C或C++的變數及物件,你卻不可以使用一些MASM用來設定資料的directives,如
DB, DW, DD, DQ, DT, DF, DUP,THIS
此外
MASM的structures及records也不能夠使用,也就是STRUC, RECORD, WIDTH, MASK.
但是此處卻「有支援」了
MASM中的EVEN及ALIGN,這可以在用來調整label的位置(藉由填入NOP指令),來使得程式能夠在某些處理器下能夠較快。此外,注意在區塊中,
MASM用文字代表節區的方式並不能夠使用,比如說是_TEXT等,我們必須直接打出暫存器名稱,像是ES:[BX]。在區塊中我們完全不能夠使用MASM的巨集功能(如MACRO,REPT,ENDM 或是>,!等方便功能),但是我們卻可以使用C中的前置處理器的#include功能,請看關於巨集的討論。
因為我們在inline assembly區塊中不能夠定義資料,所以像LENGTH、SIZE及TYPE等指令就只能夠用來計算C及C++的變數及型別所佔的大小。
LENGTH
一個陣列的元素個數SIZE
一個變數的大小,假如是陣列的話,這個值是LENGTH及TYPE的乘積。TYPE
這個型別或變數的大小,假如是陣列的話,則傳回一個元素的大小。我們可以用MASM原本的註解方式,也就是
__asm mov ax, offset buff ; Load address of buff
但因若我們要將這些組合語言碼放入
C的巨集的話,則它會將所有的程式放入同一行中(請看下面),所以請避免用這種註解方式,此外我們也可以使用C或C++的註解法。_emit
指令十分類似MASM的DB指令。它一次只能夠定義1 byte的指令。所以我們同時可以利用#define來定義出想要的指令,比如說底下程式定義出一個INT指令#define simint __asm _emit 0x4A __asm _emit 0x43 __asm _emit 0x4B
...
...
__asm {
simint
}
我們可以在裡頭使用一些
int array[10];
__asm mov array[6 * TYPE int], 0 ; Store 0 at array + 12
array[6] = 0; /* Store 0 at array + 12 */
上面兩行是等價的。
除了enum成員(就是class中以enum來定義的常數)及一些用巨集或const定義的常數外,但是它可以參考(使用或設定)所有其他的識別字(即變數,函數名稱,標籤)。但
此外它也不能夠呼叫C++的成員函式。要注意的是:
通常我們只需類似底下表示法即可存取變數:
__asm mov eax, var
至於struct,class或是union,我們得以類似下面做法取得其成員:
struct class1 {
char* wea;
int same_name;
} instant1;
底下是__asm的code
mov ebx,OFFSET instant1;
mov ecx,[ebx].wea;
不過得注意的是,假如同時存在另一個
struct class2擁有同樣名稱的成員變數same_name,則我們必須將上行改成:mov ecx,[ebx]intant1.wea;
這只是
VC的規定,其實兩種寫法產生的code都是一樣的。此外存取C++的成員變數仍必須遵守存取限制(如private等),而且我們也不能夠呼叫C++的成員函式。假如我們要用一個獨立的
int power2( int num, int power )
{
__asm
{
mov eax, num ; Get first argument
mov ecx, power ; Get second argument
shl eax, cl ; EAX = EAX * ( 2 to the power of CL )
}
/* Return with result in EAX */
}
請注意,上面函式的確傳回了
EAX了(這是函式傳出int的方法),但是compiler卻找不到return指令,若你在warning level2或更高,你會得到一個無傷大雅的warning,如果你想避免掉這個warning,你可以使用#pragma warning來關掉它。關於什麼型態的傳回值該存在什麼暫存器中,請看
後面的說明。若不知道的話,你也可以先將傳回值存在變數中,最後再以return指令傳回。 inline assembly寫自己函式的prolog/epilog,這原本必須要得用分開的assembler才能夠辦到,在VC線上說明的Naked Function Calls中對此作了較多的討論,這告訴compiler不要產生此函式的prolog/epilog code。你可以使用這個來接受某個不是C/C++程式的呼叫,可以處理不同的參數傳遞方式,不同的保留暫存器方式。並且可以幫助程式碼的最佳化。在寫VxDs的時候尤其有用。1.函式部分
但是假定你使用
compiler 的/Gr參數定義所有的函式均使用_fastcall,則我們必須將每個使用inline assembly的函式定義成_stdcall或是_cdecl(後者代表使用C的calling convention)。此外假定你在函式中使用
inline assembly,你並不需要保留EAX,EBX,ECX,EDX,ES,以及Flags(你可以在上例得到佐證),但是此外你得保留所有其他的暫存器的值,以及利用STD或CLD更動的方向旗標。2.一般部分
你在一般的C/C++敘述中插入的inline assembly,可以任意地改變EAX,EBX,ECX,EDX,因為C或C++並不假定我們會在每個敘述之間保留它們的值,同樣的也可以任意改變ESI及EDI。但是你應該保留ESP或EBP,除非你為了特定目的想改變這個的值。
只要記得一件事就好了,compiler對於__asm區塊中的碼並不會試圖去最佳化,也就是說,你寫出來的就是編譯出來的樣子,而區塊中的指令會改變外面暫存器的值,所以compiler會先看看我們在inline ASM中改變了哪些暫存器,接著避免為了最佳化的理由而將變數放在這些暫存器中。
你可以使用C的goto來跳到組合語言的label,也可以使用組合語言的jmp等指令跳到C的label。但是要注意的一點是,凡是asm中定義的label都不需考慮大小寫,而可以從C敘述或組合敘述中goto。而組合中的jmp等指令,也可以不考慮大小寫跳到C的label。
注意
:請避免使用C的函式庫中的函式名稱當作是label的名稱,比如說exit就是一個陷阱。當jmp exit時,程式可能會跳到那個函式,而不是你希望的label。此外MASM中的$符號在此也被支援。它代表了目前的位址。它的主要用途可以由下面例子中看出來:
jne $+5 ; next instruction is 5 bytes long
jmp farlabel
; $+5
.
.
farlabel:
你只要將參數用相反的順序一一放入stack中,再接著call C的函式即可,也不需要在函式前面作些加底線之類的動作,一切都非常方便。請看下列例子:
#include <stdio.h>
char format[] = "%s %s\n";
char hello[] = "Hello";
char world[] = "world";
void main( void )
{
__asm
{
mov eax, offset world
push eax
mov eax, offset hello
push eax
mov eax, offset format
push eax
call printf
}
}
有個基本常識是,C和C++的library是不同的,接下來我們馬上會講到C++的library。因為VC是C++的compiler,所以在一般的情形下,它會假定此函式是C++的形式,所以C函式庫的header檔中會用extern "C" { }來將函式的prototype括起來,以使得linker能找得到相對應的C函式。
Calling convention是指如何呼叫函式,傳遞參數,清除stack,函式名稱的decoration為何,大小寫是否相異等。關於C及C++的calling convention可以參看VC線上說明的
Using Calling Conventions。有_cdecl,_stdcall,_fastcall及thiscall四種。其中_cdecl為C/C++預設的方式,所以我們在此只解釋這個。在使用
_cdecl方式的函式,因為我們在此只討論如何「呼叫」
C函式,所以基本上用上面所講的方式照作就可以了。即使_cdecl存起來會在函式前面加上底線(_),compiler會幫inline assembly處理。
假定是別種calling convention呼叫方式就有點不同了,比如說若為_fastcall,則我們得將參數存在暫存器中而不是堆疊中,詳情請看線上說明。
此外關於32bit環境下C/C++規定函式什麼型態的傳回值會存在哪個暫存器中,我實在
無法從Visual C++的線上說明得到太多資訊,也沒時間實驗。以下是最基本的型別傳回方式:char -> AL
short -> AX
long, int, * -> EAX
至於浮點數及struct等的傳回方式,我想
The Annotated C++ Reference Manual,Ansi C++ paper或是Masm 6.11以後的線上說明可能會有較多的資訊吧。__asm指令只能夠呼叫global的C++指令(非成員函式),而且還不能夠是overloaded的
。否則的話compiler會輸出錯誤訊息。基本上
C++的calling convention和C是一樣的,所以前節的資訊你可以直接拿來這裡使用。C++的library和C的不同之處就是,C++會將函式的名稱修改以包含參數型別等的prototype,而且這修改方式是由廠商自訂的。你可以看Visual C++關於Decorated Names的說明來得到更多的資訊。這使得在MASM中呼叫C++函式變得比較麻煩。但是在inline assembly中,compiler也會幫你處理這個問題,所以你只要照前述方法push參數,再call函式即可。此外同上節所說,假如要在C++程式中call C的函式,得確保此函式的prototype定義成extern "C"。
首先必須注意的就是,C的巨集會展開成一行。所以請問舊式的C註解法(/* */),並將巨集寫成如下:
使用類似底下的寫法,你可以在敘述之中插入組合指令(雖然沒辦法像一般的C巨集可以傳回值)。
#define PORTIO __asm \
/* Port output */ \
{ \
__asm mov al, 2 \
__asm mov dx, 0xD007 \
__asm out al, dx \
}
這會展開成
__
asm /* Port output */ { __asm mov al, 2 __asm mov dx, 0xD007 __asm out al, dx }
這可以插到某個指令中,因為第一個__asm可以分隔開C/C++敘述及組合。至於大括號的用處則是用來結束巨集。因此在巨集的後面仍可以放入C/C++的敘述,而不會被compiler視作是組合語言的一部份。
像這類的巨集,我們可以讓它像一般的C巨集一樣能接受參數。但是另一方面,它卻無法傳回一個值。所以你不能夠在一些C的運算敘述中使用到他們。
請不要不經考慮輕率地使用這類巨集,比如說在
前面就討論了_fastcall類型的函式使用inline assembly的會導致的問題。If you are writing assembly routines for the floating point coprocessor, you must preserve the floating point control word and clean the coprocessor stack unless you are returning a float or double value (which your function should return in ST(0)).
使用各種語言寫同一個程式是可行的。比如說微軟的
Fortran,VC,MASM就有正式文件說明如何互相交流。而不同廠商的軟體可以藉由其他的方法,比如說OCX或是DLL來完成。而不同的函式庫如MFC及BCB也可以經由特殊的方法連結在一起。使用
MASM比inline assembly多了些好處,最重要的就是它擁有巨集,而使我們能更方便地處理迴圈,算術及字串處理等。因此也能夠用它比較省事地寫出大程式。還有一個好處是,請看前面關於用inline ASM寫函式的部分,我們在哪裡使用compiler預設的函式處理方式,而只自行處理函式內部的指令,這會產生有時多餘的prolog/epilog code(如初使化或回復暫存器,stack等,看compiler決定,請見calling convention),使用MASM,我們可以依照自己的需求來決定該如何寫這個部分,不過VC4已經能夠讓使用者利用inline assembly自行寫自己的prolog/epilog了,請看Naked function call。在
Microsoft的網頁上,可以看到MASM 6.11的產品訊息所堤到的一點點說明。從那些老舊未更新的訊息,也可以看出微軟不會再對這項產品花太多力氣。Masm
的版本更新很慢,支援MMX的版本應該是6.11d以後,而目前(98/1/4)最新版本則是6.2。只要是6.11版以後,都可以到此處抓下更新的patches來升級到6.2。因此,假如要在Windows程式設計下有較好的支援,請確定你使用的是6.11以上的版本(而不是6.1)。Masm 6.11有豐富的關於如何和windows程式一起編譯的說明,它也比較適合純粹用組合寫出「Windows程式」(在程式中直接使用SDK),但我在Masm 6.1中卻幾乎找不到關於此的任何說明。不過無論如何,只要在Masm 6.1之後,就可以產生可供Windows NT及95連結的obj檔的(FLAT mode)。此外關於混用多種程式語言,你可以在
Microsoft的Knowledge Base找到不少資料。你可以直接從VC的線上說明,MSDN,或是微軟的網站上看到Knowledge Base。關於混用各語言所必須注意的就是1.調整calling convention 2.調整函式命名的方式 3.傳值或是傳址呼叫 4.各種資料型別在不同語言中表示法的不同。
我想關於C的calling convention已經在
前面略為提過了。至於C++的calling convention及參數傳遞方式和C是一樣的。 _filename,若為_stdcl,則存成_filename@nn(nn代表了參數在stack中所佔空間),而C++則為_name@@docoration。此外這些都是大小寫相異的。在上面呼叫C++函式及關於calling convention的部分有作一點說明。假如我們要用Masm來呼叫C或C++的函式,則我們得自己處理這個問題。(但是若使用inline assembly,則不用擔心這些問題),通常由於C++的命名(name decoration)太過麻煩,而且我們其實也只能夠呼叫全域的C++函式,所以我們會寧可將此C++的全域函式用extern "C"來compile以得到較簡單的名稱。不過另一方面,我們也可利用VC所附的工具DUMPBIN來找出函式的真正docoration後的名稱,並且直接用此名稱來呼叫C++的函式。DUMPBIN可以觀看COFF格式(見下段)的OBJ,LIB,DLL,EXE等檔案內部的函式名稱。(不過,大部分若我們在組合中需要使用到的SDK,如DirectX、Win32SDK,通常是C函式,而不是C++。所以不用擔心)此外,
DOS的執行檔格式和Windows95及NT是不同的,後者使用的格式叫做COFF。你可以在Matt Pietrek的Windows 95 System Programming Secrets看到對此格式的介紹,我們若想要使MASM compile出來的函式(甚至是整個程式)可以在95下執行,則必須給ml加上/coff這個選項。還要注意:Win95及NT的記憶體組態是FLAT,這可以使用32bit的pointer,擁有4GB的定址,而不需要節區暫存器。底下我們只提出一個可以執行成功的例子:
CMAIN.C
include <stdio.h>
#ifdef __cplusplus //為了避免用C++的name decoration
extern "C" {
#endif
char MasmSub (char);
#ifdef __cplusplus
}
#endif
main ()
{
char var = 'a';
printf ("%c\n", var);
printf ("%c", MasmSub(var));
}
MASMSUB.ASM
.386
.MODEL flat, C
.CODE
MasmSub PROC, cVar:BYTE
mov al, cVar ; Load the char into AL.
add al, 25 ; Because the function returns a char (a 1-byte
ret ; value, C will get the return value from AL.
MasmSub ENDP
END
我們必須使用ML /c /Cx /coff來
compile這個asm檔。這些講得十分簡略,沒有詳細講C如何呼叫組合、及組合如何呼叫C,以及參數的傳遞。但我想也許Masm 6.11以上會有關於此更多的線上說明。不過由於我沒有這個版本,所以
……此外還有些較複雜的問題,如如何在程式中使用
Win32 SDK(必須使用H2INC這工具程式轉換C的header file),或者compile成DLL以供VB等較直譯程式使用,及使用VB Control Development Kit(CDK)來用assembly寫VBX等較複雜的課題,在此就不討論了。(我覺得會有這種需要的人,大概也不會想看這個報告了)至於像
Delphi等開發軟體,因為其OBJ檔和MS的版本似乎(?)並不相同,所以不能夠直接拿obj檔Link起來,像這類問題大概就得再看各軟體的線上說明囉!