IA-32彙編語言筆記(10)—— 子程序設計

  • 記錄彙編語言課筆記,可能有不正確的地方,歡迎指出
  • 教材《新概念彙編語言》—— 楊季文
  • 這篇文章對應書第二章 IA32處理器基本功能 3.5部分

一、子程序設計要點

  1. 兩種傳參方法

    1. 寄存器
    2. 堆棧
  2. 調用約定決定了到底怎麼傳參,在C語言寫函數定義時,寫以下關鍵詞來顯示指定調用約定,
    void _fastcall cf330(unsigned m, char *buffer)指定了約定方式爲_fastcall
    在這裏插入圖片描述

    • 注意都是從右向左入棧
    • 注意不同調用約定對於清理堆棧的情況不同,如果函數自己不清,一定要手動清,維持堆棧平衡
  3. 安排局部變量的方法

    1. 子程序往往需要定義一些局部變量。所謂的局部,也就是限於子程序,或者限於代碼片段。
    2. 寄存器作爲局部變量可以提高效率。但寄存器數量較少,一般不把局部變量安排在寄存器中。
    3. 利用堆棧來安排局部變量。這個方法雖然較複雜,但可以安排足夠多的局部變量。
      1. 用堆棧安排,關鍵在於移動esp指針位置
      2. 如果局部變量數量少,可以push一個寄存器進去;如果數量多,可以直接修改esp的值,然後用堆棧操作賦值
  4. 保護寄存器的約定

    1. 子程序可能會破壞某些寄存器內容。爲此必須對有關寄存器的內容進行保護與恢復。
    2. 事前壓入堆棧,事後從堆棧彈出。在利用堆棧進行寄存器的保護和恢復時,一定要注意堆棧的先進後出特性,一定要注意堆棧平衡
    3. 可能會降低效率。
    4. 需要主程序和子程序之間的“默契”和“約定”。子程序只保護主程序關心的那些寄存器,通常保護ebx、esi、edi和ebp。
  5. 描述子程序的說明

    1. 在給出子程序代碼時,應該給出子程序的說明信息。
    2. 子程序說明信息一般包括:
      1. 子程序名(或者入口標號);
      2. 子程序功能描述;
      3. 子程序的入口參數和出口參數;
      4. 所影響的寄存器等情況;
      5. 使用的算法和重要的性能指標;
      6. 其他調用注意事項和說明信息;
      7. 調用實例。

二、子程序設計舉例

//子程序名(入口標號):BTOHS
//功    能: 把32位二進制數轉換爲8位十六進制數的ASCII碼串
//入口參數:(1)存放ASCII碼串緩衝區的首地址(先壓入堆棧)
//          (2)二進制數據(後壓入堆棧)
//出口參數: 無
//其他說明:(1)緩衝區應該足夠大(至少9個字節)
//          (2)ASCII串以字節0爲結束標記
//          (3)影響寄存器EAX、ECX、EDX的值
_asm  
{
BTOHS:  ;子程序入口標號
    PUSH  EBP
    MOV   EBP, ESP
    PUSH  EDI				//保護EDI
    MOV   EDI, [EBP+12]
    MOV   EDX, [EBP+8]
    MOV   ECX, 8
NEXT:
    ROL   EDX, 4
    MOV   AL, DL
    AND   AL, 0FH
    ADD   AL, '0'
    CMP   AL, '9'
    JBE   LAB580
    ADD   AL, 7
	LAB580:
    MOV   [EDI], AL
    INC   EDI
    LOOP  NEXT
    MOV   BYTE PTR [EDI], 0
    POP   EDI
    POP   EBP
    RET
}


//子程序名(入口標號):ISDIGIT
//功    能:判斷字符是否爲十進制數字符
//入口參數:AL=字符
//出口參數:如果爲非數字符,AL=0;否則AL保持不變
_asm  
{
    ISDIGIT:
        CMP   AL, '0'           ;與字符'0'比較
        JL    ISDIG1            ;有效字符是'0'-'9'
        CMP   AL,'9'
        JA    ISDIG1
        RET
    ISDIG1:                     ;非數字符
        XOR   AL,AL             ; AL= 0
        RET
 }

//演示調用上述子程序as334和子程序as335
#inclue  <stdio.h>
int  main(  )
{
    char  buff1[16] = "328";
    char  buff2[16] = "1234024";
    unsigned  x1, x2;
    unsigned  sum;

    _asm  
    {
        LEA   ESI, buff1        ;轉換一個字符串
        CALL  DSTOB
        MOV   x1, EAX
        LEA   ESI, buff2        ;轉換另一個字符串
        CALL  DSTOB
        MOV   x2, EAX
        ;
        MOV   EDX, x1           ;求和
        ADD   EDX, x2
        MOV   sum, EDX
        ;           ;如這些代碼位於前面,
        JMP   OK    ;需要通過該指令來跳過隨後的子程序部分!
        }
        //
        //在這裏安排子程序DSTOB和ISDIGIT的代碼
        //
OK:
    printf("%d\n", sum);
    return  0;
}

