masm 16位汇编 函数原理

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了masm 16位汇编 函数原理相关的知识,希望对你有一定的参考价值。

本文讲述作者在学习16汇编下函数语法,以及加深了对c++对stdcall等语法本质的理解,以及函数调用带来损耗以及我们常说的线程上下文切换带来代价。本文需要读者有一定80486汇编基础基础知识.

函数

我们假设需要在汇编实现一个函数需要考虑如下:

  1. 函数的跳转传参
  2. 函数的返回地址
  3. 函数的返回值
  4. 上下文切换

我们首先实现一个简单的交换寄存器变量的函数.


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吗?这就是区别!!

  1. 在函数返回时操作:
    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

  1. 在函数返回后操作:
    自行操作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返回
调用时都是ipcs入栈

不写默认为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. 局部变量:

类型局部变量类型备注
dbbyte可以直接赋值使用
dwword可以直接赋值使用
dddword不可以直接赋值使用
dqqword不可以直接赋值使用
dttbyte不可以直接赋值使用

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位汇编 函数原理的主要内容,如果未能解决你的问题,请参考以下文章

16位汇编 多文件 intel汇编 编译器masm5.0 调用子程序库即静态库的自定义函数 WINDOWS

MASM 32位汇编 32与16汇编区别

MASM 32位汇编 32与16汇编区别

用sublime3编写运行16位汇编程序_详细教程

masm 16位汇编语法

masm 16位汇编语法