【汇编】汇编系列六 - 函数的调用过程

探究函数的调用过程。

一、call、ret指令

call 标号:将下一条指令的偏移地址入栈后,转到标号处执行指令。

ret:将栈顶的值出栈,赋值给ip。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
assume cs:code, ds:data, ss:stack

; 数据段
data segment
string "Hello World!$"
date ends

; 代码段
code segment
start: ; 标号
; 手动设置ds,ss的值
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 业务逻辑
call print
; call执行完成后会继续执行下面的代码(需要ret配合)
mov ax, 1122h
mov bx, 3344h
add ax, bx
; 退出
mov ax, 4c00h
int 21h
print: ; 标号
; ds:dx 告知字符串地址
mov dx, offset string
; 显示字符串
mov ah, 9h
int 21h
; 如果不写ret,程序就会终止
ret
code ends

end start

call和ret联合使用的作用其实就是高级语言中的函数调用(函数的本质)。

二、函数

函数的返回值其实是直接放到通用寄存器ax中的(也可以直接放在数据段中,但大部分情况下都是使用通用寄存器)。

2.1. 函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
assume cs:code, ds:data, ss:stack

; 数据段
data segment
string "Hello World!$"
date ends

; 代码段
code segment
start:
; 手动设置ds,ss的值
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 业务逻辑
call mathFunc
; call执行完成后会继续执行下面的代码(需要ret配合),此时ax就是mathFunc函数中的计算结果
mov bx, ax
; 退出
mov ax, 4c00h
int 21h
mathFunc: ; 计算2的3次方
mov ax, 2
add ax, ax
add ax, ax
; 此时计算结果放在ax寄存器中
ret
code ends

end start

2.2. 函数参数(栈平衡)

函数的参数也可以通过寄存器进行传递(使用寄存器速度会更快),但是寄存器不够的情况下怎么办?使用栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
assume cs:code, ds:data, ss:stack

; 数据段
data segment
string "Hello World!$"
date ends

; 代码段
code segment
start:
; 手动设置ds,ss的值
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 业务逻辑
push 1122h
push 3344h
call sum
; 此处要把之前入栈的参数从栈中移除,否则后续的操作可能会引起栈顶超出,这个操作也叫做栈平衡(函数调用前后的栈顶指针要一致)
; 把栈顶指针移动到参数入栈前的位置即可(因为push进去了4个字节的内容,所以让sp指针往前移动4个字节)
; 在函数外部恢复堆栈平衡,叫做外平栈。
add sp, 4

mov cx, 1122h
mov dx, 3344h
; 退出
mov ax, 4c00h
int 21h
sum:
mov bp, sp ; 把栈顶指针地址给bp,bp是基数指针寄存器,具体参考寄存器相关组成
mov ax, ss[bp+2] ; 访问栈中【栈顶+2】位置的数据
add ax, ss[bp+4]
; 此时计算结果放在ax寄存器中
ret
; 在函数内部恢复堆栈平衡,叫做内平栈。格式:ret 恢复栈的字节
; ret 4
code ends

end start

栈平衡是在函数调用结束后的操作,如果函数嵌套很多层,数据不断地入栈,也还是可能会引起栈顶超出(例如递归函数,这也是为什么递归函数一定要有递归边界)。

部分高级语言可以修改函数的栈平衡方式:

  • __cdecl:外平栈,参数从右至左入栈。
  • __stdcall:内平栈,参数从右至左入栈。
  • __fastcall:内平栈,使用cx,dx寄存器分别传递前面两个参数(一般ax用来存放返回值),其他参数从右往左入栈。

2.3. 函数的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
assume cs:code, ds:data, ss:stack

; 堆栈段
stack segment
db 100 dup(0)
stack ends

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

; 代码段
code segment
start:
; 手动设置ds,ss的值
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax

; 业务逻辑
push 1
push 2
call sum
add sp, 4

push 1
push 2
call sum
add sp, 4

; 退出
mov ax, 4c00h
int 21h

sum:
; 保护bp
push bp
; 保存sp之前的值,指向bp以前的值
mov bp, sp
; 预留10个字节的空间给局部变量
sub sp, 10
; 填充局部变量空间(防止这部分空间的值是上一次程序遗留下来的垃圾代码)
; 填充的内容是int 3(中断指令),对应的机器码是CC,当sp指向这部分内容时就会产生断点异常
; stosw(stosw中的w代表word(2个字节),根据实际情况也可以是b)的作用:将ax的值拷贝到es:di中,同时di的值会自动+2
; 0cccch,前面的0代表这个值是立即数,目的是和其他寄存器指令做区分,否则会产生歧义
mov ax, 0cccch
mov bx, ss
mov es, bx
; 让di等于bp-10
; -10是因为局部变量空间是10,而stosw指令会让di自动+2(往高级地址方向),所以需要从低地址到高地址自动填充
mov di, bp
sub di, 10
; cx决定了stosw的执行次数
mov cx, 5
; rep的作用:重复执行某个指令(执行次数由cx决定)
rep stosw

; 保护可能会用到的寄存器(因为其他函数调用可能会破坏原来寄存器中的值)
; 不用关心ax,因为ax就是用来放函数返回值的
; 这一步骤要放在保护局部变量之后操作,主要原因是保护寄存器的个数是不确定,会导致无法正确拿到局部变量的值,而且也能使用bp快速找到参数和局部变量
push si
push di
push bx

; -------- 业务逻辑 - begin
; 定义两个局部变量
mov word ptr ss:[bp-2], 3
mov word ptr ss:[bp-4], 4
mov ax, ss:[bp-2]
add ax, ss:[bp-4]
mov ss:[bp-6], ax

; 访问栈中的参数
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-6]
; -------- 业务逻辑 - end

; 恢复寄存器的值
pop bx
pop di
pop si

; 恢复sp
mov sp, bp
; 恢复bp
pop bp

ret

code ends

end start

函数的调用流程(内存):

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

三、栈帧

栈帧(Stack Frame Layout)就是一个函数执行的环境,包括:参数、局部变量、返回地址等。

栈帧也可以从字面意思理解:函数操作基本上是在栈上进行的(入栈/出栈),一个栈中可能会压入多个函数,为了统一规范,使用帧代表函数。

如上图,bp到sp之间的内容就是一个栈帧(函数内部发生的相关操作)。当一个函数执行完成后就会被释放(栈帧被释放)。