masm 16位汇编 函数原理
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了masm 16位汇编 函数原理相关的知识,希望对你有一定的参考价值。
本文讲述作者在学习16汇编下函数语法,以及加深了对c++对stdcall等语法本质的理解,以及函数调用带来损耗以及我们常说的线程上下文切换带来代价。本文需要读者有一定80486汇编基础基础知识.
函数
我们假设需要在汇编实现一个函数需要考虑如下:
- 函数的跳转传参
- 函数的返回地址
- 函数的返回值
- 上下文切换
我们首先实现一个简单的交换寄存器变量的函数.
data_seg segment
;定义一个双字,注意必须定义双字否则编译器可能视为段内跳转
g_szDst db "hello world$"
g_bLen db 0
data_seg ends
;定义一个栈段
data_seg segment stack
db 256 dup(02h)
data_seg ends
code segment
;定义了一个SWAP标签,用于交换cx和bx数值
;返回地址要求调用方存储栈段中
SWAP:
;ax作为临时变量
mov ax,cx
;将bx的数值赋值到cx
mov cx,bx
;将ax数值赋值到bx 从而完成bx和cx数值交换
mov bx,ax
;取出栈段返回值
pop ax
;返回
jmp ax
START:
assume ds:data_seg
mov ax,data_seg
mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
CLD
;将参数放入寄存器
mov bx,2
mov cx,3
;存储调用完后返回地址
lea dx,JMP_RET
push dx
;跳转到函数地址
jmp SWAP
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
对于这样的需求cpu自然提供了封装指令 call
,与ret
data_seg segment
;定义一个双字,注意必须定义双字否则编译器可能视为段内跳转
g_szDst db "hello world$"
g_bLen db 0
data_seg ends
data_seg segment stack
db 256 dup(02h)
data_seg ends
code segment
SWAP:
;ax作为临时变量
mov ax,cx
;将bx的数值赋值到cx
mov cx,bx
;将ax数值赋值到bx 从而完成bx和cx数值交换
mov bx,ax
; pop ax
; jmp ax
ret
START:
assume ds:data_seg
mov ax,data_seg
mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
CLD
mov bx,2
mov cx,3
; lea dx,JMP_RET
; push dx
; jmp SWAP
call SWAP
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
函数栈空间
我们在日常编写函数时,会有栈帧的概念,也就是一个函数会利用栈进行存储数据,在函数返回的时候会进行恢复操作。
我们看一个案例:
data_seg segment
;定义一个双字,注意必须定义双字否则编译器可能视为段内跳转
g_szDst db "hello world$"
g_bLen db 0
data_seg ends
data_seg segment stack
db 256 dup(02h)
data_seg ends
code segment
SWAP:
;申请栈空间,用来存放局部变量
;bp用来保存当前函数栈的栈底
push bp;存储当前pb数值 在返回后恢复
;bp存储当前函数栈的底部,用恢复栈,bp有称为栈帧指针
mov bp,sp
;给这个函数添加100h栈空间
sub sp,100h
;ax作为临时变量
mov ax,cx
;将bx的数值赋值到cx
mov cx,bx
;将ax数值赋值到bx 从而完成bx和cx数值交换
mov bx,ax
; pop ax
; jmp ax
;恢复栈 进行栈平衡
mov sp,bp
pop bp
ret
START:
assume ds:data_seg
mov ax,data_seg
mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
CLD
mov bx,2
mov cx,3
; lea dx,JMP_RET
; push dx
; jmp SWAP
call SWAP
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
函数跳转前SP BP数值如下图所示
我们函数返回时的BP和SP进行还原.
函数栈寄存器恢复
函数返回时必须恢复调用前寄存器的值,比如调用函数前bx为1,那么返回时必须bx还是为1.注意ax一般在16汇编中作为返回值
data_seg segment stack
g_bLen db 255 dup(0cch)
db 0
db 0
data_seg ends
code segment
ADD_FLAG:
;保存寄存器的原始值,方便返回时恢复
push bx
push cx
;申请栈空间,用来存放局部变量
;bp用来保存当前函数栈的栈底
push bp;存储当前pb数值 在返回后恢复
;bp存储当前函数栈的底部,用恢复栈,bp有称为栈帧指针
mov bp,sp
;给这个函数添加100h栈空间
sub sp,100h
;使用栈存储一个数据,这里纯粹作为演示
; push ax
;cx加bx
add bx,cx
;将bx的数值赋值到ax,ax作为返回值
mov ax,bx
; pop ax
; jmp ax
;恢复栈 进行栈平衡
mov sp,bp
;回复函数bp
pop bp
pop cx
pop bx
ret
START:
; assume ds:data_seg
; mov ax,data_seg
; mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov bx,2
mov cx,3
; lea dx,JMP_RET
; push dx
; jmp SWAP
call ADD_FLAG
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
调用前下面的bx
是2,cx
为3
函数返回后
传入参数
上面我利用寄存器传入参数,但是对于多个参数情况很明显是不够的,因此我会采用利用栈区.
我首先看下面示例代码(存在一定问题)
data_seg segment stack
g_bLen db 255 dup(0cch)
db 0
db 0
data_seg ends
code segment
ADD_FLAG:
;保存寄存器的原始值,方便返回时恢复
push bx
push cx
;申请栈空间,用来存放局部变量
;bp用来保存当前函数栈的栈底
push bp;存储当前pb数值 在返回后恢复
;bp存储当前函数栈的底部,用恢复栈,bp有称为栈帧指针
mov bp,sp
;给这个函数添加100h栈空间
sub sp,100h
;取出第一个参数
mov bx,[bp+4]
;取出第二个参数
mov cx,[bp+2]
;cx加bx
add bx,cx
;将bx的数值赋值到ax,ax作为返回值
mov ax,bx
; pop ax
; jmp ax
;恢复栈 进行栈平衡
mov sp,bp
;回复函数bp
pop bp
pop cx
pop bx
ret
START:
; assume ds:data_seg
; mov ax,data_seg
; mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov bx,2
push bx
mov cx,3
push cx
; lea dx,JMP_RET
; push dx
; jmp SWAP
call ADD_FLAG
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
跳转前
跳转后
其实一看没什么问题但是,SP
数值应该减少4 才是正确的答案,因为我利用栈压入了两个16数字,在函数返回时应当执行平衡栈操作。
这个参数栈的平衡可以函数返回时进行操作,也可以在返回后操作。
还记得c里面的__stdcall
和__cdec
吗?这就是区别!!
- 在函数返回时操作:
ret x
x必须为2的倍数,表示返回函数时清楚x个栈区
ADD_FLAG:
;保存寄存器的原始值,方便返回时恢复
push bx
push cx
;申请栈空间,用来存放局部变量
;bp用来保存当前函数栈的栈底
push bp;存储当前pb数值 在返回后恢复
;bp存储当前函数栈的底部,用恢复栈,bp有称为栈帧指针
mov bp,sp
;给这个函数添加100h栈空间
sub sp,100h
;取出第一个参数
mov bx,[bp+4]
;取出第二个参数
mov cx,[bp+2]
;cx加bx
add bx,cx
;将bx的数值赋值到ax,ax作为返回值
mov ax,bx
; pop ax
; jmp ax
;恢复栈 进行栈平衡
mov sp,bp
;回复函数bp
pop bp
pop cx
pop bx
ret 4;相当于pop ip,add sp,4
- 在函数返回后操作:
自行操作sp即可
add sp,4
略
跨段调用
在前面的案例中我们的函数调用都是在一个段中,所以只需要存储ip寄存器即可,但是跨段必须存储cs与ip。
段内调用的情况:
在执行后sp减少了2用于存储ip地址
我们看看跨段调用的SP差别:
可以看到SP减少了证明有一部分用于存储cs另一部分存储ip。
在8086
中先存储cs
在存储ip
跨段调用相关语法
;跨段函数声明
ADD_FLAG
retf 4
;调用跨段函数ADD_FLAG
call far ptr ADD_FLAG
masn 函数语法
函数名 proc [距离][调用约定][uses reg1 reg2 reg3...][参数:word,参数名:word]
local 变量:word
ret
函数名 endp
TestProc Proc far stdcall uses bx dx si di arg:word
local btVal:byte
retf
TestProc ENDP
1. 参数:[距离]:
关键字 | 说明 |
---|---|
near | 函数只能段内调用 函数使用 ret 返回 调用时 ip 入栈 |
far | 段内段间都可以调用 使用retf 返回 函数使用 retf 返回 调用时都是 ip 和cs 入栈 |
不写默认为near
我们看一个far的案例
;arg1表示参数
Func PROC far
ret
Func ENDP
START:
;assume ds:data_seg
;mov ax,data_seg
;mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov ax,2;
mov ax,3;
call Func
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
我们将far改为near
mystack segment stack
db 255 dup(0ch)
mystack ends
;stdcall arg1:word
code segment
;arg1表示参数
Func PROC near
ret
Func ENDP
START:
;assume ds:data_seg
;mov ax,data_seg
;mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov ax,2;
mov ax,3;
call Func
JMP_RET:
;停止输出
mov ax,4c00h
int 21h
LABLE1:
mov ax,ax
code ends
end START
2. 参数:[调用约定]:
关键字 | 说明 |
---|---|
c | 调用方平栈 |
stdcall | 被调用方平栈 |
c
调用:
mystack segment stack
db 255 dup(0ch)
mystack ends
code segment
Func PROC far c arg:word,arg2:word
retf
Func ENDP
START:
;传入参数
invoke Func,2,3
LABLE1:
mov ax,ax
code ends
end START
stdcall
:
mystack segment stack
db 255 dup(0ch)
mystack ends
;stdcall arg1:word
code segment
;far远跳 c调用方清理栈 arg1表示参数
Func PROC far stdcall arg1:word
ret
Func ENDP
START:
;assume ds:data_seg
;mov ax,data_seg
;mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov ax,2;
mov ax,3;
call Func
;由于使用c调用方式,因此返回后自行清理调用栈的arg1
add sp,2
JMP_RET:
LABLE1:
mov ax,ax
code ends
end START
3. 参数:[uses reg1 reg2 reg3 ]:
用于声明使用到的寄存器,可以在函数返回时还原寄存器
4. 局部变量:
类型 | 局部变量类型 | 备注 |
---|---|---|
db | byte | 可以直接赋值使用 |
dw | word | 可以直接赋值使用 |
dd | dword | 不可以直接赋值使用 |
dq | qword | 不可以直接赋值使用 |
dt | tbyte | 不可以直接赋值使用 |
mystack segment stack
db 255 dup(0ch)
mystack ends
code segment
;far远跳 c调用方清理栈 arg1表示参数
Func PROC far stdcall arg:word,arg2:word
local @btval:byte
local @wval:word
local @dwVal:dword
local @qwVal:qword
local @tbVal:tbyte
; mov si,arg1
; mov di,arg2
; mov ax,[si]
; mov bx,[di]
; mov [si],bx
; mov [di],ax
mov @btval,1
mov @wval,1
;下面的代码不可以直接赋值
; mov @dwVal,1
; mov @qwVal,1
; mov @tbVal,1
Func ENDP
START:
;assume ds:data_seg
;mov ax,data_seg
;mov ds,ax
;设置标志寄存器,目的让lodsb执行后自动si+1
; CLD
mov ax,2;
mov ax,3;
call Func
JMP_RET:
LABLE1:
mov ax,ax
code ends
end START
传入参数
mystack segment stack
db 255 dup(0ch)
mystack ends
code segment
Func PROC far stdcall arg:word,arg2:word
retf
Func ENDP
START:
;传入参数
invoke Func,2,3
LABLE1:
mov ax,ax
code ends
end START
另外有一个小技巧 invoke指令可以配合addr指令完成传入局部变量地址的作用
mystack segment stack
db 255 dup(0ch)
mystack ends
code segment
Func2 PROC far stdcall arg:word,arg2:word
Func2 ENDP
;far远跳 c调用方清理栈 arg1表示参数
Func PROC far stdcall arg:word,arg2:word
local @btval:byte
invoke Func2,addr @btval,2
Func ENDP
START:
invoke Func,2,3
LABLE1:
mov ax,ax
code ends
end START
函数声明
我们可以先声明某个函数然后在之后进行实现,但是声明不需要说明使用的寄存器(因为函数内部处理)
;进行函数声明
Func2 proto far stdcall arg:word,arg2:word
code segment
;far远跳 c调用方清理栈 arg1表示参数
Func PROC far stdcall arg:word,arg2:word
local @btval:byte
invoke Func2,addr @btval,2
Func ENDP
START:
invoke Func,2,3
LABLE1:
mov ax,ax
Func2 PROC far stdcall arg:word,arg2:word
Func2 ENDP
code ends
end START
以上是关于masm 16位汇编 函数原理的主要内容,如果未能解决你的问题,请参考以下文章