汇编语言基础(二)

函数的调用


一、函数调用

(一)call和ret指令

call指令: 相当于用来调用封装的方法
ret指令: 相当于retun指令

例子:
以下代码是输出一个Hello

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0)  //创建了100个0当做栈段
stack ends

;数据段
data segment
	db 100 dup(0)
	string db 'Hello!$'  //定义一个字符串,起个名字叫 string
data ends

;代码段
code segment
start:
	;手动赋值 ds,ss
	mov ax, stack
	mov ss, ax
	mov ax, data
	mov ds, ax
	;业务逻辑
	
	;ds:dx 在内存中找到字符串
	mov dx, offset string
	;输出字符串
	mov ax, 9h
	int 21h

	;代码退出
	mov ax, 4c00h
	int 21h
code ends
end start

现在我需要输出3个Hello(需要把输出封装一下,然后调用)

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0)  //创建了100个0当做栈段
stack ends

;数据段
data segment
	db 100 dup(0)
	string db 'Hello!$'  //定义一个字符串,起个名字叫 string
data ends

;代码段
code segment
start:
	;手动赋值 ds,ss
	mov ax, stack
	mov ss, ax
	mov ax, data
	mov ds, ax
	;业务逻辑
	call print    //调用输出语句封装的方法

	;代码退出
	mov ax, 4c00h
	int 21h
	
;把字符串封装成以下形式
print: 
	mov dx, offset string
	mov ax, 9h
	int 21h	
	ret   //记着一定要ret出去
code ends
end start

(二)call和ret原理解析

call就是将下一条指令的偏移地址入栈,然后再跳转到对应的代码处
ret:将栈顶的值出栈,赋值给IP,IP就是偏移地址CS:IP
所以ret以后,cpu还能找到继续执行的指令

注意:既然是ret是让栈顶的值出栈赋值给IP,那就不能在封装的方法中,做入栈操作(push ax),否则将会修改栈顶的值

(三)函数的返回值

思路:
1.最常用:
首先要计算出23值,然后方到寄存器ax中,在ret返回
然后回到业务逻辑这,可以直接接收用bx寄存器(mov bx, ax)
2.
在封装的方法中: mov [0], ax 把值放到ds:[0] 内存的数据段中
在业务逻辑这接收: mov bx, [0] 把值从ds:[0]中取出来放到 bx

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0)  //创建了100个0当做栈段
stack ends

;数据段
data segment
	db 100 dup(0)
	string db 'Hello!$'  //定义一个字符串,起个名字叫 string
data ends

;代码段
code segment
start:
	;手动赋值 ds,ss
	mov ax, stack
	mov ss, ax
	mov ax, data
	mov ds, ax
	;业务逻辑
	call match    
	mov bx, ax    //直接取ax的值,放到bx中就行  方式一
	
	call match1   //方法二
	mov bx, [0]   //其实是将内存地址ds:[0]中值取出放到 bx中了
	;代码退出
	mov ax, 4c00h
	int 21h
	
;我现在想着返回一个23次方
match: 
	;计算出23次方数值,然后方到ax寄存器中
	mov ax, 2
	add ax,ax
	add ax,ax
	ret   //记着一定要ret出去
	
;对应的方法二
match2: 
	;计算出23次方数值,然后方到ax寄存器中
	mov ax, 2
	add ax,ax
	add ax,ax
	mov [0],ax //其实是将ax的值,放到内存地址为ds:[0]的内存中了
	ret   //记着一定要ret出去
code ends
end start

(四)函数传参

例如:我想着把10和20相加
方法1:参考返回值,在代码段把10放到cx寄存器(mov cx, 10),把20放到dx寄存器(mov dx, 20),在封装方法中把cx和dx相加(mov ax,cx ; add ax,dx)

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0)  //创建了100个0当做栈段
stack ends

;数据段
data segment
	db 100 dup(0)
	string db 'Hello!$'  //定义一个字符串,起个名字叫 string
data ends

;代码段
code segment
start:
	;手动赋值 ds,ss
	mov ax, stack
	mov ss, ax
	mov ax, data
	mov ds, ax
	;业务逻辑
	mov cx,10
	mov dx,20
	call sum    
	mov bx, ax    //直接取ax的值,放到bx中就行
	;代码退出
	mov ax, 4c00h
	int 21h
	
