組合語言子程式呼叫call和ret
call和ret指令
call和ret指令都是轉移指令,它們都修改IP,或同時修改CS和IP。
它們經常被共同用來實現子程式的設計。
ret和retf
ret指令用棧中的資料,修改IP的內容,從而實現近轉移;
retf指令用棧中的資料,修改CS和IP的內容,從而實現遠轉移。
CPU執行ret指令時,進行下面的兩步操作:
(1)(IP) = ((ss)*16 +(sp))
(2)(sp) = (sp)+2
CPU執行retf指令時,進行下面四步操作:
(1)(IP) = ((ss)*16) + (sp)
(2)(sp) = (sp) + 2
(3)(CS) = ((ss)*16) + (sp)
(4)(sp) = (sp) + 2
用匯編語法來解釋ret和retf指令,則:
CPU執行ret指令時,相當於進行:
pop IP
CPU執行retf指令時,相當於進行:
pop IP
pop CS
call指令
CPU執行call指令時,進行兩步操作:
(1) 將當前的IP或CS和IP壓入棧中;
(2) 轉移。
call指令不能實現短轉移,除此之外,call指令實現轉移的方法和jmp指令的原理相同。
依據位移進行轉移的call指令
call 標號(將當前的IP壓棧後,轉到標號處執行指令)
CPU執行此種格式的call指令時,進行如下的操作:
(1)(sp) = (sp)-2
((ss)*16 +(sp)) = (IP)
(2)(IP) = (IP)+16位位移。
16位位移=“標號”處的地址-call指令後的第一個位元組的地址;
16位位移的範圍為-32768~32767,用補碼錶示;
16位位移由編譯程式在編譯時算出。
用匯編語法來解釋此種格式的call指令,則:
CPU執行指令“call 標號”時,相當於進行:
push IP
jmp near ptr 標號
轉移的目的地址在指令中的call指令
前面講的call指令,其對應的機器指令中並沒有轉移的目的地址,而是相對於當前IP的轉移位移。
指令“call far ptr 標號”實現的是段間轉移。
CPU執行此格式的call指令時,進行如下的操作:
(1)(sp)=(sp)-2
((ss)*16+(sp)) = (CS)
(sp)=(sp)-2
((ss)*16+(sp)) = (IP)
(2)(CS)=標號所在段的段地址
(IP)=標號在段中的偏移地址
用匯編語法來解釋此種格式的call指令,則:
CPU執行指令“call far ptr 標號”時,相當於進行:
push CS
push IP
jmp far ptr 標號
轉移地址在暫存器中的call指令
指令格式:call 16位暫存器
功能:
(sp) = (sp)-2
((ss)*16+(sp)) = (IP)
(IP) = (16位暫存器)
用匯編語法來解釋此種格式的call指令,CPU執行call 16位reg時,相當於進行:
push IP
jum 16位暫存器
轉移地址在記憶體中的call指令
有兩種格式:
1) call word ptr 記憶體單元地址
相當於:
push IP
jum word ptr 記憶體單元地址
2) call dword ptr 記憶體單元地址
相當於:
push CS
push IP
jmp dword ptr 記憶體單元地址
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 maint
mul指令
mul是乘法指令。
使用mul做乘法的時候:
1) 兩個相乘的數:兩個相乘的數,要麼都是8位,要麼都是16位。
如果是8位,一個預設放在AL中,別一個放在8位暫存器或記憶體單元中;
如果是16位,一個預設在AX中,另一個放在16位暫存器或記憶體單元中。
2) 結果:如果是8位乘法,結果預設放在AX中;如果是16位乘法,結果高位預設在DX中存放,低位在AX中存放。
格式如下:
mul reg
mul 記憶體單元
模組化程式設計
call與ret指令共同支援了組合語言程式設計中的模組化設計。
在實際程式設計中,程式的模組化是必不可少的。
因為實現的問題比較複雜,對現實問題進行分析時,把它轉化成為相互聯絡、不同層次的子問題,是必須的解決方法。
而call與ret指令對這種分析方法提供了程式實現上的支援。
利用call和ret指令,我們可以用簡捷的方法,實現多個相互聯絡、功能獨立的子程式來解決一個複雜的問題。
引數和結果傳遞的問題
子程式一般都要根據提供的引數處理一定的事務,處理後,將結果(返回值)提供給呼叫者。
其實,我們討論引數和返回值傳遞的問題,實際上就是在探討,應該如何儲存子程式需要的引數和產生的返回值。
;說明:計算N的3次方
;引數:(bx)=N
;結果:(dx:ax)=N^3
cube:mov ax,bx
mul bx
mul bx
ret
注意,程式設計時的良好風格,應有有詳細的註釋。包含對子程式的功能、引數和結果的說明。
用暫存器來儲存引數和結果是最常使用的方法。對於存放參數的暫存器和存放結果的暫存器,呼叫者和子程式的讀寫操作恰恰相反:呼叫者將引數送入引數暫存器,從結果暫存器中取到返回值;子程式從引數暫存器中取到引數,將返回值送入結果暫存器。
批量資料的傳遞
暫存器的數量終究有限,我們不可能簡單地用暫存器來存放多個需要傳遞的資料。對於返回值,也有同樣的問題。
在這種時候,我們將批量資料放到記憶體中,然後將它們所在記憶體空間的首地址放在暫存器中,傳遞給需要的子程式。對於具有批量資料的返回結果,也可用同樣的方法。
除了用暫存器傳遞引數外,還有一種通用的方法是用棧來傳遞引數。
暫存器衝突的問題
一個一般化的問題,子程式中使用的暫存器,很可能在主程式中也要使用,造成了暫存器使用上的衝突。
那麼我們如何來避免這種衝突呢?粗略地看,可以有兩個方案:
1) 在編寫呼叫子程式的程式時,注意看看子程式中有沒有用到會產生衝突的暫存器,如果有,呼叫者使用別的暫存器;
2) 在編寫子程式的時候,不要使用會產生衝突的暫存器。
以上兩個方案,不具可行性,第一種給呼叫子程式的程式的編寫造成很大麻煩。第二種不可能實現,子程式無法知道將來的呼叫情況。
我們希望:
1) 編寫呼叫子程式的程式的時候不必關心子程式到底使用了哪些暫存器;
2) 編寫子程式的時候不必關心呼叫者使用了哪些暫存器;
3) 不會發生暫存器衝突。
解決這個問題的簡捷方法是,在子程式的開始將子程式中所有用到的暫存器中的內容都儲存起來,在子程式返回前再恢復。我們可以用棧來儲存暫存器中的內容。
以後,我們編寫子程式的標準框架如下:
子程式開始:子程式中使用的暫存器入棧
子程式內容
子程式中使用的暫存器出棧
返回(ret、retf)
要注意暫存器入棧和出棧的順序。
實驗10 編寫子程式
1、 顯示字串
問題:顯示字串是現實工作中經常要用到的功能,應該編寫一個通用的子程式來實現這個功能。我們應該提供靈活的呼叫介面,使呼叫者可以決定顯示的位置(行、列)、內容和顏色。
子程式描述
名稱:show_str
功能:在指定的位置,用指定的顏色,顯示一個用0結束的字串。
引數:(dh)=行號(取值範圍0~24),(dl)=列號(取值範圍0~79),
(cl)=顏色,ds:si指向字串的首地址
返回:無
應用舉例:在螢幕的8行3列,用綠色顯示data段中的字串。
1) 子程式的入口引數是螢幕上的行號和列號,注意在子程式內部要將它們轉化為視訊記憶體中的地址,首先要分析一下螢幕上的行列位置和視訊記憶體地址的對應關係。
2) 注意儲存子程式中用到的相關暫存器。
3) 空上子程式的內部處理和視訊記憶體的結構密切相關,但是向外提供了與視訊記憶體結構無關的介面。通過呼叫這個子程式,進行字串的顯示時可以不必瞭解視訊記憶體的結構,為程式設計提供了方便。在實驗中,注意體會這種設計思想。
2、 解決除法溢位的問題
問題:div指令可以做除法。當進行8位除法的時候,用al儲存結果的商,ah儲存結果的餘數;進行16位除法的時候,用ax儲存結果的商,dx儲存結果的餘數。可是,現在有一個問題,如果結果的商大於ah或ax所能儲存的最大值,那麼將如何?
當CPU執行div等除法指令的時候,如果發生結果資料超出了暫存器所能儲存的範圍,將引發CPU的一個內部錯誤,這個錯誤被稱為:除法溢位。
子程式描述
名稱:divdw
功能:進行不會產生溢位的除法運算,被除數為dword型,除數為word型,結果為dword型。
引數:(ax)=dword型資料的低16位
(dx)=dword型資料的高16位
(cx)=除數
返回:(dx)=結果的高16位,(ax)=結果的低16位
(cx)=餘數
應用舉例:計算1000000/10(F4240H/0AH)
3、 數值顯示
問題:程式設計:將data段中的資料以十進位制的形式顯示出來。
資料在記憶體中都是二進位制資訊,標記了數值的大小。要把它們顯示到螢幕上,成為我們能夠讀懂的資訊,需要進行資訊的轉化。
比如,數值12666,在機器中儲存為二進位制資訊:11000101111010B(317AH),計算機可以理解它。而我們要在顯示器上讀到可以理解的數值12666,我們看到的應該是一串字元:“12666”,由於顯示卡遵循的是ASCII編碼,為了讓我們能在顯示器上看到這串字元,它在機器中應以ASCII碼的形式儲存為:31H、32H、36H、36H、36H(字元“0”~“9”對應的ASCII碼為30H~39H)。
通過上面的分析可以看到,在概念世界中,有一個抽象的資料12666,它表示了一個數值的大小。在現實世界中它可以有多種表示形式,可以在電子機器中以高低電平(二進位制)的形式儲存,也可以在紙上、黑板上、螢幕上以人類的語言“12666”來書寫。現在,我們面臨的問題就是,要將同一抽象的資料,從一種表示形式轉化為另一種表示形式。
要將資料用十進位制形式顯示到螢幕上,要進行兩步工作:
1) 將用二進位制資訊儲存的資料轉變為十進位制形式的字串;
2) 顯示十進位制形式的字串。
子程式描述
名稱:dtoc
功能:將word型資料轉變為表示十進位制數的字串,字串以0為結尾符。
引數:(ax)=word型資料
ds:si指向字串的首地址
返回:無
應用舉例:程式設計,將資料12666以十進位制的形式在螢幕的8行3列,用綠色顯示出來。
分析:要得到字串“12666”,就是要得到一列表示該字串的ASCII碼:31H、32H、36H、36H、36H。
十進位制數碼字元對應的ASCII碼=十進位制數碼值+30H。
要得到表示十進位制數的字串,先求十進位制數每位的值。
例如,對於12666,先求得每位的值:1、2、6、6、6。再將這些數分別加上30H,便得到了表示12666的ASCII碼串,31H、32H、36H、36H、36H。
那麼,怎樣得到每位的值呢?採用下列方法(除10取餘法):
12666/10=1266……6
1266/10=126……..6
126/10=12………6
12/10=1………..2
1/10=0………..1
可見,用10除12666,共除5次,記下每次的餘數,就得到了每位的值。
綜合以上分析,可得出處理過程如下:
用12666除以10,迴圈5次,記下每次的餘數;將每次的餘數分別加30H,使得到了表示十進位制數的ASCII碼串。
只要是除到商為0,各位的值就已經全部求出。可以使用jcxz指令來實現相關的功能。