匯編學習筆記(9) -- CALL和 RET指令


    顯示字符串
      子程序描述
      提示
 
 
call 和 ret 指令都是轉移指令,它們都修改IP,或同時修改CS 和 IP
經常被用來實現子程序的設計
 
ret 和 ret
ret指令用棧中的數據,修改IP的內容,從而實現近轉移
 
retf指令用棧中的數據,修改CS和IP的內容,從而實現遠轉移
 
 
CPU執行ret指令時,進行下面兩步操作:
(1) (IP) = ( (ss) * 16 + (sp))
(2) (sp) = (sp) + 2

 

CPU執行retf指令時,進行下面4步操作:
(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
 
例:
下面的程序中,ret指令執行后,(ip) = 0,CS:IP指向代碼段的第一條指令
assume cs:code
stack segment
    db 16 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
    int 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

 

 
問題:補全程序,實現從內存1000:0000處開始執行內存
assume cs:code
stack segment
    db 16 dup (0)
stack ends

code segment
    start:
    mov ax,stack
    mov ss,ax
    mov sp,16
    mov ax,____
    push ax
    mov ax,_____
    push ax
    retf
code ends
end start

 

 
答案:
assume cs:code
stack segment
    db 16 dup (0)
stack ends

code segment
    start:
    mov ax,stack
    mov ss,ax
    mov sp,16
    mov ax,1000h
    push ax
    mov ax,0
    push ax
    retf
code ends
end start

 

 
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 標號
 
例題:

 

 

執行后ax的數值為6
 
 
轉移的 目的地址在指令中的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
push IP
jmp far ptr 標號
 
例題:

 

 

執行后ax的數值為1010h
 
 
 
 
 
 
轉移地址在寄存器中的call指令
指令格式: call 16位 reg
功能:
(sp)=(sp)-2
((ss)* 16+(sp)=(IP)
(IP)=(16位reg)

 

用匯編語法來解釋此種格式的call 指令,CPU執行“call 16位reg”時
相當於進行:
push IP
jmp 16位reg

 

 
問題:

 

 

下列程序進行后,ax中的值為多少
答案
push IP
jmp word ptr 內存單元地址

 

 
轉移地址在內存中的call指令
轉移地址在內存中的call指令有兩種格式
(1) call word 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]

 

執行后
IP = 0123h, sp = 0Eh
 
 
(2) 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
 
 
問題
下面的程序執行后,ax中的數值為多少
(用call指令原理分析,不要用debug執行,中斷會導致結果不一致)
assume cs:code
stack segment
        dw 8 dup (0)
stack ends

code segment
        start:
        mov ax,stack
        mov ss,ax
    mov sp,16
    mov ds,ax
    mov ax,0
    call word ptr ds:[0eH]
    inc ax
    inc ax
    inc ax
    
    mov ax, 4c00h
    int 21h
code ends
end start

 

 
 
答案
ax = 3
 
 
 
 
call 和 ret 的配合使用
 
問題
下面程序返回前,bx中的值為多少
assume cx:code
code segment
        start:
     mov ax, 1
        mov cx,3
        call s
        mov bx, ax
    
        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執行calls指令,將當前的IP值(指令movbx,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提供
 
我們再來看下面的程序:
assume cs:code
stack segment
    db 8 dup (0)            1000:0000  00 00 00 00 00 00 00 00
    db 8 dup (0)            1000:0008  00 00 00 00 00 00 00 00
stack ends

code segment
    start:
    mov ax,stack                1001:0000  B8 00 10
    mov ss,ax               1001:0003  8E D0
    mov sp,16                1001:0005  BC 10 00
    mov ax,1000                1001:0008  B8 E8 03
    call s                    1001:000B  E8 05 00
  
    mov ax, 4c00h           1001:000E  B8 00 4C
    int 21h                      1001:0011  CD 21
    
    s:
    add ax,ax                1001:0013  03 C0
    ret                        1001:0015  C3
code ends
end start

 

看一下程序的主要執行過程
 
 
(1) 前3條指令執行后,棧的情況如下:
 
(2) call 指令讀入后,(IP)=000EH,CPU指令緩沖器中的代碼為: E8 05 00;

 

 

然后,(IP) = (IP) + 0005 = 0013H
0005是call的下一個指令的IP 與 標號中第一條指令所在IP 的距離
 
(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:
        :
        :
        :
         ret            ;子程序返回
code ends
end main

 

 
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) * ( (ds) * 16 + 0);
 
mul word ptr [bx+si+8]
含義: (ax) = (ax) * ( (ds) * 16 + (bx) + (si) + 8)結果的 低16位
(dx) = (ax) * ( (ds) * 16 + (bx) + (si) + 8)結果的高16位
 
例:
(1) 計算 100 * 10
mov al,100
mov bl,10
mul bl

 

(2) 計算 100*10000
mov ax,100
mov bx,10000
mul bx

 

 
 
參數和結果傳遞的問題
子程序一般都要根據提供的參數處理一定的事務,處理后,將結果(返回值)提供給調用者
其實,我們討論參數和返回值傳遞的問題,實際上就是在探討,應該如何存儲子程序需要的參數和產生的返回值
 
比如,設計一個子程序,可以根據提供的N,來計算N的3次方
這里面就有兩個問題:
(1) 將參數N存儲在什么地方?
(2)計算得到的數值,存儲在什么地方?
 
 
很顯然,可以用寄存器來存儲,可以將參數放到bx中;
因為子程序中要計算N*N*N,可以使用多個mul指令,為了方便,可將結果放到dx和ax中
 
子程序如下。
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

 

 
批量數據的傳遞
前面的例子中,子程序cube只有一個參數,放在bx中
如果有兩個參數,那么可以用兩寄存器存放
可是如果需要傳遞的數據有3個、4個或更多直至N個,該怎樣存放呢?
 
在這種時候,我們將批量數據放到內存中,然后將它們所在內存空間的首地址放在寄存器中,傳遞給需要的子程序
 
對於具有批量數據的返回結果,也可用同樣的方法。
 
下面看一個例子,設計一個子程序,功能:將一個全是字母的字符串轉化為大寫。
 
這個子程序需要知道兩件事,字符串的內容和字符串的長度。
因為字符串中的字母可能很多,所以不便將整個字符串中的所有字母都直接傳遞給子程序。
但是,可以將字符串在內存中的首地址放在寄存器中傳遞給子程序
因為子程序中要用到循環,我們可以用loop指令,而循環的次數恰恰就是字符串的長度
出於方便的考慮,可以將字符串的長度放到cx
 
 

 

 

例子
編程:將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 cap
    
    mov ax,4c00h
    int 21h
    
    cap:
    and byte ptr [si],11011111b
    inc si
    loop cap
    ret
code ends
end start

 

 
 
寄存器沖突問題
設計一個子程序
功能:將一個全是字母,以0結尾的字符串,轉化為大寫
 
 
程序要處理的字符串以0作為結尾符,這個字符串可以如下定義:
db ' conversation' ,0
 
 
應用這個子程序,字符串的內容后面一定要有一個0,標記字符串的結束
子程序可以依次讀取每個字符進行檢測,如果不是0,就進行大寫的轉化,如果是0,就結束處理
 
由於可通過檢測0而知道是否已經處理完整個字符串,所以子程序可以不需要字符串的長度作為參數
可以用jcxz 來檢測0
 
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個字符串的處理
完整的程序如下。
assume cs:code
data segment
    db 'word',0
    db 'unix',0
    db 'wind',0
    db 'good',0
data ends

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 

 

 
這個程序思路是正確,但是細節上有錯誤
 

 

 

 
問題在於cx的使用,主程序要使用cx記錄循環次數
可是子程序中也使用了cx,在執行子程序的時候,cx中保存的循環計數值被改變,使得主程序的循環出錯
 
從上面的問題中,實際上引出了一個一般化的問題:
子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的沖突
 
那么如何來避免這種沖突呢?
在子程序的開始將子程序中所有用到的寄存器中的內容都保存起來,在子程序返回前再恢復
可以用棧來保存寄存器中的內容
 
以后,我們編寫子程序的標准框架如下:
 

 

 

 
重新改進下子程序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

 

 
實驗10
編寫3個子程序
 
顯示字符串
編寫一個通用的子程序來實現這個功能
我們應該提供靈活的調用接口,使調用者可以決定顯示的位置(行、列)、內容和顏色
 
子程序描述
名稱: show_ str
功能:在指定的位置,用指定的顏色,顯示一個用0結束的字符串。
參數: (dh) = 行號(取值范圍0~24)
(dI) = 列號(取值范圍 0~79)
(cl)=顏色,ds:si指 向字符串的首地址
返回:無
應用舉例:在屏幕的8行3列,用綠色顯示data段中的字符串
assume cs:code
data segment
    db 'Welcome to masm!',0
data ends

code segment
    start: 
    mov dh, 8
    mov dl,3
    mov cl, 2
    mov ax, data
    mov ds, ax
    mov si, 0
    call show_str
   
     mov ax, 4c00h
    int 21h
    
    show_str:
    :
    :
code ends
end start

 

提示
內存地址空間中,B8000H~BFFFFH 共32KB的空間,為80X25彩色字符模式的顯示緩沖區
偏移 000~09F 對應顯示器上的第1行(80個字符占160個字節);
偏移 0A0~13F 對應顯示器上的第2行;
偏移 140~1DF 對應顯示器上的第3行;

 

 
例:在顯示器的0行0列顯示黑低綠色的字符串 'ABCDEF'
('A'的ASCII碼值為41H, 02H表示黑底綠色)
 
顯示緩沖區里的內容為:
 

 

 

 
可以看出,在顯示緩沖區中,偶地址存放字符,奇地址存放字符的顏色屬性
 

 

 

 
 
答案
assume cs:code
data segment
        db 'welcome to masm!',0
data ends

code segment
start: 
    mov dh,12
            mov dl,13
            mov cl,1
            mov ax,data
            mov ds,ax
            mov si,0
            call show_str
            mov ax,4c00h
            int 21h
        
show_str: 
    push dx
    push cx
    push si
                
    mov di,0            ;顯示緩存區中的偏移量
    mov bl,dh    
    dec bl            ; bl-1才是真正的行,因為行號從0開始計數
    mov al,160  
    mul bl            ; 每行160字節 用 行數*每行偏移量 得到目標行的偏移量
    mov bx,ax           ; mul bl之后,乘積存儲在ax中,這里要轉存入bx中
    mov al,2            ; 列的偏移量為2,兩個字節代表一列!!!
    mul dl            ; 與行偏移量同理
    add bl,al            ;將列偏移量與行偏移量相加,得到指定位置的偏移量。
    
    mov ax,0b800h
    mov es,ax        ;指定顯示緩存區的內存位置
    
    mov al,cl            ; 由於后面jcxz語句的判斷要用到cx,所以我們要將
                ; cl(顏色)先存下來。
s:     
    mov ch,0
    mov cl,ds:[si]         ;首先將當前指向字符串的某個字符存入cx中
    jcxz ok            ; 如果cx為0,則轉移到ok標號執行相應代碼
    mov es:[bx+di],cl        ;將字符傳入低地址
    mov es:[bx+di+1],al    ; 將顏色傳入高地址
    add di,2            ; 列偏移量為2
    inc si            ; 字符串的偏移量為1
    loop s            ; 不為0,繼續復制

ok:     
    pop dx        
    pop cx
    pop si            ; 還原寄存器變量
    ret            ; 結束子程序調用
code ends
end start

 

 

 

參考: 王爽 - 匯編語言 和 小甲魚零基礎匯編

https://www.cnblogs.com/nojacky/p/9523904.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM