1. 程式人生 > 實用技巧 >彙編學習筆記(9) -- CALL和 RET指令

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

ret 和 ret call指令   依據位移進行 轉移的call指令   轉移的 目的地址在指令中的call指令   轉移地址在暫存器中的call指令   轉移地址在記憶體中的call指令   call 和 ret 的配合使用 mul指令   引數和結果傳遞的問題   批量資料的傳遞   暫存器衝突問題 實驗10   顯示字串     子程式描述     提示 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

ret 和 ret call指令   依據位移進行 轉移的call指令   轉移的 目的地址在指令中的call指令   轉移地址在暫存器中的call指令   轉移地址在記憶體中的call指令   call 和 ret 的配合使用 mul指令   引數和結果傳遞的問題   批量資料的傳遞   暫存器衝突問題 實驗10   顯示字串     子程式描述     提示

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

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