目錄
王爽《彙編語言》第四版 超級筆記
第10章 CALL和RET指令
10.1 ret和retf、call指令
10.2 call指令應用場景
依據位移進行轉移的call指令
轉移的目的位址在指令中的call指令
轉移位址在寄存器中的call指令
轉移位址在記憶體中的call指令
10.3 call和ret的配合使用、mul指令
10.4 參數和結果傳遞的問題、批量資料的傳遞
10.5 寄存器沖突的問題
call和ret指令都是轉移指令,它們都修改IP,或同時修改CS和IP。
它們經常被共同用來實作子程式的設計。這一章,我們講解call和ret指令的原理。
ret指令用棧中的資料,修改IP的内容,進而實作近轉移;
retf指令用棧中的資料,修改CS和IP的内容,進而實作遠轉移。
CPU執行ret指令時,進行下面兩步操作:
(IP)=((ss)x16+(sp))
(sp)=(sp)+2
CPU執行retf指令時,進行下面4步操作:
(CS)=((ss)x16+(sp))
可以看出,如果我們用彙編文法來解釋ret和retf指令,則:
CPU執行ret指令時,相當于進行:
pop IP
CPU執行retf指令時,相當于進行:
pop CS
下面的程式中,ret指令執行後,(IP)=0,CS:IP指向代碼段的第一條指令。
下面的程式中,retf指令執行後,CS:IP指向代碼段的第一條指令。
CPU執行call指令時,進行兩步操作:
(1)将目前的IP或CS和IP壓入棧中;
(2)轉移。
call指令不能實作短轉移,除此之外,call指令實作轉移的方法和jmp指令的原理相同,下面的幾個小節中,我們以給出轉移目的位址的不同方法為主線,講解call指令的主要應用格式。
call 标号(将目前的IP壓棧後,轉到标号處執行指令)
CPU執行此種格式的call指令時,進行如下的操作:
(1) (sp)=(sp)-2
((ss)x16+(sp))=(IP)
(2) (IP)=(IP)+16位位移。
16位位移=标号處的位址-call指令後的第一個位元組的位址;
16位位移的範圍為-32768~32767,用補碼表示;
16位位移由編譯程式在編譯時算岀。
從上面的描述中,可以看出,如果我們用彙編文法來解釋此種格式的call指令,則:
CPU執行“call 标号”時,相當于進行:
push IP jmp near ptr 标号
"call far ptr 标号”實作的是段間轉移。
CPU執行此種格式的call指令時,進行如下的操作。
(1 )(sp)=(sp)-2
((ss)x16+(sp))=(CS)
(sp)=(sp)-2
(2) (CS)=标号所在段的段位址
(IP)=标号在段中的偏移位址
從上面的描述中可以看出,如果我們用彙編文法來解釋此種格式的call指令,則:
CPU執行"call far ptr 标号”時,相當于進行:
push CS jmp far ptr 标号
指令格式:call 16位 reg
功能:
(IP)=(16位reg)
用彙編文法來解釋此種格式的call指令,CPU執行“call 16位 reg”時,相當于進行:
jmp 16位 reg
轉移位址在記憶體中的call指令有兩種格式。
(1)call word ptr 記憶體單元位址
用彙編文法來解釋此種格式的call指令,則:
CPU執行“call word ptr 記憶體單元位址”時,相當于進行:
jmp word ptr 記憶體單元位址
比如,下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]
執行後,(IP)=0123H,(sp)=0EH。
(2)call dword ptr 記憶體單元位址
CPU執行“call dword ptr 記憶體單元位址”時,相當于進行:
jmp dword ptr 記憶體單元位址
mov word ptr ds:[2],0
call dword ptr ds:[0]
執行後,(CS)=0, (IP)=0123H,(sp)=0CH。
我們己經分别學習了ret和call指令的原理。
現在來看一下,如何将它們配合使用來實作子程式的機制。
問題10.1
下面程式傳回前,bx中的值是多少?
思考後看分析。
分析:
我們來看一下CPU執行這個程式的主要過程。
(1)CPU将call s指令的機器碼讀入,IP指向了call s後的指令mov bx,ax,然後CPU執行call s指令,将目前的IP值(指令mov bx,ax的偏移位址)壓棧,并将IP的值改變為标号s處的偏移位址;
(2)CPU從标号s處開始執行指令,loop循環完畢後,(ax)=8;
(3)CPU将ret指令的機器碼讀入,IP指向了ret指令後的記憶體單元,然後CPU執行ret指令,從棧中彈出一個值(即call s先前壓入的mov bx,ax指令的偏移位址)送入IP中。 則CS:IP指向指令mov bx,ax;
(4)CPU從mov bx,ax開始執行指令,直至完成。
程式傳回前,(bx)=8。可以看出,從标号s到ret的程式段的作用是計算2的N次方,計算前,N的值由cx提供。
我們再來看下面的程式:

