- 记录汇编语言课笔记,可能有不正确的地方,欢迎指出
- 教材《新概念汇编语言》—— 杨季文
- 这篇文章对应书第二章 IA32处理器基本功能 3.5部分
文章目录
- 一、子程序设计要点
- 二、子程序设计举例
- 三、子程序调用方法
- (1)调用指令
- 1. 分类
- 2. 段内直接
- 3. 段内间接
- 4、函数指针
- (2)返回指令
- 1、分类
- 2、 段内返回不带立即数
- 3、 段内返回带立即数
- 四、示例
一、子程序设计要点
- 两种传参方法
- 寄存器
- 堆栈
-
调用约定
决定了到底怎么传参,在C语言写函数定义时,写以下关键词来显示指定调用约定,
如void _fastcall cf330(unsigned m, char *buffer)
指定了约定方式为_fastcall
- 注意都是从右向左入栈
- 注意不同调用约定对于清理堆栈的情况不同,如果函数自己不清,一定要手动清,维持堆栈平衡
- 安排局部变量的方法
- 子程序往往需要定义一些局部变量。所谓的局部,也就是限于子程序,或者限于代码片段。
- 寄存器作为局部变量可以提高效率。但寄存器数量较少,一般不把局部变量安排在寄存器中。
- 利用堆栈来安排局部变量。这个方法虽然较复杂,但可以安排足够多的局部变量。
- 用堆栈安排,关键在于移动esp指针位置
- 如果局部变量数量少,可以push一个寄存器进去;如果数量多,可以直接修改esp的值,然后用堆栈操作赋值
- 保护寄存器的约定
- 子程序可能会破坏某些寄存器内容。为此必须对有关寄存器的内容进行保护与恢复。
- 事前压入堆栈,事后从堆栈弹出。在利用堆栈进行寄存器的保护和恢复时,一定要注意堆栈的先进后出特性,一定要注意堆栈平衡
- 可能会降低效率。
- 需要主程序和子程序之间的“默契”和“约定”。子程序只保护主程序关心的那些寄存器,通常保护ebx、esi、edi和ebp。
- 描述子程序的说明
- 在给出子程序代码时,应该给出子程序的说明信息。
- 子程序说明信息一般包括:
- 子程序名(或者入口标号);
- 子程序功能描述;
- 子程序的入口参数和出口参数;
- 所影响的寄存器等情况;
- 使用的算法和重要的性能指标;
- 其他调用注意事项和说明信息;
- 调用实例。
二、子程序设计举例
//子程序名(入口标号):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. 分类
- 段内直接调用
- 段内间接调用
- 段间直接调用(不介绍)
- 段间间接调用(不介绍)
2. 段内直接
名称 | call(段内直接调用指令) |
格式 | |
动作 | 把调用指令下一行指令地址压栈,然后转到LABEL处执行 |
注意 | 除了保存返回地址,其他同无条件转 |
3. 段内间接
名称 | call(段内间接调用指令) |
格式 | |
动作 | 把调用指令下一行指令地址压栈,然后OPDR内容送到EIP,转到OPDR给出偏移地址处执行 |
合法值 | OPDR:保护方式下,32位通用寄存器、双字存储单元 |
注意 | 除了保存返回地址,其他同无条件转 |
#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
可以看到
- 指针的本质就是地址
- 这里把函数入口和函数参数都放在堆栈,用堆栈传参。
- 注意传参时从ESP开始向高地址找参数,push参数的时候按从右到左的顺序
- 采用的是段内间接调用的方法
(2)返回指令
1、分类
- 按段内段间分
- 段内返回指令(对应段内调用)
- 段间返回指令(对应段间调用)(不介绍)
- 按返回时是否平衡堆栈
- 不带立即数的返回指令
- 带立即数的返回指令
2、 段内返回不带立即数
名称 | RET(段内返回不带立即数指令) |
格式 | |
动作 | 指令从堆栈弹出地址偏移,送到指令指针寄存器EIP,返回到call时压栈的返回地址处执行 |
3、 段内返回带立即数
名称 | RET(段内返回带立即数指令) |
格式 | |
动作 | 指令从堆栈弹出地址偏移(当然这也会影响esp),送到指令指针寄存器EIP,还额外把count 加到ESP |
注意 | 用于平衡堆栈 |
四、示例
- 以下是一个全汇编程序示例,它将十六进制数字符串转为数值(二进制),再转十进制输出查看,可以看一下函数调用的各种方法。
- 此程序是按8086机资源写的,在64位机器上运行此程序,需要:
- 保存以下代码为
.asm
文件 - 用nasm编译成
.com
文件 - 用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", '$' ;在这里写要转换的十六进制数