为什么要了解C语言中函数调用与堆栈的关系?因为在操作系统中,所有的上下文切换,其实就是都是关于系统栈和线程栈之间的数据交换,,要真正的理解栈,只有放在应用中去看待它,函数的调用是栈最重要的应用,其实函数还有个秘密,那就是它其实就是自成一界的上下文,是不是很神奇。
【堆栈篇】
栈也叫堆栈、他就是一种数据结构,以先入后出的方式进行数据的存储。
在C语言中栈是非常重要的,以STM32为例,虽然芯片只能执行机器指令,并且所有的语言都会被编译成机器指令,但是不同语言编译成机器指令的方式是不一样的,也就是说芯片是有运行环境的,芯片的启动都是从启动文件开始的,也就是所谓的startup.s ,这个时候,他是在汇编环境中执行的,然后他要切换到C语言环境,有一个最重要的事情,就是设置具体的堆栈结构。
然后你就可以执行应用C语言编写的代码了,那么堆栈的作用到底是什么?
答案就是:为了完成函数的调用,栈主要用来存放函数参数、局部变量、返回值等等数据。
如何来维护一个栈内的数据,是通过堆栈指针来实现的,主要涉及的esp栈顶指针、ebp栈底指针
栈顶指针主要用来完成数据的进栈和出栈操作。
栈底指针主要用来查找当前栈内数据的位置,作为基地址参考。
【函数调用篇】
要完成一个函数的调用,CPU需要做些什么?
1.保存案发现场记录。
2.给要调用的函数创建一块空间,用于该函数执行过程所产生的记录。
3.函数调用执行完成,返回到原有记录。
简单的理解,函数调用就是需要保存一些相关的记录,而这些记录就被存在堆栈中。具体看看这个记录里面都有哪些内容(按顺序记录):
1.实际参数、返回地址、edp栈底指针首先入栈。
2.局部变量、形式参数、返回值入栈。
3.CPU运行相关的寄存器进栈。
关于函数调用入栈惯例
这里不具体介绍,有很多的标准,
主要用于规定例如参数是自左至右入栈,还是自右至左入栈?等等。
感兴趣可以问问度娘,看看都有哪些标准。
深入解析函数进栈(出栈刚好相反过程)。
让我用一段最简单的代码来说明关于函数调用而引发的堆栈操作
void MyCall(int a, int b)
{
int p =11, q = 22;
}
int main(void)
{
Mycall(33, 44);
return 0;
}
跟着PC指针的位置来看一下堆栈在每一个时刻都是什么样子,
未执行调用函数时,如下:
执行Mycall函数入口,如下:
执行函数函数体时栈记录内容,如下:
关于堆栈的内容,其实还有溢出这一部分,太深了,太绕了,讲不明白。。。
能力有限,留到以后慢慢研究吧。
关于栈溢出部分还是多多少少的提及一下,在我们的程序运行进入C语言环境之前,栈的大小就要被定义好,也就是栈的容量是有限的和固定的。
函数的调用会消耗栈内存,函数的调用结束则会释放占内存,在我们实际应用过程中,最大的潜在问题是函数的嵌套调用,因为他只会进行压栈操作,不会弹栈操作,所以会导致栈内存不断被消耗,栈的内存又是有限的,当超过最大的限度时候,就会发生栈溢出错误,程序可能异常退出或者死机等现象。
这时候可能就会有我们老工程师经常给我们提的建议,函数嵌套不要太深,一般不要超过多少层,函数的内部尽量不要用大的局部变量。
当然我们也会想,那如果我们把栈定义的大点是不是就不用担心栈溢出了?
但是你要知道栈定义消耗的是RAM的空间。
而你的单片机又有多少的RAM空间呢?
一般也就几十KB吧
总要给应用程序留一点空间去申请来完成程序功能啊,所以建议栈定义不要太大,给一个特定的大小就可以,剩下的就是想办法减少函数嵌套的层数了。
关于栈的使用在PC机上又会出现其它的问题。
比如栈溢出攻击。。。超纲了,有兴趣的同学可以自行百度。