new[], delete[] 是怎样工作的
在C++里我们使用new["size"]从堆上申请空间,完事之后我们再使用delete[]释放我们申请的空间。但有人想过系统是怎么进行内部的操作的呢?这里最重要的问题是在delete[]的时候系统如何知道到底当年申请了多大的空间?还有一个相关的问题是如果我们访问时下标越界(或者我们有意访问申请范围外的内存),系统是否可以检测出来?诸位坐好,待我细细道来。
这个问题是群(本科同宿舍同学建的群)里一个同学提出的问题。因为暑假听侯捷老师的课,他讲到过这个问题,我就当即转述了下“每次new出来的内存会有记录的 叫做cookie 就放在给你的地址前面 具体自己查”,奈何人微言轻,对方居然不信。但我对于出处是深信不疑的,侯老师确实是这么讲的,至于你信不信,我反正是信了。不过,难道是我记错了?
着试试看的心理我们来测试下吧:(以下所有C++代码均使用VS2010编译)
int main()
{
int * a = new int[3];
cout<<*(a-1); //将得到地址前方四个字节视为int输出
delete[] a;
return 0;
}
得到结果-33686019,悲剧...估计真是我记错了。可想来想去还是觉得自己应该没记错,难道不是刚好就在前面?再试试,设断点,调试,查看内存。我试着把获得地址前面的数据都拿出来看看结果如下:
//1
//00 00 e3 b7 bf 01 d6 d8 00 18 00 68 42 00 d8 6a 42 00 00 00 00
//00 00 00 00 00 04 00 00 00 01 00 00 00 a9 00 00 00 fd fd fd fd
//3
//00 00 80 8b fc 18 22 31 00 18 b8 68 5e 00 98 6b 5e 00 00 00 00
//00 00 00 00 00 0c 00 00 00 01 00 00 00 a9 00 00 00 fd fd fd fd
//8
//00 00 82 2e 79 65 4b 05 00 1c 00 68 55 00 f8 6a 55 00 00 00 00
//00 00 00 00 00 20 00 00 00 01 00 00 00 a9 00 00 00 fd fd fd fd
//64
//00 00 7e 76 b5 1a f4 37 00 1c 00 68 62 00 d8 6b 62 00 00 00 00
//00 00 00 00 00 00 01 00 00 01 00 00 00 a9 00 00 00 fd fd fd fd
//512
//00 00 6c 28 7a 50 1c 43 00 1c 00 68 3c 00 08 4f 3c 00 00 00 00
//00 00 00 00 00 00 08 00 00 01 00 00 00 a9 00 00 00 fd fd fd fd
解释下,每三行是一个单位,其中第一行代表了申请的数组大小,接下来的两行代表了申请得到内存前面的40个字节数据。显然,我们会看到这些数字肯定是被系统设置的,仔细观察,我们发现其中的更多地奥妙。大家注意把我加黑的那四个字节(为什么是从第六个开始而不是第五个,请看CSAPP内存对齐)拿出来转成int(注意小端表示法),然后去掉最后两个比特再以十进制表示(以第一个为例 04000000->00000004->1),而后面的也相应是3,8,64,512。
结论可以确定了,系统确实是在返回的内存前面的一篇空间内存入了一些数据(为什么那么多乱其八糟的转换,那是因为我们不知道微软实际那个struct是什么格式),那么系统是否真的使用了这些数据呢。再来实验:
int main()
{
int * a = new int[3];
a[0] =10;
*(a-4) = 4; //我们修改这个cookie的值
delete[] a;
return 0;
}
虽然我们不是真的将记录的大小改成了4,但肯定不会是3了,我们看看系统是否会真的按我们说的delete掉更多地内存。这里要注明下,系统在执行delete的时候会用feee填充所有内存。但不幸的是我们看到执行到delete[]这句时,系统报错了

至此我们可以确定,系统设置那个单位记录了长度,而且在delete时也使用这个长度来进行内存清理。这回总算明白了系统如何new和delete的了,但又一个疑惑上来了,系统怎么知道我们把这个值改变了?
这里就牵涉到一个非常大的问题了:缓冲区溢出。缓冲区溢出一直是安全的罪魁祸首(当然栈上的溢出更危险,我们这里讲堆),绝大多数的安全漏洞都是因为缓冲区溢出,覆盖了本来有效地内容,从而导致权限获取等安全问题。微软从vista(记忆,不确定具体哪个版本)开始引入了更安全的机制,就包括缓冲区溢出检查。给大家看个图,这是我执行下面代码后的内存截图。
int main(int argc, _TCHAR* argv[])
{
int * a = new int[3];
a[0] =10;
a[1] = 11;
delete[] a;
return 0;
}

a的指针是指向中间那行的开始(526B70),注意在未初始化的a[3](不再是当年的cc烫了)之后是4个字节的fd,和8个字节的ab,而在a指针之前也是4个字节的fd。我相信不会有任何人相信这是偶然,事实上在delete后又非常多的字节被改动,看图。

途中所有红色字节都是被改动的字节(天哪,我们只是申请了12个字节的数组而已)。所以,上面提到的fd,ab其实都是监视哨兵,一旦系统发现被更改了,就会报告程序访问了错误的内存地址。(其实具体机制要复杂的多,有兴趣的同学可以自行研究)
在前面的那个例子中,我们更改了系统记录的数组长度,所以在delete[]时系统会错误的去擦除更多的内存,并检查后面的校验码,当然错误是不可避免的。了解这些机制可以让我们巧妙地绕过系统的检查:)。所以,我个人感觉这个机制更多地是用来提醒程序员是否访问了错误的内存,对于万能的黑客...唉...
文章已经很长,不耽误大家时间,以一个小程序作为结尾,大家应该能理解它为什么又不报错了。
int main()
{
int * a = new int[10];
a[0] =10;
a[1] = 11;
*(a-4) = 2<<2; //为什么写成这个格式?
memmove(a+2, a+10, 4); //经测试,移动4个就行了
delete[] a; //不再报错,但我们不该开心,这里内存泄露了
return 0;
}
致谢:
这篇文章其实没有多少参考文献,但不得不提一下侯捷老师。暑假期间侯捷老师不远万里到学校来讲课(STL源码剖析),作为一个蹭课的,我深深被侯老师的那份执着所感动。(这里吐槽下学校,教室装空调就那么难?)侯老师对于技术的热爱,对于教学的热情是我们所有人的楷模。祝愿侯老师的父亲在天堂安息,祝愿侯老师一生平安。

浙公网安备 33010602011771号