2024年2月20日发(作者:)

目录

1

2

3

4

案例描述 ................................................................................................................................................ 1

案例分析 ................................................................................................................................................ 1

解决过程 ................................................................................................................................................ 2

总结 ........................................................................................................................................................ 3

关键词:

堆栈溢出,检测

摘 要:

堆栈溢出对于我们软件开发人员来说,最严重的后果是破坏了内存中的指针,及其造成的一系列难查的bug。对于这种情况,我们应该怎么办。

1 案例描述

相信Linux程序员最讨厌的字符串是“Segmentation fault”,而Windows程序员最讨厌的是“C0000005”。其实本质上是一样的,都是内存访问错误,且八成是错误的指针导致。

指针错误一般分两种,一种是空指针,一种是野指针。对付空指针,我们不怕,因为即便出现了,偷懒的话,我们都可以不去定位原因,直接加上条件判断保护即可。对于野指针,产生的原因有两种,一种是自身指针所指内存已销毁,这种好查,找到异常点和free的地方,比较一下就查到原因了。另一种就是我今天所说的堆栈溢出造成的内存内容被篡改,假如这块内存中保存着程序指针,那么这些指针就“野”了。

堆栈溢出造成的野指针是很悲剧的,就算有dump工具,你也不知道是谁陷害了你的指针。整个进程中的模块都有嫌疑,你不得不大海捞针似地去盯代码,眼神好的话兴许能揪出bug,运气不好的话可能几个星期都没有进展。

最近我就碰到了这种情况产生的bug。是一个windows上的程序,跑若干钟头就会崩溃,且通过map文件显示异常出现在一个野指针处。因为该指针不会在中途销毁创建,所以基本上断定是哪里堆栈溢出了。在解决这个bug的过程中,我产生了一些对堆栈溢出的思考。

2 案例分析

我想到3种有关堆栈溢出的方法:

 编译器选项

Gcc提供了一个编译选项,加入-fstack-protector-all即可实现对堆栈的保护。但是实际上这个对于我们软件开发人员来说,一点用都没有。Gcc的堆栈保护实质上是一种软件安全措施,为的是防止黑客恶意篡改函数栈调用黑客代码。而作为软件开发人员,我们希望,程序崩掉没关系,重要的是我能检测出是哪里的堆栈溢出导致的,这个编译选项显然不起任何作用。且-fstack-protector-all只对栈做保护,对于堆是没用的。

 IDE集成工具,如BoundsChecker

在软件开发过程中,也可以借助相应的编程环境下的集成工具,比较常用的如BoundsChecker。正如它的名字一样,堆栈的边界检测是其一大功能。看起来这是一个很美好的工具,为了解决我那个bug,于是我将它集成到VC中,然后将我们的整个业务重新编译,运行。结果,根本不能启动!因为该工具过于强大,对于我们庞大的产品代码,运行速度实在是太慢太慢了。没办法,只好缩小范围,只用它编译了几个嫌疑比较大的库,然后运行。现在稍微好一些。但是这个bug并不是那么容易出现的,一方面,它有可能要跑几个钟头才出现;另一方面,它是在另一台比较特殊的PC(一

块ATOM架构的板子)上才出现。难道我要去到ATOM那台机子上安装VC和BoundsChecker再F5?总结下来就是使用这种集成工具真的是太不灵活太不顺手了,只好放弃。(也许是我比较菜,这个工具用的还不熟练的缘故。)

 人肉

终极杀招,只能是靠人肉定位了。一般来说,先靠dump或者map文件找到出错代码行,看看是在哪个模块的。大致而言,溢出的堆栈最大嫌疑就是在出现异常的模块内。然后就要走读代码,检查出现异常的指针所在的内存,及其这块内存周边分配出来的内存的操作,不出意外的话,80%是这些内存造的孽。运气好的话,一两天就能查出来了。最终那个bug就是靠这样解决的。

但是人肉法有很大的缺陷。第一,你不能确定溢出的堆栈就是存在于出现异常的模块,你需要关注整个进程。第二,这要求程序员要非常清楚整个产品的代码,要有一定的维护经验,这个要求在现实中往往不合理。第三,有些情况下,有可能内存中保存的函数指针被破坏,且“坏到恰好”,结果程序就跑飞了,dump或map也成了废物,我以前还真的碰到过这样的情况。

Bug虽然解决了,但是我仍然后怕,万一以后再出现堆栈溢出,难道我还要花精力一次次地人肉?这次还算顺利,但前前后后还是花了一周时间,幸好还是在集成测试阶段,时间宽裕,要是在系统测试时遇到这样的bug,那不是死翘翘。所以千言万语又回到了题目——

堆栈溢出了怎么办?

3 解决过程

有没有一种堆栈检测工具,它能够在堆栈溢出的时候打印出或往文件中输出信息,而且几乎不影响程序执行效率?俗话说求人不如求己,干脆自己写一个程序实现吧。

我就按如下的设计:

 能够监视malloc,memset,memcpy,free这四个函数的行为(栈就不检测了,一般栈溢出的情况比较少,也好查。另外new和delete由于水平有限,无法对其监视)。

 如果发现越界操作,打印出来,继续执行。也就是说该检测工具不影响程序的行为。

一旦确定了设计准则,接下来的编程就是体力活了。于是,我写了一个叫“simple heap monitor”的模块,简称SHM。

我们看下面一段程序:

可以看见,第一个memset和memcpy操作越界了。F5运行,一切OK,但是悲剧已经埋下。

我们把第二行的shm.h头文件放开,同时加入,再运行看看,结果输出:

嗯,很好,要的就是这种效果。

(SHM代码见附件)

4 总结

有了上面的检测工具,以后检测堆栈是否越界就很方便了,只要能复现,就能定位。但是也有一种极端情况不能发现,就是假如本来要操作p1的,结果偏移到p2内,且长度未越界……这种情况只能说太作孽了,那我们确实没什么办法去检测的。

总之,希望堆栈溢出不要再让我们软件开发人员花太多的精力了。