vc程序中嵌入彙編的語句的說明

爲了加速遊戲,一提起彙編語言,大家也許會感到很神祕。其實如果你學起來就會發現,它並非想象中那樣難。特別是內嵌彙編,由於它和C++緊密結合,使你不必考慮很多煩瑣的細節(例如輸入輸出函數的寫法),學習起來比較容易。使用內嵌彙編,特別是使用MMX指令,可以大大提高各種遊戲中常見特效的速度,對於編出一個漂亮的遊戲非常重要。學好彙編語言還有一個特別有趣的用處:可以觀察和看懂VC++生成的彙編代碼,從而更好地瞭解C++語言本身和優化代碼。

6.1 內嵌彙編簡介
在高級語言中,我們可以無所顧忌地使用各種語句,再由編譯器將語句經過非常複雜的編譯過程將其轉換爲機器指令後運行。事實上,處理器本身所能處理的指令不多;更糟糕的是,大部分指令不能直接施用在內存中的變量上,要藉助寄存器這個中間存儲單元(你可以把寄存器看做是一個變量)。Pentium級處理器的寄存器不多,只有8個32位通用寄存器,分別被稱爲EAX, EBX, ECX, EDX, EBP, ESP, EDI , ESI。每一個通用寄存器的低16位又分別被稱爲AX, BX, CX, DX, BP, SP, DI , SI。其中AX, BX, CX, DX的高8位被稱爲AH, BH, CH, DH;低8位被稱爲AL, BL, CL, DL。注意在內嵌彙編中不應使用EBP和ESP,它們存儲着重要的堆棧信息。
還有一個非常重要的寄存器,叫做標誌寄存器(EFLAGS),標明瞭運算結果的各個屬性,你不能直接讀取或修改它。這些屬性有:不溢出/溢出(OF)、正/負(SF)、非零/零(ZF)、偶/奇(PF)、不進位/進位(CF)等。
彙編語言中若要表示有符號整數,需先寫出該整數的絕對值的二進制形式,若此數爲正數或零則已得到結果,否則將其取反(0->1,1->0)後再加上一即爲結果。所以一個8位寄存器可表示的有符號整數範圍爲從-128到127。
與C++類似,彙編語言提供了得到指針所指內存的方法,這被稱爲"尋址"。用法很簡單,象這樣:[寄存器+寄存器*1/2/4/8+32位立即數]就可以得到這個位置的數了。舉一個例子,如果有一個數組unsigned short A[100],且EAX中存儲着A[0]的地址,那麼[EAX+58]即爲A[29]的值;如果此時EBX=9,那麼[EAX+EBX*2+4]將是A[11]的值。
那麼又怎麼把一個變量的地址裝載進寄存器呢?後面將會介紹。
內嵌彙編的使用方法是:
_asm
{
語句 //後面可加可不加分號
}
你可以把它插入程序中的任何位置,非常靈活。  