;返回值一般放到ax寄存器中
;传参2(分别放到cx, dx寄存器中)
sum:
	mov ax, cx
	add ax, dx
	ret
	
code ends
end start

方法2:
对于多个参数,一般是通过栈来传参数,在代码段把参数入栈,在封装方法中,取出。
这个取出需要注意,并不是pop取出,不能移动SP指针,而是通过SP+2这样来取值

注意: 由于系统会把调用封装方法的下一行指令的偏移地址放到栈顶,ret的时候在从栈顶取出
所以在封装的函数中,不能改变SP指针的位置

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0) //创建100个0当做栈段等着别人来用
stack ends

;数据段
data segment
	db 100 dup(0) 
data ends

;代码段
code segment
start:
	;手动赋值ss
	mov ax, stack
	mov ss, ax
	;手动赋值ds
	mov ax, data
	mov ds, ax
	//业务逻辑处理
	;入栈2个参数
	push 1122h
	push 3344h
	call sum
	add sp,4 ;涉及到栈平衡
	
	mov bx, ax
	mov ax, 4c00h //退出
	int 21h
code ends
;返回值放到ax中
;传参
sum:
	;入栈以后如图1,但是可不能pop取值,如果pop ax,指针sp就会往下移,ret就会出错
	;所以
	mov bp, sp  ;把sp指针给bp,bp是辅助sp指针
	mov ax, ss:[bp+2]   ;通过ss:[0] ,栈:偏移地址取出来,类似ds:[0]
	add ax, ss:[bp+4]
	ret
end start

在这里插入图片描述

二、栈平衡

栈平衡: 简单总结 栈使用之前和使用之后,sp指针要保持一样

(一)栈平衡原理分析

原理分析:
如上图1所示,这是当你调用1 个函数(call sum)以后栈地址的分布。如果此时再调用第二个函数的时候,sp就会从40000H开始,继续往上移动,而不是从栈顶了,如图2
在这里插入图片描述

当在调用第三次的时候,就会又累加,因此造成栈空间浪费,调用多了造成栈溢出。
因此需要解决这种情况。如何解决????
只需要将sp+4就可以解决,例如图1,第一个函数结束以后,就add sp, 4(指针就回到栈顶了)
这样就可以将原来的数据覆盖了,从而实现了内存销毁,保持栈平衡!

1.外平衡

把add sp,4 放到函数调用结束,就是外平衡!!!

push 1122h
push 3344h
call sum
add sp,4 ;涉及到栈平衡

2.内平衡

在函数返回值ret中,ret 4,这是内平衡!

sum:
	mov bp, sp  ;把sp指针给bp,bp是辅助sp指针
	mov ax, ss:[bp+2]   ;通过ss:[0] ,栈:偏移地址取出来,类似ds:[0]
	add ax, ss:[bp+4]
	ret   4

注意 ret 4 还有add sp,4 这个4可不是乱写的,是根据你入栈的空间来的,你占用多少,你就加回去多少

(二)函数递归

函数递归也会造成栈溢出,函数的递归是发生在函数内部,函数并没有结束,无法将sp指针移动
如下错误代码,会造成递归

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0) //创建100个0当做栈段等着别人来用
stack ends

;数据段
data segment
	db 100 dup(0) 
data ends

;代码段
code segment
start:
	;手动赋值ss
	mov ax, stack
	mov ss, ax
	;手动赋值ds
	mov ax, data
	mov ds, ax
	//业务逻辑处理
	;入栈2个参数
	push 1122h
	push 3344h
	call sum
	add sp,4 ;涉及到栈平衡
	
	mov bx, ax
	mov ax, 4c00h //退出
	int 21h
code ends
;返回值放到ax中
;传参
sum:
	;入栈以后如图1,但是可不能pop取值,如果pop ax,指针sp就会往下移,ret就会出错
	;所以
	mov bp, sp  ;把sp指针给bp,bp是辅助sp指针
	mov ax, ss:[bp+2]   ;通过ss:[0] ,栈:偏移地址取出来,类似ds:[0]
	add ax, ss:[bp+4]
	call sum ;在次调用,这样造成了循环
	ret
