第7章 变量在内存中的位置和访问方式
通过第6章对函数的介绍,读者已经预先接触了局部变量的定义及使用过程。除了局部变量外,还有全局变量和静态变量等。本章以VC++6.0为例,讲解各类作用域的底层机制和分析要点。在本章开始之前,我们先约定一下用语:
变量的作用域
指的是变量在源码中可以被访问到的范围。全局变量属于进程作用域,也就是说,在整个进程中都能够访问到这个全局变量;静态变量属于文件作用域,在当前源码文件内可以访问到;局部变量属于函数作用域,在函数内可以访问到;在“{}”语句块内定义的变量,属于块作用域,只能在定义变量的“{}”块内访问到。
变量的生命周期
指的是变量所在的内存从分配到释放的这段时间。变量所在的内存被分配后,我们可以形象地将这比喻为变量的生命开始;变量所在的内存被释放后,我们可以将这比喻为变量的消亡。
读者可以在阅读本章前,思考一下:以上谈到的作用域和生命周期,有区别吗?有什么区别呢?
7.1 全局变量和局部变量的区别
全局变量是如何形成的呢?2.6节中讲解了常量的识别和分析方法。常量与全局变量有着相似的特征,都是在程序执行前就存在了。在大多数情况下,在PE文件中的只读数据节中,常量的节属性被修饰为不可写;而全局变量和静态变量则在属性为可读写的数据节中。下面来定义全局变量:
int g_nVariableType=117713190;
然后获取全局变量的内存地址,如图7-1所示。
图 7-1 全局变量所在的内存地址
如图7-1所示,通过调试获取全局变量所在地址0x00421A30,地址中的数据为0x07042926,转换为十进制数为117717190。全局变量在文件中的地址定位和常量相同,也需要减去基地址,然后查阅节表得到文件地址。本示例中的基地址为0x00400000,它的全局变量对应在文件0x00021A30的偏移地址处,如图7-2所示。
图 7-2 全局变量所在的文件地址
由图7-2可见,具有初始值的全局变量,其值在链接时被写入所创建的PE文件中,当用户执行该文件时,操作系统先分析这个PE中的数据,将各个节中的数据填入对应的虚拟内存地址中,这时全局变量就已经存在了,等PE的分析和加载工作完成以后,才开始执行入口点的代码。因此全局变量可以不受作用域的影响,在程序中的任何位置都可以被访问和使用。全局变量和局部变量都是变量,它们都可以被赋值和修改。它们之间存在哪些区别呢?在反汇编代码中又该如何区分呢?下面将进一步讲解全局变量,分析其与局部变量的差别。
通过对全局变量的初步分析得出,它和常量类似,被写入文件中,因此其生命周期与所在模块相同。全局变量和局部变量的最大区别就是生命周期不同。全局变量诞生于所在执行文件被操作系统加载后,执行第一条代码前,这个时候已经具有内存地址了。当程序结束运行退出后,全局变量将被销毁。因此,全局变量可以在程序中的任何位置使用;而局部变量的生命周期则局限于函数作用域内,超出作用域后,由栈平衡操作来释放局部变量的空间。对于由“{}”划分的块作用域,其内部的局部变量的生命周期和函数作用域一致,但是编译器会在编译前检查语法,限制块外代码对其访问。
在访问方式上,局部变量的访问是通过栈指针相对间接访问,而全局变量的内存地址在全局数据区中,通过栈指针无法访问到。那么全局变量又是如何访问寻址的呢?我们先来看一个例子,如代码清单7-1所示。
代码清单7-1 全局变量的访问—Debug版
//C++源码说明:全局变量的访问
int g_nVariableType=117713190;//定义整型全局变量
void main(){
//从标准输入设备获取数据到g_nVariableType
scanf("%d",&g_nVariableType);
//将g_nVariableType输出到标准输出设备
printf("%d\r\n",g_nVariableType);
}
//C++源码与对应汇编代码讲解
void main(){
;Debug保存环境、栈空间申请初始化略
scanf("%d",&g_nVariableType);
;将全局变量的地址作为参数压入栈,与常量的处理方法相同
00401028 push offset g_nVariableType(00424a30)
0040102D push offset string"%d"(00422024)
00401032 call scanf(00401100);调用scanf函数
00401037 add esp,8;平衡scanf函数的两个参数
printf("%d\r\n",g_nVariableType);
;取全局变量内容传入eax
0040103A mov eax,[g_nVariableType(00424a30)]
0040103F push eax
00401040 push offset string"%d\r\n"(0042201c)
00401045 call printf(00401080);调用printf函数
0040104A add esp,8;平衡printf函数的两个参数
}
;Debug还原环境、栈空间略
通过对代码清单7-1的分析可知,访问全局变量与访问常量类似—都是通过立即数来访问。由于全局变量在编译期就已经确定了具体的地址,因此编译器在编译的过程中可以计算出一个固定的地址值。而局部变量需要进入作用域内,通过申请栈空间存放,利用栈指针ebp或esp间接访问,其地址是一个未知可变值,编译器无法预先计算。
上面讲解了全局变量与局部变量在指令中的寻址方式,以及生命周期的差别。不仅如此,在同时连续定义多个全局变量时,这些全局变量在内存中的地址顺序与局部变量也不一定相同。我们来看一个例子,如代码清单7-2所示。
代码清单7-2 全局变量的定义顺序
//C++源码说明:全局变量的访问
int g_nVariableType=117713190;//定义整型全局变量
int g_nVariableType1=117713191;//定义整型全局变量
void main(){
int nOne=1;int nTwo=2;//局部变量定义
//scanf与printf的使用避免常量传播优化
scanf("%d,%d",&nOne,&nTwo);
printf("%d%d\r\n",nOne, nTwo);
scanf("%d,%d",&g_nVariableType,&g_nVariableType1);
printf("%d%d\r\n",g_nVariableType, g_nVariableType1);
}
//C++源码与对应汇编代码讲解
void main(){
;Debug保存环境、栈空间申请初始化略
int nOne=1;//假设ebp为0x0012FF10
0040D9D8 mov dword ptr[ebp-4],1;nOne所在地址0x0012FF0C
int nTwo=2;
0040D9DF mov dword ptr[ebp-8],2;nTwo所在地址0x0012FF08
;scanf与printf讲解略
scanf("%d,%d",&g_nVariableType,&g_nVariableType1);
;g_nVariableType1所在地址为0x00424E78
0040DA10 push offset g_nVariableType1(00424e78)
;g_nVariableType所在地址为0x00424E4
0040DA15 push offset g_nVariableType(00424e74)
0040DA1A push offset string"%d,%d"(00422fe0)
;其他分析讲解略
通过对代码清单7-2的分析发现,全局变量在内存中的地址顺序是先定义的变量在低地址,后定义变量在高地址。有此特性即可根据反汇编代码中全局变量的所在地址,还原出其高级代码中被定义的先后顺序,更进一步接近源码。
下面对全局变量和局部变量的特征进行一下总结。
全局变量的特征如下:
所在地址为数据区,生命周期与所在模块一致;
使用立即数间接访问。
局部变量的特征如下:
所在地址为栈区,生命周期与所在的函数作用域一致;
使用ebp或esp间接访问。
通过上述对比,相信读者在分析反汇编代码时能轻松地将它们识别并分类。