2.2 使用寄存器
在前一節(jié)中的x86基本寄存器的介紹,對(duì)于一個(gè)匯編語(yǔ)言編程人員來(lái)說(shuō)是不可或缺的?,F(xiàn)在你知道,,寄存器是處理器內(nèi)部的一些保存數(shù)據(jù)的存儲(chǔ)單元。僅僅了解這些是不足以寫出一個(gè)可用的匯編語(yǔ)言程序的,,但你已經(jīng)可以大致讀懂一般匯編語(yǔ)言程序了(不必驚訝,,因?yàn)閰R編語(yǔ)言的祝記符和英文單詞非常接近),因?yàn)槟阋呀?jīng)了解了關(guān)于基本寄存器的絕大多數(shù)知識(shí),。
在正式引入第一個(gè)匯編語(yǔ)言程序之前,,我粗略地介紹一下匯編語(yǔ)言中不同進(jìn)制整數(shù)的表示方法。如果你不了解十進(jìn)制以外的其他進(jìn)制,,請(qǐng)把鼠標(biāo)移動(dòng)到這里,。
數(shù)字計(jì)算機(jī)內(nèi)部只支持二進(jìn)制數(shù),因?yàn)檫@樣計(jì)算機(jī)只需要表示兩種(某些情況是3種,,這一內(nèi)容超過了這份教程的范圍,,如果您感興趣,可以參考數(shù)字邏輯電路的相關(guān)書籍)狀態(tài). 對(duì)于電路而言,這表現(xiàn)為高,、低電平,,或者開、關(guān),,分別非常明顯,,因而工作比較穩(wěn)定,;另一方面,,由于只有兩種狀態(tài),設(shè)計(jì)起來(lái)也比較簡(jiǎn)單,。這樣,,使用二進(jìn)制意味著低成本、穩(wěn)定,,多數(shù)情況下,,這也意味著快速。
與十進(jìn)制類似,,我們可以用下面的式子來(lái)?yè)Q算出一個(gè)任意形如am-1……a3a2a1a0 的m位r進(jìn)制數(shù)對(duì)應(yīng)的數(shù)值n:
程序設(shè)計(jì)中常用十六進(jìn)制和八進(jìn)制數(shù)字代替二進(jìn)制數(shù),,其原因在于,16和8是2的整次方冪,,這樣,,一位十六或八進(jìn)制數(shù)可以表示整數(shù)個(gè)二進(jìn)制位。十六進(jìn)制中,,使用字母A,、B、C,、D,、E、F表示10-15,,而十六進(jìn)制或八進(jìn)制數(shù)制表示的的數(shù)字比二進(jìn)制數(shù)更短一些,。
EAX的內(nèi)容為000A3412h.
匯編語(yǔ)言中的整數(shù)常量表示
十進(jìn)制整數(shù)
這是匯編器默認(rèn)的數(shù)制。直接用我們熟悉的表示方式表示即可,。例如,,1234表示十進(jìn)制的1234。不過,,如果你指定了使用其他數(shù)制,,或者有凡事都進(jìn)行完整定義的小愛好,也可以寫成[十進(jìn)制數(shù)]d或[十進(jìn)制數(shù)]D的形式,。
十六進(jìn)制數(shù)
這是匯編程序中最常用的數(shù)制,,我個(gè)人比較偏愛使用十六進(jìn)制表示數(shù)據(jù),至于為什么,以后我會(huì)作說(shuō)明,。十六進(jìn)制數(shù)表示為0[十六進(jìn)制數(shù)]h或0[十六進(jìn)制數(shù)]H,,其中,如果十六進(jìn)制數(shù)的第一位是數(shù)字,,則開頭的0可以省略,。例如,7fffh, 0ffffh,,等等,。
二進(jìn)制數(shù)
這也是一種常用的數(shù)制。二進(jìn)制數(shù)表示為[二進(jìn)制數(shù)]b或[二進(jìn)制數(shù)]B,。一般程序中用二進(jìn)制數(shù)表示掩碼(mask code)等數(shù)據(jù)非常的直觀,,但需要些很長(zhǎng)的數(shù)據(jù)(4位二進(jìn)制數(shù)相當(dāng)于一位十六進(jìn)制數(shù))。例如,,1010110b,。
八進(jìn)制數(shù)
八進(jìn)制數(shù)現(xiàn)在已經(jīng)不是很常用了(確實(shí)還在用,一個(gè)典型的例子是Unix的文件屬性),。八進(jìn)制數(shù)的形式是[八進(jìn)制數(shù)]q,、[八進(jìn)制數(shù)]Q、[八進(jìn)制數(shù)]o,、[八進(jìn)制數(shù)]O,。例如,777Q,。
需要說(shuō)明的是,,這些方法是針對(duì)宏匯編器(例如,MASM,、TASM,、NASM)說(shuō)的,調(diào)試器默認(rèn)使用十六進(jìn)制表示整數(shù),,并且不需要特別的聲明(例如,,在調(diào)試器中直接用FFFF表示十進(jìn)制的65535,用10表示十進(jìn)制的16),。
現(xiàn)在我們來(lái)寫一小段匯編程序,,修改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ù)送到寄存器中,。我們來(lái)看看下面的代碼:
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ù)依然保存在原來(lái)的寄存器中,。不妨把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)容將是多少,?[答案]
下面我們將介紹一些指令,。在介紹指令之前,我們約定:
使用Intel文檔中的寄存器表示方式
reg32 32-bit寄存器(表示EAX,、EBX等) reg16 16-bit寄存器(在32位處理器中,,這AX、BX等) reg8 8-bit寄存器(表示AL,、BH等) imm32 32-bit立即數(shù)(可以理解為常數(shù)) imm16 16-bit立即數(shù) imm8 8-bit立即數(shù)
在寄存器中載入另一寄存器,,或立即數(shù)的值:
mov reg32, (reg32 | imm8 | imm16 | imm32)
mov reg32, (reg16 | imm8 | imm16)
mov reg8, (reg8 | imm8)
例如,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ò)誤(如果說(shuō)Intel的CPU在交換寄存器內(nèi)容的時(shí)候也會(huì)出錯(cuò),,那么它就不用賣CPU了。而對(duì)于你來(lái)說(shuō),,檢查一行代碼的正確性也顯然比檢查更多代碼的正確性要容易)剛才的習(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ì)指針的操作,。需要說(shuō)明的是,某些時(shí)候我們有更好的方法來(lái)處理循環(huán),,例如使用loop指令,,或rep前綴。這些將在后面的章節(jié)中介紹,。
將寄存器的數(shù)值與另一寄存器,,或立即數(shù)的值相加,并存回此寄存器:
add reg32, reg32 / imm(8,16,32)
add reg16, reg16 / imm(8,16)
add reg8, reg8 / imm(8)
例如,,add eax, edx,,將eax+edx的值存入eax。減法指令和加法類似,,只是將add換成sub,。
需要說(shuō)明的是,與高級(jí)語(yǔ)言不同,,匯編語(yǔ)言中,,如果要計(jì)算兩數(shù)之和(差、積,、商,,或一般地說(shuō),運(yùn)算結(jié)果),,那么必然有一個(gè)寄存器被用來(lái)保存結(jié)果,。在PASCAL中,我們可以用nA := nB + nC來(lái)讓nA保存nB+nC的結(jié)果,,然而,,匯編語(yǔ)言并不提供這種方法。如果你希望保持寄存器中的結(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ī)器語(yǔ)言容易記憶,而起作用就是方便人記憶,,某些資料中也稱為指令,、操作碼、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ì)了如何組織語(yǔ)句,,那么只要稍加學(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í)語(yǔ)言中,你大概不會(huì)選擇用a=a-a來(lái)給a賦值,,因?yàn)闇y(cè)試會(huì)告訴你這么做更慢,,簡(jiǎn)直就是在自找麻煩,,然而在匯編語(yǔ)言中,你會(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í)語(yǔ)言通過ebp和偏移量來(lái)訪問局部變量,;程序中,x為a相對(duì)于本地堆的偏移量,,在只包含一個(gè)32-bit整形變量的程序中,,這個(gè)值通常是4):
mov eax, dword ptr [ebp-x]
sub eax, dword ptr [ebp-x]
mov dword ptr [ebp-x],eax
而把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++)都提供了從源代碼到宏匯編語(yǔ)言程序的附加編譯輸出選項(xiàng),。這種情況下,,你可以很方便地了解編譯程序執(zhí)行的輸出結(jié)果;如果編譯程序沒有提供這樣的功能也沒有關(guān)系,,調(diào)試器會(huì)讓你看到編譯器的編譯結(jié)果,。
如果你明確地知道編譯器編譯出的結(jié)果不是最優(yōu)的,,那就可以著手用匯編語(yǔ)言來(lái)重寫那段代碼了。怎么確認(rèn)是否應(yīng)該用匯編語(yǔ)言重寫呢,?
使用匯編語(yǔ)言重寫代碼之前需要確認(rèn)的幾件事情
首先,,這種優(yōu)化最好有明顯的效果。比如,,一段循環(huán)中的計(jì)算,,等等。一條語(yǔ)句的執(zhí)行時(shí)間是很短的,,現(xiàn)在新的CPU的指令周期都在0.000000001s以下,,Intel甚至已經(jīng)做出了4GHz主頻(主頻的倒數(shù)是時(shí)鐘周期)的CPU,如果你的代碼自始至終只執(zhí)行一次,,并且你只是減少了幾個(gè)時(shí)鐘周期的執(zhí)行時(shí)間,,那么改變將是無(wú)法讓人察覺的;很多情況下,,這種“優(yōu)化”并不被提倡,,盡管它確實(shí)減少了執(zhí)行時(shí)間,但為此需要付出大量的時(shí)間,、人力,,多數(shù)情況下得不償失(極端情況,比如你的設(shè)備內(nèi)存價(jià)格非常昂貴的時(shí)候,,這種優(yōu)化也許會(huì)有意義),。 其次,確認(rèn)你已經(jīng)使用了最好的算法,,并且,,你優(yōu)化的程序的實(shí)現(xiàn)是正確的。匯編語(yǔ)言能夠提供同樣算法的最快實(shí)現(xiàn),,然而,它并不是萬(wàn)金油,,更不是解決一切的靈丹妙藥,。用高級(jí)語(yǔ)言實(shí)現(xiàn)一種好的算法,不一定會(huì)比匯編語(yǔ)言實(shí)現(xiàn)一種差的算法更慢,。不過需要注意的是,,時(shí)間、空間復(fù)雜度最小的算法不一定就是解決某一特定問題的最佳算法,。舉例說(shuō),,快速排序在完全逆序的情況下等價(jià)于冒泡排序,這時(shí)其他方法就比它快,。同時(shí),,用匯編語(yǔ)言優(yōu)化一個(gè)不正確的算法實(shí)現(xiàn),,將給調(diào)試帶來(lái)很大的麻煩。 最后,,確認(rèn)你已經(jīng)將高級(jí)語(yǔ)言編譯器的性能發(fā)揮到極致,。Microsoft的編譯器在RELEASE模式和DEBUG模式會(huì)有差異相當(dāng)大的輸出,而對(duì)于GNU系列的編譯器而言,,不同級(jí)別的優(yōu)化也會(huì)生成幾乎完全不同的代碼,。此外,在編程時(shí)對(duì)于問題的嚴(yán)格定義,,可以極大地幫助編譯器的優(yōu)化過程,。如何優(yōu)化高級(jí)語(yǔ)言代碼,使其編譯結(jié)果最優(yōu)超出了本教程的范圍,,但如果你不能確認(rèn)已經(jīng)發(fā)揮了編譯器的最大效能,,用匯編語(yǔ)言往往是一種更為費(fèi)力的方法。 還有一點(diǎn)非常重要,,那就是你明白自己做的是什么,。好的高級(jí)語(yǔ)言編譯器有時(shí)會(huì)有一些讓人難以理解的行為,比如,,重新排列指令順序,,等等。如果你發(fā)現(xiàn)這種情況,,那么優(yōu)化的時(shí)候就應(yīng)該小心——編譯器很可能比你擁有更多的關(guān)于處理器的知識(shí),,例如,對(duì)于一個(gè)超標(biāo)量處理器,,編譯器會(huì)對(duì)指令序列進(jìn)行“封包”,,使他們盡可能的并行執(zhí)行;此外,,宏匯編器有時(shí)會(huì)自動(dòng)插入一些nop指令,,其作用是將指令湊成整數(shù)字長(zhǎng)(32-bit,對(duì)于16-bit處理器,,是16-bit),。這些都是提高代碼性能的必要措施,如果你不了解處理器,,那么最好不要改動(dòng)編譯器生成的代碼,,因?yàn)檫@種情況下,盲目的修改往往不會(huì)得到預(yù)期的效果,。
曾經(jīng)在一份雜志上看到過有人用純機(jī)器語(yǔ)言編寫程序,。不清楚到底這是不是編輯的失誤,因?yàn)橐粋€(gè)頭腦正常的人恐怕不會(huì)這么做程序,,即使它不長(zhǎng),、也不復(fù)雜,。首先,匯編器能夠完成某些封包操作,,即使不行,,也可以用db偽指令來(lái)寫指令;用匯編語(yǔ)言寫程序可以防止很多錯(cuò)誤的發(fā)生,,同時(shí),,它還減輕了人的負(fù)擔(dān),很顯然,,“完全用機(jī)器語(yǔ)言寫程序”是完全沒有必要的,,因?yàn)閰R編語(yǔ)言可以做出完全一樣的事情,并且你可以依賴它,,因?yàn)橛?jì)算機(jī)不會(huì)出錯(cuò),,而人總有出錯(cuò)的時(shí)候。此外,,如前面所言,,如果用高級(jí)語(yǔ)言實(shí)現(xiàn)程序的代價(jià)不大(例如,這段代碼在程序的整個(gè)執(zhí)行過程中只執(zhí)行一遍,,并且,,這一遍的執(zhí)行時(shí)間也小于一秒),那么,,為什么不用高級(jí)語(yǔ)言實(shí)現(xiàn)呢,?
一些比較狂熱的編程愛好者可能不太喜歡我的這種觀點(diǎn)。比方說(shuō),,他們可能希望精益求精地優(yōu)化每一字節(jié)的代碼,。但多數(shù)情況下我們有更重要的事情,例如,,你的算法是最優(yōu)的嗎,?你已經(jīng)把程序在高級(jí)語(yǔ)言許可的范圍內(nèi)優(yōu)化到盡頭了嗎?并不是所有的人都有資格這樣說(shuō),。匯編語(yǔ)言是這樣一件東西,,它足夠的強(qiáng)大,能夠控制計(jì)算機(jī),,完成它能夠?qū)崿F(xiàn)的任何功能;同時(shí),,因?yàn)樗膹?qiáng)大,,也會(huì)提高開發(fā)成本,并且,,難于維護(hù),。因此,,我個(gè)人的建議是,如果在軟件開發(fā)中使用匯編語(yǔ)言,,則應(yīng)在軟件接近完成的時(shí)候使用,,這樣可以減少很多不必要的投入。
第二章中,,我介紹了x86系列處理器的基本寄存器,。這些寄存器對(duì)于x86兼容處理器仍然是有效的,如果你偏愛AMD的CPU,,那么使用這些寄存器的程序同樣也可以正常運(yùn)行,。
不過現(xiàn)在說(shuō)用匯編語(yǔ)言進(jìn)行優(yōu)化還為時(shí)尚早——不可能寫程序,而只操作這些寄存器,,因?yàn)檫@樣只能完成非常簡(jiǎn)單的操作,,既然是簡(jiǎn)單的操作,那可能就會(huì)讓人覺得乏味,,甚至找一臺(tái)足夠快的機(jī)器窮舉它的所有結(jié)果(如果可以窮舉的話),,并直接寫程序調(diào)用,因?yàn)檫@樣通常會(huì)更快,。但話說(shuō)回來(lái),,看完接下來(lái)的兩章——內(nèi)存和堆棧操作,你就可以獨(dú)立完成幾乎所有的任務(wù)了,,配合第五章中斷,、第六章子程序的知識(shí),你將知道如何駕馭處理器,,并讓它為你工作,。