如何在Windows環境下使用組合語言

學生:莊晟梃

學號:B85506056

Last 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等十分低階的程式,沒有必要為了處理系統的問題而使用到組合語言,要使用滑鼠,只需寫好對應的訊息函式;要做出公用的動態函式庫,也只需compileDLL。以一個系統所應該提供的服務來講,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:此外下面會提到了IntelC/C++ Compiler Plug-in,能針對各級x86CPU包括PIIPPro)幫助compiler產生最佳化的碼,支援MMX,並且能夠整合進VC4/5的IDE中,也許你會有興趣。

一、在VC中使用組合語言

因為Visual C++是在Windows上寫C++使用最廣泛的工具遙遙領先對手,所以在此介紹關於VC使用組合語言的方式。以下大致參考Visual C++ Online Manual of VC4.0。因此,我也只能夠確定這些適用於VC 4.0,而不敢保證在舊版的VC上仍可適用。此外因為在大多數的情況下,我們寧可使用inline assembly而不願意分開用另一個assembler編譯。所以底下用較多篇幅講解VCinline assembly

Inline Assembly的使用方式

Visual C++compiler內部支援inline assembler。所以在VC中使用組合語言,你不需要一個類似MASMassembler,也不需要那些麻煩的link步驟。同時最大的好處是,inline assemby可以使用同一個scopeC的函式及變數

不過inline assembly並不支援所有MASM提供的macrodata directive。所以有時候使用MASM反而會比較方便些。

只要在你能夠放入CC++敘述的地方,你就可以放入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區塊中的指令所改變呢?答案是會的。你可以在有關暫存器的地方看到討論。

如何在__asm區塊中寫組合語言

指令集

VC4支援Intel 486的指令集。其他新增的指令我們可以使用_emit來設定。

注意:VC 4.1,則直接支援了PProMMX指令的inline assembly。此外倘若我們使用VC 4.0(及以後版本),我們可以藉由安裝Intel的C/C++ compiler plug-in來使得VC 4.0支援MMX及較新CPUinline assembly。並且也可以獲得較高的執行效率。很不幸的是,這東西得花錢買。此外我想news上可能也可以找得到支援MMX的巨集吧。

語法

區塊中的組合語法是和MASM一模一樣的。不過雖然你可以在這個區塊中使用CC++的變數及物件,你卻不可以使用一些MASM用來設定資料的directives,如

DB, DW, DD, DQ, DT, DF, DUP,THIS

此外MASMstructuresrecords也不能夠使用,也就是

STRUC, RECORD, WIDTH, MASK.

但是此處卻「有支援」了MASM中的EVENALIGN,這可以在用來調整label的位置藉由填入NOP指令),來使得程式能夠在某些處理器下能夠較快。

此外,注意在區塊中,MASM用文字代表節區的方式並不能夠使用,比如說是_TEXT等,我們必須直接打出暫存器名稱,像是ES:[BX]

巨集

在區塊中我們完全不能夠使用MASM的巨集功能MACROREPTENDM 或是>!等方便功能,但是我們卻可以使用C中的前置處理器#include功能,請看關於巨集的討論

變數的型態及大小

因為我們在inline assembly區塊中不能夠定義資料,所以像LENGTHSIZETYPE指令就只能夠用來計算CC++的變數及型別所佔的大小。

LENGTH 一個陣列的元素個數

SIZE 一個變數的大小,假如是陣列的話,這個值是LENGTHTYPE的乘積。

TYPE 這個型別或變數的大小,假如是陣列的話,則傳回一個元素的大小。

註解

我們可以用MASM原本的註解方式,也就是

__asm mov ax, offset buff ; Load address of buff

但因若我們要將這些組合語言碼放入C的巨集的話,則它會將所有的程式放入同一行中請看下面),所以請避免用這種註解方式,此外我們也可以使用CC++的註解法。

_emit假指令

_emit指令十分類似MASMDB指令。它一次只能夠定義1 byte的指令。所以我們同時可以利用#define來定義出想要的指令,比如說底下程式定義出一個INT指令

#define simint __asm _emit 0x4A __asm _emit 0x43 __asm _emit 0x4B

...

...

__asm {

simint

}

使用C及C++的功能

  1. 運算子
  2. 我們可以在裡頭使用一些C的運算子,但是必須符合assembly的定義。比如說像array[3]:在C中會依照array資料的型別,來決定應該是偏移幾個byte,比如說若array型別為int,則偏移3*sizeof(int),但是若在__asm中,同樣的array[3],則只會偏移3byte。我們可以使用TYPE來解決這個問題:

    int array[10];

    __asm mov array[6 * TYPE int], 0 ; Store 0 at array + 12

    array[6] = 0; /* Store 0 at array + 12 */

    上面兩行是等價的。

  3. 變數,函式名稱,標籤Label
  4. 除了enum成員(就是class中以enum來定義的常數)及一些用巨集或const定義的常數外,但是它可以參考(使用或設定)所有其他的識別字(即變數,函數名稱,標籤)。但此外它也不能夠呼叫C++的成員函式

    要注意的是:

    1. 每個組合語言指令只能夠包含一個這類識別字,除非我們使用的是LENGTHTYPESIZE
    2. 函式之前必須先被宣告。
    3. 這些識別字的名稱並不能夠和MASM的關鍵字衝突不管大小寫,也就是說如PUSH,或SI都是不合法的
  5. 存取C/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++的成員函式。