三、子程序調用方法

(1)調用指令

1. 分類

  1. 段內直接調用
  2. 段內間接調用
  3. 段間直接調用(不介紹)
  4. 段間間接調用(不介紹)

2. 段內直接

名稱 call(段內直接調用指令)
格式 CALL LABEL
動作 把調用指令下一行指令地址壓棧,然後轉到LABEL處執行
注意 除了保存返回地址,其他同無條件轉JMP

3. 段內間接

名稱 call(段內間接調用指令)
格式 CALL OPDR
動作 把調用指令下一行指令地址壓棧,然後OPDR內容送到EIP,轉到OPDR給出偏移地址處執行
合法值 OPDR:保護方式下,32位通用寄存器雙字存儲單元
注意 除了保存返回地址,其他同無條件轉JMP
#include  <stdio.h>
int  subr_addr;                  //存放子程序入口地址
int  valu;                       //保存結果
int  main( )
{ 
	_asm  
	{
        LEA    EDX, SUBR2        //取得子程序二的入口地址
        MOV    subr_addr, EDX    //保存到存儲單元
        LEA    EDX, SUBR1        //取得子程序一的入口地址
        XOR    EAX, EAX          //入口參數EAX=0
        CALL    EDX              //調用子程序一(段內間接,32位Reg)
        CALL    subr_addr        //調用子程序二(段內間接,雙字存儲單元)
        MOV   valu, EAX
    }
    printf("valu=%d\n",valu);    //顯示爲valu=28
    return  0;
}

4、函數指針

//源C程序
#include  <stdio.h>
int  max(int x, int y);            //聲明函數原型
int  min(int x, int y);            //
int  main()
{   
	int  (*pf)(int,int);           //定義指向函數的指針變量
    int  val1, val2;               //存放結果的變量

    pf = max;                      //使得pf指向函數max
    val1 = (*pf)(13,15);           //調用由pf指向的函數

    pf = min;                      //使得pf指向函數min
    val2 = (*pf)(23,25);           //調用由pf指向的函數

    printf("%d,%d\n",val1,val2);   //顯示爲15,23
    return  0;
}


//反編譯(不優化)
//標號max_YAHHH、min_YAHHH分別是兩個函數入口地址
	push  ebp
    mov   ebp, esp           ;建立堆棧框架
    sub   esp, 12            ;安排3個局部變量pf、val1和val2

    ; pf = max;
    mov   DWORD PTR [ebp-4], OFFSET  max_YAHHH
                             
    ; val1 = (*pf)(13,15);
    push  15
    push  13
    call  DWORD PTR [ebp-4]  ;間接調用指針所指的函數max
    add   esp, 8			 ;平衡堆棧
   
    ; val1= 返回結果
    mov   DWORD PTR [ebp-12], eax 
  
    ; pf = min;
    mov   DWORD PTR [ebp-4], OFFSET  min_YAHHH
   
    ; val2 = (*pf)(23,25);
    push  25
    push  23
    call   DWORD PTR  [ebp-4] ;間接調用指針所指的函數min
    add   esp, 8
    
    ; val2= 返回結果
    mov   DWORD PTR [ebp-8], eax
    mov   eax, DWORD PTR [ebp-8]     ; eax= val2
    push  eax
    mov   ecx, DWORD PTR [ebp-12]    ; ecx= val1
    push  ecx
    push  OFFSET  FORMTS             ;格式字符串
    call  _printf                    ;段內直接調用
    add   esp, 12                    ;平衡堆棧
                                     ;
    xor   eax, eax                   ;準備返回值
    mov   esp, ebp                   ;撤銷局部變量
    pop   ebp                        ;撤銷堆棧框架
    ret

可以看到

  1. 指針的本質就是地址
  2. 這裏把函數入口和函數參數都放在堆棧,用堆棧傳參。
  3. 注意傳參時從ESP開始向高地址找參數,push參數的時候按從右到左的順序
  4. 採用的是段內間接調用的方法

(2)返回指令

1、分類

  1. 按段內段間分

    1. 段內返回指令(對應段內調用)
    2. 段間返回指令(對應段間調用)(不介紹)
  2. 按返回時是否平衡堆棧

    1. 不帶立即數的返回指令
    2. 帶立即數的返回指令

2、 段內返回不帶立即數

