内存的理解
内存可以说是C和C++语言学习的关键点。
这里说一点我的理解,一家之言,欢迎拍砖哈。
内存要想理解透彻,首先要理解内存编址。即不同的内存条,内存模块,插到机器上,具体对应的内存地址是多少。
最开始的PC机,IBM PC XT,只有640k内存。IBM是这么规划的,最低的128k,是BIOS的地址,毕竟BIOS也是汇编语言,它也需要合法地址,才能被CPU正确运行。
512k~640k,被定义为端口映射地址,即这部分地址,可能对应某一个外设上的地址,方便程序直接访问设备。其中,最重要的,就是显卡,当初的显卡单元都比较小,如单色显卡只有2k显存,就映射在这个地址段。
呵呵,当年比尔盖茨,开了个世纪有名的黄腔,就是家用电脑,640k内存足以。就是这么来的,可现在呢?你的显存都不止640k,可见,伟人也有犯浑的时候。
这样显然不方便,因为就在几年后,286时代,内存已经1M了。
这就麻烦了,这1M的第一个128k不准用,中间还有个128k不准用,好端端的连续内存,就被切成两块,痛苦啊。
而那会CPU也是笨得可以,16位的CPU,Intel居然在PC XT上面用的是8位地址总线,这下死翘了,所有的内存被分割为64k一块块的小块,叫段。每个程序模块,必须小于64k,否则没法跳转。以前DOS下可以执行的二进制文件分为两种,com和exe,com就是不能大于64k的,因为它文件格式里面没有段修饰,因此,只能一段完成,64k。
这样,程序员十分的不方便,写程序,稍微大点的数组,就要考虑分段访问,没办法,数组下标不能超过64k。
另外,对于640k以上的地址访问,人们想出了一个很笨的方法,把640k~1024k这384k,也切成一块一块的,每块映射相同的内存区域,靠一个IO切换来访问不同的块。那会还没有想到更好的支持1M以上内存的方法,只能这么办。
这样,程序员成了个苦恼的职业,既要做软件编程,还要随时关注自己的数据是否越界,痛苦死了。当然,程序员也不会坐以待毙,这期间,他们自己想了很多方法,比如用底层模块来解决段切换问题,对上提供一层连续编址的虚拟地址来访问等等。
ok,到了80386,32位了,大家总算长出了一口气,这个CPU地址总线有32位,可以直接编址4G内存。
但是,问题来了,PC机已经做成这个样子了。当然,可以重新开发一款计算机,连续编址,4G内存,很爽,只是,这不是PC机了。
这又让人痛苦死了,明明有能力做连续编址,跨越段界限,但还是不能这么做,因为要支持老的程序。
程序员又开始想办法,基本思路就是用虚拟地址来代替实际地址,后来又想到了,既然都是虚拟的,那我们可不可以把一块磁盘文件也虚拟成内存,这样,我们给够4G,这不是更爽,底层再用一定算法来处理动态转换的效率优化。
这样,在93年左右吧,有一个很有名的C++语言,Watcom C++出世了。这个语言,是Dos下第一款支持4G内存的C++语言。这是Sybase开发的,它底层使用了一个虚拟内存管理模块,是另外一家公司开发的,叫Dos /4G,显然,就是DOS程序使用4G内存的解决方案。
还记得DOOM、C&C,红警,金庸群侠传不?当程序一运行,就会显示一行字“DOS /4GW ...”,这就是Watcom C++写的游戏。因为游戏界是最需要大内存的,贴图,声音的处理,都需要大数组,如果分块使用,程序员太累了,做不下来这么大的程序。
嗯,DOS/4GW,是DOS 4G的Watcom C++版本,因为真正的模块要收费的,这个版本是简化版,只能支持到256M内存,且不支持磁盘虚拟内存,不过不收费。不过,那个时代的应用也够了。
我以前写过Watcom C++的程序,呵呵,真的很爽。再也不考虑段指针之类的东东了,爽翻了。
后来就多了,gcc很早就有了的,99年的时候,gcc进攻DOS市场,出了个版本叫DJgpp,比Watcom C++还好用,我一用就爱上了,当时还把它的库函数手册翻译了一遍,算学习了。
不过,这个时间点,Windows95早出来了,Windows98也出来了,因此,DOS程序趋于没落。
早在Windows 1.1开发的时候(最早一个Windows版本,1.0没发布),微软就知道,以后的操作系统要做到程序员友好才有生命力,而内存连续编址,就是最大的程序员友好。
因此,从一开始,Windows就使用了底层的内存支持技术,当Windows 3.1出世,其实Windows下开发程序,段的限制已经不是很明显了。
到Windows95,微软更是直接内置了类似于Dos /4GW之类的32位内存管理器,并内部直接包含虚拟内存和物理内存的自动切换算法,因此,从Windows 95以后,大家再开发程序,已经可以使用理论上长达4G的大数组了。
内存争议,至此告一段落。
现今32位的Windows系统,普遍支持4G内存,但,应用程序的空间只有2G。编址为低端地址,即0~2G的地址,为什么呢,因为高2G被系统占用,毕竟Windows系统那么多服务,也要运行,也需要地址空间。
Windows使用了类似切页的内存控制机制,每个应用程序,有2G的地址空间,上面2G是所有应用程序和系统共用。
呵呵,不止Windows,32位的Linux也有类似的设计。
因此,一个Windows应用程序最大能使用的内存,只有2G。
前面说的Ring0级的系统内核,一般都占用上2G的地址空间在运行。动态链接库dll,控件OCX,在调入内存中,由于要被多个进程看到,进程间重用,也是占用上面2G在运行。
这里就要说说钩子了,当我们想对一个应用程序下钩子,由于我们的程序和要勾的程序不在一个进程空间,因此,我们看到的内存是不一样的。就是它在20000这个地址单元看到的可能是个FF,而我们看到的可能是个00,因为这仅仅是逻辑地址一样,物理地址分属两个进程空间。
因此,如果要钩对方的消息,有个问题,钩到了,咋送回来?
一般的做法就是做个dll做中转站,钩子够到了,调用dll的函数,先存放到高端内存区,然后我们的程序再定时过去取,或者用回调什么的。
总之,如果要跨进程通讯,两个进程的共享内存区,一定是建立在高端的内存,就是2G以上的空间。
至于应用程序自己这2G,就看编译器咋使用了,一般都是,低端为栈空间,我们的函数代码,每次call一个函数,函数内部新建立的内部变量,是从低端向高处排。
而堆,则是从高处向底处排,啥时候,两个碰上了,啥时候,内存就满了,无法申请内存了。
栈又分为基栈和浮动栈,基栈就是编译期间就分配好了的内存。
全局变量,const的常量,static的变量,函数的代码,都是这部分。
浮动栈就是运行期间,根据函数,对象调用关系,动态分配的栈,类成员变量,函数内部变量,都是用的浮动栈。
void Func(void)
{
char i=0;
char* pBuffer=malloc(10);
//...
}
这里面,Func的代码,在栈空间,其实是在基栈了。2G的最低
int i,这个i,在浮动栈。基站上方,也还是2G底部。
pBuffer指向的内存,由于是malloc,因此是堆空间,在2G的高端。
new和malloc其实是一样的,都是malloc的,但new支持对象,要自动调用构造函数。
不过这里也说明一点,new出来的对象,是运行期对象,其内部成员变量,其实是在2G的高端,堆空间里面。
写C和C++程序,需要对内存的分配非常敏感,随时关注自己使用的变量,是属于编译期间的基栈,还是运行期的浮动栈,还是堆。
比如,我们要启动一个线程,这个线程函数,肯定是在基栈了,编译器就定好的,但是我们希望线程访问一个运行期的动态地址,比如要传递一个参数给它。
我们就不能简单把一个函数内部的变量地址传给他,由于是函数内部变量属于浮动栈,函数返回,浮动栈就自动拆除,而线程启动是异步运算,就是一个函数启动线程,很可能这个函数已经返回了,线程还没有开始运行。
因此,就绝对不能使用函数内部变量给线程传参,只能使用堆空间,用malloc的一块地址来传参数,再由线程函数收到后,free掉。
这是唯一一个,不遵守“谁分配,谁释放”原则的特例。我把它叫做“远堆传参”。
很多初学线程的朋友,线程写出来就挂掉,就是这个地方出了问题。
但是,程序表面看起来一切正常,呵呵,所以内存很重要,因为它里面基本上都是隐式bug,很难用肉眼看代码看出来。