看一下程式的主要執行過程。
(1) 前3條指令執行後,棧的情況如下:
(2) call指令讀入後,(IP)=000EH,CPU指令緩沖器中的代碼為:E8 05 00;
CPU執行E8 05 00,首先,棧中的情況變為:
然後,(IP)=(IP)+0005=0013H。
(3) CPU從cs:0013H處(即标号s處)開始執行。
(4) ret指令讀入後:
(IP)=0016H,CPU指令緩沖器中的代碼為:C3
CPU執行C3,相當于進行pop IP,執行後,棧中的情況為:
(5) CPU回到cs:000EH處(即call指令後面的指令處)繼續執行。
從上面的讨論中我們發現,可以寫一個具有一定功能的程式段,我們稱其為子程式,在需要的時候,用call指令轉去執行。
可是執行完子程式後,如何讓CPU接着call指令向下執行?
call指令轉去執行子程式之前,call指令後面的指令的位址将存儲在棧中,是以可在子程式的後面使用ret指令,用棧中的資料設定IP的值,進而轉到call指令後面的代碼處繼續執行。
這樣,我們可以利用call和ret來實作子程式的機制。子程式的架構如下。
标号: 指令 ret
具有子程式的源程式的架構如下。
現在,可以從子程式的角度,回過頭來再看一下本節中的兩個程式。
這裡介紹一下mul指令,mul是乘法指令,使用mul做乘法的時候,注意以下兩點。
(1)兩個相乘的數:兩個相乘的數,要麼都是8位,要麼都是16位。如果是8位,一個預設放在AL中,另一個放在8位reg或記憶體位元組單元中;如果是16位,一個預設在AX中,另一個放在16位reg或記憶體字單元中。
(2)結果:如果是8位乘法,結果預設放在AX中;如果是16位乘法,結果高位預設在DX中存放,低位在AX中放。
格式如下:
mul reg mul 記憶體單元
記憶體單元可以用不同的尋址方式給出,比如:
mul byte ptr ds:[0]
含義:(ax)=(al)x((ds)x16+0);
mul word ptr [bx+si+8]
含義:(ax)=(ax)x((ds)x16+(bx)+(si)+8)結果的低16位。
(dx)=(ax)x((ds)x16+(bx)+(si)+8)結果的高16 位。
例:
(1) 計算100x10
100和10小于255,可以做8位乘法,程式如下。
mov al,100 mov bl,10 mul bl
結果:(ax)=1000(03E8H)
(2) 計算100x10000
100小于255,可10000大于255,是以必須做16位乘法,程式如下。
mov ax,100 mov bx,10000 mul bx
結果:(ax)=4240H,(dx)=000FH
(F4240H=1000000)
call與ret指令共同支援了彙編語言程式設計中的子產品化設計。
在實際程式設計中,程式的子產品化是必不可少的。因為現實的問題比較複雜,對現實問題進行分析時,把它轉化成為互相聯系、不同層次的子問題,是必須的解決方法。
而call與ret指令對這種分析方法提供了程式實作上的支援。利用call和ret指令,我們可以用簡捷的方法,實作多個互相聯系、功能獨立的子程式來解決一個複雜的問題。
下面的内容中,我們來看一下子程式設計中的相關問題和解決方法。
子程式一般都要根據提供的參數處理一定的事務,處理後,将結果(傳回值)提供給調用者。
其實,我們讨論參數和傳回值傳遞的問題,實際上就是在探讨,應該如何存儲子程式需要的參數和産生的傳回值。
比如,設計一個子程式,可以根據提供的N,來計算N的3次方。
這裡面就有兩個問題:
(1)将參數N存儲在什麼地方?
(2)計算得到的數值,存儲在什麼地方?
很顯然,可以用寄存器來存儲,可以将參數放到bx中;因為子程式中要計算NxNxN,可以使用多個mul指令,為了友善,可将結果放到dx和ax中。子程式如下。
注意,我們在程式設計的時候要注意形成良好的風格,對于程式應有詳細的注釋。
用寄存器來存儲參數和結果是最常使用的方法。對于存放參數的寄存器和存放結果的寄存器,調用者和子程式的讀寫操作恰恰相反:調用者将參數送入參數寄存器,從結果寄存器中取到傳回值;子程式從參數寄存器中取到參數,将傳回值送入結果寄存器。
程式設計,計算data段中第一組資料的3次方,結果儲存在後面一組dword單元中。
我們可以用到己經寫好的子程式,程式如下:
前面的例程中,子程式cube隻有一個參數,放在bx中。如果有兩個參數,那麼可以用兩個寄存器來放,可是如果需要傳遞的資料有3個、4個或更多直至N個,該怎樣存放呢?
寄存器的數量終究有限,我們不可能簡單地用寄存器來存放多個需要傳遞的資料。對于傳回值,也有同樣的問題。
在這種時候,我們将批量資料放到記憶體中,然後将它們所在記憶體空間的首位址放在寄存器中,傳遞給需要的子程式。對于具有批量資料的傳回結果,也可用同樣的方法。
下面看一個例子,設計一個子程式,功能:将一個全是字母的字元串轉化為大寫。
這個子程式需要知道兩件事,字元串的内容和字元串的長度。因為字元串中的字母可能很多,是以不便将整個字元串中的所有字母都直接傳遞給子程式。
但是,可以将字元串在記憶體中的首位址放在寄存器中傳遞給子程式。因為子程式中要用到循環,我們可以用loop指令,而循環的次數恰恰就是字元串的長度。出于友善的考慮,可以将字元串的長度放到cx中。
程式設計,将data段中的字元串轉化為大寫。
注意,除了用寄存器傳遞參數外,還有一種通用的方法是用棧來傳遞參數。
設計一個子程式,功能:将一個全是字母,以0結尾的字元串,轉化為大寫。
程式要處理的字元串以0作為結尾符,這個字元串可以如下定義:
db 'conversation',0
應用這個子程式,字元串的内容後面一定要有一個0,标記字元串的結束。子程式可以依次讀取每個字元進行檢測,如果不是0,就進行大寫的轉化;如果是0,就結束處理。
由于可通過檢測0而知道是否己經處理完整個字元串,是以子程式可以不需要字元串的長度作為參數。可以用jcxz來檢測0。
來看一下這個子程式的應用。
(1)将data段中字元串轉化為大寫。
代碼段中的相關程式段如下。
mov ax,data mov ds,ax mov si,0 call capital
(2)将data段中的字元串全部轉化為大寫。
可以看到,所有字元串的長度都是5(算上結尾符0),使用循環,重複調用子程式capital,完成對4個字元串的處理。完整的程式如下。
問題10.2
這個程式在思想上完全正确,但在細節上卻有些錯誤,把錯誤找出來。
問題在于cx的使用,主程式要使用cx記錄循環次數,可是子程式中也使用了cx,在執行子程式的時候,cx中儲存的循環計數值被改變,使得主程式的循環出錯。
從上面的問題中,實際上引出了一個一般化的問題:子程式中使用的寄存器,很可能在主程式中也要使用,造成了寄存器使用上的沖突。
那麼如何來避免這種沖突呢?粗略地看,可以有以下兩個方案。
(1)在編寫調用子程式的程式時,注意看看子程式中有沒有用到會産生沖突的寄存器,如果有,調用者使用别的寄存器;
(2)在編寫子程式的時候,不要使用會産生沖突的寄存器。
我們來分析一下上面兩個方案的可行性:
(1)這将給調用子程式的程式的編寫造成很大的麻煩,因為必須要小心檢查所調用的子程式中是否有将産生沖突的寄存器。
比如說,在上面的例子中,我們在編寫主程式的循環的時候就得檢查子程式中是否用到了bx和cx,因為如果子程式中用到了這兩個寄存器就會出現問題。如果釆用這種方案來解決沖突的話,那麼在主程式的循環中,就不能使用cx寄存器,因為子程式中己經用到。
(2)這個方案是不可能實作的,因為編寫子程式的時候無法知道将來的調用情況。
可見,我們上面所設想的兩個方案都不可行。我們希望:
(1)編寫調用子程式的程式的時候不必關心子程式到底使用了哪些寄存器;
(2)編寫子程式的時候不必關心調用者使用了哪些寄存器;
(3)不會發生寄存器沖突。
解決這個問題的簡捷方法是,在子程式的開始将子程式中所有用到的寄存器中的内容都儲存起來,在子程式傳回前再恢複。可以用棧來儲存寄存器中的内容。
以後,我們編寫子程式的标準架構如下:
我們改進一下子程式capital的設計:
要注意寄存器入棧和出棧的順序。