分类: C/C++
2013-10-09 14:44:30
其实也就鸡毛蒜皮的小事,本来不想记录在博客上的,不过这个bug背后隐藏的东西确实比较有记录的价值,如果说解bug就像是解初高中数学题,那么有的bug就像一道出得很漂亮的题,短小精干但背后隐藏的信息量却很大,一下子就让你记住了背后的那些定理概念。
事情是这样的,segment fault,程序被谋杀,现场在libc的calloc里。发生在libc中的segment fault其实不少见,碰得最多的怕就是memcpy,一般都是过大的数据拷贝导致程序的stack corruption,这种情况通过检查程序的backtrace可以看出(被破坏的栈很容易被发现)。但这次的栈调用树却很完整;想想calloc要干 什么事,分配由size指定的内存然后填0而已,于是检查calloc的唯一的外部参数size,不大的一个数,可以说是再平凡不过的一次调用,话说回 来,就算是诡异的size出现,也应该只会导致calloc失败而已(比如请求的size太大),也不该是segment fault这种‘不可捕捉’的运行期错误。每次遇到这种程序的生命停留在系统级library的情况的时候,一定要抑制自己怀疑‘是不是xx库的bug’ 之类的冲动,毕竟这是每天都会被亿万人使用的libc中的calloc,让我发现它的bug估计和中头彩的概率差不多。真正的凶手肯定在其他地方。
怎么解决的过程就不提了,主要是不值得提。总之感谢google大神。
出错程序大概是这样一个模式,看过或写过一些driver的人应该熟悉这种用法,即将data load直接分配在用来管理data load的struct后面:
calloc/malloc等的原理其实是通过内核的brk系统服务申请虚拟内存,申请的单位以4k/页为粒度。然后自己再维护申请回来的 virtual memory,毕竟不是所有程序都会每次都向calloc/malloc请求大于一个页的memory的,所以calloc/malloc通过内核申请的 virtual memory总是会比用户需要的更多(除了brk是以页来满足calloc/malloc请求这个原因以外,calloc/malloc也需要将用户申请 的内存对齐或多申请一些空间做管理用的meta data),然后分成block的形式,再按需分配给需要的应用程序。
跑第一个函数func1()的时候其实已经发生‘内存访问越界’了,之所以在那里没有发生‘命案’的原因就是如前所述:calloc真正分配的虚拟 内存是比用户请求的大的。如果像func1()中那样在后面多写了5个字节是不会导致mmu的页异常的。也就是说,在上面那个程序里,这多写的5个字节除 了程序员自己小心以外,编译期和运行期都是不能帮助你发现它们的,这种错误其实最好是让编译器帮忙识别,但像上面那么做编译器是没法发现的,这种允许程序 员随心所欲操作memory的做法正是c语言这种贴近汇编和硬件的‘高级’语言的强大之处,当然,对于经验不够丰富的人来说,这也为可能出现的各种 segment fault埋下了祸根。
那为什么第二个函数func2()的calloc却遭‘报应’了呢?前面提到calloc/malloc以block的形式来管理已经分配的虚拟内 存,这些blocks被划分为‘allocated’和‘free’两种状态,对用户的分配请求,所要做的自然就是找到一个能满足大小的free的 block,对释放请求也是将对应的'allocate'的block和邻近的'free'的block合并。对这些blocks的管理自然少不了一些维 护它们的‘元数据’本身(如‘下一个free block的地址’等),为了更有效的处理,这些元数据本身也和block放在了一起,比如放在每个block的头或尾。说到这里,真相就很明白了。为了 方便说明,见下图:
假设func1()中调用calloc分配到的block就是Allocate(1),后面的P表示meta data,比如里面有指向最近的free块‘Free(1)’的指针。那么,当func1()中的越界访问发生后,那多写的5个0就把P的指针破坏掉了! 这个破坏当然在func1()中不会导致出错,但是,当到func2()中用calloc分配内存时,calloc试图从Allocated(1)块后面 的P找到最近的free block时,这个指针已经被corruption了,这个track操作本身导致了calloc中的segment fault。
c语言就是这样,你用得到,那么它威力无穷;你用不好,它随时会制造隐藏在你程序里的定时炸弹。