07 | 函数调用:为什么会发生stack overflow?
栈溢出(stack overflow)。
// function_example.c
#include <stdio.h>
int static add(int a, int b)
{
return a+b;
}
int main()
{
int x = 5;
int y = 10;
int u = add(x, y);
}
int static add(int a, int b)
{
0: 55 push rbp //压栈
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
return a+b;
a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
10: 01 d0 add eax,edx
}
12: 5d pop rbp //出栈
13: c3 ret
0000000000000014 <main>:
int main()
{
14: 55 push rbp //通常以 rbp 来作为参数和局部变量的基址;
15: 48 89 e5 mov rbp,rsp
18: 48 83 ec 10 sub rsp,0x10
int x = 5;
1c: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5
int y = 10;
23: c7 45 f8 0a 00 00 00 mov DWORD PTR [rbp-0x8],0xa
int u = add(x, y);
2a: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
2d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
30: 89 d6 mov esi,edx
32: 89 c7 mov edi,eax
34: e8 c7 ff ff ff call 0 <add>
39: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
3c: b8 00 00 00 00 mov eax,0x0
}
41: c9 leave
42: c3 ret
if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令,而函数调用的跳转,在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令。
那我们有没有一个可以不跳转回到原来开始的地方,来实现函数的调用呢?
可以把调用的函数指令,直接插入在调用函数的地方,替换掉对应的call 指令,然后在编译器编译代码的时候,直接就把函数调用变成对应的指令替换掉。
栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是“stack overflow”。
除了无限递归,递归层数过深,在栈空间里面创建非常占内存的变量(比如一个巨大的数组),这些情况都很可能给你带来 stack overflow。

add 函数的第 0 行,push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针,是一个存放了当前栈帧位置的寄存器。push rbp就把之前调用函数的返回地址,压到栈顶。接着,第 1 行的一条命令 mov rbp, rsp 里,则是把 rsp 这个栈指针的值复制到 rbp 里,而rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的返回地址,变成当前最新的栈顶,也就是 add 函数的返回地址了。而在函数 add 执行完成之后,又会分别调用第 12 行的 pop rbp 来将当前的栈顶出栈,然后调用第 13 行的 ret 指令,将程序的控制权返回到出栈后的栈顶,也就是 main 函数的返回地址。
如何利用函数内联进行性能优化?
函数内联(Inline),一种编译器自动优化的场景 -- 把一个实际调用的函数产生的指令,直接插入到的位置,来替换对应的函数调用指令(被调函数没有调用其他函数)。
内联带来的优化是,CPU 需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了。
不过内联并不是没有代价,内联意味着,我们把可以复用的程序指令在调用它的地方完全展开了。如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了。

这样没有调用其他函数,只会被调用的函数,我们一般称之为叶子函数(或叶子过程)。
通过加入了程序栈,我们相当于在指令跳转的过程种,加入了一个“记忆”的功能,能在跳转去运行新的指令之后,再回到跳出去的位置,能够实现更加丰富和灵活的指令执行流程。这个也为我们在程序开发的过程中,提供了“函数”这样一个抽象,使得我们在软件开发的过程中,可以复用代码和指令,而不是只能简单粗暴地复制、粘贴代码和指令。
浙公网安备 33010602011771号