版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
簡(jiǎn)明X86匯編語(yǔ)言教程目錄第Ο章寫在前面匯編語(yǔ)言簡(jiǎn)介第二章認(rèn)識(shí)處理器2.1寄存器2.2使用寄存器第三章操作內(nèi)存3.1實(shí)模式3.2保護(hù)模式3.3操作內(nèi)存3.4串操作3.5關(guān)于保護(hù)模式中內(nèi)存操作的一點(diǎn)說(shuō)明3.6堆棧本章小結(jié)第四章利用子程序與中斷4.1子程序4.2中斷第五章編譯優(yōu)化概述5.1循環(huán)優(yōu)化:強(qiáng)度削減和代碼外提5.2局部?jī)?yōu)化:表達(dá)式預(yù)計(jì)算和子表達(dá)式提取5.3全局寄存器優(yōu)化5.4x86體系結(jié)構(gòu)上的并行大化和指令封包5.5存儲(chǔ)優(yōu)化第六章LinuxX86匯編程序設(shè)計(jì)6.1編譯和鏈接6.2基本示例第七章X86匯編指令集匯總7.1數(shù)據(jù)傳輸指令7.2算術(shù)運(yùn)算指令7.3邏輯運(yùn)算指令7.4串指令7.5程序轉(zhuǎn)移指令7.6偽指令7.7寄存器7.8位操作指令,處理器控制指令FPUinstructions第八章GCC內(nèi)聯(lián)匯編基礎(chǔ)GCC匯編格式內(nèi)聯(lián)匯編基本形式擴(kuò)展形式內(nèi)聯(lián)匯編深入constra本章小束語(yǔ)第Ο章寫在前面我不想夸大或者貶低匯編語(yǔ)言。但我想說(shuō),匯編語(yǔ)言改變了20世紀(jì)的歷史。與前輩相比,我們這一代編程人員足夠的幸福,因?yàn)槲覀冇懈魇礁鳂拥木幊陶Z(yǔ)言,我們可以操作鍵盤、坐在顯示器面前,甚至使用鼠標(biāo)、語(yǔ)音識(shí)別。我們可以使用鍵盤、鼠標(biāo)來(lái)駕馭“個(gè)人計(jì)算機(jī)”,而不是和一群人共享一臺(tái)使用笨重的繼電器、開(kāi)關(guān)去操作的巨型機(jī)。相比之下,我們的前輩不得不使用機(jī)器語(yǔ)言編寫程序,他們甚至沒(méi)有簡(jiǎn)單的匯編程序來(lái)把助記符翻譯成機(jī)器語(yǔ)言,而我們可以從上千種計(jì)算機(jī)語(yǔ)言中選擇我們喜歡的一種,而匯編,雖然不是一種“常用”的具有“快速原型開(kāi)發(fā)”能力的語(yǔ)言,卻也是我們可以選擇的語(yǔ)言中的一種。每種計(jì)算機(jī)都有自己的匯編語(yǔ)言——沒(méi)必要指望匯編語(yǔ)言的可移植性,選擇匯編,意味著選擇性能而不是可移植或便于調(diào)試。這份文檔中講述的是x86匯編語(yǔ)言,此后的“匯編語(yǔ)言”一詞,如果不明示則表示IA32上的x86匯編語(yǔ)言。匯編語(yǔ)言是一種易學(xué),卻很難精通的語(yǔ)言。回想當(dāng)年,我從初學(xué)匯編到寫出第一個(gè)可運(yùn)行的程序,只用了不到4個(gè)小時(shí);然而直到今天,我仍然不敢說(shuō)自己精通它。編寫快速、高效、并且能夠讓處理器“很舒服地執(zhí)行”的程序是一件很困難的事情,如果利用業(yè)余時(shí)間學(xué)習(xí),通常需要2-3年的時(shí)間才能做到。這份教材并不期待能夠教給你大量的匯編語(yǔ)言技巧。對(duì)于讀者來(lái)說(shuō),x86匯編語(yǔ)言"就在這里"。然而,不要僵化地局限于這份教材講述的內(nèi)容,因?yàn)樗荒芨嬖V你匯編語(yǔ)言是“這樣一回事”。學(xué)好匯編語(yǔ)言,更多的要靠一個(gè)人的創(chuàng)造力與悟性,我可以告訴你我所知道的技巧,但肯定這是不夠的。一位對(duì)我的編程生涯產(chǎn)生過(guò)重要影響的人曾經(jīng)對(duì)我說(shuō)過(guò)這么一句話:寫匯編語(yǔ)言程序不是匯編語(yǔ)言難的部分,創(chuàng)新才是。 我想,愿意看這份文檔的人恐怕不會(huì)問(wèn)我“為什么要學(xué)習(xí)匯編語(yǔ)言”這樣的問(wèn)題;不過(guò),我還是想說(shuō)幾句:首先,匯編語(yǔ)言非常有用,我個(gè)人主張把它作為C語(yǔ)言的先修課程,因?yàn)橥ㄟ^(guò)學(xué)習(xí)匯編語(yǔ)言,你可以了解到如何有效地設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),讓計(jì)算機(jī)處理得更快,并使用更少的存儲(chǔ)空間;同時(shí),學(xué)習(xí)匯編語(yǔ)言可以讓你熟悉計(jì)算機(jī)內(nèi)部運(yùn)行機(jī)制,并且,有效地提高調(diào)試能力。就我個(gè)人的經(jīng)驗(yàn)而言,調(diào)試一個(gè)非結(jié)構(gòu)化的程序的困難程度,要比調(diào)試一個(gè)結(jié)構(gòu)化的程序的難度高很多,因?yàn)椤敖Y(jié)構(gòu)化”是以犧牲運(yùn)行效率來(lái)提高可讀性與可調(diào)試性,這對(duì)于完成一般軟件工程的編碼階段是非常必要的。然而,在一些地方,比如,硬件驅(qū)動(dòng)程序、操作系統(tǒng)底層,或者程序中經(jīng)常需要執(zhí)行的代碼,結(jié)構(gòu)化程序設(shè)計(jì)的這些優(yōu)點(diǎn)有時(shí)就會(huì)被它的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代碼級(jí)的調(diào)試是遠(yuǎn)遠(yuǎn)不夠的。 浮躁的人喜歡說(shuō),用C++寫程序足夠了,甚至說(shuō),他不僅僅掌握C++,而且精通STL、MFC。我不贊成這個(gè)觀點(diǎn),掌握上面的那些是每一個(gè)編程人員都應(yīng)該做到的,然而C++只是我們"常用"的一種語(yǔ)言,它不是編程的全部。低層次的開(kāi)發(fā)者喜歡說(shuō),嘿,C++是多么的強(qiáng)大,它可以做任何事情——這不是事實(shí)。便于維護(hù)、調(diào)試,這些確實(shí)是我們的追求目標(biāo),但是,寫程序不能僅僅追求這個(gè)目標(biāo)(還有性能、功耗整理者注),因?yàn)槲覀兘K的目的是滿足設(shè)計(jì)需求,而不是個(gè)人非理性的理想。這份教材適合已經(jīng)學(xué)習(xí)過(guò)某種結(jié)構(gòu)化程序設(shè)計(jì)語(yǔ)言的讀者。其內(nèi)容基于我在1995年給別人講述匯編語(yǔ)言時(shí)所寫的講義。當(dāng)然,如大家所希望的,它包含了新的處理器所支持的特性,以及相應(yīng)的內(nèi)容。我假定讀者已經(jīng)知道了程序設(shè)計(jì)的一些基本概念,因?yàn)闆](méi)有這些是無(wú)法理解匯編語(yǔ)言程序設(shè)計(jì)的;此外,我希望讀者已經(jīng)有了比較良好的程序設(shè)計(jì)基礎(chǔ),因?yàn)槿绻闳狈?duì)于結(jié)構(gòu)化程序設(shè)計(jì)的認(rèn)識(shí),編寫匯編語(yǔ)言程序很可能很快就破壞了你的結(jié)構(gòu)化編程習(xí)慣,大大降低程序的可讀性、可維護(hù)性,終讓你的程序陷于不得不廢棄的代碼堆之中。 基本上,這份文檔撰寫的目標(biāo)是盡可能地便于自學(xué)。不過(guò),它對(duì)你也有一些要求,盡管不是很高,但我還是強(qiáng)調(diào)一下。學(xué)習(xí)匯編語(yǔ)言,你需要:膽量。不要害怕去接觸那些計(jì)算機(jī)的內(nèi)部工作機(jī)制。知識(shí)。了解計(jì)算機(jī)常用的數(shù)制,特別是二進(jìn)制、十六進(jìn)制、八進(jìn)制,以及計(jì)算機(jī)保存數(shù)據(jù)的方法。開(kāi)放。接受匯編語(yǔ)言與高級(jí)語(yǔ)言的差異,而不是去指責(zé)它如何的不好讀。經(jīng)驗(yàn)。要求你擁有任意其他編程語(yǔ)言的一點(diǎn)點(diǎn)編程經(jīng)驗(yàn)。頭腦。祝您編程愉快!第一章匯編語(yǔ)言簡(jiǎn)介先說(shuō)一點(diǎn)和實(shí)際編程關(guān)系不太大的東西。當(dāng)然,如果你迫切的想看到更實(shí)質(zhì)的內(nèi)容,完全可以先跳過(guò)這一章。那么,我想可能有一個(gè)問(wèn)題對(duì)于初學(xué)匯編的人來(lái)說(shuō)非常重要,那就是:匯編語(yǔ)言到底是什么?匯編語(yǔ)言是一種接近計(jì)算機(jī)核心的編碼語(yǔ)言。不同于任何高級(jí)語(yǔ)言,匯編語(yǔ)言幾乎可以完全和機(jī)器語(yǔ)言一一對(duì)應(yīng)。不錯(cuò),我們可以用機(jī)器語(yǔ)言寫程序,但現(xiàn)在除了沒(méi)有匯編程序的那些電腦之外,直接用機(jī)器語(yǔ)言寫超過(guò)1000條以上指令的人大概只能算作那些被我們成為“圣人”的犧牲者一類了。畢竟,記憶一些短小的助記符、由機(jī)器去考慮那些瑣碎的配位過(guò)程和檢查錯(cuò)誤,比記憶大量的隨計(jì)算機(jī)而改變的十六進(jìn)制代碼、可能弄錯(cuò)而沒(méi)有任何提示要強(qiáng)的多。熟練的匯編語(yǔ)言編碼員甚至可以直接從十六進(jìn)制代碼中讀出匯編語(yǔ)言的大致意思。當(dāng)然,我們有更好的工具——匯編器和反匯編器。 簡(jiǎn)單地說(shuō),匯編語(yǔ)言就是機(jī)器語(yǔ)言的一種可以被人讀懂的形式,只不過(guò)它更容易記憶。至于宏匯編,則是包含了宏支持的匯編語(yǔ)言,這可以讓你編程的時(shí)候更專注于程序本身,而不是忙于計(jì)算和重寫代碼。匯編語(yǔ)言除了機(jī)器語(yǔ)言之外接近計(jì)算機(jī)硬件的編程語(yǔ)言。由于它如此的接近計(jì)算機(jī)硬件,因此,它可以大限度地發(fā)揮計(jì)算機(jī)硬件的性能。用匯編語(yǔ)言編寫的程序的速度通常要比高級(jí)語(yǔ)言和C/C++快很多--幾倍,幾十倍,甚至成百上千倍。當(dāng)然,解釋語(yǔ)言,如解釋型LISP,沒(méi)有采用JIT技術(shù)的Java虛擬機(jī)中運(yùn)行的Java等等,其程序速度更無(wú)法與匯編語(yǔ)言程序同日而語(yǔ)。 永遠(yuǎn)不要忽視匯編語(yǔ)言的高速。實(shí)際的應(yīng)用系統(tǒng)中,我們往往會(huì)用匯編徹底重寫某些經(jīng)常調(diào)用的部分以期獲得更高的性能。應(yīng)用匯編也許不能提高你的程序的穩(wěn)定性,但至少,如果你非常小心的話,它也不會(huì)降低穩(wěn)定性;與此同時(shí),它可以大大地提高程序的運(yùn)行速度。我強(qiáng)烈建議所有的軟件產(chǎn)品在后Release之前對(duì)整個(gè)代碼進(jìn)行Profile,并適當(dāng)?shù)赜脜R編取代部分高級(jí)語(yǔ)言代碼。至少,匯編語(yǔ)言的知識(shí)可以告訴你一些有用的東西,比如,你有多少個(gè)寄存器可以用。有時(shí),手工的優(yōu)化比編譯器的優(yōu)化更為有效,而且,你可以完全控制程序的實(shí)際行為。 我想我在羅嗦了??傊?,在我們結(jié)束這一章之前,我想說(shuō),不要在優(yōu)化的時(shí)候把希望完全寄托在編譯器上——現(xiàn)實(shí)一些,再好的編譯器也不可能總是產(chǎn)生優(yōu)的代碼。第二章認(rèn)識(shí)處理器中央處理器(CPU)在微機(jī)系統(tǒng)處于“領(lǐng)導(dǎo)核心”的地位。匯編語(yǔ)言被編譯成機(jī)器語(yǔ)言之后,將由處理器來(lái)執(zhí)行。那么,首先讓我們來(lái)了解一下處理器的主要作用,這將幫助你更好地駕馭它。 典型的處理器的主要任務(wù)包括從內(nèi)存中獲取機(jī)器語(yǔ)言指令,譯碼,執(zhí)行根據(jù)指令代碼管理它自己的寄存器根據(jù)指令或自己的需要修改內(nèi)存的內(nèi)容響應(yīng)其他硬件的中斷請(qǐng)求。一般說(shuō)來(lái),處理器擁有對(duì)整個(gè)系統(tǒng)的所有總線的控制權(quán)。對(duì)于Intel平臺(tái)而言,處理器擁有對(duì)數(shù)據(jù)、內(nèi)存和控制總線的控制權(quán),根據(jù)指令控制整個(gè)計(jì)算機(jī)的運(yùn)行。在以后的章節(jié)中,我們還將討論系統(tǒng)中同時(shí)存在多個(gè)處理器的情況。 處理器中有一些寄存器,這些寄存器可以保存特定長(zhǎng)度的數(shù)據(jù)。某些寄存器中保存的數(shù)據(jù)對(duì)于系統(tǒng)的運(yùn)行有特殊的意義。新的處理器往往擁有更多、具有更大字長(zhǎng)的寄存器,提供更靈活的取指、尋址方式。2.1寄存器 如前所述,處理器中有一些可以保存數(shù)據(jù)的地方被稱作寄存器。寄存器可以被裝入數(shù)據(jù),你也可以在不同的寄存器之間移動(dòng)這些數(shù)據(jù),或者做類似的事情?;旧希袼膭t運(yùn)算、位運(yùn)算等這些計(jì)算操作,都主要是針對(duì)寄存器進(jìn)行的。首先讓我來(lái)介紹一下80386上常用的4個(gè)通用寄存器。先瞧瞧下面的圖形,試著理解一下:上圖中,數(shù)字表示的是位。我們可以看出,EAX是一個(gè)32-bit寄存器。同時(shí),它的低16-bit又可以通過(guò)AX這個(gè)名字來(lái)訪問(wèn);AX又被分為高、低8bit兩部分,分別由AH和AL來(lái)表示。對(duì)于EAX、AX、AH、AL的改變同時(shí)也會(huì)影響與被修改的那些寄存器的值。從而事實(shí)上只存在一個(gè)32-bit的寄存器EAX,而它可以通過(guò)4種不同的途徑訪問(wèn)。也許通過(guò)名字能夠更容易地理解這些寄存器之間的關(guān)系。EAX中的E的意思是“擴(kuò)展的”,整個(gè)EAX的意思是擴(kuò)展的AX。X的意思Intel沒(méi)有明示,我個(gè)人認(rèn)為表示它是一個(gè)可變的量。而AH、AL中的H和L分別代表高和低。 為什么要這么做呢?主要由于歷史原因。早期的計(jì)算機(jī)是8位的,8086是第一個(gè)16位處理器,其通用寄存器的名字是AX,BX等等;80386是Intel推出的第一款I(lǐng)A-32系列處理器,所有的寄存器都被擴(kuò)充為32位。為了能夠兼容以前的16位應(yīng)用程序,80386不能將這些寄存器依舊命名為AX、BX,并且簡(jiǎn)單地將他們擴(kuò)充為32位——這將增加處理器在處理指令方面的成本。 Intel微處理器的寄存器列表(在本章只介紹80386的寄存器,MMX寄存器以及其他新一代處理器的新寄存器將在以后的章節(jié)介紹)通用寄存器下面介紹通用寄存器及其習(xí)慣用法。顧名思義,通用寄存器是那些你可以根據(jù)自己的意愿使用的寄存器,修改他們的值通常不會(huì)對(duì)計(jì)算機(jī)的運(yùn)行造成很大的影響。通用寄存器多的用途是計(jì)算。EAX32-bit寬通用寄存器。相對(duì)其他寄存器,在進(jìn)行運(yùn)算方面比較常用。在保護(hù)模式中,也可以作為內(nèi)存偏移指針(此時(shí),DS作為段寄存器或選擇器)EBX32-bit寬通用寄存器。通常作為內(nèi)存偏移指針使用(相對(duì)于EAX、ECX、EDX),DS是默認(rèn)的段寄存器或選擇器。在保護(hù)模式中,同樣可以起這個(gè)作用。ECX32-bit寬通用寄存器。通常用于特定指令的計(jì)數(shù)。在保護(hù)模式中,也可以作為內(nèi)存偏移指針(此時(shí),DS作為寄存器或段選擇器)。EDX32-bit寬通用寄存器。在某些運(yùn)算中作為EAX的溢出寄存器(例如乘、除)。在保護(hù)模式中,也可以作為內(nèi)存偏移指針(此時(shí),DS作為段寄存器或選擇器)。上述寄存器同EAX一樣包括對(duì)應(yīng)的16-bit和8-bit分組。用作內(nèi)存指針的特殊寄存器ESI32-bit寬通常在內(nèi)存操作指令中作為“源地址指針”使用。當(dāng)然,ESI可以被裝入任意的數(shù)值,但通常沒(méi)有人把它當(dāng)作通用寄存器來(lái)用。DS是默認(rèn)段寄存器或選擇器。EDI32-bit寬通常在內(nèi)存操作指令中作為“目的地址指針”使用。當(dāng)然,EDI也可以被裝入任意的數(shù)值,但通常沒(méi)有人把它當(dāng)作通用寄存器來(lái)用。DS是默認(rèn)段寄存器或選擇器。EBP32-bit寬這也是一個(gè)作為指針的寄存器。通常,它被高級(jí)語(yǔ)言編譯器用以建造‘堆棧幀’來(lái)保存函數(shù)或過(guò)程的局部變量,不過(guò),還是那句話,你可以在其中保存你希望的任何數(shù)據(jù)。SS是它的默認(rèn)段寄存器或選擇器。注意,這三個(gè)寄存器沒(méi)有對(duì)應(yīng)的8-bit分組。換言之,你可以通過(guò)SI、DI、BP作為別名訪問(wèn)他們的低16位,卻沒(méi)有辦法直接訪問(wèn)他們的低8位。段寄存器和選擇器實(shí)模式下的段寄存器到保護(hù)模式下?lián)u身一變就成了選擇器。不同的是,實(shí)模式下的“段寄存器”是16-bit的,而保護(hù)模式下的選擇器是32-bit的。CS代碼段,或代碼選擇器。同IP寄存器(稍后介紹)一同指向當(dāng)前正在執(zhí)行的那個(gè)地址。處理器執(zhí)行時(shí)從這個(gè)寄存器指向的段(實(shí)模式)或內(nèi)存(保護(hù)模式)中獲取指令。除了跳轉(zhuǎn)或其他分支指令之外,你無(wú)法修改這個(gè)寄存器的內(nèi)容。DS數(shù)據(jù)段,或數(shù)據(jù)選擇器。這個(gè)寄存器的低16bit連同ESI一同指向的指令將要處理的內(nèi)存。同時(shí),所有的內(nèi)存操作指令默認(rèn)情況下都用它指定操作段(實(shí)模式)或內(nèi)存(作為選擇器,在保護(hù)模式。這個(gè)寄存器可以被裝入任意數(shù)值,然而在這么做的時(shí)候需要小心一些。方法是,首先把數(shù)據(jù)送給AX,然后再把它從AX傳送給DS(當(dāng)然,也可以通過(guò)堆棧來(lái)做).ES附加段,或附加選擇器。這個(gè)寄存器的低16bit連同EDI一同指向的指令將要處理的內(nèi)存。同樣的,這個(gè)寄存器可以被裝入任意數(shù)值,方法和DS類似。FSF段或F選擇器(推測(cè)F可能是Free?)。可以用這個(gè)寄存器作為默認(rèn)段寄存器或選擇器的一個(gè)替代品。它可以被裝入任何數(shù)值,方法和DS類似。GSG段或G選擇器(G的意義和F一樣,沒(méi)有在Intel的文檔中解釋)。它和FS幾乎完全一樣。SS堆棧段或堆棧選擇器。這個(gè)寄存器的低16bit連同ESP一同指向下一次堆棧操作(push和pop)所要使用的堆棧地址。這個(gè)寄存器也可以被裝入任意數(shù)值,你可以通過(guò)入棧和出棧操作來(lái)給他賦值,不過(guò)由于堆棧對(duì)于很多操作有很重要的意義,因此,不正確的修改有可能造成對(duì)堆棧的破壞。*注意一定不要在初學(xué)匯編的階段把這些寄存器弄混。他們非常重要,而一旦你掌握了他們,你就可以對(duì)他們做任意的操作了。段寄存器,或選擇器,在沒(méi)有指定的情況下都是使用默認(rèn)的那個(gè)。這句話在現(xiàn)在看來(lái)可能有點(diǎn)稀里糊涂,不過(guò)你很快就會(huì)在后面知道如何去做。特殊寄存器(指向到特定段或內(nèi)存的偏移量):EIP這個(gè)寄存器非常的重要。這是一個(gè)32位寬的寄存器,同CS一同指向即將執(zhí)行的那條指令的地址。不能夠直接修改這個(gè)寄存器的值,修改它的唯一方法是跳轉(zhuǎn)或分支指令。(CS是默認(rèn)的段或選擇器)ESP這個(gè)32位寄存器指向堆棧中即將被操作的那個(gè)地址。盡管可以修改它的值,然而并不提倡這樣做,因?yàn)槿绻悴皇欠浅C靼鬃约涸谧鍪裁?,那么你可能造成堆棧的破壞。?duì)于絕大多數(shù)情況而言,這對(duì)程序是致命的。(SS是默認(rèn)的段或選擇器)IP:InstructionPointer,指令指針SP:StackPointer,堆棧指針 好了,上面是基本的寄存器。下面是一些其他的寄存器,你甚至可能沒(méi)有聽(tīng)說(shuō)過(guò)它們。(都是32位寬):CR0,CR2,CR3(控制寄存器)。舉一個(gè)例子,CR0的作用是切換實(shí)模式和保護(hù)模式。還有其他一些寄存器,D0,D1,D2,D3,D6和D7(調(diào)試寄存器)。他們可以作為調(diào)試器的硬件支持來(lái)設(shè)置條件斷點(diǎn)。TR3,TR4,TR5,TR6和TR?寄存器(測(cè)試寄存器)用于某些條件測(cè)試。后我們要說(shuō)的是一個(gè)在程序設(shè)計(jì)中起著非常關(guān)鍵的作用的寄存器:標(biāo)志寄存器。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)到這里。匯編語(yǔ)言中的整數(shù)常量表示十進(jìn)制整數(shù)這是匯編器默認(rèn)的數(shù)制。直接用我們熟悉的表示方式表示即可。例如,1234表示十進(jìn)制的1234。不過(guò),如果你指定了使用其他數(shù)制,或者有凡事都進(jìn)行完整定義的小愛(ài)好,也可以寫成[十進(jìn)制數(shù)]d或[十進(jìn)制數(shù)]D的形式。十六進(jìn)制數(shù)這是匯編程序中常用的數(shù)制,我個(gè)人比較偏愛(ài)使用十六進(jìn)制表示數(shù)據(jù),至于為什么,以后我會(huì)作說(shuō)明。十六進(jìn)制數(shù)表示為0[十六進(jìn)制數(shù)]h或0[十六進(jìn)制數(shù)]H,其中,如果十六進(jìn)制數(shù)的第一位是數(shù)字,則開(kāi)頭的0可以省略。例如,7fffh,0ffffh等等。二進(jìn)制數(shù)這也是一種常用的數(shù)制。二進(jìn)制數(shù)表示為[二進(jìn)制數(shù)]b或[二進(jìn)制數(shù)]B。一般程序中用二進(jìn)制數(shù)表示掩碼(maskcode)等數(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)?,F(xiàn)在我們來(lái)寫一小段匯編程序,修改EAX、EBX、ECX、EDX的數(shù)值。我們假定程序執(zhí)行之前,寄存器中的數(shù)值是全0:?XHLEAX00000000EBX00000000ECX00000000EDX00000000正如前面提到的,EAX的高16bit是沒(méi)有辦法直接訪問(wèn)的,而AX對(duì)應(yīng)它的低16bit,AH、AL分別對(duì)應(yīng)AX的高、低8bit。moveax,012345678hmovebx,0abcdeffehmovecx,1movedx,2;將012345678h送入eax;將0abcdeffeh送入ebx;將000000001h送入ecx;將000000002h送入edx則執(zhí)行上述程序段之后,寄存器的內(nèi)容變?yōu)椋?XHLEAX12345678EBXabcdeffeECX00000001EDX00000002那么,你已經(jīng)了解了mov這個(gè)指令(mov是move的縮寫)的一種用法。它可以將數(shù)送到寄存器中。我們來(lái)看看下面的代碼:moveax,ebx;ebx內(nèi)容送入eaxmovecx,edx;edx內(nèi)容送入ecx則寄存器內(nèi)容變?yōu)椋?XHLEAXabcdeffeEBXabcdeffeECX00000002EDX00000002我們可以看到,“move”之后,數(shù)據(jù)依然保存在原來(lái)的寄存器中。不妨把mov指令理解為“送入”,或“裝入”。練習(xí)題把寄存器恢復(fù)成都為全0的狀態(tài),然后執(zhí)行下面的代碼:moveax,0a1234hmovbx,axmovah,blmoval,bh;將0a1234h送入eax;將ax的內(nèi)容送入bx;將bl內(nèi)容送入ah;將bh內(nèi)容送入al思考:此時(shí),EAX的內(nèi)容將是多少?[答案]下面我們將介紹一些指令。在介紹指令之前,我們約定:使用Intel文檔中的寄存器表示方式reg3232-bit寄存器(表示EAX、EBX等)reg1616-bit寄存器(在32位處理器中,表示AX、BX等)reg88-bit寄存器(表示AL、BH等)imm3232-bit立即數(shù)(可以理解為常數(shù))imm1616-bit立即數(shù)imm88-bit立即數(shù)在寄存器中載入另一寄存器,或立即數(shù)的值:movreg32,(reg32|imm8|imm16|imm32)movreg32,(reg16|imm8|imm16)movreg8,(reg8|imm8)例如,moveax,010h表示,在eax中載入00000010h。需要注意的是,如果你希望在寄存器中裝入0,則有一種更快的方法,在后面我們將提到。交換寄存器的內(nèi)容:xchgreg32,reg32xchgreg16,reg16xchgreg8,reg8例如,xchgebx,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í)題的程序用下面的代碼將更有效:moveax,0a1234hmovbx,axxchgah,al;將0a1234h送入eax;將ax內(nèi)容送入bx;交換ah,al的內(nèi)容遞增或遞減寄存器的值:increg(8,16,32)decreg(8,16,32)這兩個(gè)指令往往用于循環(huán)中對(duì)指針的操作。需要說(shuō)明的是,某些時(shí)候我們有更好的方法來(lái)處理循環(huán),例如使用loop指令,或rep前綴。這些將在后面的章節(jié)中介紹。將寄存器的數(shù)值與另一寄存器,或立即數(shù)的值相加,并存回此寄存器:addreg32,reg32/imm(8,16,32)addreg16,reg16/imm(8,16)addreg8,reg8/imm(8)例如,addeax,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[operationcode]等);后面的參數(shù)成為操作數(shù),一個(gè)指令可以沒(méi)有操作數(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和HelpPC2.10盡管已經(jīng)很舊,但足以應(yīng)付絕大多數(shù)需要。 聰明的讀者也許已經(jīng)發(fā)現(xiàn),使用subeax,eax,或者xoreax,eax,可以得到與moveax,0類似的效果。在高級(jí)語(yǔ)言中,你大概不會(huì)選擇用a=a-a來(lái)給a賦值,因?yàn)闇y(cè)試會(huì)告訴你這么做更慢,簡(jiǎn)直就是在自找麻煩,然而在匯編語(yǔ)言中,你會(huì)得到相反的結(jié)論,多數(shù)情況下,以由快到慢的速度排列,這三條指令將是xoreax,eax、subeax,eax和moveax,0。為什么呢?處理器在執(zhí)行指令時(shí),需要經(jīng)過(guò)幾個(gè)不同的階段:取指、譯碼、取數(shù)、執(zhí)行。我們反復(fù)強(qiáng)調(diào),寄存器是CPU的一部分。從寄存器取數(shù),其速度很顯然要比從內(nèi)存中取數(shù)快。那么,不難理解,xoreax,eax要比moveax,0更快一些。那么,為什么a=a-a通常要比a=0慢一些呢?這和編譯器的優(yōu)化有一定關(guān)系。多數(shù)編譯器會(huì)把a(bǔ)=a-a翻譯成類似下面的代碼(通常,高級(jí)語(yǔ)言通過(guò)ebp和偏移量來(lái)訪問(wèn)局部變量;程序中,x為a相對(duì)于本地堆的偏移量,在只包含一個(gè)32-bit整形變量的程序中,這個(gè)值通常是4):moveax,dwordptr[ebp-x]subeax,dwordptr[ebp-x]movdwordptr[ebp-x],eax而把a(bǔ)=0翻譯成movdwordptr[ebp-x],0 上面的翻譯只是示意性的,略去了很多必要的步驟,如保護(hù)寄存器內(nèi)容、恢復(fù)等等。如果你對(duì)與編譯程序的實(shí)現(xiàn)過(guò)程感興趣,可以參考相應(yīng)的書籍。多數(shù)編譯器(特別是C/C++編譯器,如MicrosoftVisualC++)都提供了從源代碼到宏匯編語(yǔ)言程序的附加編譯輸出選項(xiàng)。這種情況下,你可以很方便地了解編譯程序執(zhí)行的輸出結(jié)果;如果編譯程序沒(méi)有提供這樣的功能也沒(méi)有關(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ú)法讓人察覺(jué)的;很多情況下,這種“優(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)一種差的算法更慢。不過(guò)需要注意的是,時(shí)間、空間復(fù)雜度小的算法不一定就是解決某一特定問(wèn)題的佳算法。舉例說(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ì)于問(wèn)題的嚴(yán)格定義,可以極大地幫助編譯器的優(yōu)化過(guò)程。如何優(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)在一份雜志上看到過(guò)有人用純機(jī)器語(yǔ)言編寫程序。不清楚到底這是不是編輯的失誤,因?yàn)橐粋€(gè)頭腦正常的人恐怕不會(huì)這么做程序,即使它不長(zhǎng)、也不復(fù)雜。首先,匯編器能夠完成某些封包操作,即使不行,也可以用db偽指令來(lái)寫指令;用匯編語(yǔ)言寫程序可以防止很多錯(cuò)誤的發(fā)生,同時(shí),它還減輕了人的負(fù)擔(dān),很顯然,“完全用機(jī)器語(yǔ)言寫程序”是完全沒(méi)有必要的,因?yàn)閰R編語(yǔ)言可以做出完全一樣的事情,并且你可以依賴它,因?yàn)橛?jì)算機(jī)不會(huì)出錯(cuò),而人總有出錯(cuò)的時(shí)候。此外,如前面所言,如果用高級(jí)語(yǔ)言實(shí)現(xiàn)程序的代價(jià)不大(例如,這段代碼在程序的整個(gè)執(zhí)行過(guò)程中只執(zhí)行一遍,并且,這一遍的執(zhí)行時(shí)間也小于一秒),那么,為什么不用高級(jí)語(yǔ)言實(shí)現(xiàn)呢? 一些比較狂熱的編程愛(ài)好者可能不太喜歡我的這種觀點(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ì)提高開(kāi)發(fā)成本,并且,難于維護(hù)。因此,我個(gè)人的建議是,如果在軟件開(kāi)發(fā)中使用匯編語(yǔ)言,則應(yīng)在軟件接近完成的時(shí)候使用,這樣可以減少很多不必要的投入。 第二章中,我介紹了x86系列處理器的基本寄存器。這些寄存器對(duì)于x86兼容處理器仍然是有效的,如果你偏愛(ài)AMD的CPU,那么使用這些寄存器的程序同樣也可以正常運(yùn)行。 不過(guò)現(xiàn)在說(shuō)用匯編語(yǔ)言進(jìn)行優(yōu)化還為時(shí)尚早——不可能寫程序,而只操作這些寄存器,因?yàn)檫@樣只能完成非常簡(jiǎn)單的操作,既然是簡(jiǎn)單的操作,那可能就會(huì)讓人覺(jué)得乏味,甚至找一臺(tái)足夠快的機(jī)器窮舉它的所有結(jié)果(如果可以窮舉的話),并直接寫程序調(diào)用,因?yàn)檫@樣通常會(huì)更快。但話說(shuō)回來(lái),看完接下來(lái)的兩章——內(nèi)存和堆棧操作,你就可以獨(dú)立完成幾乎所有的任務(wù)了,配合第五章中斷、第六章子程序的知識(shí),你將知道如何駕馭處理器,并讓它為你工作。第三章操作內(nèi)存 在前面的章節(jié)中,我們已經(jīng)了解了寄存器的基本使用方法。而正如結(jié)尾提到的那樣,僅僅使用寄存器做一點(diǎn)運(yùn)算是沒(méi)有什么太大意義的,畢竟它們不能保存太多的數(shù)據(jù),因此,對(duì)編程人員而言,他肯定迫切地希望訪問(wèn)內(nèi)存,以保存更多的數(shù)據(jù)。我將分別介紹如何在保護(hù)模式和實(shí)模式操作內(nèi)存,然而在此之前,我們先熟悉一下這兩種模式中內(nèi)存的結(jié)構(gòu)。3.1實(shí)模式事實(shí)上,在實(shí)模式中,內(nèi)存比保護(hù)模式中的結(jié)構(gòu)更令人困惑。內(nèi)存被分割成段,并且,操作內(nèi)存時(shí),需要指定段和偏移量。不過(guò),理解這些概念是非常容易的事情。請(qǐng)看下面的圖: 段-寄存器這種格局是早期硬件電路限制留下的一個(gè)傷疤。地址總線在當(dāng)時(shí)有20-bit。然而20-bit的地址不能放到16-bit的寄存器里,這意味著有4-bit必須放到別的地方。因此,為了訪問(wèn)所有的內(nèi)存,必須使用兩個(gè)16-bit寄存器。 這一設(shè)計(jì)上的折衷方案導(dǎo)致了今天的段-偏移量格局。初的設(shè)計(jì)中,其中一個(gè)寄存器只有4-bit有效,然而為了簡(jiǎn)化程序,兩個(gè)寄存器都是16-bit有效,并在執(zhí)行時(shí)求出加權(quán)和來(lái)標(biāo)識(shí)20-bit地址。偏移量是16-bit的,因此,一個(gè)段是64KB。下面的圖可以幫助你理解20-bit地址是如何形成的:段-偏移量標(biāo)識(shí)的地址通常記做段:偏移量的形式。由于這樣的結(jié)構(gòu),一個(gè)內(nèi)存有多個(gè)對(duì)應(yīng)的地址。例如,0000:0010和0001:0000指的是同一內(nèi)存地址。又如,0000:1234=0123:0004=0120:0034=0100:02340001:1234=0124:0004=0120:0044=0100:0244作為負(fù)面影響之一,在段上加1相當(dāng)于在偏移量上加16,而不是一個(gè)“全新”的段。反之,在偏移量上加16也和在段上加1等價(jià)。某些時(shí)候,據(jù)此認(rèn)為段的“粒度”是16字節(jié)。練習(xí)題嘗試一下將下面的地址轉(zhuǎn)化為20bit的地址:2EA8:D67826CF:8D5F453A:CFAD2933:31A65924:DCCF694E:175A2B3C:D218728F:657868E1:A7DC57EC:AEEA稍高一些的要求是,寫一個(gè)程序?qū)⒍螢锳X、偏移量為BX的地址轉(zhuǎn)換為20bit的地址,并保存于EAX中。[上面習(xí)題的答案]我們現(xiàn)在可以寫一個(gè)真正的程序了。經(jīng)典程序:Hello,world;;;應(yīng)該得到一個(gè)29字節(jié)的.com文件.MODELTINY.CODECRequ13LFequ10TERMINATORequ'$'ORG100hMainPROC;.COM文件的內(nèi)存模型是‘TINY’;代碼段開(kāi)始;回車;換行;DOS字符串結(jié)束符;代碼起始地址為CS:0100hmovdx,offsetsMessagemovah,9int21hmovax,4c00hint21hMainENDPsMessage:DB'Hello,World!'DBCR,LF,TERMINATORENDMain;令DS:DX指向Message;int21h(DOS中斷)功能9-;顯示字符串到標(biāo)準(zhǔn)輸出設(shè)備;int21h功能4ch-;終止程序并返回AL的錯(cuò)誤代碼;程序結(jié)束的同時(shí)指定入口點(diǎn)為Main那么,我們需要解釋很多東西。 首先,作為匯編語(yǔ)言的抽象,C語(yǔ)言擁有“指針”這個(gè)數(shù)據(jù)類型。在匯編語(yǔ)言中,幾乎所有對(duì)內(nèi)存的操作都是由對(duì)給定地址的內(nèi)存進(jìn)行訪問(wèn)來(lái)完成的。這樣,在匯編語(yǔ)言中,絕大多數(shù)操作都要和指針產(chǎn)生或多或少的聯(lián)系。 這里我想強(qiáng)調(diào)的是,由于這一特性,匯編語(yǔ)言中同樣會(huì)出現(xiàn)C程序中常見(jiàn)的緩沖區(qū)溢出問(wèn)題。如果你正在設(shè)計(jì)一個(gè)與安全有關(guān)的系統(tǒng),那么好是仔細(xì)檢查你用到的每一個(gè)串,例如,它們是否一定能夠以你預(yù)期的方式結(jié)束,以及(如果使用的話)你的緩沖區(qū)是否能保證實(shí)際可能輸入的數(shù)據(jù)不被寫入到它以外的地方。作為一個(gè)匯編語(yǔ)言程序員,你有義務(wù)檢查每一行代碼的可用性。 程序中的equ偽指令是宏匯編特有的,它的意思接近于C或Pascal中的const(常量)。多數(shù)情況下,equ偽指令并不為符號(hào)分配空間。 此外,匯編程序執(zhí)行一項(xiàng)操作是非常繁瑣的,通常,在對(duì)與效率要求不高的地方,我們習(xí)慣使用系統(tǒng)提供的中斷服務(wù)來(lái)完成任務(wù)。例如本例中的中斷21h,它是DOS時(shí)代的中斷服務(wù),在Windows中,它也被認(rèn)為是WindowsAPI的一部分(這一點(diǎn)可以在Microsoft的文檔中查到)。中斷可以被理解為高級(jí)語(yǔ)言中的子程序,但又不完全一樣——中斷使用系統(tǒng)棧來(lái)保存當(dāng)前的機(jī)器狀態(tài),可以由硬件發(fā)起,通過(guò)修改機(jī)器狀態(tài)字來(lái)反饋信息,等等。那么,后一段通過(guò)DB存放的數(shù)據(jù)到底保存在哪里了呢?答案是緊挨著代碼存放。在匯編語(yǔ)言中,DB和普通的指令的地位是相同的。如果你的匯編程序并不知道新的助記符(例如,新的處理器上的CPUID指令),而你很清楚,那么可以用DB機(jī)器碼的方式強(qiáng)行寫下指令。這意味著,你可以超越匯編器的能力撰寫匯編程序,然而,直接用機(jī)器碼編程是幾乎肯定是一件費(fèi)力不討好的事——匯編器廠商會(huì)經(jīng)常更新它所支持的指令集以適應(yīng)市場(chǎng)需要,而且,你可以期待你的匯編其能夠產(chǎn)生正確的代碼,因?yàn)闄C(jī)器查表是不會(huì)出錯(cuò)的。既然機(jī)器能夠幫我們做將程序轉(zhuǎn)換為代碼這件事情,那么為什么不讓它來(lái)做呢? 細(xì)心的讀者不難發(fā)現(xiàn),在程序中我們沒(méi)有對(duì)DS進(jìn)行賦值。那么,這是否意味著程序的結(jié)果將是不可預(yù)測(cè)的呢?答案是否定的。DOS(或Windows中的MS-DOSVM)在加載.com文件的時(shí)候,會(huì)對(duì)寄存器進(jìn)行很多初始化。.com文件被限制為小于64KB,這樣,它的代碼段、數(shù)據(jù)段都被裝入同樣的數(shù)值(即,初始狀態(tài)下DS=CS)。 也許會(huì)有人說(shuō),“嘿,這聽(tīng)起來(lái)不太好,一個(gè)64KB的程序能做得了什么呢?還有,你吹得天花亂墜的堆棧段在什么地方?”那么,我們來(lái)看看下面這個(gè)新的Helloworld程序,它是一個(gè)EXE文件,在DOS實(shí)模式下運(yùn)行。;;;應(yīng)該得到一個(gè)561字節(jié)的EXE文件.MODELSMALL.STACK200hCRequ13LFequ10TERMINATORequ'$'.DATAMessageDB'Hello,World!'DBCR,LF,TERMINATOR.CODEMainPROCmovax,DGROUPmovds,axmovdx,offsetMessagemovah,9int21hmovax,4c00hint21hMainENDPENDmain;采用“SMALL”內(nèi)存模型;堆棧段;回車;換行;DOS字符串結(jié)束符;定義數(shù)據(jù)段;定義顯示串;定義代碼段;將數(shù)據(jù)段;加載到DS寄存器;設(shè)置DX;顯示;終止程序561字節(jié)?實(shí)現(xiàn)相同功能的程序大了這么多!為什么呢?我們看到,程序擁有了完整的堆棧段、數(shù)據(jù)段、代碼段,其中堆棧段足足占掉了512字節(jié),其余的基本上沒(méi)什么變化。分成多個(gè)段有什么好處呢?首先,它讓程序顯得更加清晰——你肯定更愿意看一個(gè)結(jié)構(gòu)清楚的程序,代碼中hard-coded的字符串、數(shù)據(jù)讓人覺(jué)得費(fèi)解。比如,movdx,0152h肯定不如movdx,offsetMessage來(lái)的親切。此外,通過(guò)分段你可以使用更多的內(nèi)存,比如,代碼段騰出的空間可以做更多的事情。exe文件另一個(gè)吸引人的地方是它能夠?qū)崿F(xiàn)“重定位”?,F(xiàn)在你不需要指定程序入口點(diǎn)的地址了,因?yàn)橄到y(tǒng)會(huì)找到你的程序入口點(diǎn),而不是死板的100h。 程序中的符號(hào)也會(huì)在系統(tǒng)加載的時(shí)候重新賦予新的地址。exe程序能夠保證你的設(shè)計(jì)容易地被實(shí)現(xiàn),不需要考慮太多的細(xì)節(jié)。 當(dāng)然,我們的主要目的是將匯編語(yǔ)言作為高級(jí)語(yǔ)言的一個(gè)有用的補(bǔ)充。如我在開(kāi)始提到的那樣,真正完全用匯編語(yǔ)言實(shí)現(xiàn)的程序不一定就好,因?yàn)樗槐阌诰S護(hù),而且,由于結(jié)構(gòu)的原因,你也不太容易確保它是正確的;匯編語(yǔ)言是一種非結(jié)構(gòu)化的語(yǔ)言,調(diào)試一個(gè)精心設(shè)計(jì)的匯編語(yǔ)言程序,即使對(duì)于一個(gè)老手來(lái)說(shuō)也不啻是一場(chǎng)惡夢(mèng),因?yàn)槟愫芸赡艿舻絼e人預(yù)設(shè)的“陷阱”中——這些技巧確實(shí)提高了代碼性能,然而你很可能不理解它,于是你把它改掉,接著就發(fā)現(xiàn)程序徹底敗掉了。使用匯編語(yǔ)言加強(qiáng)高級(jí)語(yǔ)言程序時(shí),你要做的通常只是使用匯編指令,而不必搭建完整的匯編程序。絕大多數(shù)(也是目前我遇到的全部)C/C++編譯器都支持內(nèi)嵌匯編,即在程序中使用匯編語(yǔ)言,而不必撰寫單獨(dú)的匯編語(yǔ)言程序——這可以節(jié)省你的不少精力,因?yàn)榍懊嬷v述的那些偽指令,如equ等,都可以用你熟悉的高級(jí)語(yǔ)言方式來(lái)編寫,編譯器會(huì)把它轉(zhuǎn)換為適當(dāng)?shù)男问健?需要說(shuō)明的是,在高級(jí)語(yǔ)言中一定要注意編譯結(jié)果。編譯器會(huì)對(duì)你的匯編程序做一些修改,這不一定符合你的要求(附帶說(shuō)一句,有時(shí)編譯器會(huì)很聰明地調(diào)整指令順序來(lái)提高性能,這種情況下好測(cè)試一下哪種寫法的效果更好),此時(shí)需要做一些更深入的修改,或者用db來(lái)強(qiáng)制編碼。3.2保護(hù)模式 實(shí)模式的東西說(shuō)得太多了,盡管我已經(jīng)刪掉了許多東西,并把一些原則性的問(wèn)題拿到了這一節(jié)討論。這樣做不是沒(méi)有理由的——保護(hù)模式才是現(xiàn)在的程序(除了操作系統(tǒng)的底層啟動(dòng)代碼)常用的CPU模式。保護(hù)模式提供了很多令人耳目一新的功能,包括內(nèi)存保護(hù)(這是保護(hù)模式這個(gè)名字的來(lái)源)、進(jìn)程支持、更大的內(nèi)存支持,等等。 對(duì)于一個(gè)編程人員來(lái)說(shuō),能“偷懶”是一件令人愉快的事情。這里“偷懶”是說(shuō)把“應(yīng)該”由系統(tǒng)做的事情做的事情全都交給系統(tǒng)。為什么呢?這出自一個(gè)基本思想——人總有犯錯(cuò)誤的時(shí)候,然而規(guī)則不會(huì),正確地了解規(guī)則之后,你可以期待它像你所了解的那樣執(zhí)行。對(duì)于C程序來(lái)說(shuō),你自己用C語(yǔ)言寫的實(shí)現(xiàn)相同功能的函數(shù)通常沒(méi)有系統(tǒng)提供的函數(shù)性能好(除非你用了比函數(shù)庫(kù)好很多的算法),因?yàn)橄到y(tǒng)的函數(shù)往往使用了更好的優(yōu)化,甚至可能不是用C語(yǔ)言直接編寫的。 當(dāng)然,“偷懶”的意思是說(shuō),把那些應(yīng)該讓機(jī)器做的事情交給計(jì)算機(jī)來(lái)做,因?yàn)樗龅酶?。我們?yīng)該把精力集中到設(shè)計(jì)算法,而不是編寫源代碼本身上,因?yàn)榫幾g器幾乎只能做等價(jià)優(yōu)化,而實(shí)現(xiàn)相同功能,但使用更好算法的程序?qū)崿F(xiàn),則幾乎只能由人自己完成。舉個(gè)例子,這樣一個(gè)函數(shù):intfun(){inta=0;registerinti;for(i=0;i<1000;i++)a+=i;returna;}在某種編譯模式[DEBUG]下被編譯為pushebpmovebp,espsubesp,48hpushebxpushesipushedi;子程序入口;保護(hù)現(xiàn)場(chǎng)leaedi,[ebp-48h]movecx,12hmoveax,0CCCCCCCChrepstosdwordptr[edi]movdwordptr[ebp-4],0movdwordptr[ebp-8],0jmpfun+31hmoveax,dwordptr[ebp-8]addeax,1movdwordptr[ebp-8],eaxcmpdwordptr[ebp-8],3E8hjgefun+45hmovecx,dwordptr[ebp-4]addecx,dwordptr[ebp-8]movdwordptr[ebp-4],ecxjmpfun+28hmoveax,dwordptr[ebp-4]popedipopesipopebxmovesp,ebppopebpret;初始化變量-調(diào)試版本特有。;本質(zhì)是在堆中挖一塊地兒,存CCCCCCCC。;用串操作進(jìn)行,這將發(fā)揮Intel處理器優(yōu)勢(shì);‘a(chǎn)=0’;‘i=0’;走著;i++;i<1000?;a+=i;;returna;;恢復(fù)現(xiàn)場(chǎng);返回而在另一種模式[RELEASE/MINSIZE]下卻被編譯為xoreax,eaxxorecx,ecxaddeax,ecxincecxcmpecx,3E8hjlfun+4ret;a=0;;i=0;;a+=i;;i++;;i<1000?;是->繼續(xù)繼續(xù);returna如果讓我來(lái)寫,多半會(huì)寫成moveax,079f2chret;return499500 為什么這樣寫呢?我們看到,i是一個(gè)外界不能影響、也無(wú)法獲知的內(nèi)部狀態(tài)量。作為這段程序來(lái)說(shuō),對(duì)它的計(jì)算對(duì)于結(jié)果并沒(méi)有直接的影響——它的存在不過(guò)是方便算法描述而已。并且我們看到的,這段程序?qū)嶋H上無(wú)論執(zhí)行多少次,其結(jié)果都不會(huì)發(fā)生變化,因此,直接返回計(jì)算結(jié)果就可以了,計(jì)算是多余的(如果說(shuō)一定要算,那么應(yīng)該是編譯器在編譯過(guò)程中完成它)。更進(jìn)一步,我們甚至希望編譯器能夠直接把這個(gè)函數(shù)變成一個(gè)符號(hào)常量,這樣連操作堆棧的過(guò)程也省掉了。第三種結(jié)果屬于“等效”代碼,而不是“等價(jià)”代碼。作為用戶,很多時(shí)候是希望編譯器這樣做的,然而由于目前的技術(shù)尚不成熟,有時(shí)這種做法會(huì)造成一些問(wèn)題(gcc和g++的頂級(jí)優(yōu)化可以造成編譯出的FreeBSD內(nèi)核行為異常,這是我在FreeBSD上遇到的唯一一次軟件原因的kernelpanic),因此,并不是所有的編譯器都這樣做(另一方面的原因是,如果編譯器在這方面做的太過(guò)火,例如自動(dòng)求解全部“固定”問(wèn)題,那么如果你的程序是解決固定的問(wèn)題“很大”,如求解迷宮,那么在編譯過(guò)程中你就會(huì)找錘子來(lái)砸計(jì)算機(jī)了)。然而,作為編譯器制造商,為了提高自己的產(chǎn)品的競(jìng)爭(zhēng)力,往往會(huì)使用第三種代碼來(lái)做函數(shù)庫(kù)。正如前面所提到的那樣,這種優(yōu)化往往不是編譯器本身的作用,盡管現(xiàn)代編譯程序擁有編譯執(zhí)行、循環(huán)代碼外提、無(wú)用代碼去除等諸多優(yōu)化功能,但它都不能保證程序優(yōu)。后一種代碼恐怕很少有編譯器能夠做到,不信你可以用自己常用的編譯器加上各種優(yōu)化選項(xiàng)試試:)發(fā)現(xiàn)什么了嗎?三種代碼中,對(duì)于內(nèi)存的訪問(wèn)一個(gè)比一個(gè)少。這樣做的理由是,盡可能地利用寄存器并減少對(duì)內(nèi)存的訪問(wèn),可以提高代碼性能。在某些情況下,使代碼既小又快是可能的。 書歸正傳,我們來(lái)說(shuō)說(shuō)保護(hù)模式的內(nèi)存模型。保護(hù)模式的內(nèi)存和實(shí)模式有很多共同之處。毫無(wú)疑問(wèn),以protectedmode(保護(hù)模式),globaldescriptortable(全局描述符表),localdescriptortable(本地描述符表)和selector(選擇器)搜索,你會(huì)得到完整介紹它們的大量信息。保護(hù)模式與實(shí)模式的內(nèi)存類似,然而,它們之間大的區(qū)別就是保護(hù)模式的內(nèi)存是“線性”的。 新的計(jì)算機(jī)上,32-bit的寄存器已經(jīng)不是什么新鮮事(如果你哪天聽(tīng)說(shuō)你的CPU的寄存器不是32-bit的,那么簡(jiǎn)直可以肯定地說(shuō),它的字長(zhǎng)要比32-bit還要多。新的個(gè)人機(jī)上已經(jīng)開(kāi)始逐步采用64-bit的CPU了),換言之,實(shí)際上段/偏移量這一格局已經(jīng)不再需要了。盡管如此,在繼續(xù)看保護(hù)模式內(nèi)存結(jié)構(gòu)時(shí),仍請(qǐng)記住段/偏移量的概念。不妨把段寄存器看作對(duì)于保護(hù)模式中的選擇器的一個(gè)模擬。選擇器是全局描述符表(GlobalDescriptorTable,GDT)或本地描述符表(LocalDescriptorTable,LDT)的一個(gè)指針。如圖所示,GDT和LDT的每一個(gè)項(xiàng)目都描述一塊內(nèi)存。例如,一個(gè)項(xiàng)目中包含了某塊被描述的內(nèi)存的物理的基地址、長(zhǎng)度,以及其他一些相關(guān)信息。保護(hù)模式是一個(gè)非常重要的概念,同時(shí)也是目前撰寫應(yīng)用程序時(shí),常用的CPU模式(運(yùn)行在新的計(jì)算機(jī)上的操作系統(tǒng)很少有在實(shí)模式下運(yùn)行的)。為什么叫保護(hù)模式呢?它“保護(hù)”了什么?答案是進(jìn)程的內(nèi)存。保護(hù)模式的主要目的在于允許多個(gè)進(jìn)程同時(shí)運(yùn)行,并保護(hù)它們的內(nèi)存不受其他進(jìn)程的侵犯。這有點(diǎn)類似于C++中的機(jī)制,然而它的強(qiáng)制力要大得多。如果你的進(jìn)程在保護(hù)模式下以不恰當(dāng)?shù)姆绞皆L問(wèn)了內(nèi)存(例如,寫了“只讀”內(nèi)存,或讀了不可讀的內(nèi)存,等等),那么CPU就會(huì)產(chǎn)生一個(gè)異常。這個(gè)異常將交給操作系統(tǒng)處理,而這種處理,假如你的程序沒(méi)有特別說(shuō)明操作系統(tǒng)該如何處理的話,一般就是殺掉做錯(cuò)了事情的進(jìn)程。我像這樣的對(duì)話框大家一定非常熟悉(臨時(shí)寫了一個(gè)程序故意造成的錯(cuò)誤):好的,只是一個(gè)程序崩潰了,而操作系統(tǒng)的其他進(jìn)程照常運(yùn)行(同樣的程序在DOS中幾乎是板上釘釘?shù)乃罊C(jī),因?yàn)镹ULL指針的位置恰好是中斷向量表),你甚至還可以調(diào)試它。保護(hù)模式還有其他很多好處,在此就不一一贅述了。實(shí)模式和保護(hù)模式之間的切換問(wèn)題我打算放在后面的“高級(jí)技巧”一章來(lái)講,因?yàn)槎鄶?shù)程序并不涉及這個(gè)。了解了內(nèi)存的格局,我們就可以進(jìn)入下一節(jié)——操作內(nèi)存了。3.3操作內(nèi)存前兩節(jié)中,我們介紹了實(shí)模式和保護(hù)模式中使用的不同的內(nèi)存格局?,F(xiàn)在開(kāi)始解釋如何使用這些知識(shí)?;貞浺幌虑懊嫖覀冋f(shuō)過(guò)的,寄存器可以用作內(nèi)存指針?,F(xiàn)在,是他們發(fā)揮作用的時(shí)候了??梢詫?nèi)存想象為一個(gè)順序的字節(jié)流。使用指針,可以任意地操作(讀寫)內(nèi)存。現(xiàn)在我們需要一些其他的指令格式來(lái)描述對(duì)于內(nèi)存的操作。操作內(nèi)存時(shí),首先需要的就是它的地址。讓我們來(lái)看看下面的代碼:movax,[0]方括號(hào)表示,里面的表達(dá)式指定的不是立即數(shù),而是偏移量。在實(shí)模式中,DS:0中的那個(gè)字(16-bit長(zhǎng))將被裝入AX。然而0是一個(gè)常數(shù),如果需要在運(yùn)行的時(shí)候加以改變,就需要一些特殊的技巧,比如程序自修改。匯編支持這個(gè)特性,然而我個(gè)人并不推薦這種方法——自修改大大降低程序的可讀性,并且還降低穩(wěn)定性,性能還不一定好。我們需要另外的技術(shù)。movbx,0movax,[bx]看起來(lái)舒服了一些,不是嗎?BX寄存器的內(nèi)容可以隨時(shí)更改,而不需要用冗長(zhǎng)的代碼去修改自身,更不用擔(dān)心由此帶來(lái)的不穩(wěn)定問(wèn)題。同樣的,mov指令也可以把數(shù)據(jù)保存到內(nèi)存中:mov[0],ax在存儲(chǔ)器與寄存器之間交換數(shù)據(jù)應(yīng)該足夠清楚了。有些時(shí)候我們會(huì)需要操作符來(lái)描述內(nèi)存數(shù)據(jù)的寬度:操作符意義byteptr一個(gè)字節(jié)(8-bit,1byte)wordptr一個(gè)字(16-bit)dwordptr一個(gè)雙字(32-bit)例如,在DS:100h處保存1234h,以字存放:movwordptr[100h],01234h于是我們將mov指令擴(kuò)展為:movreg(8,16,32),mem(8,16,32)movmem(8,16,32),reg(8,16,32)movmem(8,16,32),imm(8,16,32)需要說(shuō)明的是,加減同樣也可以在[]中使用,例如:movax,[bx+10]movax,[bx+si]movax,es:[di+bp]等等。我們看到,對(duì)于內(nèi)存的操作,即使使用MOV指令,也有許多種可能的方式。下一節(jié)中,我們將介紹如何操作串。地址轉(zhuǎn)換2EA8:D678->物理的3C0F8694E:175A->物理的6AC4A26CF:8D5F->物理的2FA5F2B3C:D218->物理的385E8453A:CFAD->物理的5235D728F:6578->物理的78E682933:31A6->物理的2C4D668E1:A7DC->物理的735FC編程shleax,4addeax,bx注意編程問(wèn)題答案并不唯一,但給出的這份參考答案應(yīng)該已經(jīng)是“優(yōu)化到頭”了。3.4串操作 我們前面已經(jīng)提到,內(nèi)存可以和寄存器交換數(shù)據(jù),也可以被賦予立即數(shù)。問(wèn)題是,如果我們需要把內(nèi)存的某部分內(nèi)容復(fù)制到另一個(gè)地址,又怎么做呢?設(shè)想將DS:SI處的連續(xù)512字節(jié)內(nèi)容復(fù)制到ES:DI(先不考慮可能的重疊)。也許會(huì)有人寫出這樣的代碼:NextByte:movcx,512moval,ds:[si]moves:[di],alincsiincdiloopNextByte;循環(huán)次數(shù)我不喜歡上面的代碼。它的確能達(dá)到作用,但是,效率不好。如果你是在做優(yōu)化,那么寫出這樣的代碼意味著賠了夫人又折兵。Intel的CPU的強(qiáng)項(xiàng)是串操作。所謂串操作就是由CPU去完成某一數(shù)量的、重復(fù)的內(nèi)存操作。需要說(shuō)明的是,我們常用的KMP算法(用于匹配字符串中的模式)的改進(jìn)——Boyer算法,由于沒(méi)有利用串操作,因此在Intel的CPU上的效率并非優(yōu)。好的編譯器往往可以利用IntelCPU的這一特性優(yōu)化代碼,然而,并非所有的時(shí)候它都能產(chǎn)生好的代碼。某些指令可以加上REP前綴(repeat,反復(fù)之意),這些指令通常被叫做串操作指令。舉例來(lái)說(shuō),STOSD指令將EAX的內(nèi)容保存到ES:DI,同時(shí)在DI上加或減四。類似的,STOSB和STOSW分別作1字節(jié)或1字的上述操作,在DI上加或減的數(shù)是1或2。計(jì)算機(jī)語(yǔ)言通常是不允許二義性的。為什么我要說(shuō)“加或減”呢?沒(méi)錯(cuò),孤立地看STOS?指令,并不能知道到底是加還是減,因?yàn)檫@取決于“方向”標(biāo)志(DF,DirectionFlag)。如果DF被復(fù)位,則加;反之則減。置位、復(fù)位的指令分別是STD和CLD。當(dāng)然,REP只是幾種可用前綴之一。常用的還包括REPNE,這個(gè)前綴通常被用來(lái)比較兩個(gè)串,或搜索某個(gè)特定字符(字、雙字)。REPZ、REPE、REPNZ也是非常常用的指令前綴,分別代表ZF(ZeroFlag)在不同狀態(tài)時(shí)重復(fù)執(zhí)行。下面說(shuō)三個(gè)可以復(fù)制數(shù)據(jù)的指令:助記符意義movsb將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI++、DI++movsw將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI+=2、DI+=2movsd將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI+=4、DI+=4于是上面的程序改寫為cldmovcx,128repmovsd;復(fù)位DF;512/4=128,共128個(gè)雙字;行動(dòng)!第一句cld很多時(shí)候是多余的,因?yàn)閷?shí)際寫程序時(shí),很少會(huì)出現(xiàn)置DF的情況。不過(guò)在正式?jīng)Q定刪掉它之前,建議你仔細(xì)地調(diào)試自己的程序,并確認(rèn)每一個(gè)能夠走到這里的路徑中都不會(huì)將DF置位。錯(cuò)誤(非預(yù)期的)的DF是危險(xiǎn)的。它很可能斷送掉你的程序,因?yàn)檫@直接造成緩沖區(qū)溢出問(wèn)題。 什么是緩沖區(qū)溢出呢?緩沖區(qū)溢出分為兩類,一類是寫入緩沖區(qū)以外的內(nèi)容,一類是讀取緩沖區(qū)以外的內(nèi)容。后一種往往更隱蔽,但隨便哪一個(gè)都有可能斷送掉你的程序。 緩沖區(qū)溢出對(duì)于一個(gè)網(wǎng)絡(luò)服務(wù)來(lái)說(shuō)很可能更加危險(xiǎn)。懷有惡意的用戶能夠利用它執(zhí)行自己希望的指令。服務(wù)通常擁有更高的特權(quán),而這很可能會(huì)造成特權(quán)提升;即使不能提升攻擊者擁有的特權(quán),他也可以利用這種問(wèn)題使服務(wù)崩潰,從而形成一次成功的DoS(拒絕服務(wù))攻擊。每年CERT的安全公告中,都有6成左右的問(wèn)題是由于緩沖區(qū)溢出造成的。 在使用匯編語(yǔ)言,或C語(yǔ)言編寫程序時(shí),很容易在無(wú)意中引入緩沖區(qū)溢出。然而并不是所有的語(yǔ)言都會(huì)引入緩沖區(qū)溢出問(wèn)題,Java和C#,由于沒(méi)有指針,并且緩沖區(qū)采取動(dòng)態(tài)分配的方式,有效地消除了造成緩沖區(qū)溢出的土壤。匯編語(yǔ)言中,由于REP*前綴都用CX作為計(jì)數(shù)器,因此情況會(huì)好一些(當(dāng)然,有時(shí)也會(huì)更糟糕,因?yàn)橛捎贑X的限制,很可能使原本可能改變程序行為的緩沖區(qū)溢出的范圍縮小,從而更為隱蔽)。避免緩沖區(qū)溢出的一個(gè)主要方法就是仔細(xì)檢查,這包括兩方面:設(shè)置合理的緩沖區(qū)大小,和根據(jù)大小編寫程序。除此之外,非常重要的一點(diǎn)就是,在匯編語(yǔ)言這個(gè)級(jí)別寫程序,你肯定希望去掉所有的無(wú)用指令,然而再去掉之前,一定要進(jìn)行嚴(yán)格的測(cè)試;更進(jìn)一步,如果能加上注釋,并通過(guò)善用宏來(lái)做調(diào)試模式檢查,往往能夠達(dá)到更好的效果。3.5關(guān)于保護(hù)模式中內(nèi)存操作的一點(diǎn)說(shuō)明正如3.2節(jié)提到到的那樣,保護(hù)模式中,你可以使用32位的線性地址,這意味著直接訪問(wèn)4GB的內(nèi)存。由于這個(gè)原因,選擇器不用像實(shí)模式中段寄存器那樣頻繁地修改。順便提一句,這份教程中所說(shuō)的保護(hù)模式指的是386以上的保護(hù)模式,或者,Microsoft通常稱為“增強(qiáng)模式”的那種。在為選擇器裝入數(shù)值的時(shí)候一定要非常小心。錯(cuò)誤的數(shù)值往往會(huì)導(dǎo)致無(wú)效頁(yè)面錯(cuò)誤(在Windows中經(jīng)常出現(xiàn):)。同時(shí),也不要忘記你的地址是32位的,這也是保護(hù)模式的主要優(yōu)勢(shì)之一?,F(xiàn)在假設(shè)存在一個(gè)描述符描述從物理的0:0開(kāi)始的全部?jī)?nèi)存,并已經(jīng)加載進(jìn)DS(數(shù)據(jù)選擇器),則我們可以通過(guò)下面的程序來(lái)操作VGA的VRAM:movedi,0a0000hmovbyteptr[edi],0fh;VGA顯存的偏移量;將第一字節(jié)改為0fh很明顯,這比實(shí)模式下的程序movax,0a000hmovds,axmovdi,0mov[di],0fh;AX->VGA段地址;將AX值載入DS;DI清零;修改第一字節(jié)看上去要舒服一些。3.6堆棧 到目前為止,您已經(jīng)了解了基本的寄存器以及內(nèi)存的操作知識(shí)。事實(shí)上,您現(xiàn)在已經(jīng)可以寫出很多的底層數(shù)據(jù)處理程序了。下面我來(lái)說(shuō)說(shuō)堆棧。堆棧實(shí)在不是一個(gè)讓人陌生的數(shù)據(jù)結(jié)構(gòu),它是一個(gè)先進(jìn)后出(FILO)的線性表,能夠幫助你完成很多很好的工作。先進(jìn)后出(FILO)是這樣一個(gè)概念:后放進(jìn)表中的數(shù)據(jù)在取出時(shí)先出來(lái)。先進(jìn)后出(FILO)和先進(jìn)先出(FIFO和先進(jìn)后出的規(guī)則相反),以及隨機(jī)存取是主要的三種存儲(chǔ)器訪問(wèn)方式。對(duì)于堆棧而言,后放入的數(shù)據(jù)在取出時(shí)先出現(xiàn)。對(duì)于子程序調(diào)用,特別是遞歸調(diào)用來(lái)說(shuō),這是一個(gè)非常有用的特性。一個(gè)鐵桿的匯編語(yǔ)言程序員有時(shí)會(huì)發(fā)現(xiàn)系統(tǒng)提供的寄存器不夠。很顯然,你可以使用普通的內(nèi)存操作來(lái)完成這個(gè)工作,就像C/C++中所做的那樣。沒(méi)錯(cuò),沒(méi)錯(cuò),可是,如果數(shù)據(jù)段(數(shù)據(jù)選擇器)以及偏移量發(fā)生變化怎么辦?更進(jìn)一步,如果希望保存某些在這種操作中可能受到影響的寄存器的時(shí)候怎么辦?確實(shí),你可以把他們也存到自己的那片內(nèi)存中,自己實(shí)現(xiàn)堆棧。太麻煩了……既然系統(tǒng)提供了堆棧,并且性能比自己寫一份更好,那么為什么不直接加以利用呢?系統(tǒng)堆棧不僅僅是一段內(nèi)存。由于CPU對(duì)它實(shí)施管理,因此你不需要考慮堆棧指針的修正問(wèn)題??梢园鸭拇嫫鲀?nèi)容,甚至一個(gè)立即數(shù)直接放到堆棧里,并在需要的時(shí)候?qū)⑵淙〕?。同時(shí),系統(tǒng)并不要求取出的數(shù)據(jù)仍然回到原來(lái)的位置。除了顯式地操作堆棧(使用PUSH和POP指令)之外,很多指令也需要使用堆棧,如INT、CALL、LEAVE、RET、RETF、IRET等等。配對(duì)使用上述指令并不會(huì)造成什么問(wèn)題,然而,如果你打算使用LEAVE、RET、RETF、IRET這樣的指令實(shí)現(xiàn)跳轉(zhuǎn)(比JMP更為麻煩,然而有時(shí),例如在加密軟件中,或者需要修改調(diào)用者狀態(tài)時(shí),這是必要的)的話,那么我的建議是,先搞清楚它們做的到底是什么,并且,精確地了解自己要做什么。正如前面所說(shuō)的,有兩個(gè)顯式地操作堆棧的指令:助記符功能PUSH將操作數(shù)存入堆棧,同時(shí)修正堆棧指針POP將棧頂內(nèi)容取出并存到目的操作數(shù)中,同時(shí)修正堆棧指針我們現(xiàn)在來(lái)看看堆棧的操作。執(zhí)行之前執(zhí)行代碼movax,1234hmovbx,10pushaxpushbx之后,堆棧的狀態(tài)為之后,再執(zhí)行popdxpopcx堆棧的狀態(tài)成為當(dāng)然,dx、cx中的內(nèi)容將分別是000ah和1234h。注意,后這張圖中,我沒(méi)有抹去1234h和000ah,因?yàn)镻OP指令并不從內(nèi)存中抹去數(shù)值。不過(guò)盡管如此,我個(gè)人仍然非常反對(duì)繼續(xù)使用這兩個(gè)數(shù)(你可以通過(guò)修改SP來(lái)再次POP它們),然而這很容易導(dǎo)致錯(cuò)誤。一定要保證堆棧段有足夠的空間來(lái)執(zhí)行中斷,以及其他一些隱式的堆棧操作。僅僅統(tǒng)計(jì)PUSH的數(shù)量并據(jù)此計(jì)算堆棧所需的大小很可能造成問(wèn)題。CALL指令將返回地址放到堆棧中。絕大多數(shù)C/C++編譯器提供了“堆棧檢查”這個(gè)編譯選項(xiàng),其作用在于保證C程序段中沒(méi)有忘記對(duì)堆棧中多余的數(shù)據(jù)進(jìn)行清理,從而保證返回地址有效。本章小結(jié)本章中介紹了內(nèi)存的操作的一些入門知識(shí)。限于篇幅,我不打算展開(kāi)細(xì)講指令,如cmps*,lods*,stos*,等等。這些指令的用法和前面介紹的movs*基本一樣,只是有不同的作用而已。第四章利用子程序與中斷 已經(jīng)掌握了匯編語(yǔ)言?沒(méi)錯(cuò),你現(xiàn)在已經(jīng)可以去破譯別人代碼中的秘密。然而,我們還有一件重要的東西沒(méi)有提到,那就是子程序和中斷。這兩件東西是如此的重要,以至于你的程序幾乎不可能離開(kāi)它們。4.1子程序在高級(jí)語(yǔ)言中我們經(jīng)常要用到子程序。高級(jí)語(yǔ)言中,子程序是如此的神奇,我們能夠定義和主程序或其他子程序一樣的變量名,而訪問(wèn)不同的變量,并且,還不和程序的其他部分相沖突。然而遺憾的是,這種“優(yōu)勢(shì)”在匯編語(yǔ)言中是不存在的。匯編語(yǔ)言并不注重如何減輕程序員的負(fù)擔(dān);相反,匯編語(yǔ)言依賴程序員的良好設(shè)計(jì),以期發(fā)揮CPU的佳性能。匯編語(yǔ)言不是結(jié)構(gòu)化的語(yǔ)言,因此,它不提供直接的“局部變量”。如果需要“局部變量”,只能通過(guò)堆或棧自行實(shí)現(xiàn)。從這個(gè)意義上講,匯編語(yǔ)言的子程序更像GWBASIC中的GOSUB調(diào)用的那些“子程序”。所有的“變量”(本質(zhì)上,屬于進(jìn)程的內(nèi)存和寄存器)為整個(gè)程序所共享,高級(jí)語(yǔ)言編譯器所做的將局部變量放到堆或棧中的操作只能自行實(shí)現(xiàn)。參數(shù)的傳遞是靠寄存器和堆棧來(lái)完成的。高級(jí)語(yǔ)言中,子程序(函數(shù)、過(guò)程,或類似概念的東西)依賴于堆和棧來(lái)傳遞。讓我們來(lái)簡(jiǎn)單地分析一下一般高級(jí)語(yǔ)言的子程序的執(zhí)行過(guò)程。無(wú)論C、C++、BASIC、Pascal,這一部分基本都是一致的。調(diào)用者將子程序執(zhí)行完成時(shí)應(yīng)返回的地址、參數(shù)壓入堆棧子程序使用BP指針+偏移量對(duì)棧中的參數(shù)尋址,并取出、完成操作子程序使用RET或RETF指令返回。此時(shí),CPU將IP置為堆棧中保存的地址,并繼續(xù)予以執(zhí)行毋庸置疑,堆棧在整個(gè)過(guò)程中發(fā)揮著非常重要的作用。不過(guò),本質(zhì)上對(duì)子程序重要的還是返回地址。如果子程序不知道這個(gè)地址,系統(tǒng)將會(huì)崩潰。調(diào)用子程序的指令是CALL,對(duì)應(yīng)的返回指令是RET。此外,還有一組指令,即ENTER和LEAVE,它們可以幫助進(jìn)行堆棧的維護(hù)。CALL指令的參數(shù)是被調(diào)用子程序的地址。使用宏匯編的時(shí)候,這通常是一個(gè)標(biāo)號(hào)。CALL和RET,以及ENTER和LEAVE配對(duì),可以實(shí)現(xiàn)對(duì)于堆棧的自動(dòng)操作,而不需要程序員進(jìn)行PUSH/POP,以及跳轉(zhuǎn)的操作,從而提高了效率。作為一個(gè)編譯器的實(shí)現(xiàn)實(shí)例,我用VisualC++編譯了一段C++程序代碼,這段匯編代碼是使用特定的編譯選項(xiàng)得到的結(jié)果,正常的RELEASE代碼會(huì)比它精簡(jiǎn)得多。包含源代碼的部分反匯編結(jié)果如下(取自VisualC++調(diào)試器的運(yùn)行結(jié)果,我刪除了10條int3指令,并加上了一些注釋,除此之外,沒(méi)有做任何修改):1:intmyTransform(intnInput){00401000pushebp;保護(hù)現(xiàn)場(chǎng)原先的EBP指針00401001movebp,esp2:return(nInput*2+3)%7;00401003moveax,dwordptr[nInput];取參數(shù)00401006leaeax,[eax+eax+3];LEA比ADD加法更快0040100Acdq;DWORD->QWORD(擴(kuò)展字長(zhǎng))0040100Bmovecx,7;除數(shù)00401010idiveax,ecx;除00401012moveax,edx;商->eax(eax中保存返回值)3:}00401014popebp;恢復(fù)現(xiàn)場(chǎng)的ebp指針00401015ret;返回;此處刪除10條int3指令,它們是方便調(diào)試用的,并不影響程序行為。4:5:intmain(intargc,char*argv[])6:{00401020pushebp;保護(hù)現(xiàn)場(chǎng)原先的EBP指針00401021movebp,esp00401023subesp,10h;為取argc,argv修正堆棧指針。7:inta[3];8:for(registerinti=0;i<3;i++){00401026movdwordptr[i],0;0->i0040102Djmpmain+18h(00401038);判斷循環(huán)條件0040102Fmoveax,dwordptr[i];i->eax00401032addeax,1;eax++00401035movdwordptr[i],eax;eax->i00401038cmpdwordptr[i],3;循環(huán)條件:i與3比較0040103Cjgemain+33h(00401053);如果不符合條件,則應(yīng)結(jié)束循環(huán)9:a[i]=myTransform(i);0040103Emovecx,dwordptr[i];i->ecx00401041pushecx;ecx(i)->堆棧00401042callmyTransform(00401000);調(diào)用myTransform00401047addesp,4;esp+=4:在堆中的新單元;準(zhǔn)備存放返回結(jié)果0040104Amovedx,dwordptr[i];i->edx0040104Dmovdwordptra[edx*4],eax;將eax(myTransform返回值);放回a[i]10:}00401051jmpmain+0Fh(0040102f);計(jì)算i++,并繼續(xù)循環(huán)11:return0;00401053xoreax,eax;返回值應(yīng)該是012:}00401055movesp,ebp;恢復(fù)堆棧指針00401057popebp;恢復(fù)BP00401058ret;返回調(diào)用者(C++運(yùn)行環(huán)境)上述代碼確實(shí)做了一些無(wú)用功,當(dāng)然,這是因?yàn)榫幾g器沒(méi)有對(duì)這段代碼進(jìn)行優(yōu)化。讓我們來(lái)關(guān)注一下這段代碼中,是如何調(diào)用子程序的。不考慮myTransform這個(gè)函數(shù)實(shí)際進(jìn)行的數(shù)值運(yùn)算,讓我感興趣的是這一行代碼:00401003moveax,d
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 企業(yè)員工績(jī)效考核測(cè)評(píng)題庫(kù)
- 新學(xué)期家長(zhǎng)溝通書寫指南
- 電影作品觀后感寫作指導(dǎo)
- 北京市2016版房屋租賃合同范本
- 2025河北秦皇島市社會(huì)保險(xiǎn)事業(yè)服務(wù)中心選調(diào)6人備考題庫(kù)及一套參考答案詳解
- 2025下半年廣東肇慶市懷集縣事業(yè)單位招聘16人備考題庫(kù)附答案詳解
- 餐廳開(kāi)業(yè)前期市場(chǎng)調(diào)研及推廣方案
- 2026中共昆明市晉寧區(qū)委社會(huì)工作部招聘編外聘用人員3人備考題庫(kù)(云南)及1套參考答案詳解
- 影視動(dòng)畫語(yǔ)言表達(dá)教學(xué)設(shè)計(jì)方案
- 2026四川大學(xué)華西醫(yī)院神經(jīng)和共病研究室陳蕾教授團(tuán)隊(duì)基礎(chǔ)實(shí)驗(yàn)科研助理助理招聘1人備考題庫(kù)及答案詳解(考點(diǎn)梳理)
- 2025年上半年山東高速集團(tuán)有限公司校園招聘(255人)筆試參考題庫(kù)附答案
- 故意傷害案件課件
- 膽管狹窄護(hù)理
- 消防操作員其他實(shí)操技能
- 2025年高考數(shù)學(xué)試題分類匯編:數(shù)列解析版
- 吉林省戶用光伏施工方案
- 工程部物業(yè)消防知識(shí)培訓(xùn)課件
- 江西省婺源縣聯(lián)考2026屆數(shù)學(xué)七年級(jí)第一學(xué)期期末學(xué)業(yè)水平測(cè)試試題含解析
- 2025至2030水蛭素產(chǎn)品行業(yè)發(fā)展研究與產(chǎn)業(yè)戰(zhàn)略規(guī)劃分析評(píng)估報(bào)告
- 非煤礦山安全員題庫(kù)及答案解析
- 餐飲連鎖加盟店標(biāo)準(zhǔn)運(yùn)營(yíng)手冊(cè)
評(píng)論
0/150
提交評(píng)論