其他
函数运行时在内存中是什么样子?
void func(int a) {
if (a > 100000000) return;
int arr[100] = {0};
func(a + 1);
}
你能看出这段代码会有什么问题吗?2. 我们在上一篇文章《高性能高并发服务器是如何实现的》中提到了一项关键技术——协程,你知道协程的本质是什么吗?有的同学可能会说是用户态线程,那么什么是用户态线程,这是怎么实现的?3. 函数运行起来后在内存中是什么样子?这几个问题看似没什么关联,但这背后都指向一样东西,这就是所谓的函数运行时栈,run time stack。接下来我们就好好看看到底什么是函数运行时栈,为什么彻底理解函数运行时栈对程序员来说非常重要。从进程、线程到函数调用
其中,我们创造了进程、线程这样的概念来记录有哪些程序正在运行,关于进程和线程的概念请参见《看完这篇还不懂进程、线程与线程池你来打我》。进程和线程的运行体现在函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。因此接下来我们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。
函数执行的活动轨迹:栈
A Box
控制转移
我从哪里来 (返回)
要到去哪里 (跳转)
函数A对于的机器指令执行到了哪里 (我从哪里来,返回)
函数B第一条机器指令所在的地址 (要到哪里去,跳转)
call 0x400540
这条机器指令是什么意思呢?这条机器指令对应的就是我们在代码中所写的函数调用,注意call后有一条机器指令地址,注意观察上图你会看到,该地址就是函数B的第一条机器指令,从这条机器指令后CPU将跳转到函数B。现在我们已经解决了控制跳转的“要到哪里去”问题,当函数B执行完毕后怎么跳转回来呢?原来,call指令除了给出跳转地址之外还有这样一个作用,也就是把call指令的下一条指令的地址,也就是0x40056a push到函数A的栈帧中,如图所示:传递参数与获取返回值
在x86-64中,多数情况下参数的传递与获取返回值是通过寄存器来实现的。假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就可以从这些寄存器中获取参数了。同样的,函数B也可以将返回值写入寄存器,当函数B执行结束后函数A从该寄存器中就可以读取到返回值了。我们知道寄存器的数量是有限的,当传递的参数个数多于寄存器的数量该怎么办呢?这时那个属于函数的小盒子也就是栈帧又能发挥作用了。原来,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就可以从前一个函数的栈帧中获取到参数了。现在栈帧的样子又可以进一步丰富了,如图所示:
局部变量
Big Picture
void func(int a) {
if (a > 100000000) return;
int arr[100] = {0};
func(a + 1);
}
void main(){
func(0);
}
想一想这段代码会有什么问题?原来,栈区是有大小限制的,当超过限制后就会出现著名的栈溢出问题,显然上述代码会导致这一问题的出现。因此:不要创建过大的局部变量
函数栈帧,也就是调用层次不能太多
总结
长按识别二维码关注
往期精选
看完这篇还不懂线程与线程池你来打我读取文件时,程序经历了什么?终于明白了,一文彻底理解I/O多路复用从小白到高手,你需要理解同步与异步程序员应如何彻底理解回调函数扫码关注
码农的荒岛求生