名稱 RET(段內返回不帶立即數指令)
格式 RET
動作 指令從堆棧彈出地址偏移,送到指令指針寄存器EIP,返回到call時壓棧的返回地址處執行

3、 段內返回帶立即數

名稱 RET(段內返回帶立即數指令)
格式 RET count
動作 指令從堆棧彈出地址偏移(當然這也會影響esp),送到指令指針寄存器EIP,還額外把count 加到ESP
注意 用於平衡堆棧

四、示例

  • 以下是一個全彙編程序示例,它將十六進制數字符串轉爲數值(二進制),再轉十進制輸出查看,可以看一下函數調用的各種方法。
  • 此程序是按8086機資源寫的,在64位機器上運行此程序,需要:
    1. 保存以下代碼爲.asm文件
    2. 用nasm編譯成.com文件
    3. 用DOSbox模擬8086環境運行
;說明:將十六進制數字符串轉爲數值(二進制),再轉十進制輸出查看	
	segment code		;不分段,所有段共用一片內存空間
	org 100H			;100H開始

	MOV    AX, CS       ;使得數據段與代碼段相同
   	MOV    DS, AX       ;DS = CS	

	MOV AX, string		;取到要轉換的字符串首地址 
	PUSH AX 			
	CALL Hex2Bin		;轉換		
	CALL PutWordDec		;顯示轉換結果
		
	MOV   AH, 4CH		
    	INT   21H   

;子程序名:Hex2Bin
;功    能:把十六進制字符串轉數值
;入口參數:堆棧存字符串起始
;出口參數:ax
Hex2Bin:
	PUSH BP
	MOV BP, SP		;建立堆棧框架
	MOV SI, [BP+4]	;字長16位
	
	MOV CX, -1		;避免提前結束
	XOR AX,AX		;存結果	
	
	DEC SI			;方便循環
TOBIN:				
	INC SI
	MOV DL, '$'		;字符串結尾用$標記,DX在MUL的時候會被刷掉,這裏要重新賦值
	CMP [SI],DL		
	JE DONE

	MOV BX,16		;乘數16,BX在下面Hex2Bin_WORD的時候會被刷掉,要重新賦值
	MUL BX			;AX是被乘數,積的低16位仍在AX

	PUSH WORD [SI]		;取一個16進制字符,轉值存到BX(這裏入棧後面要手動平衡)
	CALL Hex2Bin_WORD	
	ADD SP,2			;平衡堆棧
	
	ADD AX,BX		
	
	;CALL PutWordDec
	;CALL PutSpace
	LOOP TOBIN
DONE:
	POP BP			;撤銷堆棧框架
	RET

;子程序名:Hex2Bin_WORD
;功    能:把一個十六進制字符轉成二進制值
;入口參數:堆棧
;出口參數:BX
Hex2Bin_WORD:
	PUSH BP
	MOV BP, SP		;建立堆棧框架
	
	MOV BX,[BP+4]	                
	MOV BH,0
	CMP BL,'A'
	JB NUM
	SUB BL,'A'-10
	JMP OK
NUM:
	SUB BL,'0'
OK:
	POP BP			;撤銷堆棧框架
	RET
		
;子程序名:PutWordDec
;功    能:把一個字的值轉十進制輸出
;入口參數:AX
;出口參數:無
PutWordDec:
	PUSH BP
	MOV BP, SP		;建立堆棧框架
	PUSHA			;保護所有reg(關鍵是AX/BX/CX/DX)
	
	MOV CX, -1
	MOV BX,10
LoopPWD1:
	XOR DX, DX
	DIV BX
	PUSH DX
	CMP AX, 0
	LOOPNE LoopPWD1

	NOT CX
LoopPWD2:
	POP DX
	ADD DL, '0'
	CALL PutChar
	LOOP LoopPWD2

	POPA			;恢復所有reg
	POP BP			;撤銷堆棧框架
	RET

;子程序名:PutChar
;功    能:顯示輸出一個字符
;入口參數:DL = 顯示輸出字符ASCII碼
;出口參數:無
PutChar:
    PUSH AX			;簡單的函數,可以不建立堆棧框架
    MOV   AH,2
    INT   21H       ;調用2號系統功能顯示輸出
    POP AX
    RET

;子程序名:PutSpace
;功    能:顯示輸出一個空格
;入口參數:無
;出口參數:無
PutSpace:
    PUSH AX			;簡單的函數,可以不建立堆棧框架
    PUSH DX
    MOV   DL,20H
    MOV   AH,2
    INT   21H       ;調用2號系統功能顯示輸出
    POP DX
    POP AX
    RET
;---------------------------------------------	
string db "1234", '$'		;在這裏寫要轉換的十六進制數







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章