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", '$'		;在这里写要转换的十六进制数







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