




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)
文檔簡介
1、第章 寫在前面 我不想夸大或者貶低匯編語言。但我想說,匯編語言改變了20世紀(jì)的歷史。與前輩相比,我們這一代編程人員足夠的幸福,因為我們有各式各樣的編程語言,我們可以操作鍵盤、坐在顯示器面前,甚至使用鼠標(biāo)、語音識別。我們可以使用鍵盤、鼠標(biāo)來駕馭“個人計算機”,而不是和一群人共享一臺使用笨重的繼電器、開關(guān)去操作的巨型機。相比之下,我們的前輩不得不使用機器語言編寫程序,他們甚至沒有最簡單的匯編程序來把助記符翻譯成機器語言,而我們可以從上千種計算機語言中選擇我們喜歡的一種,而匯編,雖然不是一種“常用”的具有“快速原型開發(fā)”能力的語言,卻也是我們可以選擇的語言中的一種。每種計算機都有自己的匯編語言沒必要
2、指望匯編語言的可移植性,選擇匯編,意味著選擇性能而不是可移植或便于調(diào)試。這份文檔中講述的是x86匯編語言,此后的“匯編語言”一詞,如果不明示則表示ia32上的x86匯編語言。匯編語言是一種易學(xué),卻很難精通的語言?;叵氘?dāng)年,我從初學(xué)匯編到寫出第一個可運行的程序,只用了不到4個小時;然而直到今天,我仍然不敢說自己精通它。編寫快速、高效、并且能夠讓處理器“很舒服地執(zhí)行”的程序是一件很困難的事情,如果利用業(yè)余時間學(xué)習(xí),通常需要2-3年的時間才能做到。這份教材并不期待能夠教給你大量的匯編語言技巧。對于讀者來說,x86匯編語言就在這里。然而,不要僵化地局限于這份教材講述的內(nèi)容,因為它只能告訴你匯編語言是“
3、這樣一回事”。學(xué)好匯編語言,更多的要靠一個人的創(chuàng)造力于悟性,我可以告訴你我所知道的技巧,但肯定這是不夠的。一位對我的編程生涯產(chǎn)生過重要影響的人曾經(jīng)對我說過這么一句話:寫匯編語言程序不是匯編語言最難的部分,創(chuàng)新才是。我想,愿意看這份文檔的人恐怕不會問我“為什么要學(xué)習(xí)匯編語言”這樣的問題;不過,我還是想說幾句:首先,匯編語言非常有用,我個人主張把它作為C語言的先修課程,因為通過學(xué)習(xí)匯編語言,你可以了解到如何有效地設(shè)計數(shù)據(jù)結(jié)構(gòu),讓計算機處理得更快,并使用更少的存儲空間;同時,學(xué)習(xí)匯編語言可以讓你熟悉計算機內(nèi)部運行機制,并且,有效地提高調(diào)試能力。就我個人的經(jīng)驗而言,調(diào)試一個非結(jié)構(gòu)化的程序的困難程度,要
4、比調(diào)試一個結(jié)構(gòu)化的程序的難度高很多,因為“結(jié)構(gòu)化”是以犧牲運行效率來提高可讀性與可調(diào)試性,這對于完成一般軟件工程的編碼階段是非常必要的。然而,在一些地方,比如,硬件驅(qū)動程序、操作系統(tǒng)底層,或者程序中經(jīng)常需要執(zhí)行的代碼,結(jié)構(gòu)化程序設(shè)計的這些優(yōu)點有時就會被它的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代碼級的調(diào)試是遠(yuǎn)遠(yuǎn)不夠的。浮躁的人喜歡說,用C+寫程序足夠了,甚至說,他不僅僅掌握C+,而且精通STL、MFC。我不贊成這個觀點,掌握上面的那些是每一個編程人員都應(yīng)該做到的,然而C+只是我們常用的一種語言,它不是編程的全部。低層次的開發(fā)者喜歡說,嘿,C+是多么的強大,它可以做任何事情這
5、不是事實。便于維護、調(diào)試,這些確實是我們的追求目標(biāo),但是,寫程序不能僅僅追求這個目標(biāo),因為我們最終的目的是滿足設(shè)計需求,而不是個人非理性的理想。這份教材適合已經(jīng)學(xué)習(xí)過某種結(jié)構(gòu)化程序設(shè)計語言的讀者。其內(nèi)容基于我在1995年給別人講述匯編語言時所寫的講義。當(dāng)然,如大家所希望的,它包含了最新的處理器所支持的特性,以及相應(yīng)的內(nèi)容。我假定讀者已經(jīng)知道了程序設(shè)計的一些基本概念,因為沒有這些是無法理解匯編語言程序設(shè)計的;此外,我希望讀者已經(jīng)有了比較良好的程序設(shè)計基礎(chǔ),因為如果你缺乏對于結(jié)構(gòu)化程序設(shè)計的認(rèn)識,編寫匯編語言程序很可能很快就破壞了你的結(jié)構(gòu)化編程習(xí)慣,大大降低程序的可讀性、可維護性,最終讓你的程序陷
6、于不得不廢棄的代碼堆之中?;旧希@份文檔撰寫的目標(biāo)是盡可能地便于自學(xué)。不過,它對你也有一些要求,盡管不是很高,但我還是強調(diào)一下。學(xué)習(xí)匯編語言,你需要膽量。不要害怕去接觸那些計算機的內(nèi)部工作機制。 知識。了解計算機常用的數(shù)制,特別是二進制、十六進制、八進制,以及計算機保存數(shù)據(jù)的方法。 開放。接受匯編語言與高級語言的差異,而不是去指責(zé)它如何的不好讀。 經(jīng)驗。要求你擁有任意其他編程語言的一點點編程經(jīng)驗。 頭腦。 祝您編程愉快!第一章 匯編語言簡介 先說一點和實際編程關(guān)系不太大的東西。當(dāng)然,如果你迫切的想看到更實質(zhì)的內(nèi)容,完全可以先跳過這一章。那么,我想可能有一個問題對于初學(xué)匯編的人來說非常重要,那
7、就是:匯編語言到底是什么?匯編語言是一種最接近計算機核心的編碼語言。不同于任何高級語言,匯編語言幾乎可以完全和機器語言一一對應(yīng)。不錯,我們可以用機器語言寫程序,但現(xiàn)在除了沒有匯編程序的那些電腦之外,直接用機器語言寫超過1000條以上指令的人大概只能算作那些被我們成為“圣人”的犧牲者一類了。畢竟,記憶一些短小的助記符、由機器去考慮那些瑣碎的配位過程和檢查錯誤,比記憶大量的隨計算機而改變的十六進制代碼、可能弄錯而沒有任何提示要強的多。熟練的匯編語言編碼員甚至可以直接從十六進制代碼中讀出匯編語言的大致意思。當(dāng)然,我們有更好的工具匯編器和反匯編器。簡單地說,匯編語言就是機器語言的一種可以被人讀懂的形式
8、,只不過它更容易記憶。至于宏匯編,則是包含了宏支持的匯編語言,這可以讓你編程的時候更專注于程序本身,而不是忙于計算和重寫代碼。匯編語言除了機器語言之外最接近計算機硬件的編程語言。由于它如此的接近計算機硬件,因此,它可以最大限度地發(fā)揮計算機硬件的性能。用匯編語言編寫的程序的速度通常要比高級語言和C/C+快很多-幾倍,幾十倍,甚至成百上千倍。當(dāng)然,解釋語言,如解釋型LISP,沒有采用JIT技術(shù)的Java虛機中運行的Java等等,其程序速度更無法與匯編語言程序同日而語 。永遠(yuǎn)不要忽視匯編語言的高速。實際的應(yīng)用系統(tǒng)中,我們往往會用匯編徹底重寫某些經(jīng)常調(diào)用的部分以期獲得更高的性能。應(yīng)用匯編也許不能提高你
9、的程序的穩(wěn)定性,但至少,如果你非常小心的話,它也不會降低穩(wěn)定性;與此同時,它可以大大地提高程序的運行速度。我強烈建議所有的軟件產(chǎn)品在最后Release之前對整個代碼進行Profile,并適當(dāng)?shù)赜脜R編語言取代部分高級語言代碼。至少,匯編語言的知識可以告訴你一些有用的東西,比如,你有多少個寄存器可以用。有時,手工的優(yōu)化比編譯器的優(yōu)化更為有效,而且,你可以完全控制程序的實際行為。我想我在羅嗦了??傊?,在我們結(jié)束這一章之前,我想說,不要在優(yōu)化的時候把希望完全寄托在編譯器上現(xiàn)實一些,再好的編譯器也不可能總是產(chǎn)生最優(yōu)的代碼。第二章 認(rèn)識處理器 中央處理器(CPU)在微機系統(tǒng)處于“領(lǐng)導(dǎo)核心”的地位。匯編語言
10、被編譯成機器語言之后,將由處理器來執(zhí)行。那么,首先讓我們來了解一下處理器的主要作用,這將幫助你更好地駕馭它。典型的處理器的主要任務(wù)包括 從內(nèi)存中獲取機器語言指令,譯碼,執(zhí)行 根據(jù)指令代碼管理它自己的寄存器 根據(jù)指令或自己的的需要修改內(nèi)存的內(nèi)容 響應(yīng)其他硬件的中斷請求 一般說來,處理器擁有對整個系統(tǒng)的所有總線的控制權(quán)。對于Intel平臺而言,處理器擁有對數(shù)據(jù)、內(nèi)存和控制總線的控制權(quán),根據(jù)指令控制整個計算機的運行。在以后的章節(jié)中,我們還將討論系統(tǒng)中同時存在多個處理器的情況。處理器中有一些寄存器,這些寄存器可以保存特定長度的數(shù)據(jù)。某些寄存器中保存的數(shù)據(jù)對于系統(tǒng)的運行有特殊的意義。新的處理器往往擁有更
11、多、具有更大字長的寄存器,提供更靈活的取指、尋址方式。寄存器如前所述,處理器中有一些可以保存數(shù)據(jù)的地方被稱作寄存器。寄存器可以被裝入數(shù)據(jù),你也可以在不同的寄存器之間移動這些數(shù)據(jù),或者做類似的事情?;旧?,像四則運算、位運算等這些計算操作,都主要是針對寄存器進行的。首先讓我來介紹一下80386上最常用的4個通用寄存器。先瞧瞧下面的圖形,試著理解一下:上圖中,數(shù)字表示的是位。我們可以看出,EAX是一個32-bit寄存器。同時,它的低16-bit又可以通過AX這個名字來訪問;AX又被分為高、低8bit兩部分,分別由AH和AL來表示。對于EAX、AX、AH、AL的改變同時也會影響與被修改的那些寄存器的
12、值。從而事實上只存在一個32-bit的寄存器EAX,而它可以通過4種不同的途徑訪問。也許通過名字能夠更容易地理解這些寄存器之間的關(guān)系。EAX中的E的意思是“擴展的”,整個EAX的意思是擴展的AX。X的意思Intel沒有明示,我個人認(rèn)為表示它是一個可變的量 。而AH、AL中的H和L分別代表高和低 。為什么要這么做呢?主要由于歷史原因。早期的計算機是8位的,8086是第一個16位處理器,其通用寄存器的名字是AX,BX等等;80386是Intel推出的第一款I(lǐng)A-32系列處理器,所有的寄存器都被擴充為32位。為了能夠兼容以前的16位應(yīng)用程序,80386不能將這些寄存器依舊命名為AX、BX,并且簡單地
13、將他們擴充為32位這將增加處理器在處理指令方面的成本。Intel微處理器的寄存器列表(在本章先只介紹80386的寄存器,MMX寄存器以及其他新一代處理器的新寄存器將在以后的章節(jié)介紹)通用寄存器下面介紹通用寄存器及其習(xí)慣用法。顧名思義,通用寄存器是那些你可以根據(jù)自己的意愿使用的寄存器,修改他們的值通常不會對計算機的運行造成很大的影響。通用寄存器最多的用途是計算。EAX32-bit寬通用寄存器。相對其他寄存器,在進行運算方面比較常用。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為段 寄存器或選擇器) EBX32-bit寬通用寄存器。通常作為內(nèi)存偏移指針使用(相對于EAX、ECX、EDX),D
14、S是默認(rèn)的段寄存器或選擇器。在保護模式中,同樣可以起這個作用。 ECX32-bit寬通用寄存器。通常用于特定指令的計數(shù)。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為 寄存器或段選擇器)。 EDX32-bit寬通用寄存器。在某些運算中作為EAX的溢出寄存器(例如乘、除)。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為段 寄存器或選擇器)。上述寄存器同EAX一樣包括對應(yīng)的16-bit和8-bit分組。用作內(nèi)存指針的特殊寄存器ESI32-bit寬 通常在內(nèi)存操作指令中作為“源地址指針”使用。當(dāng)然,ESI可以被裝入任意的數(shù)值,但通常沒有人把它當(dāng)作通用寄存器來用。DS是默認(rèn)段寄存器或選擇器
15、。 EDI32-bit寬通常在內(nèi)存操作指令中作為“目的地址指針”使用。當(dāng)然,EDI也可以被裝入任意的數(shù)值,但通常沒有人把它當(dāng)作通用寄存器來用。DS是默認(rèn)段寄存器或選擇器。 EBP32-bit寬這也是一個作為指針的寄存器。通常,它被高級語言編譯器用以建造堆棧幀來保存函數(shù)或過程的局部變量,不過,還是那句話,你可以在其中保存你希望的任何數(shù)據(jù)。SS是它的默認(rèn)段寄存器或選擇器。DS數(shù)據(jù)段,或數(shù)據(jù)選擇器。這個寄存器的低16 bit連同ESI一同指向的指令將要處理的內(nèi)存。同時,所有的內(nèi)存操作指令 默認(rèn)情況下都用它指定操作段(實模式)或內(nèi)存(作為選擇器,在保護模式。這個寄存器可以被裝入任意數(shù)值,然而在這么做的
16、時候需要小心一些。方法是,首先把數(shù)據(jù)送給AX,然后再把它從AX傳送給DS(當(dāng)然,也可以通過堆棧來做).ES附加段,或附加選擇器。這個寄存器的低16 bit連同EDI一同指向的指令將要處理的內(nèi)存。同樣的,這個寄存器可以被裝入任意數(shù)值,方法和DS類似。FSF段或F選擇器(推測F可能是Free?)??梢杂眠@個寄存器作為默認(rèn)段寄存器或選擇器的一個替代品。它可以被裝入任何數(shù)值,方法和DS類似。GSG段或G選擇器(G的意義和F一樣,沒有在Intel的文檔中解釋)。它和FS幾乎完全一樣。SS堆棧段或堆棧選擇器。這個寄存器的低16 bit連同ESP一同指向下一次堆棧操作(push和pop)所要使用的堆棧地址。
17、這個寄存器也可以被裝入任意數(shù)值,你可以通過入棧和出棧操作來給他賦值,不過由于堆棧對于很多操作有很重要的意義,因此,不正確的修改有可能造成對堆棧的破壞。 * 注意 一定不要在初學(xué)匯編的階段把這些寄存器弄混。他們非常重要,而一旦你掌握了他們,你就可以對他們做任意的操作了。段寄存器,或選擇器,在沒有指定的情況下都是使用默認(rèn)的那個。這句話在現(xiàn)在看來可能有點稀里糊涂,不過你很快就會在后面知道如何去做。特殊寄存器(指向到特定段或內(nèi)存的偏移量):EIP這個寄存器非常的重要。這是一個32位寬的寄存器 ,同CS一同指向即將執(zhí)行的那條指令的地址。不能夠直接修改這個寄存器的值,修改它的唯一方法是跳轉(zhuǎn)或分支指令。(C
18、S是默認(rèn)的段或選擇器)ESP這個32位寄存器指向堆棧中即將被操作的那個地址。盡管可以修改它的值,然而并不提倡這樣做,因為如果你不是非常明白自己在做什么,那么你可能造成堆棧的破壞。對于絕大多數(shù)情況而言,這對程序是致命的。(SS是默認(rèn)的段或選擇器) IP: Instruction Pointer, 指令指針SP: Stack Pointer, 堆棧指針好了,上面是最基本的寄存器。下面是一些其他的寄存器,你甚至可能沒有聽說過它們。(都是32位寬):CR0, CR2, CR3(控制寄存器)。舉一個例子,CR0的作用是切換實模式和保護模式。還有其他一些寄存器,D0, D1, D2, D3, D6和D7(
19、調(diào)試寄存器)。他們可以作為調(diào)試器的硬件支持來設(shè)置條件斷點。TR3, TR4, TR5, TR6 和 TR? 寄存器(測試寄存器)用于某些條件測試。最后我們要說的是一個在程序設(shè)計中起著非常關(guān)鍵的作用的寄存器:標(biāo)志寄存器。2.2 使用寄存器 在前一節(jié)中的x86基本寄存器的介紹,對于一個匯編語言編程人員來說是不可或缺的。現(xiàn)在你知道,寄存器是處理器內(nèi)部的一些保存數(shù)據(jù)的存儲單元。僅僅了解這些是不足以寫出一個可用的匯編語言程序的,但你已經(jīng)可以大致讀懂一般匯編語言程序了(不必驚訝,因為匯編語言的祝記符和英文單詞非常接近),因為你已經(jīng)了解了關(guān)于基本寄存器的絕大多數(shù)知識。在正式引入第一個匯編語言程序之前,我粗略
20、地介紹一下匯編語言中不同進制整數(shù)的表示方法。如果你不了解十進制以外的其他進制,請把鼠標(biāo)移動到這里。匯編語言中的整數(shù)常量表示 十進制整數(shù)這是匯編器默認(rèn)的數(shù)制。直接用我們熟悉的表示方式表示即可。例如,1234表示十進制的1234。不過,如果你指定了使用其他數(shù)制,或者有凡事都進行完整定義的小愛好,也可以寫成十進制數(shù)d或十進制數(shù)D的形式。 十六進制數(shù)這是匯編程序中最常用的數(shù)制,我個人比較偏愛使用十六進制表示數(shù)據(jù),至于為什么,以后我會作說明。十六進制數(shù)表示為0十六進制數(shù)h或0十六進制數(shù)H,其中,如果十六進制數(shù)的第一位是數(shù)字,則開頭的0可以省略。例如,7fffh, 0ffffh,等等。 二進制數(shù)這也是一種
21、常用的數(shù)制。二進制數(shù)表示為二進制數(shù)b或二進制數(shù)B。一般程序中用二進制數(shù)表示掩碼(mask code)等數(shù)據(jù)非常的直觀,但需要些很長的數(shù)據(jù)(4位二進制數(shù)相當(dāng)于一位十六進制數(shù))。例如,b。 八進制數(shù)八進制數(shù)現(xiàn)在已經(jīng)不是很常用了(確實還在用,一個典型的例子是Unix的文件屬性)。八進制數(shù)的形式是八進制數(shù)q、八進制數(shù)Q、八進制數(shù)o、八進制數(shù)O。例如,777Q。 需要說明的是,這些方法是針對宏匯編器(例如,MASM、TASM、NASM)說的,調(diào)試器默認(rèn)使用十六進制表示整數(shù),并且不需要特別的聲明(例如,在調(diào)試器中直接用FFFF表示十進制的65535,用10表示十進制的16)?,F(xiàn)在我們來寫一小段匯編程序,修
22、改EAX、EBX、ECX、EDX的數(shù)值。我們假定程序執(zhí)行之前,寄存器中的數(shù)值是全0:?XHLEAX00000000EBX00000000ECX00000000EDX00000000 正如前面提到的,EAX的高16bit是沒有辦法直接訪問的,而AX對應(yīng)它的低16bit,AH、AL分別對應(yīng)AX的高、低8bit。mov eax, hmov ebx, 0abcdeffehmov ecx, 1mov edx, 2; 將h送入eax; 將0abcdeffeh送入ebx; 將h送入ecx; 將h送入edx 則執(zhí)行上述程序段之后,寄存器的內(nèi)容變?yōu)椋?XHLEAX12345678EBXabcdeffeECX00
23、000001EDX00000002 那么,你已經(jīng)了解了mov這個指令(mov是move的縮寫)的一種用法。它可以將數(shù)送到寄存器中。我們來看看下面的代碼:mov eax, ebxmov ecx, edx; ebx內(nèi)容送入eax; edx內(nèi)容送入ecx 則寄存器內(nèi)容變?yōu)椋?XHLEAXabcdeffeEBXabcdeffeECX00000002EDX00000002 我們可以看到,“move”之后,數(shù)據(jù)依然保存在原來的寄存器中。不妨把mov指令理解為“送入”,或“裝入”。練習(xí)題把寄存器恢復(fù)成都為全0的狀態(tài),然后執(zhí)行下面的代碼:mov eax, 0a1234hmov bx, axmov ah, bl
24、mov al, bh; 將0a1234h送入eax; 將ax的內(nèi)容送入bx; 將bl內(nèi)容送入ah; 將bh內(nèi)容送入al 思考:此時,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, (reg3
25、2 | imm8 | imm16 | imm32)mov reg32, (reg16 | imm8 | imm16)mov reg8, (reg8 | imm8)例如,mov eax, 010h表示,在eax中載入h。需要注意的是,如果你希望在寄存器中裝入0,則有一種更快的方法,在后面我們將提到。交換寄存器的內(nèi)容:xchg reg32, reg32xchg reg16, reg16xchg reg8, reg8 例如,xchg ebx, ecx,則ebx與ecx的數(shù)值將被交換。由于系統(tǒng)提供了這個指令,因此,采用其他方法交換時,速度將會較慢,并需要占用更多的存儲空間,編程時要避免這種情況,即,盡
26、量利用系統(tǒng)提供的指令,因為多數(shù)情況下,這意味著更小、更快的代碼,同時也杜絕了錯誤(如果說Intel的CPU在交換寄存器內(nèi)容的時候也會出錯,那么它就不用賣CPU了。而對于你來說,檢查一行代碼的正確性也顯然比檢查更多代碼的正確性要容易)剛才的習(xí)題的程序用下面的代碼將更有效:mov eax, 0a1234hmov bx, axxchg ah, al; 將0a1234h送入eax; 將ax內(nèi)容送入bx; 交換ah, al的內(nèi)容 遞增或遞減寄存器的值:inc reg(8,16,32)dec reg(8,16,32) 這兩個指令往往用于循環(huán)中對指針的操作。需要說明的是,某些時候我們有更好的方法來處理循環(huán),
27、例如使用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。需要說明的是,與高級語言不同,匯編語言中,如果要計算兩數(shù)之和(差、積、商,或一般地說,運算結(jié)果),那么必然有一個寄存器被用來保存結(jié)果。在PASCAL中,我們可以用nA := nB + nC來讓nA保存nB
28、+nC的結(jié)果,然而,匯編語言并不提供這種方法。如果你希望保持寄存器中的結(jié)果,需要用另外的指令。這也從另一個側(cè)面反映了“寄存器”這個名字的意義。數(shù)據(jù)只是“寄存”在那里。如果你需要保存數(shù)據(jù),那么需要將它放到內(nèi)存或其他地方。類似的指令還有and、or、xor(與,或,異或)等等。它們進行的是邏輯運算。我們稱add、mov、sub、and等稱為為指令助記符(這么叫是因為它比機器語言容易記憶,而起作用就是方便人記憶,某些資料中也稱為指令、操作碼、opcodeoperation code等);后面的參數(shù)成為操作數(shù),一個指令可以沒有操作數(shù),也可以有一兩個操作數(shù),通常有一個操作數(shù)的指令,這個操作數(shù)就是它的操作
29、對象;而兩個參數(shù)的指令,前一個操作數(shù)一般是保存操作結(jié)果的地方,而后一個是附加的參數(shù)。我不打算在這份教程中用大量的篇幅介紹指令很多人做得比我更好,而且指令本身并不是重點,如果你學(xué)會了如何組織語句,那么只要稍加學(xué)習(xí)就能輕易掌握其他指令。更多的指令可以參考Intel提供的資料。編寫程序的時候,也可以參考一些在線參考手冊。Tech!Help和HelpPC 2.10盡管已經(jīng)很舊,但足以應(yīng)付絕大多數(shù)需要。聰明的讀者也許已經(jīng)發(fā)現(xiàn),使用sub eax, eax,或者xor eax, eax,可以得到與mov eax, 0類似的效果。在高級語言中,概不會選擇用a=a-a來給a賦值,因為測試會告訴你這么做更慢,簡
30、直就是在自找麻煩,然而在匯編語言中,你會得到相反的結(jié)論,多數(shù)情況下,以由快到慢的速度排列,這三條指令將是xor eax, eax、sub eax, eax和mov eax, 0。為什么呢?處理器在執(zhí)行指令時,需要經(jīng)過幾個不同的階段:取指、譯碼、取數(shù)、執(zhí)行。我們反復(fù)強調(diào),寄存器是CPU的一部分。從寄存器取數(shù),其速度很顯然要比從內(nèi)存中取數(shù)快。那么,不難理解,xor eax, eax要比mov eax, 0更快一些。那么,為什么a=a-a通常要比a=0慢一些呢?這和編譯器的優(yōu)化有一定關(guān)系。多數(shù)編譯器會把a=a-a翻譯成類似下面的代碼(通常,高級語言通過ebp和偏移量來訪問局部變量;程序中,x為a相對
31、于本地堆的偏移量,在只包含一個32-bit整形變量的程序中,這個值通常是4):mov eax, dword ptr ebp-xsub eax, dword ptr ebp-xmov dword ptr ebp-x,eax而把a=0翻譯成mov dword ptr ebp-x, 0上面的翻譯只是示意性的,略去了很多必要的步驟,如保護寄存器內(nèi)容、恢復(fù)等等。如果你對與編譯程序的實現(xiàn)過程感興趣,可以參考相應(yīng)的書籍。多數(shù)編譯器(特別是C/C+編譯器,如Microsoft Visual C+)都提供了從源代碼到宏匯編語言程序的附加編譯輸出選項。這種情況下,你可以很方便地了解編譯程序執(zhí)行的輸出結(jié)果;如果編譯
32、程序沒有提供這樣的功能也沒有關(guān)系,調(diào)試器會讓你看到編譯器的編譯結(jié)果。如果你明確地知道編譯器編譯出的結(jié)果不是最優(yōu)的,那就可以著手用匯編語言來重寫那段代碼了。怎么確認(rèn)是否應(yīng)該用匯編語言重寫呢?使用匯編語言重寫代碼之前需要確認(rèn)的幾件事情 首先,這種優(yōu)化最好有明顯的效果。比如,一段循環(huán)中的計算,等等。一條語句的執(zhí)行時間是很短的,現(xiàn)在新的CPU的指令周期都在0.s以下,Intel甚至已經(jīng)做出了4GHz主頻(主頻的倒數(shù)是時鐘周期)的CPU,如果你的代碼自始至終只執(zhí)行一次,并且你只是減少了幾個時鐘周期的執(zhí)行時間,那么改變將是無法讓人察覺的;很多情況下,這種“優(yōu)化”并不被提倡,盡管它確實減少了執(zhí)行時間,但為此
33、需要付出大量的時間、人力,多數(shù)情況下得不償失(極端情況,比如你的設(shè)備內(nèi)存價格非常昂貴的時候,這種優(yōu)化也許會有意義)。 其次,確認(rèn)你已經(jīng)使用了最好的算法,并且,你優(yōu)化的程序的實現(xiàn)是正確的。匯編語言能夠提供同樣算法的最快實現(xiàn),然而,它并不是萬金油,更不是解決一切的靈丹妙藥。用高級語言實現(xiàn)一種好的算法,不一定會比匯編語言實現(xiàn)一種差的算法更慢。不過需要注意的是,時間、空間復(fù)雜度最小的算法不一定就是解決某一特定問題的最佳算法。舉例說,快速排序在完全逆序的情況下等價于冒泡排序,這時其他方法就比它快。同時,用匯編語言優(yōu)化一個不正確的算法實現(xiàn),將給調(diào)試帶來很大的麻煩。 最后,確認(rèn)你已經(jīng)將高級語言編譯器的性能發(fā)
34、揮到極致。Microsoft的編譯器在RELEASE模式和DEBUG模式會有差異相當(dāng)大的輸出,而對于GNU系列的編譯器而言,不同級別的優(yōu)化也會生成幾乎完全不同的代碼。此外,在編程時對于問題的嚴(yán)格定義,可以極大地幫助編譯器的優(yōu)化過程。如何優(yōu)化高級語言代碼,使其編譯結(jié)果最優(yōu)超出了本教程的范圍,但如果你不能確認(rèn)已經(jīng)發(fā)揮了編譯器的最大效能,用匯編語言往往是一種更為費力的方法。 還有一點非常重要,那就是你明白自己做的是什么。好的高級語言編譯器有時會有一些讓人難以理解的行為,比如,重新排列指令順序,等等。如果你發(fā)現(xiàn)這種情況,那么優(yōu)化的時候就應(yīng)該小心編譯器很可能比你擁有更多的關(guān)于處理器的知識,例如,對于一個
35、超標(biāo)量處理器,編譯器會對指令序列進行“封包”,使他們盡可能的并行執(zhí)行;此外,宏匯編器有時會自動插入一些nop指令,其作用是將指令湊成整數(shù)字長(32-bit,對于16-bit處理器,是16-bit)。這些都是提高代碼性能的必要措施,如果你不了解處理器,那么最好不要改動編譯器生成的代碼,因為這種情況下,盲目的修改往往不會得到預(yù)期的效果。 曾經(jīng)在一份雜志上看到過有人用純機器語言編寫程序。不清楚到底這是不是編輯的失誤,因為一個頭腦正常的人恐怕不會這么做程序,即使它不長、也不復(fù)雜。首先,匯編器能夠完成某些封包操作,即使不行,也可以用db偽指令來寫指令;用匯編語言寫程序可以防止很多錯誤的發(fā)生,同時,它還減
36、輕了人的負(fù)擔(dān),很顯然,“完全用機器語言寫程序”是完全沒有必要的,因為匯編語言可以做出完全一樣的事情,并且你可以依賴它,因為計算機不會出錯,而人總有出錯的時候。此外,如前面所言,如果用高級語言實現(xiàn)程序的代價不大(例如,這段代碼在程序的整個執(zhí)行過程中只執(zhí)行一遍,并且,這一遍的執(zhí)行時間也小于一秒),那么,為什么不用高級語言實現(xiàn)呢?一些比較狂熱的編程愛好者可能不太喜歡我的這種觀點。比方說,他們可能希望精益求精地優(yōu)化每一字節(jié)的代碼。但多數(shù)情況下我們有更重要的事情,例如,你的算法是最優(yōu)的嗎?你已經(jīng)把程序在高級語言許可的范圍內(nèi)優(yōu)化到盡頭了嗎?并不是所有的人都有資格這樣說。匯編語言是這樣一件東西,它足夠的強大
37、,能夠控制計算機,完成它能夠?qū)崿F(xiàn)的任何功能;同時,因為它的強大,也會提高開發(fā)成本,并且,難于維護。因此,我個人的建議是,如果在軟件開發(fā)中使用匯編語言,則應(yīng)在軟件接近完成的時候使用,這樣可以減少很多不必要的投入。第二章中,我介紹了x86系列處理器的基本寄存器。這些寄存器對于x86兼容處理器仍然是有效的,如果你偏愛AMD的CPU,那么使用這些寄存器的程序同樣也可以正常運行。不過現(xiàn)在說用匯編語言進行優(yōu)化還為時尚早不可能寫程序,而只操作這些寄存器,因為這樣只能完成非常簡單的操作,既然是簡單的操作,那可能就會讓人覺得乏味,甚至找一臺足夠快的機器窮舉它的所有結(jié)果(如果可以窮舉的話),并直接寫程序調(diào)用,因為
38、這樣通常會更快。但話說回來,看完接下來的兩章內(nèi)存和堆棧操作,你就可以獨立完成幾乎所有的任務(wù)了,配合第五章中斷、第六章子程序的知識,你將知道如何駕馭處理器,并讓它為你工作。第三章 操作內(nèi)存 在前面的章節(jié)中,我們已經(jīng)了解了寄存器的基本使用方法。而正如結(jié)尾提到的那樣,僅僅使用寄存器做一點運算是沒有什么太大意義的,畢竟它們不能保存太多的數(shù)據(jù),因此,對編程人員而言,他肯定迫切地希望訪問內(nèi)存,以保存更多的數(shù)據(jù)。我將分別介紹如何在保護模式和實模式操作內(nèi)存,然而在此之前,我們先熟悉一下這兩種模式中內(nèi)存的結(jié)構(gòu)。3.1 實模式 事實上,在實模式中,內(nèi)存比保護模式中的結(jié)構(gòu)更令人困惑。內(nèi)存被分割成段,并且,操作內(nèi)存時
39、,需要指定段和偏移量。不過,理解這些概念是非常容易的事情。請看下面的圖:段-寄存器這種格局是早期硬件電路限制留下的一個傷疤。地址總線在當(dāng)時有20-bit。然而20-bit的地址不能放到16-bit的寄存器里,這意味著有4-bit必須放到別的地方。因此,為了訪問所有的內(nèi)存,必須使用兩個16-bit寄存器。這一設(shè)計上的折衷方案導(dǎo)致了今天的段-偏移量格局。最初的設(shè)計中,其中一個寄存器只有4-bit有效,然而為了簡化程序,兩個寄存器都是16-bit有效,并在執(zhí)行時求出加權(quán)和來標(biāo)識20-bit地址。偏移量是16-bit的,因此,一個段是64KB。下面的圖可以幫助你理解20-bit地址是如何形成的:段-偏
40、移量標(biāo)識的地址通常記做 段:偏移量 的形式。由于這樣的結(jié)構(gòu),一個內(nèi)存有多個對應(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,而不是一個“全新”的段。反之,在偏移量上加16也和在段上加1等價。某些時候,據(jù)此認(rèn)為段的“粒度”是16字節(jié)。練習(xí)題嘗試一下將下面的地址轉(zhuǎn)化為20bit的地址:2EA8:D678 26CF:8D5F 453A:CF
41、AD 2933:31A6 5924:DCCF694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA 稍高一些的要求是,寫一個程序?qū)⒍螢锳X、偏移量為BX的地址轉(zhuǎn)換為20bit的地址,并保存于EAX中。上面習(xí)題的答案我們現(xiàn)在可以寫一個真正的程序了。經(jīng)典程序:Hello, world; 應(yīng)該得到一個29字節(jié)的.com文件.MODEL TINY.CODE CR equ 13 LF equ 10 TERMINATOR equ $ ORG 100h Main PROCmov dx,offset sMessage mov ah,9int 21h mov ax,4
42、c00hint 21hMain ENDPsMessage: DB Hello, World! DB CR,LF,TERMINATOREND Main; .COM文件的內(nèi)存模型是TINY; 代碼段開始; 回車; 換行; DOS字符串結(jié)束符; 代碼起始地址為CS:0100h; 令DS:DX指向Message; int 21h(DOS中斷)功能9 -; 顯示字符串到標(biāo)準(zhǔn)輸出設(shè)備; int 21h功能4ch -; 終止程序并返回AL的錯誤代碼 ; 程序結(jié)束的同時指定入口點為Main那么,我們需要解釋很多東西。首先,作為匯編語言的抽象,C語言擁有“指針”這個數(shù)據(jù)類型。在匯編語言中,幾乎所有對內(nèi)存的操作都
43、是由對給定地址的內(nèi)存進行訪問來完成的。這樣,在匯編語言中,絕大多數(shù)操作都要和指針產(chǎn)生或多或少的聯(lián)系。這里我想強調(diào)的是,由于這一特性,匯編語言中同樣會出現(xiàn)C程序中常見的緩沖區(qū)溢出問題。如果你正在設(shè)計一個與安全有關(guān)的系統(tǒng),那么最好是仔細(xì)檢查你用到的每一個串,例如,它們是否一定能夠以你預(yù)期的方式結(jié)束,以及(如果使用的話)你的緩沖區(qū)是否能保證實際可能輸入的數(shù)據(jù)不被寫入到它以外的地方。作為一個匯編語言程序員,你有義務(wù)檢查每一行代碼的可用性。程序中的equ偽指令是宏匯編特有的,它的意思接近于C或Pascal中的const(常量)。多數(shù)情況下,equ偽指令并不為符號分配空間。此外,匯編程序執(zhí)行一項操作是非常
44、繁瑣的,通常,在對與效率要求不高的地方,我們習(xí)慣使用系統(tǒng)提供的中斷服務(wù)來完成任務(wù)。例如本例中的中斷21h,它是DOS時代的中斷服務(wù),在Windows中,它也被認(rèn)為是Windows API的一部分(這一點可以在Microsoft的文檔中查到)。中斷可以被理解為高級語言中的子程序,但又不完全一樣中斷使用系統(tǒng)棧來保存當(dāng)前的機器狀態(tài),可以由硬件發(fā)起,通過修改機器狀態(tài)字來反饋信息,等等。那么,最后一段通過DB存放的數(shù)據(jù)到底保存在哪里了呢?答案是緊挨著代碼存放。在匯編語言中,DB和普通的指令的地位是相同的。如果你的匯編程序并不知道新的助記符(例如,新的處理器上的CPUID指令),而你很清楚,那么可以用DB
45、 機器碼的方式強行寫下指令。這意味著,你可以超越匯編器的能力撰寫匯編程序,然而,直接用機器碼編程是幾乎肯定是一件費力不討好的事匯編器廠商會經(jīng)常更新它所支持的指令集以適應(yīng)市場需要,而且,你可以期待你的匯編其能夠產(chǎn)生正確的代碼,因為機器查表是不會出錯的。既然機器能夠幫我們做將程序轉(zhuǎn)換為代碼這件事情,那么為什么不讓它來做呢?細(xì)心的讀者不難發(fā)現(xiàn),在程序中我們沒有對DS進行賦值。那么,這是否意味著程序的結(jié)果將是不可預(yù)測的呢?答案是否定的。DOS(或Windows中的MS-DOS VM)在加載.com文件的時候,會對寄存器進行很多初始化。.com文件被限制為小于64KB,這樣,它的代碼段、數(shù)據(jù)段都被裝入同
46、樣的數(shù)值(即,初始狀態(tài)下DS=CS)。也許會有人說,“嘿,這聽起來不太好,一個64KB的程序能做得了什么呢?還有,你吹得天花亂墜的堆棧段在什么地方?”那么,我們來看看下面這個新的Hello world程序,它是一個EXE文件,在DOS實模式下運行。; 應(yīng)該得到一個561 字節(jié)的EXE文件.MODEL SMALL.STACK 200hCR equ 13LF equ 10TERMINATOR equ $.DATAMessage DB Hello, World ! DB CR,LF,TERMINATOR.CODEMain PROCmov ax, DGROUPmov ds, axmov dx, off
47、set Messagemov ah, 9int 21hmov ax, 4c00hint 21hMain ENDPEND main ; 采用“SMALL”內(nèi)存模型; 堆棧段; 回車; 換行; DOS字符串結(jié)束符; 定義數(shù)據(jù)段; 定義顯示串; 定義代碼段; 將數(shù)據(jù)段; 加載到DS寄存器; 設(shè)置DX; 顯示; 終止程序561字節(jié)?實現(xiàn)相同功能的程序大了這么多!為什么呢?我們看到,程序擁有了完整的堆棧段、數(shù)據(jù)段、代碼段,其中堆棧段足足占掉了512字節(jié),其余的基本上沒什么變化。分成多個段有什么好處呢?首先,它讓程序顯得更加清晰你肯定更愿意看一個結(jié)構(gòu)清楚的程序,代碼中hard-coded的字符串、數(shù)據(jù)讓人
48、覺得費解。比如,mov dx, 0152h肯定不如mov dx, offset Message來的親切。此外,通過分段你可以使用更多的內(nèi)存,比如,代碼段騰出的空間可以做更多的事情。exe文件另一個吸引人的地方是它能夠?qū)崿F(xiàn)“重定位”。現(xiàn)在你不需要指定程序入口點的地址了,因為系統(tǒng)會找到你的程序入口點,而不是死板的100h。程序中的符號也會在系統(tǒng)加載的時候重新賦予新的地址。exe程序能夠保證你的設(shè)計容易地被實現(xiàn),不需要考慮太多的細(xì)節(jié)。當(dāng)然,我們的主要目的是將匯編語言作為高級語言的一個有用的補充。如我在開始提到的那樣,真正完全用匯編語言實現(xiàn)的程序不一定就好,因為它不便于維護,而且,由于結(jié)構(gòu)的原因,你也
49、不太容易確保它是正確的;匯編語言是一種非結(jié)構(gòu)化的語言,調(diào)試一個精心設(shè)計的匯編語言程序,即使對于一個老手來說也不啻是一場惡夢,因為你很可能掉到別人預(yù)設(shè)的“陷阱”中這些技巧確實提高了代碼性能,然而你很可能不理解它,于是你把它改掉,接著就發(fā)現(xiàn)程序徹底敗掉了。使用匯編語言加強高級語言程序時,你要做的通常只是使用匯編指令,而不必搭建完整的匯編程序。絕大多數(shù)(也是目前我遇到的全部)C/C+編譯器都支持內(nèi)嵌匯編,即在程序中使用匯編語言,而不必撰寫單獨的匯編語言程序這可以節(jié)省你的不少精力,因為前面講述的那些偽指令,如equ等,都可以用你熟悉的高級語言方式來編寫,編譯器會把它轉(zhuǎn)換為適當(dāng)?shù)男问健P枰f明的是,在高
50、級語言中一定要注意編譯結(jié)果。編譯器會對你的匯編程序做一些修改,這不一定符合你的要求(附帶說一句,有時編譯器會很聰明地調(diào)整指令順序來提高性能,這種情況下最好測試一下哪種寫法的效果更好),此時需要做一些更深入的修改,或者用db來強制編碼。3.2 保護模式 實模式的東西說得太多了,盡管我已經(jīng)刪掉了許多東西,并把一些原則性的問題拿到了這一節(jié)討論。這樣做不是沒有理由的保護模式才是現(xiàn)在的程序(除了操作系統(tǒng)的底層啟動代碼)最常用的CPU模式。保護模式提供了很多令人耳目一新的功能,包括內(nèi)存保護(這是保護模式這個名字的來源)、進程支持、更大的內(nèi)存支持,等等。對于一個編程人員來說,能“偷懶”是一件令人愉快的事情。
51、這里“偷懶”是說把“應(yīng)該”由系統(tǒng)做的事情做的事情全都交給系統(tǒng)。為什么呢?這出自一個基本思想人總有犯錯誤的時候,然而規(guī)則不會,正確地了解規(guī)則之后,你可以期待它像你所了解的那樣執(zhí)行。對于C程序來說,你自己用C語言寫的實現(xiàn)相同功能的函數(shù)通常沒有系統(tǒng)提供的函數(shù)性能好(除非你用了比函數(shù)庫好很多的算法),因為系統(tǒng)的函數(shù)往往使用了更好的優(yōu)化,甚至可能不是用C語言直接編寫的。當(dāng)然,“偷懶”的意思是說,把那些應(yīng)該讓機器做的事情交給計算機來做,因為它做得更好。我們應(yīng)該把精力集中到設(shè)計算法,而不是編寫源代碼本身上,因為編譯器幾乎只能做等價優(yōu)化,而實現(xiàn)相同功能,但使用更好算法的程序?qū)崿F(xiàn),則幾乎只能由人自己完成。舉個例
52、子,這樣一個函數(shù):int fun() int a=0; register int i; for(i=0; i1000; i+) a+=i; return a; 在某種編譯模式DEBUG下被編譯為push ebpmov ebp,espsub esp,48hpush ebxpush esipush edilea edi,ebp-48hmov ecx,12hmov eax,0CCCCCCCChrep stos dword ptr edimov dword ptr ebp-4,0mov dword ptr ebp-8,0jmp fun+31hmov eax,dword ptr ebp-8add eax
53、,1mov dword ptr ebp-8,eaxcmp dword ptr ebp-8,3E8hjge fun+45hmov ecx,dword ptr ebp-4add ecx,dword ptr ebp-8mov dword ptr ebp-4,ecxjmp fun+28hmov eax,dword ptr ebp-4pop edipop esipop ebxmov esp,ebppop ebpret; 子程序入口 ; 保護現(xiàn)場; 初始化變量-調(diào)試版本特有。; 本質(zhì)是在堆中挖一塊地兒,存CCCCCCCC。; 用串操作進行,這將發(fā)揮Intel處理器優(yōu)勢; a=0; i=0; 走著; i+;
54、 i1000?; a+=i; return a; 恢復(fù)現(xiàn)場; 返回而在另一種模式RELEASE/MINSIZE下卻被編譯為xor eax,eaxxor ecx,ecxadd eax,ecxinc ecxcmp ecx,3E8hjl fun+4ret ; a=0; i=0; a+=i; i+; i繼續(xù)繼續(xù); return a如果讓我來寫,多半會寫成mov eax, 079f2chret ; return 為什么這樣寫呢?我們看到,i是一個外界不能影響、也無法獲知的內(nèi)部狀態(tài)量。作為這段程序來說,對它的計算對于結(jié)果并沒有直接的影響它的存在不過是方便算法描述而已。并且我們看到的,這段程序?qū)嶋H上無論執(zhí)行
55、多少次,其結(jié)果都不會發(fā)生變化,因此,直接返回計算結(jié)果就可以了,計算是多余的(如果說一定要算,那么應(yīng)該是編譯器在編譯過程中完成它)。更進一步,我們甚至希望編譯器能夠直接把這個函數(shù)變成一個符號常量,這樣連操作堆棧的過程也省掉了。第三種結(jié)果屬于“等效”代碼,而不是“等價”代碼。作為用戶,很多時候是希望編譯器這樣做的,然而由于目前的技術(shù)尚不成熟,有時這種做法會造成一些問題(gcc和g+的頂級優(yōu)化可以造成編譯出的FreeBSD內(nèi)核行為異常,這是我在FreeBSD上遇到的唯一一次軟件原因的kernel panic),因此,并不是所有的編譯器都這樣做(另一方面的原因是,如果編譯器在這方面做的太過火,例如自動求解全部“固定”問題,那么如果你的程序是解決固定的問題“很大”,如求解迷宮,那么在編譯過程中你就會找錘子來砸計算機了)。然而,作為編譯器制造商,為了提高自己的產(chǎn)品的競爭力,往往會使用第三種代碼來做函數(shù)庫。正如前面所提到的那樣,這種優(yōu)化往往不是編譯器本身的作用,盡管現(xiàn)代編譯程序擁有編譯執(zhí)行、循環(huán)代碼外提、無用代碼去除等諸多優(yōu)化功能,但它都不能保證程序最優(yōu)。最后一種代碼恐怕很少有編譯器能夠做到,不信你可以用自己常用的編譯器加上各種優(yōu)化選項試試:)發(fā)現(xiàn)什么了嗎?三種代碼中,對于內(nèi)存的
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 校級課題申報書字?jǐn)?shù)
- 旅游課題申報書范文
- 鄉(xiāng)村教師培訓(xùn)課題申報書
- 合同范本 發(fā)布單位
- 高校廉政課題申報書
- 代理采購電器合同范本
- 合伙退出機制合同范本
- 加工承攬訂制合同范本
- 課題申報立項書模版
- 發(fā)票臨時增量合同范本
- 醫(yī)療器械高值耗材
- 委托辦理供電委托書模板
- 車間生產(chǎn)現(xiàn)場5S管理基礎(chǔ)知識培訓(xùn)課件
- 2024北京高一(上)期末地理匯編:地球上的大氣章節(jié)綜合
- 區(qū)域保護合同模板
- 幕墻工程材料見證取樣檢測要求
- 《現(xiàn)代家政導(dǎo)論》電子教案 1.2模塊一項目二家政學(xué)內(nèi)容和價值認(rèn)知
- 《網(wǎng)絡(luò)數(shù)據(jù)安全管理條例》課件
- 讀《這樣教學(xué)很有效-任務(wù)驅(qū)動式課堂教學(xué)》心得體會
- DB11 945-2012 建設(shè)工程施工現(xiàn)場安全防護、場容衛(wèi)生及消防保衛(wèi)標(biāo)準(zhǔn)
- BEC商務(wù)英語初級考試歷年真題及答案6套
評論
0/150
提交評論