函数的调用
一、函数调用
(一)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
;我现在想着返回一个2的3次方
match:
;计算出2的3次方数值,然后方到ax寄存器中
mov ax, 2
add ax,ax
add ax,ax
ret //记着一定要ret出去
;对应的方法二
match2:
;计算出2的3次方数值,然后方到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] ;将3344和1122相加
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] ;将3344和1122相加
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] ;将3344和1122相加
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, 参数所占空间)