02 汇编语言的函数

本文详细介绍了汇编语言中函数的调用和退出机制,包括call和ret指令,函数返回值和传参的方式,以及如何通过平衡栈来管理函数参数和局部变量。同时讨论了函数调用的完整流程,强调了保护bp寄存器的重要性以及为何要填充局部空间为CCCC。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.函数调用和函数正常退出—call和ret指令

  • call
    • 下一条指令的偏移地址入栈
    • 转到标号处执行指令

在这里插入图片描述
在这里插入图片描述

  • ret 将栈顶的值出栈,赋值给ip
    在这里插入图片描述
;函数调用打印hello
      assume ds:data,ss,stack,cs:code
       ;-------1 数据段--------          
       data segment
        string   db 'hello$'
        data ends
      
       ;-------2.栈段--------   
              
			stack segment
				db 100 dup(2)			
			stack ends
        ;-------3. 代码段--------     
     
    code  segment                            
        start:                                     
             mov ax,data
             mov ds,ax  
             
             mov ax,stack
             mov ss,ax
              ;函数调用
            call print 
             mov ah,4cH
             int 21h          
     
      print:
        mov dx,string offset  
        mov ah,9h
        int 21h   
        ;函数返回
      ret
     
        code ends
       end start

二.函数的返回值和传参

2.1 函数返回值
  • 默认:存储在ax寄存器
  • (不推荐) 存在在数据段 读取没有寄存器快
2.2.传参
  • 方式一: 使用通用寄存器传参数,优点快。在寄存器不够用的情况下,剩余参数会使用栈来代替
  • 方式二(不推荐):存储在数据段中,再从数据段取值.缺点:函数使用完毕参数还存在数据段中,其他函数依旧可以访问
  • 方式三(推荐):使用栈 调用完毕后 做一次栈平衡操作,即可保证函数参数只有当前函数可用。
; 函数返回值+函数传参  
assume cs:code,ss:stack,ds:data
 ;1.数据段     
 data segment
         db 100 dup(8)  
 data ends
      
 ;2.栈段 
 stack segment     
    db 100 dup(9)     
 stack ends 
      
;3.代码段
code segment        
        start:  
   ;手动设置ds ss的值
   mov ax,data
   mov ds,ax
   mov ax,stack
   mov ss,ax   
   
   ;传参方式一:使用cx,dx 传参                
   mov cx,11h
   mov dx,22h              
   call mathFunc1 
   
  ; 传参方式二:使用数据段传参数
    mov word ptr[0],1122h
    mov word ptr[2],3344h 
    call mathFunc2  
        
    ;传参方式三:使用栈
    push 5566h
    push 7788h 
    call mathFunc3 
    ;栈平衡:函数调用前后 栈顶指针地址要一致 在高级语言中 函数调用结束后,栈平衡自己做 
    add sp,4 
      
     
    ;返回值方式一:不建议将返回值存储在内存中再从内存中取值  mov bx,[0]        
    mov bx,ax
     
    
     mov ah,4cH       
     int 21h 
     
     ;函数传参方式一: 约定两个寄存器通用。例如 将参数传入cx dx中。
      mathFunc1:    
      mov ax,cx
      mov bx,dx 
      
      ;返回值方式二:返回值存在ax寄存器。调用完函数后从寄存器中取值就可以拿到函数的返回值
      ;不能存储在栈中,因为栈顶元素是函数调用结束后下条指令的地址,返回值入栈导致栈顶元素不是下条指令的地址。
      add ax,bx  
      ;不建议存储的内存中,寄存器访问速度比内存块 
      ;mov [0],ax
      ret                    
         
         
     ;函数传参方式二:将值存储到数据段中,再从数据段中取值.缺点是全局变量,函数使用完毕后参数还存储在数据段中,其他函数依旧可以访问  
      mathFunc2: 
      mov ax,word ptr[0]
      mov bx,word ptr[2]             
      add ax,bx  
      ret 
      
     ;函数传参方式三:使用栈来传参数,缺点只有栈顶出栈了。参数还在栈中,久而久之栈中的垃圾数据越来越多 会导致栈越界
     ;怎么破:sp+4 
      mathFunc3:
      mov bp,sp 
      mov ax,ss:[bp+2]                                                                         
      add ax,ss:[bp+4]
      ret        
 code ends
 end start                                        

  • 为什么函数内部自己调自己会导致栈溢出?

函数自己调自己时,栈中的参数一致在累积。一直没有恢复栈平衡。久而久之 栈空间就会溢出。

2.3 函数平衡栈方法
  1. 外平栈 -在函数调用结束后平衡栈
    push 5566h
    push 7788h 
    call mathFunc3 
    ;栈平衡:函数调用前后 栈顶指针地址要一致 在高级语言中 函数调用结束后,栈平衡自己做 
    add sp,4 
  1. 内平栈 -在函数内部平衡栈 优点函数调用者不用关注栈平衡的问题
   mathFunc3:
      mov bp,sp 
      mov ax,ss:[bp+2]                                                                         
      add ax,ss:[bp+4]
      ; 相当于 add sp,4
      ret 4
2.4 c/c++函数平衡栈的方式
  • __cdecl:外平栈 参数从右到左入栈

在这里插入图片描述

  • __stdcall:内平栈 参数从右到左入栈

在这里插入图片描述

  • __fastcall:内平栈 cx dx 分别传递前两个参数 其他参数从右到左入栈。传参数最快的一种方式

在这里插入图片描述

