王爽《匯編語言》第四版 超級筆記
第10章 CALL和RET指令
call和ret指令都是轉移指令,它們都修改IP,或同時修改CS和IP。
它們經常被共同用來實現子程序的設計。這一章,我們講解call和ret指令的原理。
10.1 ret和retf、call指令
ret指令用棧中的數據,修改IP的內容,從而實現近轉移;
retf指令用棧中的數據,修改CS和IP的內容,從而實現遠轉移。
CPU執行ret指令時,進行下面兩步操作:
- (IP)=((ss)x16+(sp))
- (sp)=(sp)+2
CPU執行retf指令時,進行下面4步操作:
- (IP)=((ss)x16+(sp))
- (sp)=(sp)+2
- (CS)=((ss)x16+(sp))
- (sp)=(sp)+2
可以看出,如果我們用匯編語法來解釋ret和retf指令,則:
CPU執行ret指令時,相當於進行:
pop IP
CPU執行retf指令時,相當於進行:
pop IP
pop CS
下面的程序中,ret指令執行后,(IP)=0,CS:IP指向代碼段的第一條指令。
assume cs:code
stack segment
db 15 dup (0)
stack ends
code segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
ret
code ends
end start
下面的程序中,retf指令執行后,CS:IP指向代碼段的第一條指令。
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
mov ax,4c00h
inc 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push cs
push ax
mov bx,0
retf
code ends
end start
CPU執行call指令時,進行兩步操作:
(1)將當前的IP或CS和IP壓入棧中;
(2)轉移。
call指令不能實現短轉移,除此之外,call指令實現轉移的方法和jmp指令的原理相同,下面的幾個小節中,我們以給出轉移目的地址的不同方法為主線,講解call指令的主要應用格式。
10.2 call指令應用場景
依據位移進行轉移的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指令
"call far ptr 標號”實現的是段間轉移。
CPU執行此種格式的call指令時,進行如下的操作。
(1 )(sp)=(sp)-2
((ss)x16+(sp))=(CS)
(sp)=(sp)-2
((ss)x16+(sp))=(IP)
(2) (CS)=標號所在段的段地址
(IP)=標號在段中的偏移地址
從上面的描述中可以看出,如果我們用匯編語法來解釋此種格式的call指令,則:
CPU執行"call far ptr 標號”時,相當於進行:
push CS
push IP
jmp far ptr 標號
轉移地址在寄存器中的call指令
指令格式:call 16位 reg
功能:
(sp)=(sp)-2
((ss)x16+(sp))=(IP)
(IP)=(16位reg)
用匯編語法來解釋此種格式的call指令,CPU執行“call 16位 reg”時,相當於進行:
push IP
jmp 16位 reg
轉移地址在內存中的call指令
轉移地址在內存中的call指令有兩種格式。
(1)call word ptr 內存單元地址
用匯編語法來解釋此種格式的call指令,則:
CPU執行“call word ptr 內存單元地址”時,相當於進行:
push IP
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 內存單元地址
用匯編語法來解釋此種格式的call指令,則:
CPU執行“call dword ptr 內存單元地址”時,相當於進行:
push CS
push IP
jmp dword ptr 內存單元地址
比如,下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]
執行后,(CS)=0, (IP)=0123H,(sp)=0CH。
10.3 call和ret的配合使用、mul指令
我們己經分別學習了ret和call指令的原理。
現在來看一下,如何將它們配合使用來實現子程序的機制。
問題10.1
下面程序返回前,bx中的值是多少?
assume cs:code
code segment
start: mov ax,1
mov ex,3
call s
mov bx,ax ;(bx)=?
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start
思考后看分析。
分析:
我們來看一下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
具有子程序的源程序的框架如下。
assume cs:code
code segment
main:
...
...
...
call sub1 ;調用子程序sub1
...
...
...
mov ax,4c00h
int 21h
sub1: ;子程序sub1開始
...
...
...
call sub2 ;調用子程序sub2
...
...
...
ret ;子程序返回
sub2: ;子程序sub2開始
...
...
...
ret ;子程序返回
code ends
end main
現在,可以從子程序的角度,回過頭來再看一下本節中的兩個程序。
這里介紹一下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)
10.4 參數和結果傳遞的問題、批量數據的傳遞
call與ret指令共同支持了匯編語言編程中的模塊化設計。
在實際編程中,程序的模塊化是必不可少的。因為現實的問題比較復雜,對現實問題進行分析時,把它轉化成為相互聯系、不同層次的子問題,是必須的解決方法。
而call與ret指令對這種分析方法提供了程序實現上的支持。利用call和ret指令,我們可以用簡捷的方法,實現多個相互聯系、功能獨立的子程序來解決一個復雜的問題。
下面的內容中,我們來看一下子程序設計中的相關問題和解決方法。
子程序一般都要根據提供的參數處理一定的事務,處理后,將結果(返回值)提供給調用者。
其實,我們討論參數和返回值傳遞的問題,實際上就是在探討,應該如何存儲子程序需要的參數和產生的返回值。
比如,設計一個子程序,可以根據提供的N,來計算N的3次方。
這里面就有兩個問題:
(1)將參數N存儲在什么地方?
(2)計算得到的數值,存儲在什么地方?
很顯然,可以用寄存器來存儲,可以將參數放到bx中;因為子程序中要計算NxNxN,可以使用多個mul指令,為了方便,可將結果放到dx和ax中。子程序如下。
;說明:計算N的3次方
;參數:(bx)=N
;結果:(dx:ax)=N^3
cube:mov ax,bx
mul bx
mul bx
ret
注意,我們在編程的時候要注意形成良好的風格,對於程序應有詳細的注釋。
用寄存器來存儲參數和結果是最常使用的方法。對於存放參數的寄存器和存放結果的寄存器,調用者和子程序的讀寫操作恰恰相反:調用者將參數送入參數寄存器,從結果寄存器中取到返回值;子程序從參數寄存器中取到參數,將返回值送入結果寄存器。
編程,計算data段中第一組數據的3次方,結果保存在后面一組dword單元中。
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
我們可以用到己經寫好的子程序,程序如下:
code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向第一組word單元
mov di,16 ;ds:di指向第二組dword單元
mov cx,8
s: mov bx,[si]
call cube
mov [di],ax
mov [di].2,dx
add si,2 ;ds:si指向下一個word單元
add di,4 ;ds:di指向下一個dword單元
loop s
mov ax,4c00h
int 21h
cube: mov ax,bx
mul bx
mul bx
ret
code ends
end start
前面的例程中,子程序cube只有一個參數,放在bx中。如果有兩個參數,那么可以用兩個寄存器來放,可是如果需要傳遞的數據有3個、4個或更多直至N個,該怎樣存放呢?
寄存器的數量終究有限,我們不可能簡單地用寄存器來存放多個需要傳遞的數據。對於返回值,也有同樣的問題。
在這種時候,我們將批量數據放到內存中,然后將它們所在內存空間的首地址放在寄存器中,傳遞給需要的子程序。對於具有批量數據的返回結果,也可用同樣的方法。
下面看一個例子,設計一個子程序,功能:將一個全是字母的字符串轉化為大寫。
這個子程序需要知道兩件事,字符串的內容和字符串的長度。因為字符串中的字母可能很多,所以不便將整個字符串中的所有字母都直接傳遞給子程序。
但是,可以將字符串在內存中的首地址放在寄存器中傳遞給子程序。因為子程序中要用到循環,我們可以用loop指令,而循環的次數恰恰就是字符串的長度。出於方便的考慮,可以將字符串的長度放到cx中。
capital:and byte ptr [si],11011111b ;將ds:si所指單元中的字母轉化為大寫
inc si ;ds:si指向下一個單元
loop capital
ret
編程,將data段中的字符串轉化為大寫。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向字符串(批量數據)所在空間的首地址
mov cx,12 ;cx存放字符串的長度
call capital
mov ax,4c00h
int 21h
capital:and byte ptr [si],11011111b
inc si
loop capital
ret
code ends
end start
注意,除了用寄存器傳遞參數外,還有一種通用的方法是用棧來傳遞參數。
10.5 寄存器沖突的問題
設計一個子程序,功能:將一個全是字母,以0結尾的字符串,轉化為大寫。
程序要處理的字符串以0作為結尾符,這個字符串可以如下定義:
db 'conversation',0
應用這個子程序,字符串的內容后面一定要有一個0,標記字符串的結束。子程序可以依次讀取每個字符進行檢測,如果不是0,就進行大寫的轉化;如果是0,就結束處理。
由於可通過檢測0而知道是否己經處理完整個字符串,所以子程序可以不需要字符串的長度作為參數。可以用jcxz來檢測0。
;說明:將一個全是字母,以0結尾的字符串,轉化為大寫
;參數:ds:si指向字符串的首地址
;結果:沒有返回值
capital:mov cl,[si]
mov ch,0
jcxz ok ;如果(cx)=0,結束;如果不是0,處理
and byte ptr [si],11011111b ;將ds:si所指單元中的字母轉化為大寫
inc si ;ds:si指向下一個單元
jmp short capital
ok:ret
來看一下這個子程序的應用。
(1)將data段中字符串轉化為大寫。
assume cs:code
data segment
db 'conversation',0
data ends
代碼段中的相關程序段如下。
mov ax,data
mov ds,ax
mov si,0
call capital
(2)將data段中的字符串全部轉化為大寫。
assume cs:code
data segment
db 'word',0
db 'unix',0
db 'wind',0
db 'good',0
data ends
可以看到,所有字符串的長度都是5(算上結尾符0),使用循環,重復調用子程序capital,完成對4個字符串的處理。完整的程序如下。
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s: mov si,bx
call capital
add bx,5
loop s
mov ax,4c00h
int 21h
capital:mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital
ok:ret
code ends
end start
問題10.2
這個程序在思想上完全正確,但在細節上卻有些錯誤,把錯誤找出來。
思考后看分析。
分析:
問題在於cx的使用,主程序要使用cx記錄循環次數,可是子程序中也使用了cx,在執行子程序的時候,cx中保存的循環計數值被改變,使得主程序的循環出錯。
從上面的問題中,實際上引出了一個一般化的問題:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的沖突。
那么如何來避免這種沖突呢?粗略地看,可以有以下兩個方案。
(1)在編寫調用子程序的程序時,注意看看子程序中有沒有用到會產生沖突的寄存器,如果有,調用者使用別的寄存器;
(2)在編寫子程序的時候,不要使用會產生沖突的寄存器。
我們來分析一下上面兩個方案的可行性:
(1)這將給調用子程序的程序的編寫造成很大的麻煩,因為必須要小心檢查所調用的子程序中是否有將產生沖突的寄存器。
比如說,在上面的例子中,我們在編寫主程序的循環的時候就得檢查子程序中是否用到了bx和cx,因為如果子程序中用到了這兩個寄存器就會出現問題。如果釆用這種方案來解決沖突的話,那么在主程序的循環中,就不能使用cx寄存器,因為子程序中己經用到。
(2)這個方案是不可能實現的,因為編寫子程序的時候無法知道將來的調用情況。
可見,我們上面所設想的兩個方案都不可行。我們希望:
(1)編寫調用子程序的程序的時候不必關心子程序到底使用了哪些寄存器;
(2)編寫子程序的時候不必關心調用者使用了哪些寄存器;
(3)不會發生寄存器沖突。
解決這個問題的簡捷方法是,在子程序的開始將子程序中所有用到的寄存器中的內容都保存起來,在子程序返回前再恢復。可以用棧來保存寄存器中的內容。
以后,我們編寫子程序的標准框架如下:
子程序開始:子程序中使用的寄存器入棧
子程序內容
子程序中使用的寄存器出棧
返回(ret、retf)
我們改進一下子程序capital的設計:
capital:push cx
push si
change:mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok:pop si
pop cx
ret
要注意寄存器入棧和出棧的順序。