8086匯編語言學習(八) 8086子程序


1.8086過程跳轉指令

  作為一門通用的編程語言,需要具有對代碼邏輯進行抽象封裝的能力。這一抽象元素,在有的語言中被稱為函數方法或者過程,而在8086匯編中被稱為子程序。子程序和子程序組合能夠構造出更復雜的子程序,如此往復以至無窮。子程序的存在,使得開發人員可以使用不同層次的抽象,構建出越來越復雜的系統。

  8086匯編子程序的調用、返回本質上依然是程序指令的跳轉。過程跳轉和無條件跳轉的不同之處在於,跳轉的子程序執行完畢后,還需要能夠正確的返回子程序執行完成后的第一條指令上,執行之后的程序。

  子程序可以調用子程序,互相之間理論上可以無限制的嵌套。程序跳轉時,可以將當前的CS:IP值壓入棧中,當子程序執行完畢后再將棧中的CS:IP彈出。棧的先進后出的特性使得棧這一結構可以很好的完成任務。

  雖然使用無條件跳轉指令和顯式的CS:IP壓棧出棧也能實現子程序的調用和返回,但8086匯編為此提供了專門的跳轉指令,這被成為過程跳轉指令。過程跳轉指令通過將CS:IP的壓棧/出棧和之后的跳轉合而為一,降低了使用子程序時的復雜度。

  8086匯編的子程序跳轉指令可以分為兩類,一是子程序調用指令,二是子程序返回指令

子程序調用指令

  子程序調用指令call,執行時有兩步操作,將IP或者CS/IP壓入當前棧中,隨后進行對應跳轉。call指令主要有以下幾種格式:

  call [標號]:其相當於push IP;jmp near ptr [標號]。是段內轉移,位移的值由編譯器在編譯時根據標號位置動態指定,偏移的IP范圍也如jmp near一致(-32678~32767)

  call far ptr [標號]:其相當於 push CS;push IP;jmp far ptr [標號]

  call [16位寄存器]:相當於push IP;jmp near [16位寄存器]

  call word ptr [內存單元地址]: 相當於 push IP; jmp word ptr [內存單元地址]

  call dword ptr [內存單元地址]: 相當於push IP; jmp dword ptr [內存單元地址]

子程序返回指令

  有了子程序調用指令,在跳轉前先將CS/IP的值壓入棧中,並跳轉。與之相對的子程序返回指令則是一個逆向的操作,先將棧中的CS/IP彈出,覆蓋還原調用者在調用子程序跳轉前的CS/IP值,再進行跳轉,這樣便能夠正確的返回子程序執行完畢后調用者對應的指令處。

  ret指令: 其相當於pop IP;彈出棧中的一個數據,用於復原IP的值,從而實現近轉移。

  ret n指令:類似ret,在ret的基礎上進行了棧頂指針sp的偏移(例如 ret 4),相當於pop IP;add sp,n

  retf指令: 其相當於pop IP; pop CS;(和call far ptr的入棧順序正好相反)彈出棧中的兩個數據,分別用於復原CS、IP的值,從而實現遠轉移。 

  retf n指令:類似retf,在retf的基礎上進行了棧頂指針sp的偏移(例如 retf 4),相當於pop IP;pop CS;add sp,n 。

call和ret組合使用

  子程序的調用和返回跳轉指令通常是配對使用的,call近轉移和ret配對,而call遠轉移則和retf配對。

下面是使用call/ret構造子程序的基礎模版:

assume cs:code
code segment
main: ..
      ..
      call sub1; 調用sub1子程序
      ..
      ..
      mov ax,4c00h
      int 21h
sub1: ..
      ..
      call sub2; 調用sub2子程序
      ..
      ..
      ret; sub1子程序返回
sub2: ..
      ..
      ..
      ret; sub2子程序返回
code ends
end main

2.子程序與調用者之間參數/返回值傳遞的問題

  參數返回值傳遞的問題解決方法其實質是如何通過某一媒介,使得調用者和子程序都能訪問到其中的數據。這一媒介主要有三種:寄存器、通用內存以及棧。

