在前一節(jié)中的x86基本寄存器的介紹,對(duì)于一個(gè)匯編語言編程人員來說是不可或缺的,F(xiàn)在你知道,寄存器是處理器內(nèi)部的一些保存數(shù)據(jù)的存儲(chǔ)單元。僅僅了解這些是不足以寫出一個(gè)可用的匯編語言程序的,但你已經(jīng)可以大致讀懂一般匯編語言程序了(不必驚訝,因?yàn)閰R編語言的祝記符和英文單詞非常接近),因?yàn)槟阋呀?jīng)了解了關(guān)于基本寄存器的絕大多數(shù)知識(shí)。
在正式引入第一個(gè)匯編語言程序之前,我粗略地介紹一下匯編語言中不同進(jìn)制整數(shù)的表示方法。如果你不了解十進(jìn)制以外的其他進(jìn)制,請(qǐng)把鼠標(biāo)移動(dòng)到這里。
匯編語言中的整數(shù)常量表示
|
需要說明的是,這些方法是針對(duì)宏匯編器(例如,MASM、TASM、NASM)說的,調(diào)試器默認(rèn)使用十六進(jìn)制表示整數(shù),并且不需要特別的聲明(例如,在調(diào)試器中直接用FFFF表示十進(jìn)制的65535,用10表示十進(jìn)制的16)。
現(xiàn)在我們來寫一小段匯編程序,修改EAX、EBX、ECX、EDX的數(shù)值。
我們假定程序執(zhí)行之前,寄存器中的數(shù)值是全0:
? | X | ||
H | L | ||
EAX | 0000 | 00 | 00 |
EBX | 0000 | 00 | 00 |
ECX | 0000 | 00 | 00 |
EDX | 0000 | 00 | 00 |
正如前面提到的,EAX的高16bit是沒有辦法直接訪問的,而AX對(duì)應(yīng)它的低16bit,AH、AL分別對(duì)應(yīng)AX的高、低8bit。
mov eax, 012345678h mov ebx, 0abcdeffeh mov ecx, 1 mov edx, 2 |
; 將012345678h送入eax ; 將0abcdeffeh送入ebx ; 將000000001h送入ecx ; 將000000002h送入edx |
則執(zhí)行上述程序段之后,寄存器的內(nèi)容變?yōu)椋?/P>
? | X | ||
H | L | ||
EAX | 1234 | 56 | 78 |
EBX | abcd | ef | fe |
ECX | 0000 | 00 | 01 |
EDX | 0000 | 00 | 02 |
那么,你已經(jīng)了解了mov這個(gè)指令(mov是move的縮寫)的一種用法。它可以將數(shù)送到寄存器中。我們來看看下面的代碼:
mov eax, ebx mov ecx, edx |
; ebx內(nèi)容送入eax ; edx內(nèi)容送入ecx |
則寄存器內(nèi)容變?yōu)椋?/P>
? | X | ||
H | L | ||
EAX | abcd | ef | fe |
EBX | abcd | ef | fe |
ECX | 0000 | 00 | 02 |
EDX | 0000 | 00 | 02 |
我們可以看到,“move”之后,數(shù)據(jù)依然保存在原來的寄存器中。不妨把mov指令理解為“送入”,或“裝入”。
練習(xí)題
把寄存器恢復(fù)成都為全0的狀態(tài),然后執(zhí)行下面的代碼:
mov eax, 0a1234h mov bx, ax mov ah, bl mov al, bh |
; 將0a1234h送入eax ; 將ax的內(nèi)容送入bx ; 將bl內(nèi)容送入ah ; 將bh內(nèi)容送入al |
思考:此時(shí),EAX的內(nèi)容將是多少?[答案]
下面我們將介紹一些指令。在介紹指令之前,我們約定:
|
在寄存器中載入另一寄存器,或立即數(shù)的值:
mov reg32, (reg32 | imm8 | imm16 | imm32) |
例如,mov eax, 010h表示,在eax中載入00000010h。需要注意的是,如果你希望在寄存器中裝入0,則有一種更快的方法,在后面我們將提到。
交換寄存器的內(nèi)容:
xchg reg32, reg32 xchg reg16, reg16 xchg reg8, reg8 |
例如,xchg ebx, ecx,則ebx與ecx的數(shù)值將被交換。由于系統(tǒng)提供了這個(gè)指令,因此,采用其他方法交換時(shí),速度將會(huì)較慢,并需要占用更多的存儲(chǔ)空間,編程時(shí)要避免這種情況,即,盡量利用系統(tǒng)提供的指令,因?yàn)槎鄶?shù)情況下,這意味著更小、更快的代碼,同時(shí)也杜絕了錯(cuò)誤(如果說Intel的CPU在交換寄存器內(nèi)容的時(shí)候也會(huì)出錯(cuò),那么它就不用賣CPU了。而對(duì)于你來說,檢查一行代碼的正確性也顯然比檢查更多代碼的正確性要容易)剛才的習(xí)題的程序用下面的代碼將更有效:
mov eax, 0a1234h mov bx, ax xchg ah, al |
; 將0a1234h送入eax ; 將ax內(nèi)容送入bx ; 交換ah, al的內(nèi)容 |
遞增或遞減寄存器的值:
inc reg(8,16,32) dec reg(8,16,32) |
這兩個(gè)指令往往用于循環(huán)中對(duì)指針的操作。需要說明的是,某些時(shí)候我們有更好的方法來處理循環(huán),例如使用loop指令,或rep前綴。這些將在后面的章節(jié)中介紹。
將寄存器的數(shù)值與另一寄存器,或立即數(shù)的值相加,并存回此寄存器:
add reg32, reg32 / imm(8,16,32) |
例如,add eax, edx,將eax+edx的值存入eax。減法指令和加法類似,只是將add換成sub。
需要說明的是,與高級(jí)語言不同,匯編語言中,如果要計(jì)算兩數(shù)之和(差、積、商,或一般地說,運(yùn)算結(jié)果),那么必然有一個(gè)寄存器被用來保存結(jié)果。在PASCAL中,我們可以用nA := nB + nC來讓nA保存nB+nC的結(jié)果,然而,匯編語言并不提供這種方法。如果你希望保持寄存器中的結(jié)果,需要用另外的指令。這也從另一個(gè)側(cè)面反映了“寄存器”這個(gè)名字的意義。數(shù)據(jù)只是“寄存”在那里。如果你需要保存數(shù)據(jù),那么需要將它放到內(nèi)存或其他地方。
類似的指令還有and、or、xor(與,或,異或)等等。它們進(jìn)行的是邏輯運(yùn)算。
我們稱add、mov、sub、and等稱為為指令助記符(這么叫是因?yàn)樗葯C(jī)器語言容易記憶,而起作用就是方便人記憶,某些資料中也稱為指令、操作碼、opcode[operation code]等);后面的參數(shù)成為操作數(shù),一個(gè)指令可以沒有操作數(shù),也可以有一兩個(gè)操作數(shù),通常有一個(gè)操作數(shù)的指令,這個(gè)操作數(shù)就是它的操作對(duì)象;而兩個(gè)參數(shù)的指令,前一個(gè)操作數(shù)一般是保存操作結(jié)果的地方,而后一個(gè)是附加的參數(shù)。
我不打算在這份教程中用大量的篇幅介紹指令——很多人做得比我更好,而且指令本身并不是重點(diǎn),如果你學(xué)會(huì)了如何組織語句,那么只要稍加學(xué)習(xí)就能輕易掌握其他指令。更多的指令可以參考Intel提供的資料。編寫程序的時(shí)候,也可以參考一些在線參考手冊(cè)。Tech!Help和HelpPC 2.10盡管已經(jīng)很舊,但足以應(yīng)付絕大多數(shù)需要。
聰明的讀者也許已經(jīng)發(fā)現(xiàn),使用sub eax, eax,或者xor eax, eax,可以得到與mov eax, 0類似的效果。在高級(jí)語言中,你大概不會(huì)選擇用a=a-a來給a賦值,因?yàn)闇y(cè)試會(huì)告訴你這么做更慢,簡直就是在自找麻煩,然而在匯編語言中,你會(huì)得到相反的結(jié)論,多數(shù)情況下,以由快到慢的速度排列,這三條指令將是xor eax, eax、sub eax, eax和mov eax, 0。
為什么呢?處理器在執(zhí)行指令時(shí),需要經(jīng)過幾個(gè)不同的階段:取指、譯碼、取數(shù)、執(zhí)行。
我們反復(fù)強(qiáng)調(diào),寄存器是CPU的一部分。從寄存器取數(shù),其速度很顯然要比從內(nèi)存中取數(shù)快。那么,不難理解,xor eax, eax要比mov eax, 0更快一些。
那么,為什么a=a-a通常要比a=0慢一些呢?這和編譯器的優(yōu)化有一定關(guān)系。多數(shù)編譯器會(huì)把a(bǔ)=a-a翻譯成類似下面的代碼(通常,高級(jí)語言通過ebp和偏移量來訪問局部變量;程序中,x為a相對(duì)于本地堆的偏移量,在只包含一個(gè)32-bit整形變量的程序中,這個(gè)值通常是4):
mov eax, dword ptr [ebp-x] |
而把a(bǔ)=0翻譯成
mov dword ptr [ebp-x], 0 |
上面的翻譯只是示意性的,略去了很多必要的步驟,如保護(hù)寄存器內(nèi)容、恢復(fù)等等。如果你對(duì)與編譯程序的實(shí)現(xiàn)過程感興趣,可以參考相應(yīng)的書籍。多數(shù)編譯器(特別是C/C++編譯器,如Microsoft Visual C++)都提供了從源代碼到宏匯編語言程序的附加編譯輸出選項(xiàng)。這種情況下,你可以很方便地了解編譯程序執(zhí)行的輸出結(jié)果;如果編譯程序沒有提供這樣的功能也沒有關(guān)系,調(diào)試器會(huì)讓你看到編譯器的編譯結(jié)果。
如果你明確地知道編譯器編譯出的結(jié)果不是最優(yōu)的,那就可以著手用匯編語言來重寫那段代碼了。怎么確認(rèn)是否應(yīng)該用匯編語言重寫呢?
使用匯編語言重寫代碼之前需要確認(rèn)的幾件事情
|
曾經(jīng)在一份雜志上看到過有人用純機(jī)器語言編寫程序。不清楚到底這是不是編輯的失誤,因?yàn)橐粋(gè)頭腦正常的人恐怕不會(huì)這么做程序,即使它不長、也不復(fù)雜。首先,匯編器能夠完成某些封包操作,即使不行,也可以用db偽指令來寫指令;用匯編語言寫程序可以防止很多錯(cuò)誤的發(fā)生,同時(shí),它還減輕了人的負(fù)擔(dān),很顯然,“完全用機(jī)器語言寫程序”是完全沒有必要的,因?yàn)閰R編語言可以做出完全一樣的事情,并且你可以依賴它,因?yàn)橛?jì)算機(jī)不會(huì)出錯(cuò),而人總有出錯(cuò)的時(shí)候。此外,如前面所言,如果用高級(jí)語言實(shí)現(xiàn)程序的代價(jià)不大(例如,這段代碼在程序的整個(gè)執(zhí)行過程中只執(zhí)行一遍,并且,這一遍的執(zhí)行時(shí)間也小于一秒),那么,為什么不用高級(jí)語言實(shí)現(xiàn)呢?
一些比較狂熱的編程愛好者可能不太喜歡我的這種觀點(diǎn)。比方說,他們可能希望精益求精地優(yōu)化每一字節(jié)的代碼。但多數(shù)情況下我們有更重要的事情,例如,你的算法是最優(yōu)的嗎?你已經(jīng)把程序在高級(jí)語言許可的范圍內(nèi)優(yōu)化到盡頭了嗎?并不是所有的人都有資格這樣說。匯編語言是這樣一件東西,它足夠的強(qiáng)大,能夠控制計(jì)算機(jī),完成它能夠?qū)崿F(xiàn)的任何功能;同時(shí),因?yàn)樗膹?qiáng)大,也會(huì)提高開發(fā)成本,并且,難于維護(hù)。因此,我個(gè)人的建議是,如果在軟件開發(fā)中使用匯編語言,則應(yīng)在軟件接近完成的時(shí)候使用,這樣可以減少很多不必要的投入。
第二章中,我介紹了x86系列處理器的基本寄存器。這些寄存器對(duì)于x86兼容處理器仍然是有效的,如果你偏愛AMD的CPU,那么使用這些寄存器的程序同樣也可以正常運(yùn)行。
不過現(xiàn)在說用匯編語言進(jìn)行優(yōu)化還為時(shí)尚早——不可能寫程序,而只操作這些寄存器,因?yàn)檫@樣只能完成非常簡單的操作,既然是簡單的操作,那可能就會(huì)讓人覺得乏味,甚至找一臺(tái)足夠快的機(jī)器窮舉它的所有結(jié)果(如果可以窮舉的話),并直接寫程序調(diào)用,因?yàn)檫@樣通常會(huì)更快。但話說回來,看完接下來的兩章——內(nèi)存和堆棧操作,你就可以獨(dú)立完成幾乎所有的任務(wù)了,配合第五章中斷、第六章子程序的知識(shí),你將知道如何駕馭處理器,并讓它為你工作。