end start

(三)函数中调用函数与函数结束调用函数比较

问题:
在封装的函数1中,调用函数2,在函数2中调用涵数3,…这样效率高 ???
还是在函数1调用结束以后,调用函数2,函数2结束调用函数3,…这样效率高?
分析:
在函数1中调用函数2,此时函数1并没有结束,就调用了函数2,因此,栈空间开辟了函数1和2的。
同理,在函数2还未结束的时候就调用函数3,此时栈空间继续开辟,以此类推,调用的函数越多,栈空间占用的越多,直到最后一个函数调用结束,才能依次销毁,最终返回栈顶!
而,在函数1调用结束以后,在调用函数2,栈空间就不会依次累加,2结束以后调用3,也不会累加!!!
结论:
函数调用结束以后在调用函数 > 函数内调用函数

(四)函数的约定

__cdecl :参数从右至左入栈,是外平衡
sum (a, b)
push b
push a
add sp, 4

__stdcall: 参数从右至左入栈,是内平衡
sum (a, b)
push b
push a
ret 4

__fastcall: 优先使用寄存器传参,寄存器不够用,在用push从右至左,并且是内平衡
sum (a, b, c, d, e, f)
mov bx ,a
mov cx, b
mov dx, c
push f
push e
push d
ret 6
Xcode使用的就是__fastcall

总结:函数调用的本质

1.参数: push参数值
2.返回值: 一般都放在ax寄存器中
1.栈平衡: 保持栈平衡

三、函数的局部变量

例如:一段函数如下,分析其中的汇编流程

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 (10, 2)
	return 0;
}

转换汇编流程:

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0) //创建100个0当做栈段等着别人来用
stack ends

;数据段
data segment
	db 100 dup(0) 
data ends

;代码段
code segment
start:
	;手动赋值ss
	mov ax, stack
	mov ss, ax
	;手动赋值ds
	mov ax, data
	mov ds, ax
	//业务逻辑处理
	;入栈2个参数
	push 1122h
	push 3344h
	call sum
	add sp,4 ;涉及到栈平衡
	
	mov bx, ax
	mov ax, 4c00h //退出
	int 21h
code ends
;返回值放到ax中
;传参
sum:
	mov bp,sp ;先把sp的值赋值给bp
	sub sp,10 ;在给sp-10,向上拉伸10个字节空间,用来放局部变量
	;定义2个局部变量
	mov word prt ss:[bp-2],3
	mov word ptr ss:[bp-4],4
	mov ax, ss:[bp-2] ;3取出来给了ax
	add ax, ss:[bp-4] ;ax在和4相加给了ax
	mov word ptr ss:[bp-6],ax ;在把值继续存入bp-6中
	
	mov ax, ss:[bp+2] ;取出参数3344
	add ax, ss:[bp+4] ;33441122相加
	add ax, ss:[bp-6]  ;取出局部变量的结果7,在相加
	;恢复sp
	mov sp, bp
	ret
end start

(一)局部变量流程分析

在这里插入图片描述

(二)bp指针无法恢复问题

但是,如果在函数中再次调用另一段函数,会出现什么问题?
在这里插入图片描述

(三)保护bp指针

因此我们需要把bp的值进行保护起来!
在这里插入图片描述
保护了bp寄存器需要注意一下,取参数的时候就不是ss:[bp+2]了,就是ss:[bp+4] 和 ss:[bp+6] 了

‘bp-’ 往上走肯定是局部变量!
‘bp+’ 往下走肯定是参数!

(四)保护其它寄存器

在函数当中,有的时候可能还会用到其它的通用寄存器,例如:si di bx寄存器,因此需要把他们原有的值保护起来
保护方式同样和bp一样。

assume cs:code, ds:data, ss:stack
;栈段
stack segment
	db 100 dup(0) //创建100个0当做栈段等着别人来用
stack ends

;数据段
data segment
	db 100 dup(0) 
data ends

