编译器1
2013-09-28 19:02
211 查看
第一部分:变量的内存分配
在介绍之前,先说明一下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>
intmain()
{
charch='a';
return0;
}
反汇编结果:
00011000>/$55pushebp
;将原来寄存器ebp的值入栈
00011001|.8BECmovebp,esp
;将寄存器esp的值传送到ebp
00011003|.51pushecx
;将寄存器ecx的值入栈
00011004|.C645FF61movbyteptrss:[ebp-0x1],0x61
;将字符'a'的ASCII码0x61传送到内存地址为ebp-0x1的内存空间里,
;这个内存空间就是变量ch的内存空间,注意到这个内存空间是怎么
;来的,后面详细解释
00011008|.33C0xoreax,eax
;VC的编译器通常将寄存器EAX作为返回值,对eax自身进行异或运算
;结果为0,于是就对应源码里面的return0;
0001100A|.8BE5movesp,ebp
0001100C|.5Dpopebp
;上面两句与函数开头的两句指令相对应,作用是恢复函数调用前寄
;存器的值,清理现场。
0001100D.C3retn
;返回
大部分指令都好理解,唯一不太好理解的是“ch变量的内存空间是怎么来的?”。
下面做一个假设来模拟运行上诉指令:
假设刚进入main函数的时候,esp的值为0025F9E8,即此时栈顶的地址为0025F9E8
pushebp
这时esp的值减去4变为0025F9E4,因为寄存器ebp是4个字节长度
这里要说明一下实际压栈的原理,CPU实际的压栈操作并不是真的把数据往下面“压”,而是在栈顶的“上面”添加数据,再把esp寄存器减去相应的长度数值(栈是中的数据是越靠近栈顶,内存地址越低,所以减少esp的值就相当于“升高”栈顶),这样就“变相”地完成了“压栈”操作了。这是一种灵活的处理方法,毕竟如果真是“压”的话,要将后面的数据全部往下面移动,性能开销太大了。
movebp,esp
将esp现在的值传送给ebp,所以ebp保存着当前esp的值0025F9E4
pushecx
将ecx的值“压”到栈顶,此时esp-4,所以esp的值是0025F9E0,ebp的值仍然是0025F9E4
movbyteptrss:[ebp-0x1],0×61
此时实际上就是把'a'的ASCII码0×61保存到0025F9E4-1=0025F9E3。这个0025F9E3就是变量ch的内存地址。
好了,到此为止,看明白了么?那句pushecx就是分配ch变量的内存的关键,其实ecx的值是无关数据,压栈的目的不是为了临时保存ecx的值,而是将栈“空”四个字节出来,即给ch分配内存空间,共分配了4个字节(0025F9E0-0025F9E3),但实际上变量ch只用了1个字节,但区区浪费3个字节无所谓啦,毕竟只是短时间占用,没什么影响的。接下来看另一个例子。
实例2
C源码:#include<stdio.h>
intmain()
{
inta=100;
intb=200;
intc=300;
intd=400;
return0;
}
反汇编结果:
00EE1000>55pushebp
00EE10018BECmovebp,esp
00EE100383EC10subesp,0x10
;将esp减去0x10,即减去16
;在实例1的时候就讲过CPU压栈的原理,这里
;将esp减去16,就是将栈顶“向上移动”16个字节
;就相当于在栈顶预留16个字节
;这16个字节就是给a、b、c、d这四个int变量分配的内存空间
00EE1006C745FC6400000>movdwordptrss:[ebp-0x4],0x64
;a=100
;将0x64,也就是十进制的100保存到内存地址为ebp-0x4的内存空间
;这和实例1的处理方法相同,请参照实例1后面的说明
;都是用ebp的值作为基址来寻址变量内存空间的
00EE100DC745F8C800000>movdwordptrss:[ebp-0x8],0xC8
;b=200
00EE1014C745F42C01000>movdwordptrss:[ebp-0xC],0x12C
;c=300
00EE101BC745F09001000>movdwordptrss:[ebp-0x10],0x190
;d=400
;这四句指令类似,只是通过不同的偏移来寻址到各自的内存空间
00EE102233C0xoreax,eax
;将eax清零,作为返回值
00EE10248BE5movesp,ebp
00EE10265Dpopebp
;这两句指令用于还原寄存器的值,使其值恢复到调用函数之前,
00EE1027C3retn
;返回
实例2和实例1不同的地方在于:实例1是通过对ecx压栈来分配内存,实例2是直接通过减掉寄存器esp的值来移动栈顶来分配内存。
相同的地方在于:局部非静态变量都是在栈上分配的内存空间,函数执行完以后,esp的值被还原成执行函数之前的值,就相当于释放了函数运行过程中占用的栈,这也是为什么局部非静态变量在函数执行结束后会数据会丢失的原因。
经过试验,
当函数里的局部非静态变量总大小小于等于4字节的时候,编译器会采取pushecx的方法分配这四个字节;
当函数里的局部非静态变量总大小大于4字节的时候,编译器会采取subesp,0xXX的方法来分配这些变量的内存。
这样做的目的是减少指令长度,因为pushecx对应的机器指令只有1个字节长度,而subesp,0xXX对应的机器指令则有3个字节。
有人又会问,局部非静态变量总大小为8字节的时候,为什么不采取连续两次pushecx的方法分配8字节内存呢?两次pushecx需要2字节,但也比3字节少啊?我个人认为这里有个平衡问题。
pushecx
CPU执行的时候,实际上将它分为两步
subesp,0x4
mov[esp],ecx
本来这句mov[esp],ecx指令就是没有什么用处的东西,我们根本不需要保存ecx的值,我们现在需要的仅仅只是将栈顶“上移”也就是subesp,0×4,如果两次pushecx就会做两次无用功。而且相对于访问CPU寄存器而言,访问内存效率要低得多(CPU访问内存必须经过总线),如果把这些因素考虑在内,为了达到性能和大小的平衡,两次pushecx还不如用subesp,0×8
二.局部静态变量的内存分配
实例1
C源码:#include<stdio.h>
intmain()
{
staticinta;
staticcharb;
a=1994;
b='X';
return0;
}
反汇编结果:
001A1000>55pushebp
001A10018BECmovebp,esp
001A1003C70520301A00C>movdwordptrds:[0x1A3020],0x7CA
;将1994赋值给a
001A100DC60524301A005>movbyteptrds:[0x1A3024],0x58
;将'X'赋值给b
001A101433C0xoreax,eax
001A10165Dpopebp
001A1017C3retn
很明显,变量a和b的内存地址分别是0x1A3020和0x1A3024。由此看出局部静态变量所占内存空间的内存地址是固定的。
实例2
C源码:#include<stdio.h>
intmain()
{
staticinta=1994;
staticcharb='X';
return0;
}
反汇编结果:
00961000>55pushebp
009610018BECmovebp,esp
0096100333C0xoreax,eax
009610055Dpopebp
00961006C3retn
奇怪!怎么没有赋值过程啊?从反汇编结果来看在main函数里面似乎什么都没有做。确实,什么都没有做!这也是局部静态变量和局部非静态变量初始化值的方式的差异。为了继续探究,我们在原来的代码中加入一句printf()来找到这两个变量的内存地址。
修改后的C源码如下:
#include<stdio.h>
intmain()
{
staticinta=1994;
staticcharb='X';
printf("%d%d",&a,&b);
return0;
}
反汇编结果:
011F1000>55pushebp
011F10018BECmovebp,esp
011F10036804301F01push0x11F3004
011F10086800301F01push0x11F3000
011F100D68F8201F01push0x11F20F8;ASCII"%d%d"
011F1012>FF1590201F01calldwordptrds:[0x11F2090];msvcr110.printf
011F1018>83C40Caddesp,0xC
011F101B33C0xoreax,eax
011F101D5Dpopebp
011F101EC3retn
现在我们能够看到变量a和b的内存地址分别是0x11F3000和0x11F3004,我们在查看一下相应的内存(内存数据是用十六进制表示的)。
0x11F3000>CA070000580000000100000000000000?..X..........
在0x11F3000处的“CA070000”就是整数1994的十六进制的“小端方式(Little-endian)”存储值,在0x11F3004处的“58”就是'X'的ASCII码。
事实上,局部静态变量的初始值是由编译器硬编码到程序中的,随着程序的启动,这些值就随即映射到相应的内存空间里面,也就是说其初始值在程序启动的时候就已经有了。
由上面两个实例也可以看出,
TYPEVAR;
VAR=VALUE;
和
TYPEVAR=VALUE;
所表达的意思其实并不相同。
前者是声明一个变量,然后再给它赋值;后者是声明一个初始值为多少的变量。对于局部非静态变量,两者的意义虽不同但是实现方法方法是一样的,因为局部非静态变量的初始值不能像静态变量那样,编译的时候就硬编码到程序.data段里面,局部非静态变量必须临时分配含有未知数据的内存空间,然后再赋值才能实现初始值,但是对静态变量而言,就看得出来这两种代码的差异了。
(当然,在打开编译器的优化选项以后,编译器会对源代码进行灵活处理,那样的话编译器对这两种代码的处理可能是相同的。对于开启优化选项的情况本文暂且不提,正如本文开头所说的那样,本文只是探究编译器是如何“按照我们的意思”编译程序的。开启优化选项的情况,以后会专门写一篇博文来探究)
三.全局(静态)变量的内存分配
“全局变量”本身其实也是“静态”的,但有些地方喜欢用全称——全局静态变量,为了简单,这里再声明一个约定:下文中均用“全局变量”这个术语。实例1
C源码:#include<stdio.h>
inta=1994;
intmain()
{
a=820;
return0;
}
反汇编结果:
01031000>55pushebp
010310018BECmovebp,esp
01031003C705003003013>movdwordptrds:[0x1033000],0x334
;将820赋值给变量a
0103100D33C0xoreax,eax
0103100F5Dpopebp
01031010C3retn
我们很容易就看出,变量a的内存地址为0×1033000
联想第二节所分析的,全局变量和局部静态变量一样,内存空间是“硬分配”的,其初始值也是“硬编码”的。从底层上看全局变量和局部静态变量确实是一样的,只是编译器在检查代码的时候限制了局部变量的静态访问范围而已,但他们的实现方式和工作方式都相同。
有第二节和第三节可看出,静态变量(包括全局变量和局部静态变量)所占的内存空间的内存地址是固定的,它们是由编译器硬编码到程序中的,且静态变量所占用的内存空间从程序开始运行就一直被占用。另外,局部静态变量的初始值也是由编译器硬编码到程序中的相应数据段上的,随着程序的运行,这些初始值也随即映射到相应的内存空间里面供程序使用。
事实上:
[b]1.编译器对没有显式指定初始值的静态变量,默认是按初始值为0来处理的(这一点大多数C/C++的书都有提到)。
2.硬编码到程序数据段上的静态变量的初始值,随着程序的运行数据段上的数据被映射到内存中,他们所占用的内存空间正是这些全局变量所用的内存空间,这一切都是编译器事先“计算和设计”好的,所以静态变量的内存地址是不变的。[/b]
四.数组的内存分配
数组也是一种重要的数据结构,他就是“在一块的多个变量”,类似数学中“集合”的概念。类似但不同,不同点在于,数组的元素没有要求“互异性”,数组的元素可以是相同的值,而且数组所包含的元素的内存地址是连续的。下面我们来看一下VC是如何分配数组所占内存的。实例1
C源码:intmain()
{
inta[50];
return0;
}
反汇编结果:
01331000>/$55pushebp
01331001|.8BECmovebp,esp
01331003|.33C0xoreax,eax
01331005|.5Dpopebp
01331006\.C3retn
怎么回事?从反汇编结果来看似乎main函数什么都没做啊?联想到第二节的实例2,因为那个地方也出现了这种情况。进而产生疑问,是不是数组也是静态分配的?为此我们也和之前一样加入一句printf()函数调用,并且向其传入数组的地址。
C源码:
#include<stdio.h>
intmain()
{
inta[50];
printf("%d",a);
return0;
}
反汇编结果:
00981000>55pushebp
009810018BECmovebp,esp
0098100381ECCC000000subesp,0xCC
;分配204个字节,一个int型是4个字节,数组是由50个int组成的,也就是200字节,
;而多分配的4个字节是用于下面的安全检查,这个安全检查是防止缓冲区越界,这不
;在本篇文章的讨论范围之内。
00981009A100309800moveax,dwordptrds:[0x983000]
0098100E>33C5xoreax,ebp
009810108945FCmovdwordptrss:[ebp-0x4],eax
;ebp-0x4用于下面安全检查,本文不讨论
009810138D8534FFFFFFleaeax,dwordptrss:[ebp-0xCC]
;将ebp-0xCC传入eax
0098101950pusheax
;将eax压栈,作为printf()的第二个参数。也就是说ebp-0xCC就是数组的内存地址
;长度为200字节
0098101A>68F8209800push0x9820F8;ASCII"%d"
0098101F>FF1590209800calldwordptrds:[0x982090];msvcr110.printf
;调用printf()函数
0098102583C408addesp,0x8
00981028>33C0xoreax,eax
0098102A8B4DFCmovecx,dwordptrss:[ebp-0x4]
0098102D33CDxorecx,ebp
0098102FE804000000call00981038;反汇编分.__security_check_cookie
;基于cookie的安全检查
009810348BE5movesp,ebp
009810365Dpopebp
00981037C3retn
事实证明,刚刚的代码编译出来的程序中,局部非静态数组不是静态分配的,而是动态分配的。
虽然我们关闭了编译器的优化,但是从实际情况看,如果程序中没有用到这个数组,那么编译器会省略掉对这个数组的内存分配。但从本实例可以看出,编译器对数组的内存分配也很简单:
TYPEVAR ;
就是分配N个TYPE型的变量而已。由于数组是按一个整体来分配的,所以其成员的内存地址是连续的。
五.结构与对象的内存分配
首先,我说明一下,为什么我将结构和对象放在一起,原因是,在C++中,结构体已经被扩展为类了,什么?没搞错吧?类和结构是一样的?是的,至少在底层来看,他们是一样的。如果你还不相信,你可以试试下面的代码:C源码:
#include<iostream>
#include<cstdlib>
structstructa
{
public:
voidset(intx,inty);
intadd();
private:
inta;
intb;
};
voidstructa::set(intx,inty)
{
a=x;
b=y;
}
intstructa::add()
{
returna+b;
}
usingnamespacestd;
intmain()
{
structaas;
as.set(2,3);
cout<<as.add()<<endl;
system("pause");
}
是不是发现编译顺利通过了?对的。
我刚刚说了C++编译器在实现对象和结构的时候,在底层上,两者没有区别,但没有说“在编译阶段他们没有区别”,其实在编译阶段的区别很简单:类的成员默认属性是private,而结构的成员默认属性是public.读者可以自己去尝试。
正是因为从底层上,C++的编译器对类(对象)和结构的处理没有区别,所以下面的分析以类(对象)为准,好了,步入正题。
实例一
C源码:#include<cstdlib>
classclassA
{
public:
inta;
intb;
private:
intc;
intd;
};
intmain()
{
classAvar;
var.a=1;
var.b=2;
system("pause");
}
反汇编结果:
000F1F50/$55pushebp
000F1F51|.8BECmovebp,esp
000F1F53|.83EC10subesp,0x10
;分配16字节的内存
000F1F56|.8D4DF0leaecx,dwordptrss:[ebp-0x10]
000F1F59|.E832F3FFFFcall000F1290;反汇编分.000F1290
000F1F5E|.C745F001000>movdwordptrss:[ebp-0x10],0x1
;将1赋值给成员a
000F1F65|.C745F402000>movdwordptrss:[ebp-0xC],0x2
;将2赋值给成员b
000F1F6C|.6808310F00push0xF3108;/command="pause"
000F1F71|.FF15BC300F00calldwordptrds:[0xF30BC];\system
000F1F77|.83C404addesp,0x4
000F1F7A|.8D4DF0leaecx,dwordptrss:[ebp-0x10]
000F1F7D|.E8AEF3FFFFcall000F1330;反汇编分.000F1330
000F1F82|.33C0xoreax,eax
000F1F84|.8BE5movesp,ebp
000F1F86|.5Dpopebp
000F1F87\.C3retn
从上面结果看,进入main函数以后,首先移动栈顶分配16个字节的内存空间用来存放var对象的a、b、c、d四个整数型成员,这和分配数组的内存空间的方法差不多。还有就是public和private属性在底层根本没有体现出来,原因是这些属性只是在编译阶段检查和约束访问权限,也就是说这只是编译器在编译的时候对代码进行检查,如果发现“违规”访问,然后就报告错误并且终止编译,而编译后,在底层是没有这个约束的。
但是我们知道,数组成员的类型必须相同,结构和类成员的类型可以不同,这就意味着结构和类(对象)的成员长度可能“参差不齐”,那么这会导致什么现象呢?
实例二
C源码:#include<cstdlib>
classclassA
{
public:
shorta;
intb;
shortc;
intd;
};
intmain()
{
classAvar;
var.a=1;
var.b=2;
var.c=3;
var.d=4;
system("pause");
}
反汇编结果:
01141000/$55pushebp
01141001|.8BECmovebp,esp
01141003|.83EC14subesp,0x14
;分配20字节内存空间
01141006|.A100301401moveax,dwordptrds:[0x1143000]
0114100B|.33C5xoreax,ebp
0114100D|.8945FCmovdwordptrss:[ebp-0x4],eax
;前面4个字节用于安全检查
01141010|.B801000000moveax,0x1
01141015|.66:8945ECmovwordptrss:[ebp-0x14],ax
;var.a=1;
01141019|.C745F002000>movdwordptrss:[ebp-0x10],0x2
;var.b=2;
01141020|.B903000000movecx,0x3
01141025|.66:894DF4movwordptrss:[ebp-0xC],cx
;var.c=3;
01141029|.C745F804000>movdwordptrss:[ebp-0x8],0x4
;var.d=4;
01141030|.68B8201401push0x11420B8;/command="pause"
01141035|.FF1590201401calldwordptrds:[0x1142090];\system
0114103B|.83C404addesp,0x4
0114103E|.33C0xoreax,eax
01141040|.8B4DFCmovecx,dwordptrss:[ebp-0x4]
01141043|.33CDxorecx,ebp
01141045|.E804000000call0114104E;反汇编分.0114104E
;安全检查
0114104A|.8BE5movesp,ebp
0114104C|.5Dpopebp
0114104D\.C3retn
注意,在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工作效率。
相关文章推荐
- 设置Vim的errorformat以支持mxmlc编译器
- CLion最好用的编译器&Xcode
- 编译器错误~写JS还是谨慎点好
- ARM编译器中对齐的使用__align和__packed
- Ue + Devc++ 打造简单C/C++ 编译器
- C/C++ 编译器优化
- 配置vim为php开发编译器增加导航自动提示
- 用25行JavaScript语句实现一个简单的编译器
- 编译器和链接器的工作过程
- 自制编译器第十四章笔记
- 关于CC2530的IAR工程编译器相关
- codeblock中没有gcc编译器的解决方案
- 如何查看Linux GCC编译器里预定义的宏
- 在命令行中使用vs编译器
- 在线编译器的使用
- 在Linux安装Fortran编译器
- 编译器对全局变量的处理____AV_COPY128比memcpy快
- JUnit学习笔记-IDEA编译器中JUnit的安装和使用
- 4tH 3.62 发布,Forth 编译器
- 百度UEditor编译器中添加HTML最快捷的办法