6.2 基本指令
基本指令均不影響標誌寄存器。
第一條指令是傳送指令:MOV DEST, SRC。其作用爲將DEST賦以值SRC。其中DEST和SRC可爲整數(稱爲立即數)、變量或[地址](存儲器),寄存器。需注意的是有的操作是不允許的:在彙編語言中你永遠不能將存儲器或寄存器內容賦給立即數(你見過5=a這樣的語句嗎?);也不能將存儲器內容直接賦給另一存儲器,必須藉助寄存器作爲中間變量來實現。關於MOV還有一點要注意的是DEST和SRC必須都爲32位/16位/8位,即同一大小。值得特別注意的是,數據在內存中的存儲方式是以字節爲單位顛倒的,即:如果內存地址0000存儲的字節是5F,地址0001存儲的字節是34,地址0002存儲的字節是6A,地址0003存儲的字節是C4,那麼地址0000處存儲的字(WORD,16位)爲345F,雙字(DWORD,32位)爲C46A345F。
第二條指令是地址裝載指令:LEA A, B。其作用爲將B變量的地址裝載進A寄存器(A需爲32位)。要注意的是不能像LEA EAX, Temp[5]這樣直接調數組中某個元素的地址。這個指令還可以用來進行簡單的運算,考慮下面的語句:LEA EAX, [EBX+ECX*4+8],此語句可將EBX+ECX*4+8的值賦給EAX。
OK,讓我們看一個可以將兩個正整數相加的程序:
#include <iostream>
using namespace std;
//此程序也展示了內嵌彙編應如何使用C++中的指針
void main( )
{
unsigned int a,b;
cin>>a;
cin>>b;
int *c = &a;
__asm //下面是內嵌彙編...
{
mov eax, c; //c中存儲的a的地址->eax
mov eax, [eax]; //a的值->eax
//注意直接mov eax, [c]是錯誤的
mov ebx, b; //可以像這樣直接對ebx賦值
lea eax, [eax+ebx];
mov a, eax; //可以直接將eax的值->a
} //內嵌彙編部分結束...
cout<<a;
}
第三條指令是交換指令,形式爲XCHG A, B。A和B中至少有一個須爲寄存器。如果你想交換兩處內存中的數據則要使用寄存器作爲中間人。
接着是擴展傳送指令,共有兩條,爲MOVSX DEST, SRC和MOVZX DEST, SRC,它們的用處分別是將SRC中的有符號數或無符號數賦給DEST。這時你就可以將字長較短的寄存器的內容賦給字長較長的寄存器,反之則不行。
大家會發現,8個通用寄存器實在無法滿足編程的要求。爲了解決這一矛盾,引入了堆棧這一聰明的設想。你可以把堆棧想象爲一塊放箱子的區域,用入棧(PUSH)可將一個箱子放在現有箱子的最頂端,而出棧(POP)可將現有箱子最頂端的那個箱子取出。看看下面的指令吧:
push eax //eax進棧, 堆棧爲eax
push ebx //eax進棧, 堆棧爲eax ebx
push ecx //eax進棧, 堆棧爲eax ebx ecx
pop ebx //ebx=ecx, 堆棧爲eax ebx
pop eax //eax=ebx, 堆棧爲eax
pop ecx //ecx=eax, 堆棧空
可以看到,堆棧不僅可以方便地暫時存儲數據而且還可以調整他們的次序。

6.3 算術指令
算術指令大都影響標誌寄存器。這些指令比較容易明白,現在將其列出:
表6.1
clc CF=0
stc CF=1
cmc CF=1-CF
add a,b a=a+b (結果過大可能會有古怪的結果,且置CF 1)
adc a,b a=a+b+CF (加上進位)
sub a,b a=a-b (如結果小於0會加上2的16或32次方,且置CF 1)
sbb a,b a=a-b-CF (減去退位)
inc a a++
dec a a- -
neg a a=-a
mul a eax=eax*a後的低32位, edx=高32位例: mov eax,234723 mov edx, 12912189 mul edx; 則eax=2835794967 edx=705
div a eax=(edx eax)/a的商, edx=餘數例: mov eax,12121 mov edx,2 此時(edx eax)=8589946713 mov ebx,121 div ebx; 則eax=70991295 edx=18
imul / idiv dest, src 有符號數乘 / 除法,dest=dest乘 / 除src
imul / idiv dest, s1, s2 有符號數乘 / 除法,dest=s1乘 / 除s2

爲了讓大家弄懂標誌,請看兩段程序(出現的數都爲十六進制數):
表6.2
指令 CF ZF SF OF PF AX或BX
mov ax, 7896 ? ? ? ? ? 7896
add al, ah 1 0 0 0 0 780e
add ah, al 0 0 1 1 0 860e
add al, f2 1 1 0 0 1 8600
add al, 1234 0 0 1 0 0 9834

mov bx, 9048 ? ? ? ? ? 9048
sub bh, bl 0 0 0 1 1 4848
sub bl, bh 0 1 0 0 1 4800
sub bl, 5 1 0 1 0 0 48fb
sub bx, 8f34 1 0 1 1 0 b9c7  

6.4 邏輯與移位指令
邏輯指令會將標誌寄存器中的OF和CF清零。
表6.3
not a a=~a(注意not與neg不同!)
and a, b a=a&b
or a, b a=a|b
xor a, b a=a^b