;代码段
code segment
start:
	;手动赋值ss
	mov ax, stack
	mov ss, ax
	;手动赋值ds
	mov ax, data
	mov ds, ax
	//业务逻辑处理
	;入栈2个参数
	push 1122h
	push 3344h
	call sum
	add sp,4 ;涉及到栈平衡
	
	mov bx, ax
	mov ax, 4c00h //退出
	int 21h
code ends
;返回值放到ax中
;传参
sum:
	push bp  ;把原有bp的值保护起来
	mov bp,sp ;在把sp的值赋值给bp
	sub sp,10 ;在给sp-10,向上拉伸10个字节空间,用来放局部变量
	;保护其它寄存器
	push si
	push di
	push bx

	;业务逻辑了----定义2个局部变量
	mov word prt ss:[bp-2],3
	mov word ptr ss:[bp-4],4
	mov ax, ss:[bp-2] ;3取出来给了ax
	add ax, ss:[bp-4] ;ax在和4相加给了ax
	mov word ptr ss:[bp-6],ax ;在把值继续存入bp-6中
	
	mov ax, ss:[bp+4] ;取出参数3344 
	add ax, ss:[bp+6] ;33441122相加
	add ax, ss:[bp-6]  ;取出局部变量的结果7,在相加

	;恢复其它寄存器,注意恢复其它寄存器的顺序
	pop bx
	pop di
	pop si
	;恢复sp
	mov sp, bp
	;恢复bp
	pop bp
	
	ret
end start

用图片区域来表示栈空间分布情况:
在这里插入图片描述

(五)保护局部变量的空间

如果说你拉伸一段栈空间,(sub sp,10), 正好sp指针所指的位置是一个退出函数,那么程序就会出问题。
因为你永远不知道你拉伸的空间内之前存在什么指令。因此像这种不确定的,我们需要填充一堆数据来覆盖它。
在windows中填充的是 int 3 (CCCC)指令,也就是一堆断点指令。

;只从sum函数写起
sum:
	push bp  ;把原有bp的值保护起来
	mov bp,sp ;在把sp的值赋值给bp
	sub sp,10 ;在给sp-10,向上拉伸10个字节空间,用来放局部变量
	
	;保护其它寄存器
	push si
	push di
	push bx
	
	;给局部变量填充int 3(cccc)指令
	;stosw的作用,将ax的值拷贝到es:di中,同时di的值会+2
	mov ax, 0cccch
	;让es=ss,不能这样写mov es,ss ,必须通过一个中间变量
	mov bx, ss
	mov es, bx
	;让di-10,和也就是整个局部变量的最上面(一开始不是拉伸了10个空间吗)
	;因为每次复制di都会+2,
	mov di,bp
	sub di,10 ;
	;cx觉得这个复制,执行几次,
	mov cx,5 ;执行5次整个di+10了就,di就会回到bp的位置
	rep stosw
	;rep 的作用是重复执行某个命令(执行此时是由cx决定)


	;业务逻辑了----定义2个局部变量
	mov word prt ss:[bp-2],3
	mov word ptr ss:[bp-4],4
	mov ax, ss:[bp-2] ;3取出来给了ax
	add ax, ss:[bp-4] ;ax在和4相加给了ax
	mov word ptr ss:[bp-6],ax ;在把值继续存入bp-6中
	
	mov ax, ss:[bp+4] ;取出参数3344 
	add ax, ss:[bp+6] ;33441122相加
	add ax, ss:[bp-6]  ;取出局部变量的结果7,在相加

	;恢复其它寄存器,注意恢复其它寄存器的顺序
	pop bx
	pop di
	pop si
	;恢复sp
	mov sp, bp
	;恢复bp
	pop bp

总结:函数调用本质(内存)

1. push参数
2. push函数返回地址
3. push bp (保留之前bp的值)
4. mov bp, sp (保留sp之前的值,方便以后恢复)
5. sub sp,空间大小 (拉伸空间给局部变量)
6. 保护可能用到其它的寄存器
7. 使用cc(int 3)填充局部变量空间
8. ----------------执行业务逻辑----------
9. 恢复其它寄存器之前的值
10. mov sp, bp (恢复sp)
11. pop bp (恢复bp)
12. ret (将函数的返回值出栈,执行下一条指令)
13. 恢复栈平衡 (add sp, 参数所占空间)

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值