具有初始值的全局变量在源代码链接时就被写入所创建的PE文件,当该文件被执行时操作系统分析各个节中的数据填入对应的内存地址中,这时全局变量就已经存在了,等PE文件的分析和加载工作完成之后才执行入口点的代码。所以全局变量不受作用域的影响,程序的任何位置都可以访问。下面就来具体讲述不同点。
全局变量和常量类似都是被写入文件中,因此生命周期和模块相同,而其与局部变量最大的不同之处便是生命周期。按照上面所说,全局变量在执行第一条代码前便存在,知道程序退出销毁,而局部变量的生命周期则仅限于函数作用域内,超出作用域便会由栈平衡操作来释放其空间。在访问方式上,局部变量是通过栈指针来访问的,但是全部变量不在栈中,就无法用栈指针访问。下面就来说说是如何访问全局变量的。还是先看一个例子(Debug版本):
19: void main()
20: {
;栈空间分配和初始化保存环境略
21: scanf("%d", &g_nVariableType);
0042B45E push offset g_nVariableType (48C000h) ;通过直接寻址来直接访问全局变量
0042B463 push offset string "%d, %d" (47CC80h)
0042B468 call @ILT+3005(_scanf) (429BC2h)
0042B46D add esp,8
22: printf("%d\r\n",g_nVariableType);
0042B470 mov eax,dword ptr [g_nVariableType (48C000h)] ;此处同上
0042B475 push eax
0042B476 push offset string "%d %d\r\n" (47CC74h)
0042B47B call @ILT+4085(_printf) (429FFAh)
0042B480 add esp,8
23: };栈平衡操作及环境恢复略
可以很清楚的看到全局变量是用直接寻址来访问的,这是因为全局变量存储在文件中,加载至内存时也会随着文件加载到内存的固定偏移位置,所以编译器可以决定其用直接寻址找到它。而局部变量是存储在栈中的,无法确定其地址,故无法使用直接寻址方式,应采用相对寻址。下面再来看一下多个全局变量的情况(Debug版本):
19: void main()
20: {;栈空间分配和初始化保存环境略
21: /*scanf("%d", &g_nVariableType);
22: printf("%d\r\n",g_nVariableType);*/
23: //全局变量与局部变量对比
24: intnOne = 1;
0042D8EE mov [ebp-8], 1 ;局部变量的定义
25: intnTwo = 2;
0042D8F5 mov [ebp-14], 2 ;局部变量的定义
26:
27: scanf("%d,%d", &nOne, &nTwo);
0042D8FC lea eax, [ebp-14] ;取局部变量的地址,下同
0042D8FF push eax
0042D900 lea ecx, [ebp+var_8]
0042D903 push ecx
0042D904 push offset "%d, %d"
0042D909 call @ILT+3005(_scanf) (429BC2h)
0042D90E add esp,0Ch
28: printf("%d%d\r\n", nOne, nTwo);
0042D90E add esp, 0Ch
0042D911 mov eax, [ebp-14]
0042D914 push eax
0042D915 mov ecx, [ebp-8]
0042D918 push ecx
0042D919 push offset "%d %d\r\n"
29: scanf("%d, %d", &g_nVariableType,&g_nVariableType1);
0042D926 push offset g_nVariableType1 (48C28Ch) ;指令中的数据即为操作数的地址,书上说是利用立即数间接寻址,我认为和直接寻址一样,下同
0042D92B push offset g_nVariableType (48C288h)
0042D930 push offset string "%d" (47CC80h)
0042D935 call @ILT+3005(_scanf) (429BC2h)
0042D93A add esp,0Ch
30: printf("%d%d\r\n", g_nVariableType, g_nVariableType1);
0042D93D mov eax,dword ptr [g_nVariableType1 (48C28Ch)]
0042D942 push eax
0042D943 mov ecx,dword ptr [g_nVariableType (48C288h)]
0042D949 push ecx
0042D94A push offset string "%d\r\n"(47CC74h)
0042D94F call @ILT+4085(_printf)(429FFAh)
0042D954 add esp,0Ch
31: };栈平衡操作及环境恢复略
从上面的例子可以看到,全局变量的内存分配是从低地址开始的,通俗点说就是先定义的变量在低地址,后定义的在高地址。同时这里也可以清楚的看到局部变量和全局变量的不同。
全局静态变量的访问及生命周期和全局变量相同,只是限制了不能从其他文件访问该变量,因此这里不做介绍。局部静态变量的生命周期和全局变量相同,并且都是在编译链接时就写入文件之中,但是其作用域确是和局部变量相同。事实上,局部静态变量刚开始是作为全局变量处理,而它的初始化仅仅是对它进行赋值操作,但是编译器是如何做到在多次执行函数时赋值操作只进行一次呢?下面通过一个例子来了解(Debug版本):
Main函数:
30: void main()
31: {;先前代码略
32: ;局部静态变量被初始化为常量,下面会有详细介绍原因
33: staticint g_snOne = 1;
34: printf("%d \r\n", g_snOne);
004272BE mov eax,dword ptr [g_snOne (480008h)]
004272C3 push eax
004272C4 push offset string "%d \r\n" (471C6Ch)
004272C9 call @ILT+3875(_printf) (425F28h)
004272CE add esp,8
35: ;多次对局部静态变量初始化
36: for (int i = 0; i < 5; i++)
004272D1 mov dword ptr [i],0
004272D8 jmp main+43h (4272E3h)
004272DA mov eax,dword ptr [i]
004272DD add eax,1
004272E0 mov dword ptr [i],eax
004272E3 cmp dword ptr [i],5
004272E7 jge main+57h (4272F7h)
37: {
38: ShowStatic(i);
004272E9 mov eax,dword ptr [i]
004272EC push eax
004272ED call ShowStatic (42571Ch)
004272F2 add esp,4
39: }
004272F5 jmp main+3Ah (4272DAh)
40:};保存环境及栈平衡操作略
下面是被调用的ShowStatic函数:
11: void ShowStatic(int nNumber)
12: {;先前代码略
13: static int g_snNumber1 = nNumber;
0042721E mov eax,dword ptr [$S1 (481328h)] ; 0x00481328空间存储的就是g_snNumber1
;判断标志中的第一位是否为1,若为1则表明已经被初始化,无需再次进行初始化
00427223 and eax,1
00427226 jne ShowStatic+3Dh (42723Dh)
00427228 mov eax,dword ptr [$S1 (481328h)]
0042722D or eax,1 ;若标志位不为1则置1
00427230 mov dword ptr [$S1 (481328h)],eax ;将标志字节写回内存
00427235 mov eax,dword ptr [nNumber]
00427238 mov dword ptr [g_snNumber1 (481324h)],eax ;变量赋值
14: staticint g_snNumber2 = nNumber;
;这个局部静态变量的赋值操作和上面的一样
0042723D mov eax,dword ptr [$S1 (481328h)]
;判断标志中的第二位是否为1,若为1则表明已经被初始化,无需再次进行初始化
00427242 and eax,2
00427245 jne ShowStatic+5Ch (42725Ch)
00427247 mov eax,dword ptr [$S1 (481328h)]
0042724C or eax,2 ;若第二位标志位不为1则置1
0042724F mov dword ptr [$S1 (481328h)],eax
00427254 mov eax,dword ptr [nNumber]
00427257 mov dword ptr [g_snNumber2 (481320h)],eax
15: printf("%d \r\n", g_snNumber1);
0042725C mov eax,dword ptr [g_snNumber1(481324h)]
00427261 push eax
00427262 push offset string "%d \r\n" (471C6Ch)
00427267 call @ILT+3875(_printf) (425F28h)
0042726C add esp,8
16: printf("%d \r\n", g_snNumber2);
0042726F mov eax,dword ptr [g_snNumber2(481320h)]
00427274 push eax
00427275 push offset string "%d \r\n" (471C6Ch)
0042727A call @ILT+3875(_printf) (425F28h)
0042727F add esp,8
17: };后续代码略
为了区分全局变量和静态局部变量,这里采用了一个标志字节,由于有8位,故最多可表示8个静态局部变量的初始化状态,并且这个标志位一般都在最先定义的局部静态变量附近。如果变量超过了8个,那么再定义一个标志字节,这个字节一般在第9个变量的附近。在两个变量都定义完成之后可以查看内存中的标志字节0x00481328 03,可以很清楚的看到变成了03,也就是0x00000011,表示前两个静态局部变量均已被初始化。
现在再来看main函数中的静态局部变量为何会变成全局变量。用WinHex十六进制编辑器打开相应的.obj文件,可以在最后找到如下数据片段:
可以看到有g_snOne字符串,但是又有点不同,这里是经过名称粉碎之后的名字,以此来确定变量的作用域不会超出其范围。
C/C++中使用malloc/free或new/delete来分配和释放堆空间。在申请空间时会返回堆空间的首地址,若堆空间没有得到及时地释放,则会造成内存泄漏。只要在程序的反汇编代码中发现有如下特点的代码,那么就很容易识别出堆变量:
.text:004282FE push 0Ah
.text:00428300 call j_malloc
.text:00428305 add esp, 4
.text:00428308 mov [ebp+var_8], eax
以及
.text:0042831B push 0Ah
.text:0042831D call j_operator_new
等,delete和free的调用也是如此。当然也要注意他们申请和释放的地址必须对应起来看,否则会判断错误。
了解了堆变量的申请及销毁,再来看看编译器是如何管理堆空间的。堆结构的每一次分配形成一个结点,每个节点都是使用双向链表存储的,结点的数据结构如下定义:
typedef struct _CrtMemBlockHeader
{
struct_CrtMemBlockHeader * pBlockHeaderNext;
struct_CrtMemBlockHeader * pBlockHeaderPrev;
char* szFileName;
int nLine;
#ifdef _WIN64
/* Theseitems are reversed on Win64 to eliminate gaps in the struct
* and ensure that sizeof(struct)%16 ==0, so 16-byte alignment is
* maintained in the debug heap.
*/
int nBlockUse;
size_t nDataSize;
#else /* _WIN64 */
size_t nDataSize;//堆空间的数据大小
int nBlockUse;
#endif /* _WIN64 */
long lRequest;//堆空间的申请次数
unsignedchar gap[nNoMansLandSize];//堆空间数据,第一个数据的指针就是//申请的变量的指针
/* followedby:
* unsigned char data[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];
*/
}_CrtMemBlockHeader;
如上面的结构所示,#ifdef后面的是Win64程序的定义,此处用不到。#else后面定义的是用到的数据项。pBlockHeaderNext和pBlockHeaderPrev分别指向这个结点的后继结点(指向前一次申请的堆空间)和前导结点(指向后一次申请的堆空间)。下面就来看看内存中的一个堆结点:
char * pCharMalloc = (char*)malloc(10);语句分配了10个char类型的空间给pCharMalloc指针。
在0x00392A10处发现10个为0的字节,这就是分配给pCharMalloc的10个char空间。这段空间的前后都有0xfdfdfdfd数据,这是在Debug版本下的越界检查标志。往前四个字节是0x0000002d表示的是堆的申请次数,0x00392A00处的数据是堆空间的大小,这里是10所以为0x0a,0x003929F0后面的两个数据则是前面说的两个存储指针的变量的值。空间释放后,这个结点所在的内存变成了如下状况:
这样一来程序下次便可以再次分配这块空间。