三.函数的局部变量

  • mov bp,sp
  • 在栈中给函数的局部变量开辟指定大小的空间。sub sp,10
  • sp指向局部元素的栈顶
  • 使用sp向局部变量的空间存值

在这里插入图片描述

#include "stdafx.h"
int sum(int a,int b){
       int c=3;
	   int d=4;
	   int e=c+d;
     return a+b+e;
}
int main(int argc, char* argv[])
{

	 sum(1,2);
	 return 0;
}
    ;参数局部变量求和
  
  assume cs:code,ss:stack,ds:data
  ;1.数据段
  data segment
    
    
  data ends
    ;2.栈段
    
  stack segment
         db 100 bup(9) 
  stack ends 
    
    
    ;3.代码段             
  
  code segment
        start:
        
        ;3.1 ss ds 赋值
        
        mov ax,data
        mov ds,ax
        
        mov ax,stack
        mov ss,ax 
        
         push 2h
         push 1h
        call sum
        ;栈平衡
        add sp,4
        
sum:
   ;如果牵扯到函数调函数  
   push bp
   mov bp,sp
   ; 预留10个字节的空间给局部变量 
   ;为什么sp-10? 
   ;如果不给sp-10 直接使用bp存储局部变量,当在函数中调用函数时,
   ;会将函数地址直接入栈,导致定义局部变量的值给覆盖掉
   sub sp,10
   
   ;定义两个局部变量 3,4
   mov word ptr ss:[bp-2], 3
   mov word ptr ss:[bp-4], 4 
   ;bp+ 取参数 bp- 取局部变量
   mov ax,word ptr ss:[bp-2]
   add ax,word ptr ss:[bp-4]
    ;局部变量e=c+d
   mov word ptr ss:[bp-6],ax
     
    ;执行加法
    mov ax,ss:[bp+4]
    add ax,ss:[bp+6]
    add ax,ss:[bp-6]
       
   ;恢复sp 
   mov sp,bp 
   ;恢复bp 
   pop bp  
   ret  
     
  code ends                 
              
        end start        
          
  • 为什么要保护bp?
    牵扯到函数调用新函数时,如果不保护bp,bp就回不到上一个函数的位置

在这里插入图片描述

在这里插入图片描述

四.函数的调用完整流程(内存)

4.1 内存空间结构图:

在这里插入图片描述

4.2 调用流程:
  1. 函数参数入栈
  2. 函数调用结束,下条指令偏移地址入栈
  3. 保护bp值 psuh bp 入栈
  4. 开辟局部变量空间 并填充CC
  5. 保护其他寄存器的值,其他寄存器的值入栈
  6. 业务逻辑
  7. 其他寄存器的值出栈
  8. 恢复sp bp
  9. 调用结束 恢复栈平衡
4.3 调用流程:
  1. C++
#include "stdafx.h"
int sum(int a,int b){
       int c=3;
	   int d=4;
	   int e=c+d;
     return a+b+e;
}
int main(int argc, char* argv[])
{
	 sum(1,2);
	 return 0;
}


2.汇编代码

    ;参数局部变量求和
  
  assume cs:code,ss:stack,ds:data
  ;1.数据段
  data segment
    
    
  data ends
    ;2.栈段
    
  stack segment
         db 100 bup(9) 
  stack ends 
    
    
    ;3.代码段             
  
  code segment
        start:
        
        ;3.1 ss ds 赋值
        
        mov ax,data
        mov ds,ax
        
        mov ax,stack
        mov ss,ax 
             
          push 2
          push 1
          call sum  
          add sp,4
           
          mov ah,4cH
          int 21h   
             
       
 ;函数
 ;传入两个参数放入栈中       
sum:
    ;1.保护bp
    push bp
    mov bp,sp 
    ;2.开辟局部变量空间 开辟的局部变量空间大小大于变量的字节数
    sub sp,10
   
    ;3 保护可能会用到的寄存器的值
    
    push bx
    push si
    push di
    ;4. 局部变量空降 填充CCCC 
         ; 4.1  stosw的作用:将ax的值拷贝到es:di的地址中,同时di的值会+2
 
         mov ax,0cccch
         
         mov bx,ss
         mov es,bx
         
         mov di,bp
         ;di在局部变量的地址最小区域
         sub di,10    
         mov cx,5
         ;重复执行某个指令(执行次数由cx决定)
         rep stosw
         
         
    ;4.局部量赋值
    mov word ptr[bp-2],3
    mov word ptr[bp-4],4
    ;e=c+d
    mov ax,ss:[bp-2]
    add ax,ss:[bp-4]
    mov word ptr[bp-6],ax 
     ;a+b+c   bp加是参数 bp- 是局部变量
    add ax,ss:[bp+4]
    add ax,ss:[bp+6]
    
    ;  恢复寄存器的值
    pop bx
    pop di
    pop si
    
    
    mov sp,bp
    pop bp
 
   ret  
      
  code ends                 
    end start        

4.4 调用流程的答疑
  • 为什么要保护其他寄存器的值 ?

业务逻辑可能会改变通用寄存器的值,为了代码的安全,函数调用前后要保证寄存器的值一致。

  • 为什么要给局部空间填充CCCC ?

int3->cc->断点调试 防止局部空间的垃圾数值影响程序正常运行。开辟的局部空间可能残留其他函数使用过的垃圾数据。防止指针指到栈空间的垃圾数据,导致一些危险的操作(删除文件/关机等操作)

  • 为什么 保护其他寄存器值在局部变量之后呢 ?

为了快速的访问局部变量和参数。bp 的值指向返回地址 ,bp+ bp- 就可以访问局部变量和参数。快速高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值