文章目录
一.函数调用和函数正常退出—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 函数平衡栈方法
- 外平栈 -在函数调用结束后平衡栈
push 5566h
push 7788h
call mathFunc3
;栈平衡:函数调用前后 栈顶指针地址要一致 在高级语言中 函数调用结束后,栈平衡自己做
add sp,4
- 内平栈 -在函数内部平衡栈 优点函数调用者不用关注栈平衡的问题
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 调用流程:
- 函数参数入栈
- 函数调用结束,下条指令偏移地址入栈
- 保护bp值 psuh bp 入栈
- 开辟局部变量空间 并填充CC
- 保护其他寄存器的值,其他寄存器的值入栈
- 业务逻辑
- 其他寄存器的值出栈
- 恢复sp bp
- 调用结束 恢复栈平衡
4.3 调用流程:
- 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- 就可以访问局部变量和参数。快速高效。