3.4 串操作
我們前面已經(jīng)提到,,內(nèi)存可以和寄存器交換數(shù)據(jù),也可以被賦予立即數(shù),。問題是,,如果我們需要把內(nèi)存的某部分內(nèi)容復(fù)制到另一個(gè)地址,又怎么做呢,?
設(shè)想將DS:SI處的連續(xù)512字節(jié)內(nèi)容復(fù)制到ES:DI(先不考慮可能的重疊),。也許會有人寫出這樣的代碼:
NextByte: mov cx,512
mov al,ds:[si]
mov es:[di],al
inc si
inc di
loop NextByte ; 循環(huán)次數(shù)
我不喜歡上面的代碼。它的確能達(dá)到作用,,但是,,效率不好。如果你是在做優(yōu)化,,那么寫出這樣的代碼意味著賠了夫人又折兵,。
Intel的CPU的強(qiáng)項(xiàng)是串操作。所謂串操作就是由CPU去完成某一數(shù)量的,、重復(fù)的內(nèi)存操作,。需要說明的是,我們常用的KMP算法(用于匹配字符串中的模式)的改進(jìn)——Boyer算法,,由于沒有利用串操作,,因此在Intel的CPU上的效率并非最優(yōu)。好的編譯器往往可以利用Intel CPU的這一特性優(yōu)化代碼,,然而,,并非所有的時(shí)候它都能產(chǎn)生最好的代碼。
某些指令可以加上REP前綴(repeat, 反復(fù)之意),,這些指令通常被叫做串操作指令,。
舉例來說,STOSD指令將EAX的內(nèi)容保存到ES:DI,,同時(shí)在DI上加或減四,。類似的,STOSB和STOSW分別作1字節(jié)或1字的上述操作,,在DI上加或減的數(shù)是1或2,。
計(jì)算機(jī)語言通常是不允許二義性的,。為什么我要說“加或減”呢?沒錯(cuò),,孤立地看STOS?指令,,并不能知道到底是加還是減,因?yàn)檫@取決于“方向”標(biāo)志(DF, Direction Flag),。如果DF被復(fù)位,,則加;反之則減,。
置位,、復(fù)位的指令分別是STD和CLD。
當(dāng)然,,REP只是幾種可用前綴之一,。常用的還包括REPNE,這個(gè)前綴通常被用來比較兩個(gè)串,,或搜索某個(gè)特定字符(字,、雙字)。REPZ,、REPE、REPNZ也是非常常用的指令前綴,,分別代表ZF(Zero Flag)在不同狀態(tài)時(shí)重復(fù)執(zhí)行,。
下面說三個(gè)可以復(fù)制數(shù)據(jù)的指令:
助記符 意義
movsb 將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI++,、DI++
movsw 將DS:SI的一字節(jié)復(fù)制到ES:DI,,之后SI+=2、DI+=2
movsd 將DS:SI的一字節(jié)復(fù)制到ES:DI,,之后SI+=4,、DI+=4
于是上面的程序改寫為
cld
mov cx, 128
rep movsd ; 復(fù)位DF
; 512/4 = 128,共128個(gè)雙字
; 行動,!
第一句cld很多時(shí)候是多余的,,因?yàn)閷?shí)際寫程序時(shí),很少會出現(xiàn)置DF的情況,。不過在正式?jīng)Q定刪掉它之前,,建議你仔細(xì)地調(diào)試自己的程序,并確認(rèn)每一個(gè)能夠走到這里的路徑中都不會將DF置位,。
錯(cuò)誤(非預(yù)期的)的DF是危險(xiǎn)的,。它很可能斷送掉你的程序,因?yàn)檫@直接造成緩沖區(qū)溢出問題,。
什么是緩沖區(qū)溢出呢,?緩沖區(qū)溢出分為兩類,,一類是寫入緩沖區(qū)以外的內(nèi)容,一類是讀取緩沖區(qū)以外的內(nèi)容,。后一種往往更隱蔽,,但隨便哪一個(gè)都有可能斷送掉你的程序。
緩沖區(qū)溢出對于一個(gè)網(wǎng)絡(luò)服務(wù)來說很可能更加危險(xiǎn),。懷有惡意的用戶能夠利用它執(zhí)行自己希望的指令,。服務(wù)通常擁有更高的特權(quán),而這很可能會造成特權(quán)提升,;即使不能提升攻擊者擁有的特權(quán),,他也可以利用這種問題使服務(wù)崩潰,從而形成一次成功的DoS(拒絕服務(wù))攻擊,。每年CERT的安全公告中,,都有6成左右的問題是由于緩沖區(qū)溢出造成的。
在使用匯編語言,,或C語言編寫程序時(shí),,很容易在無意中引入緩沖區(qū)溢出。然而并不是所有的語言都會引入緩沖區(qū)溢出問題,,Java和C#,,由于沒有指針,并且緩沖區(qū)采取動態(tài)分配的方式,,有效地消除了造成緩沖區(qū)溢出的土壤,。
匯編語言中,由于REP*前綴都用CX作為計(jì)數(shù)器,,因此情況會好一些(當(dāng)然,,有時(shí)也會更糟糕,因?yàn)橛捎贑X的限制,,很可能使原本可能改變程序行為的緩沖區(qū)溢出的范圍縮小,,從而更為隱蔽)。避免緩沖區(qū)溢出的一個(gè)主要方法就是仔細(xì)檢查,,這包括兩方面:設(shè)置合理的緩沖區(qū)大小,,和根據(jù)大小編寫程序。除此之外,,非常重要的一點(diǎn)就是,,在匯編語言這個(gè)級別寫程序,你肯定希望去掉所有的無用指令,,然而再去掉之前,,一定要進(jìn)行嚴(yán)格的測試;更進(jìn)一步,,如果能加上注釋,,并通過善用宏來做調(diào)試模式檢查,,往往能夠達(dá)到更好的效果。
3.5 關(guān)于保護(hù)模式中內(nèi)存操作的一點(diǎn)說明
正如3.2節(jié)提到到的那樣,,保護(hù)模式中,,你可以使用32位的線性地址,這意味著直接訪問4GB的內(nèi)存,。由于這個(gè)原因,,選擇器不用像實(shí)模式中段寄存器那樣頻繁地修改。順便提一句,,這份教程中所說的保護(hù)模式指的是386以上的保護(hù)模式,,或者,Microsoft通常稱為“增強(qiáng)模式”的那種,。
在為選擇器裝入數(shù)值的時(shí)候一定要非常小心,。錯(cuò)誤的數(shù)值往往會導(dǎo)致無效頁面錯(cuò)誤(在Windows中經(jīng)常出現(xiàn):)。同時(shí),,也不要忘記你的地址是32位的,,這也是保護(hù)模式的主要優(yōu)勢之一。
現(xiàn)在假設(shè)存在一個(gè)描述符描述從物理的0:0開始的全部內(nèi)存,,并已經(jīng)加載進(jìn)DS(數(shù)據(jù)選擇器),,則我們可以通過下面的程序來操作VGA的VRAM:
mov edi,0a0000h
mov byte ptr [edi],0fh ; VGA顯存的偏移量
; 將第一字節(jié)改為0fh
很明顯,這比實(shí)模式下的程序
mov ax,0a000h
mov ds,ax
mov di,0
mov [di],0fh ; AX -> VGA段地址
; 將AX值載入DS
; DI清零
; 修改第一字節(jié)
看上去要舒服一些,。
3.6 堆棧
到目前為止,,您已經(jīng)了解了基本的寄存器以及內(nèi)存的操作知識。事實(shí)上,,您現(xiàn)在已經(jīng)可以寫出很多的底層數(shù)據(jù)處理程序了,。
下面我來說說堆棧,。堆棧實(shí)在不是一個(gè)讓人陌生的數(shù)據(jù)結(jié)構(gòu),,它是一個(gè)先進(jìn)后出(FILO)的線性表,能夠幫助你完成很多很好的工作,。
先進(jìn)后出(FILO)是這樣一個(gè)概念:最后放進(jìn)表中的數(shù)據(jù)在取出時(shí)最先出來,。先進(jìn)后出(FILO)和先進(jìn)先出(FIFO, 和先進(jìn)后出的規(guī)則相反),以及隨機(jī)存取是最主要的三種存儲器訪問方式,。
對于堆棧而言,,最后放入的數(shù)據(jù)在取出時(shí)最先出現(xiàn)。對于子程序調(diào)用,,特別是遞歸調(diào)用來說,,這是一個(gè)非常有用的特性。
一個(gè)鐵桿的匯編語言程序員有時(shí)會發(fā)現(xiàn)系統(tǒng)提供的寄存器不夠,。很顯然,,你可以使用普通的內(nèi)存操作來完成這個(gè)工作,,就像C/C++中所做的那樣。
沒錯(cuò),,沒錯(cuò),,可是,如果數(shù)據(jù)段(數(shù)據(jù)選擇器)以及偏移量發(fā)生變化怎么辦,?更進(jìn)一步,,如果希望保存某些在這種操作中可能受到影響的寄存器的時(shí)候怎么辦?確實(shí),,你可以把他們也存到自己的那片內(nèi)存中,,自己實(shí)現(xiàn)堆棧。
太麻煩了……
既然系統(tǒng)提供了堆棧,,并且性能比自己寫一份更好,,那么為什么不直接加以利用呢?
系統(tǒng)堆棧不僅僅是一段內(nèi)存,。由于CPU對它實(shí)施管理,,因此你不需要考慮堆棧指針的修正問題??梢园鸭拇嫫鲀?nèi)容,,甚至一個(gè)立即數(shù)直接放到堆棧里,并在需要的時(shí)候?qū)⑵淙〕?。同時(shí),,系統(tǒng)并不要求取出的數(shù)據(jù)仍然回到原來的位置。
除了顯式地操作堆棧(使用PUSH和POP指令)之外,,很多指令也需要使用堆棧,,如INT、CALL,、LEAVE,、RET、RETF,、IRET等等,。配對使用上述指令并不會造成什么問題,然而,,如果你打算使用LEAVE,、RET、RETF,、IRET這樣的指令實(shí)現(xiàn)跳轉(zhuǎn)(比JMP更為麻煩,,然而有時(shí),例如在加密軟件中,或者需要修改調(diào)用者狀態(tài)時(shí),,這是必要的)的話,,那么我的建議是,先搞清楚它們做的到底是什么,,并且,,精確地了解自己要做什么。
正如前面所說的,,有兩個(gè)顯式地操作堆棧的指令:
助記符 功能
PUSH 將操作數(shù)存入堆棧,,同時(shí)修正堆棧指針
POP 將棧頂內(nèi)容取出并存到目的操作數(shù)中,同時(shí)修正堆棧指針
我們現(xiàn)在來看看堆棧的操作,。
執(zhí)行之前
執(zhí)行代碼
mov ax,1234h
mov bx,10
push ax
push bx
之后,,堆棧的狀態(tài)為
之后,再執(zhí)行
pop dx
pop cx
堆棧的狀態(tài)成為
當(dāng)然,,dx,、cx中的內(nèi)容將分別是000ah和1234h。
注意,,最后這張圖中,,我沒有抹去1234h和000ah,因?yàn)镻OP指令并不從內(nèi)存中抹去數(shù)值,。不過盡管如此,,我個(gè)人仍然非常反對繼續(xù)使用這兩個(gè)數(shù)(你可以通過修改SP來再次POP它們),然而這很容易導(dǎo)致錯(cuò)誤,。
一定要保證堆棧段有足夠的空間來執(zhí)行中斷,,以及其他一些隱式的堆棧操作。僅僅統(tǒng)計(jì)PUSH的數(shù)量并據(jù)此計(jì)算堆棧所需的大小很可能造成問題,。
CALL指令將返回地址放到堆棧中,。絕大多數(shù)C/C++編譯器提供了“堆棧檢查”這個(gè)編譯選項(xiàng),其作用在于保證C程序段中沒有忘記對堆棧中多余的數(shù)據(jù)進(jìn)行清理,,從而保證返回地址有效,。
本章小結(jié)
本章中介紹了內(nèi)存的操作的一些入門知識。限于篇幅,,我不打算展開細(xì)講指令,,如cmps*,,lods*,,stos*,等等,。這些指令的用法和前面介紹的movs*基本一樣,,只是有不同的作用而已。