使用inline asm寫函式

假如我們要用一個獨立的assembler寫函式,則我們必須設定模式,取得參數,接著分開編譯,再連結到一起,但是若我們使用inline assembly,則事情會簡單很多,底下例子會傳回一個數及2的某次方的乘積:

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.函式部分

你不能夠對一個有使用到inline assembly的函式定義calling convention_fastcall,這代表藉由register而非stack來傳遞參數,但是你卻不能夠確定compiler使用哪個暫存器來傳遞參數,也就是說,假使compiler使用EAX來傳遞,但你在組合程式碼一開始的地方就更動EAX,則那個參數的值就會遺失別忘了我們是以參數名稱來取得其值的。此外,對任何使用_fastcall的函式,你也都必須保留ECX

但是假定你使用compiler /Gr參數定義所有的函式均使用_fastcall,則我們必須將每個使用inline assembly的函式定義成_stdcall或是_cdecl後者代表使用Ccalling convention)。

此外假定你在函式中使用inline assembly,你並不需要保留EAXEBXECXEDXES,以及Flags你可以在上例得到佐證),但是此外你得保留所有其他的暫存器的值,以及利用STD或CLD更動的方向旗標。

2.一般部分

你在一般的C/C++敘述中插入的inline assembly,可以任意地改變EAX,EBX,ECX,EDX,因為C或C++並不假定我們會在每個敘述之間保留它們的值,同樣的也可以任意改變ESI及EDI。但是你應該保留ESP或EBP,除非你為了特定目的想改變這個的值。

只要記得一件事就好了,compiler對於__asm區塊中的碼並不會試圖去最佳化,也就是說,你寫出來的就是編譯出來的樣子,而區塊中的指令改變外面暫存器的值,所以compiler會先看看我們在inline ASM中改變了哪些暫存器,接著避免為了最佳化的理由而將變數放在這些暫存器中。

關於使用label要注意的事項

你可以使用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:

呼叫C的函式

你只要將參數用相反的順序一一放入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 ManualAnsi C++ paper或是Masm 6.11以後的線上說明可能會有較多的資訊吧。

呼叫C++的函式

__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的巨集定義__asm區塊

首先必須注意的就是,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)).

二、利用MASM寫Windows下的組合程式

使用各種語言寫同一個程式是可行的。比如說微軟的Fortran,VC,MASM就有正式文件說明如何互相交流。而不同廠商的軟體可以藉由其他的方法,比如說OCX或是DLL來完成。而不同的函式庫如MFC及BCB也可以經由特殊的方法連結在一起。

使用MASMinline 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.1Masm 6.11有豐富的關於如何和windows程式一起編譯的說明,它也比較適合純粹用組合寫出「Windows程式」(在程式中直接使用SDK),但我在Masm 6.1中卻幾乎找不到關於此的任何說明。不過無論如何,只要在Masm 6.1之後,就可以產生可供Windows NT95連結的obj檔的FLAT mode

此外關於混用多種程式語言,你可以在MicrosoftKnowledge Base找到不少資料。你可以直接從VC的線上說明,MSDN,或是微軟的網站上看到Knowledge Base

關於混用各語言所必須注意的就是1.調整calling convention 2.調整函式命名的方式 3.傳值或是傳址呼叫 4.各種資料型別在不同語言中表示法的不同。

我想關於C的calling convention已經在前面略為提過了。至於C++的calling convention及參數傳遞方式和C是一樣的。

關於命名法,C若為cdecl,則存成_filename,若為_stdcl,則存成_filename@nnnn代表了參數在stack中所佔空間,而C++則為_name@@docoration。此外這些都是大小寫相異的。在上面呼叫C++函式關於calling convention的部分有作一點說明。假如我們要用Masm來呼叫CC++的函式,則我們得自己處理這個問題。但是若使用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的執行檔格式和Windows95NT是不同的,後者使用的格式叫做COFF。你可以在Matt PietrekWindows 95 System Programming Secrets看到對此格式的介紹,我們若想要使MASM compile出來的函式甚至是整個程式可以在95下執行,則必須給ml加上/coff這個選項。還要注意:Win95NT的記憶體組態是FLAT,這可以使用32bitpointer,擁有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這工具程式轉換Cheader file,或者compileDLL以供VB等較直譯程式使用,及使用VB Control Development KitCDK來用assembly寫VBX等較複雜的課題,在此就不討論了。我覺得會有這種需要的人,大概也不會想看這個報告了

至於像Delphi等開發軟體,因為其OBJ檔和MS的版本似乎?並不相同,所以不能夠直接拿obj檔Link起來,像這類問題大概就得再看各軟體的線上說明囉!