学C语言时,局部变量、函数运用广泛
但他们的底层原理如何,听我细细道来。
目录
一、预备知识
1.汇编语言简介
在这之前,需要先了解一下汇编语言的知识。
凡是一门高级语言,机器不可能直接识别,会通过编译器转换为低级语言。
汇编语言正是这样一门低级语言。
可它一点也不低级,所有编译语言最终都要经过它之手才可被机器识别。
2.寄存器
汇编语言中,有个很重要的概念叫作寄存器。
寄存器,说白了就是用来存储数据、地址的,它的优先级最高,CPU优先使用其内部数据。
寄存器依靠名称来区分数据,而不通过地址,可以把他们都当作变量看待。
寄存器的名字最常用的有以下几种:
看不懂没关系,后面会用例子来解释。
3.栈(stack)
栈,就是内存的一种模型。
可以把它理解成一个桶,用来装一些临时占用内存的数据,比如函数、局部变量。
另外,栈的使用是从高地址到低地址调用的。
每当有一个函数开始执行,都会在栈里建立一个帧 (frame), 所有变量放入帧中。
当函数执行结束,帧便被自动收回,释放内存空间。
二、函数栈帧
有了上面知识,我们步入正题。首先邀请两位主角,ebp和esp。
他们都是寄存器,用来存放地址,这个地址是用来维护函数栈帧的。
接下来看一串示例代码:
int add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = add(a, b);
printf("%d\n", c);
return 0;
}
这串代码以 main() 函数为起点,main也是函数,需要在内存开辟一块空间。
ebp和esp 就分别去维护那一块的栈帧
但是这里问题来了,main函数又是被谁调用的呢?
在VS2013中,main函数是由__tmainCRTStartup 函数调用的,
而__tmainCRTStartup 又是被mainCRTStarup 所调用。
因为都是函数也要有栈帧。
过程详析:
1.main函数的汇编过程
一开始内存是这样的,要对内存直接动手,还是要请到我们的汇编代码。
这便是main函数调用的一系列汇编代码。不怕,我会一步一步地拆分解析。
看第一行,指的是将 ebp 这个寄存器放到__tmainCRTStartup 上,也叫压栈。
将ebp 压上去后,esp也往上指,即esp向上移动4个字节的空间。
再看这条指令
指的是将esp 的值给ebp,也就是ebp 也往上指了,它们指向了同一空间。
再看这条指令,即将 esp 地址减去 0E4h,对应图就是将 esp 往上指。
(减了就是变小,途中对应的下面是高地址,上面是低地址,所以往上)
结果可以发现,ebp和esp都往上了,他们来到了一块新大陆。
这块大陆就是为main函数开辟的空间
这三条连一起,刚刚讲过push,即压栈,
最后把 esp 向上,即地址减去12个字节。
如图:
这四步一起看,lea 指load effective address(加载有效地址), 即将后面这个地址放到edi里去。
ebp-24h 是将ebp减去一段地址,到达ebx下的位置。
最后一步要配合前面三步,所以他们的整体意思是:
从edi开始到ebp,中间的9个空间,全部转换为0CCCCCCCCh。
2. 局部变量的汇编过程
走了半天,main函数终于结束了。我们来看以下的局部变量汇编代码。
mov, 即将 0Ah 放入ebp-8的地址中,0Ah就是我们 a的值10,如图:
(这里 ebp 往上都是main函数的预留空间)
以此类推,后面赋值的代码也就好解释了。
(这里的14h和20h对应内存中如图,他们中间都隔着2串cc)
赋值完成,结果如图:
3.调用函数的汇编过程
局部变量定义完后,终于到了我们的自定义函数阶段,函数内部图如下:
从头来看
ebp-14h 这块地址放着20,mov语句将值赋给 eax 上,再将eax压入栈。
这一步同理,ebp - 8的值为10,mov语句将值赋给ecx, 再将 ecx 压入栈。
(其实这一步就是在创建局部变量x,y)
再看这条,call的作用是先调用函数,函数结束后会来到下一条指令的地址,先将其压栈
此刻便进入函数
进入函数,会发现一些与main函数相似的地方,其实就是再内存里给函数预留空间的作用。
过程和main函数一样,这里不细说了,直接看结果:
空间预留好了,看函数内部。
第一步,将ebp-8的地址处赋值为0(即创建一个局部变量z);
第二步,将ebp+8的位置的值赋值给eax,
(ebp+8的结果在下面,即我们刚刚创建的局部变量x)
再将eax加上ebp+0Ch地址的值,这个加后的结果保存在eax中。
(ebp+0Ch即局部变量y)
再将eax内的 30 给ebp-8 的位置上(此时ebp-8位置上的变量就是z)
函数内最后一句话
将ebp-8 的值30给eax。因为eax是一个寄存器,不会销毁,而局部变量z会出函数而销毁。
4.结尾回收空间
先看最后的汇编代码
pop与push相反,它指将其从栈内弹出,可以理解为从桶里把他们按顺序拿出来。
每次拿出来 esp 都要顺势往下,如图:
其实,此时的add函数栈帧也失去了意义,会被一起回收。
这是最后三条指令。第一条将ebp赋给esp,即esp往下到达和ebp同样的位置。
再将此时ebp弹出,回到一开始的地方,最后整个程序又回到了main函数内。
5.回到main函数
esp到达了我们刚刚call留下的地址 ,函数退出后便会来到这里,如下图。
来到这串地址,还剩下两串汇编代码。
第一步将esp+8,就是esp往下,此时形参便被销毁了。如图:
最后将eax寄存器内刚刚存下的30给ebp-20h地址的局部变量c
这个函数结束了。
最后把main函数剩余空间清理掉即可,与add函数同理。
三、结尾
本篇博客尽可能通俗地讲述函数栈帧的实现,如能帮到大家欢迎大家给一个赞。