【底层原理】一次程序crash后的调试之旅 —— GDB调试心得

来自:码农有道(微信号:b497155298),作者:库里

程序出现bug导致crash估计是每位程序员最不想看到的事情,但是一旦出现,那么也只能迎难而上了,俗话说的好,自己写的码,跪着也要调试完。本文记录了一次程序crash之后的调试之旅,难点在于crash的位置出现在库函数中,没有对应的源码信息,需要通过阅读一定的汇编代码来定位错误原因。基本的gdb调试命令以及汇编指令这里不再细讲,不懂的百度下就OK了。

1、查看堆栈信息

某次crash的堆栈如下:

从堆栈中可以看到,居然是memcpy函数出core了。这些libc库函数都久经考验,几乎不可能会出错。所以,通常情况都是我们传入的参数有问题,但具体是什么问题呢?

2、剖析出错位置的汇编代码


该条汇编的意思是,把rax寄存器中的值,放到rdi寄存器中的值作为地址指向的内存中。猜想可能是rdi寄存器中存放的地址不可访问,导致了越界 


3、破译寄存器所指


可以发现,该地址确实不可访问,既然如此,那么rdi寄存器中的值是如何得到的呢?


4、反汇编利器


纵观函数开始到出错位置的整个汇编代码,没有为rdi寄存器赋值的代码。但是不要忘了,在X86-64架构下,函数调用时,是通过rdi(第一个参数),rsi(第二个参数),rdx(第三个参数)等寄存器传递参数的。也就是说,memcpy中rdi寄存器的值是调用memcpy时传入的第一个参数。从上面堆栈中可以知道,调用memcpy的上层函数是sdscatlen


5、分析sdscatlen源码


memcpy函数的第一个参数是s + curlen得到的。在分析s和curlen的值是什么之前,先看看s 和 curlen分别是干嘛的?


6、探究s和curlen的作用


从上面这段代码不难猜到, sdshdr是一个简单的buf管理结构,len可能表示buf空间的总长度,也可能表示buf空间已使用长度,free表示buf空间中的剩余可用长度,buf是一个0长度数组,表示buf空间的起始地址,char * s即指向了该起始地址。 

从free和buf的作用来看,如果len表示buf空间总长度,那么就没有任何办法知道buf已用空间,从而定位到应该从哪里开始保存新的数据。因此,len字段表示buf空间已使用长度。sdslen函数就是根据buf空间的起始地址获得buf空间已使用长度。在知道了s 和 curlen的作用之后,再来看看到底是哪个值导致了异常

从s 和 curlen相加的值来看,确实和memcpy中访问的地址一样。细心的同学可能会发现,curlen的值似乎太大了,如果curlen表示buf空间的已使用长度,这个值显然超出了当前机器的物理内存上限。猜测问题出在该变量值上

curlen通过sdslen函数返回得到,在该函数中,通过s偏移得到长度,其值为多少呢?

现今几乎所有64位的类Unix平台均使用LP64数据模型,即int的长度为32位:

因此,sizeof *sh的结果为两个int长度,8个字节( 0长度数组char buf[] 不计算长度):

该值为0x80000060,而len是一个int类型的变量,该变量值最高位为1。因此,len本身就是个负数,然后在把len的值赋值给curlen(64位)的时候,会有符号位扩展,得到0xffffffff80000060。

现在明了了,是因为buf管理结构中记录buf空间已使用长度的len变量的值超过了32位有符号整型变量所能记录的最大值,0x7FFFFFFF,从而取反,导致内存访问越界

那么问题来了,这个长度值是怎么来的呢?


7、再回sdscatlen


从第8行可以看到,当把t中内容复制到buf空间之后,会把len的值修改为 当前值 + 新拷贝的内容的长度,即拷贝之后buf空间已使用的长度。

从上面的堆栈可以发现,每调用一次redisAsyncCommand函数,都会把要执行的命令拷贝到buf空间中。这里可以大胆的假设,是否是因为执行redis命令之后,没有正确的释放,导致buf空间不停增加,已使用长度不停增加,最终导致len值溢出。


8、深究buf空间的秘密

当前命令长度为0xe0(224)个字节,buf空间中的内容,也正好为0xe0个字节后开始出现重复。通过这里可以印证上面的猜测,确实是在不停的执行同一个命令,但是又没有释放buf空间,从而造成溢出,导致内存访问越界。

core文件的大小为6.9G也能说明,确实是占用了相当大的内存(超过2G)。

分析到这里,基本可以确认之前的猜测了,剩下的就是去代码中查找具体是哪里没有释放buf空间了。本次core的具体原因也确实是程序中跳过了某段逻辑的执行,导致buf空间没有正确释放。

gdb确实是个强大的工具,能够帮助快速准确定位程序Bug。当程序Crash点没有源码时,通过阅读汇编代码和查看堆栈信息来跟踪代码执行路径和运行状态,将会是一个非常不错的选择。

来自:码农有道(微信号:b497155298)

码农有道.jpg

推荐↓↓↓
C语言与C++编程
上一篇:继 Linux 之父之后,独立开发者 Jonathan Blow 再次炮轰 C++ 是可怕的语言 下一篇:C++ 被炮轰是糟糕的语言 !