MS-CRT的malloc以及MS的HeapAlloc--本质基础上的改进

时间:2022-07-13 03:19:14

微软的CRT实现了malloc,但是阅读源代码之后发现竟然是如此简单,debug版本的还有点意思,release版本的几乎就是每次调用首先将一个数据头的长度附加于所需长度其上,然后调用HeapAlloc,成功后将该头带领的结构体一同链接进一个全局的链表,free的时候将该元素从全局链表摘除,然后调用HeapFree,这么简单也太不像微软的作风了吧,一点分配策略都没有体现,仅仅是将HeapXXX做了一个包装,俨然成为了一系列标准C函数。

其实windows的用户空间内存管理使用的机制都在dll中实现,标准c本身就是一个包装,很多意义上为了跨平台,linux的glibc实现了malloc,而windows中对应的实现模块就是kernel32.dll,不管是glibc的linux方式还是kernel32.dll的windows方式都是在用户空间实现的内存管理方式,多数情况也就是堆管理方式,其实堆只在用户空间有意义,而栈对内核和用户空间都有意义,glibc一直在努力向posix靠拢,而kernel32.dll却始终只封装自己独有的win32的api实现,这显然是两种截然不同的方式,glibc趋向于开放而kernel32.dll的实现却只能靠别的方式得到。

不管怎么说,kernel32.dll实现了堆的管理,只要通过HeapAlloc和HeapFree来体现的,对比AT&T的实现,发现其基本的原理并没有改变,本质没有变,仍然是维护一个全局的空闲链表,将分配出去的内存块从空闲链表摘除,释放的时候在重新加回空闲链表之前要确定一下能否和相邻的块合并,本质就是这样,windows的实现并没有变,windows实现的HeapAlloc改进的地方在于细化了全局的空闲链表,不再像AT&T那样的方式完全随意分配和随意回收,而是加入了固定的大小的子链表,其实就是一个吊链结构,类似于linux内核的伙伴系统,全局上有一个链表,该链表的元素是一个子链表,该全局链表一共N个元素,第0个元素代表的子链表中元素是不确定大小的大内存块,也就是都大于一个值,但是不确定具体是多少,这个子链表用于分配稍微大一些的内存块,以下对于从第1个元素到第N-1个元素的每一个元素索引i,该元素代表的子链表代表的是大小为i*8个字节的内存块组成的空闲链表,实际上这才是真正的空闲链表,整个系统中拥有N条空闲链表,和AT&T的实现一样,被分配出去的内存块将不再留在空闲链表,那么就可以由数据区来保存空闲链表的前向和后向索引指针,因为数据区因为对齐因素最小8个字节,一个指针4个字节正好够两个指针使用,这样的话就不必另外开辟空间保存前向后向指针了,节省了空间开销,这一点上要比AT&T的有进步,AT&T的数据结构虽然巧妙,但是考虑到被使用的非空闲块,next指针实际上没有什么用,不过带来的算法的简便足以弥补这点不起眼的损耗。为了在回收的时候可以尝试和相邻的内存块合并,需要一个头部来保存该内存块的信息,比如是否空闲,谁和自己相邻以及相邻块的大小等等,是否空闲只要是说检查相邻的块是否空闲,因此寻找谁是和自己相邻的块就是必须要解决的问题了,主要就是要找到这个 块的头部,这个问题初看起来很困难,实际上很简单,只要知道堆就是由一个一个的内存块组成的并且是在虚拟内存中顺序排列开的就可以了,只是将所有的空闲内存块连接成了所谓的逻辑链表,即使一块内存被分配了,它的位置也还是不会变化的,那么既然所有内存块是顺序排列开的,只要知道相邻块的大小和自己的大小就可以找到相邻块了,要找前面的一块,就需要前面一块的大小,这样当前块的地址减去前面一块的大小,再减去头的大小就是前一块的头部了,而要是找后面的一块,很简单只需要将指针往后移动一个头的大小和当前块的大小就可以了,找到了相邻的块,是否空闲的信息就保留在块的头部,头部当然还有别的有用的信息,如果空闲,那么就可以合并了,合并了之后就是一个新的空闲块了,大小为两个合并块的大小总和加上一个头部的大小,然后再根据新的大小确定将之加入到哪一个全局链表的子链表,如果足够大了,那就加入到0号元素代表的链表,如果不够大并且大小为M,那么就加入到第M/8号元素代表的链表,分配过程自然要比释放简单,没有那么多整理操作,就是一个简单的查找过程,如果需要的内存块太大,那么从0号元素的链表分配,如果比较小就从相应大小链表分配,如果对应大小链表没有空闲内存块,那么就在稍微大一号的链表搜索,直到找到为止,然后分出去自己需要的大小的那个块,将剩下的重新按照其大小排入相应大小的空闲链表,注意以上操作都有对齐操作,这样会更加简单和高效,如果实在没有找到空闲内存,那么就只有向操作系统索要了,也就是扩展堆的大小。

windows的debug版本的crt对堆的保护采用了一种很傻很天真的做法,就是分配的时候在内存块的末尾加入固定长度并且固定数据的数据,然后在释放的时候检查这些固定的数据是不是被改变了,如果被改变了就说明堆出错了,反之只能认为没有,为何说很傻呢,我要是黑客并且我知道填充的是0xcd的话,那么我完全可以填充成0xcd以欺骗windows,更幸运的是release版本根本就不检查堆溢出,debug版本之所以检查并不是为了安全,如果从安全考虑的话,再安全的机制都可能被攻破,相反windows的debug版本crt采用的这种溢出检测是为了让程序员在开发的时候就检查出会出现的错误,主要是为了纠错而不是防御。最终,安全的重任在程序员而不是操作系统,操作系统只能保证自己提供机制的内在安全,也就是说这种安全是内秉的,比如我托付给操作系统的文件不应该被你看到等等,任何防护诸如缓冲区溢出攻击的操作系统都是失败的,操作系统内核根本防不住的,最好的保护就是程序员写出健壮的程序,并且程序安全的责任是程序员的而不是操作系统的。

以上就是windows的堆管理逻辑,和AT&T相比,它增加了很多规则的东西,从理论上将所谓规则的意义不是很大,然而计算机的和谐不仅仅是理论的和谐,更多的是执行上的高效,因此从执行上讲,规则的含义就变得重要了,规则的好处在于很多策略都是内定的,静态的,因此就可以将很多需要权衡的操作变成简单的查找,也就是说不规则的机制往往需要在策略上做很多优化其效率才差强人意,比如做一些启发式算法等等,windows的Heap管理做到了一定的规则,但是也仅仅做到了这些,如果在这个基础上添加一些策略就更好了,windows不知道为何没有做到这些而仅仅实现了一个简单的规则方案,然而glibc做到了,且听下文分解。