zoukankan      html  css  js  c++  java
  • 编译器1

    第一部分:变量的内存分配

    在介绍之前,先说明一下CPU的寄存器,我的计算机是64位的,但是为了方便,所以编译的程序是32位的,而且使用的32位调试器来进行分析的。
    这里简单介绍一下常用的寄存器。 32位平台常用的CPU寄存器如下:
    EAX
    ECX
    EDX
    EBX //前面四个寄存器通常是存放临时数据
    ESP //ESP是非常关键的一个寄存器,它的作用是记录栈顶的内存地址
    EBP //在VC编译器编译出来的程序中,EBP的值通常作为局部变量寻址的基址
    ESI
    EDI
    EIP //EIP用于记录程序当前执行指令所在的内存地址

    上述寄存器的长度都是32位,即4字节长度,本部分着重需要弄明白ESP,EBP的用途,下面举实例做讲解:

    一.局部非静态变量的内存分配

    实例1

    C源码:

    #include <stdio.h>
    int main()
    {
    char ch='a';
    return 0;
    }

    反汇编结果:

    00011000 >/$  55            push ebp
    ;将原来寄存器ebp的值入栈
    00011001  |.  8BEC          mov ebp,esp
    ;将寄存器esp的值传送到ebp
    00011003  |.  51            push ecx
    ;将寄存器ecx的值入栈
    00011004  |.  C645 FF 61    mov byte ptr ss:[ebp-0x1],0x61
    ;将字符'a'的ASCII码0x61传送到内存地址为ebp-0x1的内存空间里,
    ;这个内存空间就是变量ch的内存空间,注意到这个内存空间是怎么
    ;来的,后面详细解释
    00011008  |.  33C0          xor eax,eax
    ;VC的编译器通常将寄存器EAX作为返回值,对eax自身进行异或运算
    ;结果为0,于是就对应源码里面的return 0;
    0001100A  |.  8BE5          mov esp,ebp
    0001100C  |.  5D            pop ebp
    ;上面两句与函数开头的两句指令相对应,作用是恢复函数调用前寄
    ;存器的值,清理现场。
    0001100D  .  C3            retn
    ;返回

    大部分指令都好理解,唯一不太好理解的是“ch变量的内存空间是怎么来的?”。
    下面做一个假设来模拟运行上诉指令:
    假设刚进入main函数的时候,esp的值为0025F9E8,即此时栈顶的地址为0025F9E8
    push ebp
    这时esp的值减去4变为0025F9E4,因为寄存器ebp是4个字节长度
    这里要说明一下实际压栈的原理,CPU实际的压栈操作并不是真的把数据往下面“压”,而是在栈顶的“上面”添加数据,再把esp寄存器减去相应的长度数值(栈是中的数据是越靠近栈顶,内存地址越低,所以减少esp的值就相当于“升高”栈顶),这样就“变相”地完成了“压栈”操作了。这是一种灵活的处理方法,毕竟如果真是“压”的话,要将后面的数据全部往下面移动,性能开销太大了。
    mov ebp,esp
    将esp现在的值传送给ebp,所以ebp保存着当前esp的值0025F9E4
    push ecx
    将ecx的值“压”到栈顶,此时esp-4,所以esp的值是0025F9E0,ebp的值仍然是0025F9E4
    mov byte ptr ss:[ebp-0x1],0×61
    此时实际上就是把'a'的ASCII码0×61保存到0025F9E4-1=0025F9E3。这个0025F9E3就是变量ch的内存地址。

    好了,到此为止,看明白了么? 那句push ecx就是分配ch变量的内存的关键,其实ecx的值是无关数据,压栈的目的不是为了临时保存ecx的值,而是将栈“空”四个字节出来,即给ch分配内存空间,共分配了4个字节(0025F9E0-0025F9E3),但实际上变量ch只用了1个字节,但区区浪费3个字节无所谓啦,毕竟只是短时间占用,没什么影响的。接下来看另一个例子。

    实例2

    C源码:

    #include <stdio.h>
    int main()
    {
            int a=100;
            int b=200;
            int c=300;
            int d=400;
            return 0;
    }

    反汇编结果:

    00EE1000 >  55              push ebp
    00EE1001    8BEC            mov ebp,esp
    00EE1003    83EC 10         sub esp,0x10
    ;将esp减去0x10,即减去16
    ;在实例1的时候就讲过CPU压栈的原理,这里
    ;将esp减去16,就是将栈顶“向上移动”16个字节
    ;就相当于在栈顶预留16个字节
    ;这16个字节就是给a、b、c、d这四个int变量分配的内存空间
    00EE1006    C745 FC 6400000>mov dword ptr ss:[ebp-0x4],0x64
    ;a=100
    ;将0x64,也就是十进制的100保存到内存地址为ebp-0x4的内存空间
    ;这和实例1的处理方法相同,请参照实例1后面的说明
    ;都是用ebp的值作为基址来寻址变量内存空间的
    00EE100D    C745 F8 C800000>mov dword ptr ss:[ebp-0x8],0xC8
    ;b=200
    00EE1014    C745 F4 2C01000>mov dword ptr ss:[ebp-0xC],0x12C
    ;c=300
    00EE101B    C745 F0 9001000>mov dword ptr ss:[ebp-0x10],0x190
    ;d=400
    ;这四句指令类似,只是通过不同的偏移来寻址到各自的内存空间
    00EE1022    33C0            xor eax,eax
    ;将eax清零,作为返回值
    00EE1024    8BE5            mov esp,ebp
    00EE1026    5D              pop ebp
    ;这两句指令用于还原寄存器的值,使其值恢复到调用函数之前,
    00EE1027    C3              retn
    ;返回

    实例2和实例1不同的地方在于:实例1是通过对ecx压栈来分配内存,实例2是直接通过减掉寄存器esp的值来移动栈顶来分配内存。
    相同的地方在于:局部非静态变量都是在栈上分配的内存空间,函数执行完以后,esp的值被还原成执行函数之前的值,就相当于释放了函数运行过程中占用的栈,这也是为什么局部非静态变量在函数执行结束后会数据会丢失的原因。
    经过试验,
    当函数里的局部非静态变量总大小小于等于4字节的时候,编译器会采取push ecx的方法分配这四个字节;
    当函数里的局部非静态变量总大小大于4字节的时候,编译器会采取sub esp,0xXX的方法来分配这些变量的内存。
    这样做的目的是减少指令长度,因为push ecx对应的机器指令只有1个字节长度,而sub esp,0xXX对应的机器指令则有3个字节。
    有人又会问,局部非静态变量总大小为8字节的时候,为什么不采取连续两次push ecx的方法分配8字节内存呢?两次push ecx需要2字节,但也比3字节少啊?我个人认为这里有个平衡问题。

    push ecx

    CPU执行的时候,实际上将它分为两步

    sub esp,0x4
    mov [esp],ecx

    本来这句mov [esp],ecx指令就是没有什么用处的东西,我们根本不需要保存ecx的值,我们现在需要的仅仅只是将栈顶“上移”也就是sub esp,0×4,如果两次push ecx就会做两次无用功。而且相对于访问CPU寄存器而言,访问内存效率要低得多(CPU访问内存必须经过总线),如果把这些因素考虑在内,为了达到性能和大小的平衡,两次push ecx还不如用sub esp,0×8 

    二.局部静态变量的内存分配

    实例1

    C源码:

    #include <stdio.h>
    int main()
    {
            static int a;
            static char b;
            a=1994;
            b='X';
            return 0;
    }

    反汇编结果:

    001A1000 >  55              push ebp
    001A1001    8BEC            mov ebp,esp
    001A1003    C705 20301A00 C>mov dword ptr ds:[0x1A3020],0x7CA
    ;将1994赋值给a
    001A100D    C605 24301A00 5>mov byte ptr ds:[0x1A3024],0x58
    ;将'X'赋值给b
    001A1014    33C0            xor eax,eax
    001A1016    5D              pop ebp
    001A1017    C3              retn

    很明显,变量a和b的内存地址分别是0x1A3020和0x1A3024。由此看出局部静态变量所占内存空间的内存地址是固定的。

    实例2

    C源码:

    #include <stdio.h>
    int main()
    {
            static int a=1994;
            static char b='X';
            return 0;
    }

    反汇编结果:

    00961000 >  55              push ebp
    00961001    8BEC            mov ebp,esp
    00961003    33C0            xor eax,eax
    00961005    5D              pop ebp
    00961006    C3              retn

    奇怪!怎么没有赋值过程啊?从反汇编结果来看在main函数里面似乎什么都没有做。确实,什么都没有做!这也是局部静态变量和局部非静态变量初始化值的方式的差异。为了继续探究,我们在原来的代码中加入一句printf()来找到这两个变量的内存地址。
    修改后的C源码如下:

    #include <stdio.h>
    int main()
    {
            static int a=1994;
            static char b='X';
            printf("%d %d",&a,&b);
            return 0;
    }

    反汇编结果:

    011F1000 >  55              push ebp
    011F1001    8BEC            mov ebp,esp
    011F1003    68 04301F01     push 0x11F3004
    011F1008    68 00301F01     push 0x11F3000
    011F100D    68 F8201F01     push 0x11F20F8                           ; ASCII "%d %d"
    011F1012 >  FF15 90201F01   call dword ptr ds:[0x11F2090]            ; msvcr110.printf
    011F1018 >  83C4 0C         add esp,0xC
    011F101B    33C0            xor eax,eax
    011F101D    5D              pop ebp
    011F101E    C3              retn

    现在我们能够看到变量a和b的内存地址分别是0x11F3000和0x11F3004,我们在查看一下相应的内存(内存数据是用十六进制表示的)。

    0x11F3000 >CA 07 00 00 58 00 00 00 01 00 00 00 00 00 00 00  ?..X..........

    在0x11F3000处的“CA070000”就是整数1994的十六进制的“小端方式(Little-endian)”存储值,在0x11F3004处的“58”就是'X'的ASCII码。
    事实上,局部静态变量的初始值是由编译器硬编码到程序中的,随着程序的启动,这些值就随即映射到相应的内存空间里面,也就是说其初始值在程序启动的时候就已经有了。

    由上面两个实例也可以看出,

    TYPE VAR;
    VAR = VALUE;

    TYPE VAR = VALUE;

    所表达的意思其实并不相同。
    前者是声明一个变量,然后再给它赋值;后者是声明一个初始值为多少的变量。对于局部非静态变量,两者的意义虽不同但是实现方法方法是一样的,因为局部非静态变量的初始值不能像静态变量那样,编译的时候就硬编码到程序.data段里面,局部非静态变量必须临时分配含有未知数据的内存空间,然后再赋值才能实现初始值,但是对静态变量而言,就看得出来这两种代码的差异了。
    (当然,在打开编译器的优化选项以后,编译器会对源代码进行灵活处理,那样的话编译器对这两种代码的处理可能是相同的。对于开启优化选项的情况本文暂且不提,正如本文开头所说的那样,本文只是探究编译器是如何按照我们的意思”编译程序的。开启优化选项的情况,以后会专门写一篇博文来探究)

    三.全局(静态)变量的内存分配

    “全局变量”本身其实也是“静态”的,但有些地方喜欢用全称——全局静态变量,为了简单,这里再声明一个约定:下文中均用“全局变量”这个术语。

    实例1

    C源码:

    #include <stdio.h>
    int a=1994;
    int main()
    {
            a=820;
            return 0;
    }

    反汇编结果:

    01031000 >  55              push ebp
    01031001    8BEC            mov ebp,esp
    01031003    C705 00300301 3>mov dword ptr ds:[0x1033000],0x334
    ;将820赋值给变量a
    0103100D    33C0            xor eax,eax
    0103100F    5D              pop ebp
    01031010    C3              retn

    我们很容易就看出,变量a的内存地址为0×1033000
    联想第二节所分析的,全局变量和局部静态变量一样,内存空间是“硬分配”的,其初始值也是“硬编码”的。从底层上看全局变量和局部静态变量确实是一样的,只是编译器在检查代码的时候限制了局部变量的静态访问范围而已,但他们的实现方式和工作方式都相同。

    有第二节和第三节可看出,静态变量(包括全局变量和局部静态变量)所占的内存空间的内存地址是固定的,它们是由编译器硬编码到程序中的,且静态变量所占用的内存空间从程序开始运行就一直被占用。另外,局部静态变量的初始值也是由编译器硬编码到程序中的相应数据段上的,随着程序的运行,这些初始值也随即映射到相应的内存空间里面供程序使用。

    事实上:
    1.编译器对没有显式指定初始值的静态变量,默认是按初始值为0来处理的(这一点大多数C/C++的书都有提到)。
    2.硬编码到程序数据段上的静态变量的初始值,随着程序的运行数据段上的数据被映射到内存中,他们所占用的内存空间正是这些全局变量所用的内存空间,这一切都是编译器事先“计算和设计”好的,所以静态变量的内存地址是不变的。

    四.数组的内存分配

    数组也是一种重要的数据结构,他就是“在一块的多个变量”,类似数学中“集合”的概念。类似但不同,不同点在于,数组的元素没有要求“互异性”,数组的元素可以是相同的值,而且数组所包含的元素的内存地址是连续的。下面我们来看一下VC是如何分配数组所占内存的。

    实例1

    C源码:

    int main()
    {
            int a[50];
            return 0;
    }

    反汇编结果:

    01331000 >/$  55            push ebp
    01331001  |.  8BEC          mov ebp,esp
    01331003  |.  33C0          xor eax,eax
    01331005  |.  5D            pop ebp
    01331006  .  C3            retn

    怎么回事?从反汇编结果来看似乎main函数什么都没做啊?联想到第二节的实例2,因为那个地方也出现了这种情况。进而产生疑问,是不是数组也是静态分配的?为此我们也和之前一样加入一句printf()函数调用,并且向其传入数组的地址。

    C源码:

    #include <stdio.h>
    int main()
    {
            int a[50];
            printf("%d",a);
            return 0;
    }

    反汇编结果:

    00981000 >  55              push ebp
    00981001    8BEC            mov ebp,esp
    00981003    81EC CC000000   sub esp,0xCC
    ;分配204个字节,一个int型是4个字节,数组是由50个int组成的,也就是200字节,
    ;而多分配的4个字节是用于下面的安全检查,这个安全检查是防止缓冲区越界,这不
    ;在本篇文章的讨论范围之内。
    00981009    A1 00309800     mov eax,dword ptr ds:[0x983000]
    0098100E >  33C5            xor eax,ebp
    00981010    8945 FC         mov dword ptr ss:[ebp-0x4],eax
    ;ebp-0x4用于下面安全检查,本文不讨论
    00981013    8D85 34FFFFFF   lea eax,dword ptr ss:[ebp-0xCC]
    ;将ebp-0xCC传入eax
    00981019    50              push eax
    ;将eax压栈,作为printf()的第二个参数。也就是说ebp-0xCC就是数组的内存地址
    ;长度为200字节
    0098101A >  68 F8209800     push 0x9820F8                            ; ASCII "%d"
    0098101F >  FF15 90209800   call dword ptr ds:[0x982090]             ; msvcr110.printf
    ;调用printf()函数
    00981025    83C4 08         add esp,0x8
    00981028 >  33C0            xor eax,eax
    0098102A    8B4D FC         mov ecx,dword ptr ss:[ebp-0x4]
    0098102D    33CD            xor ecx,ebp
    0098102F    E8 04000000     call 00981038                            ; 反汇编分.__security_check_cookie
    ;基于cookie的安全检查
    00981034    8BE5            mov esp,ebp
    00981036    5D              pop ebp
    00981037    C3              retn

    事实证明,刚刚的代码编译出来的程序中,局部非静态数组不是静态分配的,而是动态分配的。
    虽然我们关闭了编译器的优化,但是从实际情况看,如果程序中没有用到这个数组,那么编译器会省略掉对这个数组的内存分配。但从本实例可以看出,编译器对数组的内存分配也很简单:

    TYPE VAR[N];

    就是分配N个TYPE型的变量而已。由于数组是按一个整体来分配的,所以其成员的内存地址是连续的。

    五.结构与对象的内存分配

    首先,我说明一下,为什么我将结构和对象放在一起,原因是,在C++中,结构体已经被扩展为类了,什么?没搞错吧?类和结构是一样的?是的,至少在底层来看,他们是一样的。如果你还不相信,你可以试试下面的代码:

    C源码:

    #include <iostream>
    #include <cstdlib>
     
    struct structa
    {
    public:
        void set(int x,int y);
        int add();
    private:
        int a;
        int b;
     
    };
     
    void structa::set(int x,int y)
    {
        a=x;
        b=y;
    }
     
    int    structa::add()
    {
        return a+b;
    }
     
    using namespace std;
    int main()
    {
        structa as;
        as.set(2,3);
        cout<<as.add()<<endl;
        system("pause");
    }

    是不是发现编译顺利通过了?对的。

    我刚刚说了C++编译器在实现对象和结构的时候,在底层上,两者没有区别,但没有说“在编译阶段他们没有区别”,其实在编译阶段的区别很简单:类的成员默认属性是private,而结构的成员默认属性是public.读者可以自己去尝试。

    正是因为从底层上,C++的编译器对类(对象)和结构的处理没有区别,所以下面的分析以类(对象)为准,好了,步入正题。

    实例一

    C源码:

    #include <cstdlib>
     
    class classA
    {
    public:
        int a;
        int b;
     
    private:
        int c;
        int d;
     
    };
     
    int main()
    {
        classA var;
        var.a=1;
        var.b=2;
        system("pause");
    }

    反汇编结果:

    000F1F50  /$  55            push ebp
    000F1F51  |.  8BEC          mov ebp,esp
    000F1F53  |.  83EC 10       sub esp,0x10
    ;分配16字节的内存
    000F1F56  |.  8D4D F0       lea ecx,dword ptr ss:[ebp-0x10]
    000F1F59  |.  E8 32F3FFFF   call 000F1290                            ;  反汇编分.000F1290
    000F1F5E  |.  C745 F0 01000>mov dword ptr ss:[ebp-0x10],0x1
    ;将1赋值给成员a
    000F1F65  |.  C745 F4 02000>mov dword ptr ss:[ebp-0xC],0x2
    ;将2赋值给成员b
    000F1F6C  |.  68 08310F00   push 0xF3108                             ; /command = "pause"
    000F1F71  |.  FF15 BC300F00 call dword ptr ds:[0xF30BC]              ; system
    000F1F77  |.  83C4 04       add esp,0x4
    000F1F7A  |.  8D4D F0       lea ecx,dword ptr ss:[ebp-0x10]
    000F1F7D  |.  E8 AEF3FFFF   call 000F1330                            ;  反汇编分.000F1330
    000F1F82  |.  33C0          xor eax,eax
    000F1F84  |.  8BE5          mov esp,ebp
    000F1F86  |.  5D            pop ebp
    000F1F87  .  C3            retn

    从上面结果看,进入main函数以后,首先移动栈顶分配16个字节的内存空间用来存放var对象的a、b、c、d四个整数型成员,这和分配数组的内存空间的方法差不多。还有就是public和private属性在底层根本没有体现出来,原因是这些属性只是在编译阶段检查和约束访问权限,也就是说这只是编译器在编译的时候对代码进行检查,如果发现“违规”访问,然后就报告错误并且终止编译,而编译后,在底层是没有这个约束的。

    但是我们知道,数组成员的类型必须相同,结构和类成员的类型可以不同,这就意味着结构和类(对象)的成员长度可能“参差不齐”,那么这会导致什么现象呢?

    实例二

    C源码:

    #include <cstdlib>

    class classA
    {
    public:
        short a;
        int b;
        short c;
        int d;

    };

    int main()
    {
        classA var;
        var.a=1;
        var.b=2;
        var.c=3;
        var.d=4;
        system("pause");
    }

    反汇编结果:

    01141000  /$  55            push ebp
    01141001  |.  8BEC          mov ebp,esp
    01141003  |.  83EC 14       sub esp,0x14
    ;分配20字节内存空间
    01141006  |.  A1 00301401   mov eax,dword ptr ds:[0x1143000]
    0114100B  |.  33C5          xor eax,ebp
    0114100D  |.  8945 FC       mov dword ptr ss:[ebp-0x4],eax
    ;前面4个字节用于安全检查
    01141010  |.  B8 01000000   mov eax,0x1
    01141015  |.  66:8945 EC    mov word ptr ss:[ebp-0x14],ax
    ;var.a=1;
    01141019  |.  C745 F0 02000>mov dword ptr ss:[ebp-0x10],0x2
    ;var.b=2;
    01141020  |.  B9 03000000   mov ecx,0x3
    01141025  |.  66:894D F4    mov word ptr ss:[ebp-0xC],cx
    ;var.c=3;
    01141029  |.  C745 F8 04000>mov dword ptr ss:[ebp-0x8],0x4
    ;var.d=4;
    01141030  |.  68 B8201401   push 0x11420B8                           ; /command = "pause"
    01141035  |.  FF15 90201401 call dword ptr ds:[0x1142090]            ; system
    0114103B  |.  83C4 04       add esp,0x4
    0114103E  |.  33C0          xor eax,eax
    01141040  |.  8B4D FC       mov ecx,dword ptr ss:[ebp-0x4]
    01141043  |.  33CD          xor ecx,ebp
    01141045  |.  E8 04000000   call 0114104E                            ;  反汇编分.0114104E
    ;安全检查
    0114104A  |.  8BE5          mov esp,ebp
    0114104C  |.  5D            pop ebp
    0114104D  .  C3            retn

    注意,在32位系统下,int是4字节,short是2字节,所以按道理a、b、c、d四个成员合起来应该是12字节,从上面反汇编的结果看总共分配了20字节内存,除去用于安全检查的4字节,为什么分配了16字节的内存呢?这里有一个内存对齐的问题,我的VC11(VS2012)默认是4字节对齐,所以会进行如下处理:

    a是short型,长度为2字节,所以直接分配2字节用来存放a;此时总共分配了2字节

    b是int型,长度为4字节,但是由于要按4字节进行内存对齐,而前面的a只占用了2字节,不满足4的倍数,所以要在a后面分配2字节,这样才能满足内存对齐,然后再分配4字节来存放b;此时总共分配了8字节

    c是short型,长度为2字节,由于前面总共分配了8字节,满足内存对齐,所以直接分配2字节来保存c;此时总共分配了10字节;

    d是int型,长度为4字节,和b类似,需要先补齐2字节,然后再分配4字节用于保存d;此时总共分配了16字节。

    16字节就是这么来的,多分配的就是用于内存对齐了。至于为什么要内存对齐,是为了提高CPU工作效率。

  • 相关阅读:
    python之常用内置函数
    python基础之文件操作
    简洁版三级菜单
    JS 事件代理
    捕获当前事件作用的对象event.target和event.srcElement
    【javascript 技巧】谈谈setTimeout的作用域以及this的指向问题
    JSON详解
    多线程小例子
    jquery中attr和prop的区别
    django 过滤器
  • 原文地址:https://www.cnblogs.com/xiao-fang/p/3344547.html
Copyright © 2011-2022 走看看