通過寄存器傳遞參數返回值

  下面是一個計算N的三次方的子程序,其通過寄存器來進行參數和返回值傳遞。

;說明:計算N的三次方
;參數:(bx)=N
;返回值: (dx:ax)=N^3
cube:mov ax,bx
     mul bx; mul bx可以簡單理解為ax = ax * bx
     mul bx
     ret

  使用寄存器傳遞參數/返回值時,調用者需要將參數送入子程序指定的參數寄存器中,並在執行完畢后從指定的結果寄存器中獲取返回值。相對的,子程序從參數寄存器中取出參數,將返回值送入結果寄存器中。

通過通用內存傳遞參數返回值

  使用寄存器傳遞參數/返回值雖然簡單,但存在一個致命缺陷:寄存器的數量是有限的,當子程序所需要傳遞的參數達到4、5個甚至十幾個,幾十個時(雖然不推薦傳遞過多參數,但理論上大多數編程語言是不限制參數個數的),使用寄存器傳遞參數/返回值就變得不可行了。可以考慮使用一片連續的內存來傳遞參數。

  下面是一個將ascll碼字母轉為大寫的子程序。

;說明:將ascll字母轉為大寫
;參數: 將(ds:si)指向的內存單元中的字母轉為大寫
capital:
  and byte ptr [si],11011111b; 利用字母大小寫ascll碼的規律進行大小寫轉換 inc si; si指向下一個內存單元 loop capital ret

完整的示例程序:

data segment
    db 'helloworld'
data ends

code segment
start:
    mov ax,data
    mov ds,ax
    mov si,0
    mov cx,10; 'helloworld'的長度
    call capital
    mov ax,4c00h
    int 21h
capital:
    and byte ptr [si],11011111b; 利用字母大小寫ascll碼的規律進行大小寫轉換
    inc si; si指向下一個內存單元
    loop capital
    ret 
code ends
end start

通過棧傳遞參數返回值

  使用通用內存可以批量的傳遞參數,同理也可以使用棧來實現參數/返回值的傳遞。調用者將所需要傳遞的參數壓入棧中,而子程序則從棧中彈出、取出參數。

使用棧來傳遞參數比起使用通用內存來說具有幾個優點:

  1.通用內存范圍過於寬泛,不同的設計者會約定使用不同的內存空間進行參數傳遞,不利於理解。統一的使用棧進行參數傳遞能讓代碼易於理解。

  2.子程序與調用者之間存在着共享寄存器沖突的問題,通常使用棧來緩存子程序與調用者沖突的寄存器內容。

  3.一般高級程序語言的實現中存在着作用域的概念,子程序中的臨時局部變量(也包括傳入的參數)無法在調用者所處的外部作用域中被訪問。出於空間效率的考量,子程序中的臨時局部變量應該在當前子程序執行完畢后被銷毀。棧這一后進先出的特性很適合這樣的場景,在子程序執行時將臨時局部變量壓入棧中,並在子程序執行完畢后將棧中元素有序彈出復原。

  下面是一個子程序,用於計算兩數之差的立方(a-b)^3 (demo中a=3,b=1)

assume cs:code

code segment
start:
; 參數b先壓入棧中,參數a后壓入棧中
    mov ax,1
    push ax
    mov ax,3
    push ax
    call difcube
    mov ax,4c00h
    int 21h
; difcube 計算兩數之差的立方 依賴子程序cube
; 參數a=[sp+4];b=[sp+6] (call指令會將當前IP壓入棧中,因此IP=[sp+2],棧中元素占用兩個內存單元)
; 返回值 ax = (a-b)^3
difcube:
    push bp
    mov bp,sp
    mov ax,[bp+4]
    sub ax,[bp+6]
    push ax
    call cube
    pop bp
    ret 4; ret時需要將進行sp的偏移(參數個數為2,偏移量為4),將參數彈出棧中,使得程序得以正確的返回
