C++反汇编学习笔记6——变量在内存中的位置和访问方式

两年前写的,欢迎大家吐槽!

转载请注明出处。

1.   全局变量和局部变量的区别

具有初始值的全局变量在源代码链接时就被写入所创建的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: };栈平衡操作及环境恢复略

从上面的例子可以看到,全局变量的内存分配是从低地址开始的,通俗点说就是先定义的变量在低地址,后定义的在高地址。同时这里也可以清楚的看到局部变量和全局变量的不同。

2.   局部静态变量

全局静态变量的访问及生命周期和全局变量相同,只是限制了不能从其他文件访问该变量,因此这里不做介绍。局部静态变量的生命周期和全局变量相同,并且都是在编译链接时就写入文件之中,但是其作用域确是和局部变量相同。事实上,局部静态变量刚开始是作为全局变量处理,而它的初始化仅仅是对它进行赋值操作,但是编译器是如何做到在多次执行函数时赋值操作只进行一次呢?下面通过一个例子来了解(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字符串,但是又有点不同,这里是经过名称粉碎之后的名字,以此来确定变量的作用域不会超出其范围。

3.   堆变量

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指针。


C++反汇编学习笔记6——变量在内存中的位置和访问方式_第1张图片

在0x00392A10处发现10个为0的字节,这就是分配给pCharMalloc的10个char空间。这段空间的前后都有0xfdfdfdfd数据,这是在Debug版本下的越界检查标志。往前四个字节是0x0000002d表示的是堆的申请次数,0x00392A00处的数据是堆空间的大小,这里是10所以为0x0a,0x003929F0后面的两个数据则是前面说的两个存储指针的变量的值。空间释放后,这个结点所在的内存变成了如下状况:

C++反汇编学习笔记6——变量在内存中的位置和访问方式_第2张图片

这样一来程序下次便可以再次分配这块空间。

你可能感兴趣的:(C++逆向)