下面是移位指令,其中x可爲8位立即數或CL寄存器。
表6.4
sal(也可寫成shl) a, x 將a左移x位,CF=移出的那一位數空位用0補足
sar a, x 將有符號a右移x位,CF=移出的那一位數空位按a的符號用0/1補足
shr a, x 將無符號a右移x位,CF=移出的那一位數空位用0補足
rol a, x 將a循環左移(左邊出去的數又從最右邊回來)
ror a, x 將a循環右移(右邊出去的數又從最左邊回來)
rcl / rcr a, x 把CF放在目標最左邊然後循環左/右移
shld a, b, x 將a左移x位, 空出位用b高端m位填充例:shld edx, eax, 16可將eax的高16位 放入dx中。
shrd a, b, x 將a右移x位, 空出位用b低端m位填充

6.5 比較、測試、轉移與循環指令
比較與測試指令基本上總是與轉移指令相配合使用,其形式分別爲CMP a, b和TEST a, b。CMP實際上是根據a-b的值改變標誌寄存器但不改變a和b,可以檢測兩個數的大小關係。TEST則是根據a&b的值改變標誌寄存器,同樣不改變a和b。這條指令可以用來測試a中哪些位爲1。執行完這些指令後,立刻用轉移指令就可實現條件轉移,因爲條件轉移語句會根據標誌寄存器決定是否轉移。轉移指令的使用方法就像這樣:
__asm{
_addax: add ax,1; //_addax是標號
jmp _addax;
}
轉移指令有:
JMP 無條件轉移
JE / JZ ZF=1時轉移
JNE / JNZ ZF=0時轉移

JS SF=1時轉移
JNS SF=0時轉移
JO OF=1時轉移
JNO OF=0時轉移
JP / JPE PF=1時轉移
JNP / JPO PF=0時轉移

根據兩無符號數關係轉移:
JA / JNBE 大於時轉移 (CF或ZF=0)
JBE / JNA 不大於時轉移 (CF或ZF=1)
JB / JNAE / JC 小於時轉移 (CF=1)
JNB / JAE / JNC 不小於時轉移 (CF=0)

根據兩有符號數關係轉移:
JNLE / JG 大於時轉移 ((SF異或OF)或ZF)=0 )
JLE / JNG 不大於時轉移 ((SF異或OF)或ZF)=1 )
JL / JNGE 小於時轉移 (SF異或OF=1)
JNL / JGE 不小於時轉移 (SF異或OF=0)

特殊轉移語句:
JECXZ CX=0時轉移

爲了記住這麼多條指令,你只需知道一點,就是無符號數之間的關係分別被稱爲Above,Equal,Below,分別代表大於,等於,小於;有符號數之間相應的關係則分別被稱爲Great,Equal,Less。
事實上,有些轉移是可以避免的。舉個例子,要算一個數的絕對值是否要用轉移呢?請看一段程序:
MOV EDX,EAX
SAR EDX,31 //EDX現在全爲EAX的符號位
XOR EAX,EDX
SUB EAX,EDX

找出兩個數中較大的一個應該要用轉移吧?不過也可以象下面的解決方案那樣利用標誌,真是絕了:
SUB EBX,EAX
SBB ECX,ECX //如果EBX≥EAX,現在ECX=0,否則ECX=FFFFFFFF
AND ECX,EBX
ADD EAX,ECX

下面的一段程序實現了if (a != 0) a = b; else a = c;
CMP EAX,1
SBB EAX,EAX
XOR ECX,EBX
AND EAX,ECX
XOR EAX,EBX

循環語句常用的是LOOP,它等價於DEC CX加上JNZ。

下面看一個彙編的綜合運用:冒泡排序。
#include <iostream>
using namespace std;

#define array_size 10

int a[array_size]={42, 73, 65, 97, 23, 59, 18, 84, 36, 6};

void main()
{
int *p;
p=&a[0];
p--;

__asm
{
mov esi,p;
mov ecx,array_size;
_outloop:
mov edx,ecx;
_inloop:
mov eax, [ esi+ecx*4 ]; //一個int佔4字節
mov ebx, [ esi+edx*4 ];
cmp eax, ebx;
jnb _noxchg; //不交換
mov [ esi+ecx*4 ], ebx;
mov [ esi+edx*4 ], eax;
_noxchg:
dec edx;
jnz _inloop;
loop _outloop;
}

for (int i=0;i<10;i++)
cout<<a[i]<<" ";
}  

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