; cube 計算N的立方
; 參數n=[sp+4]
; 返回值 ax = n^3
cube:
    push bp
    mov bp,sp
    mov bx,[bp+4]
    mov ax,bx
    mul bx
    mul bx
    pop bp
    ret 2; ret時需要將進行sp的偏移(參數個數為1,偏移量為2),將參數彈出棧中,使得程序得以正確的返回
code ends
end start

3.子程序與調用者之間寄存器沖突的問題

   子程序與調用者之間寄存器沖突通過一個示例程序來說明。

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   ; 共有4個字符串需要處理         
s:
    mov si,bx
    call capital
    add bx,5  ; 每個字符串長度為5,bx增加指向下一字符串起始位置
    loop s
    
    mov ax,4c00h
    int 21h      
capital: 
    mov cl,[si]
    mov ch,0
    jcxz ok  ; 當前字符串到達結尾,cl+ch=cx=0
    and byte ptr [si],11011111b ; 當前字母轉換為大寫
    inc si    ; 指向當前字符串下一個字母
    jmp short capital
ok:
    ret
code ends

end start

  程序的思路大致是對每一字符串(和字符數組不同以0結尾,表示字符串的結束)循環調用capital子程序,並將字符串中的所有字母轉為大寫。乍看一下並沒有什么問題,但由於外部調用者s以及capital都使用了條件跳轉指令(loop、jcxz),導致了寄存器cx中的數據沖突。從高級語言作用域的角度來看,一個全局變量被調用者和子程序所共享,互相覆蓋。

  要想解決這一問題有幾種思路:調用者仔細檢查以避免和子程序使用相同的寄存器;將子程序和調用者使用的寄存器解耦,不互相沖突,使得調用者和子程序互相之間都不必關心彼此使用的寄存器。

避免調用者使用子程序依賴的寄存器

  由於寄存器數量是極其有限的,當程序足夠復雜時(子程序調用子程序),很難做到完全不沖突。由於必須檢查全局共享寄存器的存在,避免沖突導致bug,對開發人員也是一個極大的負擔。

調用者和子程序寄存器解耦

  將子程序和調用者之間的寄存器解耦,自然是最好不過的方案了。子程序只需要和調用者在參數/返回值處進行交互,而不必考慮例如cx計數寄存器之類的沖突。

  一個簡單的寄存器解耦思路是使用棧。當程序指針進入子程序時,將子程序使用到的寄存器首先壓入棧中,並在子程序執行完畢返回之前,按照相反的順序將其彈出,還原進入子程序前的寄存器。這樣,無論子程序使用的寄存器是否和調用者產生沖突,都不會產生沖突;如果子程序的設計者按照上述思路編寫了代碼,調用者也無需關心寄存器沖突的問題。

  因此,在設計子程序時應該將模版進一步優化,使之能夠解決調用者和子程序之間寄存器沖突的問題。

子程序開始:
    子程序所使用的寄存器入棧
    子程序內容
    子程序所使用的寄存器出棧
    子程序返回(ret retf)

  上文使用棧傳遞參數的例子中,子程序頭部和尾部對寄存器BP的入棧/出棧便是使用了這一技巧,從而避免了上下文BP寄存器的沖突。

改進后的程序如下:

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   ; 共有4個字符串需要處理         
s:
    mov si,bx
    call capital
    add bx,5  ; 每個字符串長度為5,bx增加指向下一字符串起始位置
    loop s
    
    mov ax,4c00h
    int 21h      
capital: 
    push cx
    push si
change:
    mov cl,[si]
    mov ch,0
    jcxz ok  ; 當前字符串到達結尾,cl+ch=cx=0
    and byte ptr [si],11011111b ; 當前字母轉換為大寫
    inc si    ; 指向當前字符串下一個字母
    jmp short change
ok:
    pop si
    pop cx
    ret
code ends

end start

 


免責聲明!

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



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