疑难问题定位案例复盘(一)

我们在开发过程中经常需要使用自行申请、释放内存(malloc/free),这提供了便利性同时也带来安全性问题。如我们可能使用了一个已经释放的内存,即野指针,这会给程序带来不可预测的后果,而且出现问题后定位起来也较为困难。最近定位一个coredump时就碰到了这类问题。

问题现象

产生了coredump,且信号来源是段错误信号SIGSEGV 。

分析过程    

1、通过对coredump的初步分析,可以判断直接原因是一个局部指针异常导致。通过查看代码函数调用关系,追踪该异常指针的来源,发现该异常指针来源于一个malloc分配的堆内存,假设分配的堆内存指针为pstNewTestSpace,因此怀疑堆内存指针pstNewTestSpace异常。

2、继续跟踪内存指针异常值pstNewTestSpace来源,发现在最开始给pstNewTestSpace赋值的接口中已经出现异常,因此需要重点分析该接口流程。

给pstNewTestSpace赋值的函数流程如下(做了一定简化):

TEST_DEVICE_S g_stTestDevice;

/* 重新初始化接口 */
static int TEST_ReInitDevice(TEST_DEVICE_S *pstObjDevice)
{
	TEST_NAMESPACE_S *pstNewTestSpace		= NULL;

    pstNewTestSpace = (TEST_NAMESPACE_S *)malloc(sizeof(TEST_NAMESPACE_S));
    memset(pstNewTestSpace, 0. sizeof(TEST_NAMESPACE_S));
  
  	/* 省略对pstTestSpace填充数据过程 */
  
  	free(pstObjDevice->pstTestSpace);
  	pstObjDevice->pstTestSpace = pstNewTestSpace;
  
  	/* 加锁 */
  	pthread_mutex_lock(&g_stTestLock);
  
  	/* 使用pstNewTestSpace数据流程,也即产生coredump位置 */
  
  	/* 释放锁 */
  	pthread_mutex_unlock(&g_stTestLock);
  
  	return 0;
}

/* 线程1 */
void TestTread1(void *pArg)
{

    while (1)
    {
        /* 一定条件下初始化设备 */
        TEST_ReInitDevice(&g_stTestDevice);
    }
}

/* 线程2 */
void TestTread2(void *pArg)
{

    while (1)
    {
        /* 一定条件下初始化设备 */
        TEST_ReInitDevice(&g_stTestDevice);
    }
}

局部变量(指针)被越界

        从上面可以看出,产生异常的指针变量pstNewTestSpace是一个局部变量,指向了堆内存。作为局部变量产生了异常,开始我以为是由于该局部变量被越界了,而局部变量被越界有2种情况:

局部变量被栈中相邻局部变量越界

        如栈中局部变量指针pstNewTestSpace的下面(低地址方向)存放了一个int型数组aiArray。若在使用过程中对aiArray的赋值超越了数组范围,则可能造成局部变量指针pstNewTestSpace异常。

        为了验证这种猜想,我打印了函数TEST_ReInitDevice中相关变量值,未发现有局部变量值异常,因此排除了这种可能。

在函数调用变量压栈出栈过程中被“被调用函数”越界

        具体场景如下:

  1. 比如TEST_ReInitDevice函数在执行过程中,汇编指令将局部变量pstNewTestSpace一直存放在寄存器r6中。
  2. TEST_ReInitDevice函数中间又调用了函数TEST_GetDeviceInfo,而TEST_GetDeviceInfo函数的汇编指令也使用了寄存器r6,因此TEST_GetDeviceInfo的汇编指令一开始就会将r6寄存器值压栈(push r6),也即此时pstNewTestSpace的值通过寄存器暂时保存在了TEST_GetDeviceInfo函数栈中。
  3. 若TEST_GetDeviceInfo函数在执行过程中有越界行为,可能将栈中保存pstNewTestSpace变量值的位置改写了。
  4. 当TEST_GetDeviceInfo函数返回后会将栈中已经被改写的pstNewTestSpace值通过r6寄存器带回来(pop r6),这样pstNewTestSpace变量值将异常。

        由于若r6寄存器压栈了,r4、r5也必将压栈,而且通常压栈指令为pus {r4, r5, r6},会按照r6、r5、r4顺序依次压栈,因此先压栈的r6寄存器值在栈的上面(高地址位置),后压栈的r5、r4寄存器值在栈的下面(低地址位置),而TEST_GetDeviceInfo函数的局部变量在栈中更下面(更低的位置)。根据通常理解,此时若r6寄存器值被TEST_GetDeviceInfo函数越界了,则r4、r5寄存器值也将被越界(跳跃式越界概率很低,暂不考虑)。

        为了验证这种猜想,通过梳理TEST_ReInitDevice汇编代码,查看了TEST_ReInitDevice函数中寄存器r4、r5保存的变量,在出现coredump时对应的变量值也正常,因此排除了这种可能。

        经过上面推理、论证,基本排除了局部变量pstNewTestSpace被越界的可能。而且,pstNewTestSpace保存的值看起来也像一个堆内存地址,并且通过x命令也可以正常打印pstNewTestSpace指向的内存区域,且该内存区域也是一块看起来正常的堆内存块(通过查看pstNewTestSpace地址前的2个UINT字节,即堆内存块头结构struct malloc_chunk,分析对应变量值异常情况)。为了继续定位本问题,开始推测pstNewTestSpace保存的地址本身没有问题,而指向的该部分内存被改写或者越界了。

变量指向的内存被改写

针对指针指向的内存被改写也有2种情况:

指针指向的内存被前面堆内存块越界

        当pstNewTestSpace指向的内存块的前一个堆内存块写越界时,将破坏pstNewTestSpace指向的堆内存内容,从而产生异常,如下图所示。

 假设pstNewTestSpace的前一个内存块地址(高地址方向)由pstOtherSpace保存。当pstOtherSpace使用过程中写越界时,将破坏pstNewTestSpace指向的内存,从而产生异常。

根据上图可知,若是这种情况越界,则必将改写pstNewTestSpace的堆内存头结构,其对应struct malloc_chunk结构体。而struct malloc_chunk结构体定义如下:

 堆内存结构struct malloc_chunk的用法及含义可参考:linux malloc内存数据结构分析_malloc数据结构_sy4331的博客-CSDN博客

具体定位流程如下:

  1. 我们可以首先通过x /wx命令将pstNewTestSpace指向的内存及之前的一大片内存以16进制都打印出来,分析pstNewTestSpace的内存头结构struct malloc_chunk各个数据成员是否正常,特别是mchunk_size成员是否填充的是申请的堆内存大小
  2. 若pstNewTestSpace的内存头结构各数据都看起来正常,则排除pstNewTestSpace堆内存被前面的pstOtherSpace指向的堆内存越界的可能(暂不考虑跳跃式越界改写);否则,我们认为前面的pstOtherSpace指向的堆内存越界改写了pstNewTestSpace指向的堆内存,下面要确定pstOtherSpace堆内存是由谁申请、使用的,找出罪魁祸首。继续步骤3定位分析。
  3. 分析上面打印的pstNewTestSpace前后的内存16进制数据内容,查看是否有特征数据(主要看指针)。若有一些看起来像指针的数据,可以进一步使用x /s命令查看对应内存保存的特征字符串,从而确定越界的pstOtherSpace保存了什么数据,从而帮助我们pstOtherSpace属于哪块业务代码定义、使用的。若本步骤看不出来什么特征16进制数据,我们继续步骤4定位分析。
  4. 通过x /s命令将pstNewTestSpace指向的堆内存及之前的一大片内存以字符串形式都打印出来,查看pstNewTestSpace被越界前后部分有没有什么特征字符串。若发现了一些特征字符串,则查找业务代码,看看哪些业务模块定义、使用了该特征字符串,从而确定越界内存是由谁申请、使用的。若本步骤看不出来什么特征字符串数据,则我们继续不住5定位分析。
  5. 从pstNewTestSpace指向的内存往前看(高地址方向),看能不能根据经验找出pstOtherSpace的起始地址。我们通常根据pstOtherSpace的struct malloc_chunk结构可能呈现的数据猜测pstOtherSpace的起始地址,比如struct malloc_chunk的mchunk_prev_size通常为0,mchunk_size表示内存块的大小,通常为一个可接受的数据大小。若我们能猜测出或者确定pstOtherSpace内存块的起始地址,则继续步骤6定位分析。
  6. 由于pstOtherSpace的地址我们已经确定,假设为0x12345678,可以想象当初利用malloc等接口申请内存的时候必然使用了一个指针变量来接收这个地址,因此我们可以尝试找到接收这个地址的变量位置。接收这个地址的变量通常有几种情况:局部变量、全局变量、堆内存变量,我们一一分析。
  7. 若为局部变量接收地址0x12345678,我们知道局部变量保存在栈中,而通常发生内存越界和最终产生coredump距离相隔不远,因此我们优先查找TEST_ReInitDevice的栈。利用x /500a $sp命令将TEST_ReInitDevice栈中数据以地址形式打印出来,并查找是否存在目标地址0x12345678。如找到了,则说明在当前函数中申请使用了该内存。具体有两种方法:                   a)可以使用p &变量的形式打印当前函数所有的局部变量地址,查看哪个局部变量地址对应的是栈中保存0x12345678的位置,从而确定具体哪个局部变量保存了该地址,也即申请使用内存导致越界的罪魁祸首。                                                                                                        b)如刚刚方法a不能找到具体哪个局部变量保存了地址0x12345678,则查看保存地址0x12345678位置相对于栈顶sp的偏移,假设偏移为40。根据经验,我们通常使用sp寄存器栈顶地址+偏移的方式访问栈中局部变量,因此我们猜测汇编指令中也会是通过“[sp, #40]”的形式访问该局部变量。我们将TEST_ReInitDevice的反汇编代码都打印出来,并全局搜索"[sp, #40]"。根据搜索结果,我们可以看到具体什么位置保存了这个地址(ldr指令),什么地方使用了这个地址(str指令),从而确定具体哪个变量申请、使用了这块内存。                    若TEST_ReInitDevice的栈中未找到目标地址0x12345678,则可以尝试打印往前的函数栈数据并查找,甚至使用find命令大范围查找。若找到了,则仿照上面的方法a、b追踪具体申请使用内存的位置。
  8. 若为全局变量接收了地址0x12345678,则可以尝试利用x /1000a命令将所有的全局数据区打印出来并查找,或者直接使用find命令查找,若找到了x /a命令会显示该部分内存属于哪个全局变量。全局变量区(即.data段)的起始地址可以通过info proc mappings找到。
  9. 若为堆内存变量接收了地址0x12345678,则查找情况较为复杂,可以尝试使用find命令在堆内存中大范围查找一下。若找到了则尝试使用x /s命令查看特征字符串,以便确认具体使用的业务模块。
  10. 通过上面的7、8、9步骤若确认了pstOtherSpace申请使用的业务模块,则通过走读代码流程确认越界的位置、原因;若仍然没有找到,则只能硬着头皮分析TEST_ReInitDevice及之前调用的代码流程,重点关注利用malloc等接口申请、使用堆内存部分的逻辑,查看有没有内存越界的怀疑点。

根据上面的分析定位步骤,我们发现pstNewTestSpace的内存块头结构比较正常,因此排除了pstNewTestSpace指向的内存块被前面的pstOtherSpace内存块越界的可能。

指针指向的内存被其他指针修改

 当pstNewTestSpace指向的内存块被其他模块流程修改时,也可能导致pstNewTestSpace数据异常,从而产生coredump。实际场景中有2种情况:

  • pstNewTestSpace通过全局变量被其他模块引用、修改,导致内存异常

        比如pstNewTestSpace指针变量地址赋值给g_pstSharedData,而g_pstSharedData作为全局变量在当前进程的多个线程中共同使用,可能有其他线程也在操作该部分内存,甚至写错数据导致内存异常。

        分析pstNewTestSpace的赋值情况,确实通过TEST_ReInitDevice的入参将指针变量值赋值给全局变量g_stTestDevice的pstTestSpace成员。但未发现外部模块通过该全局变量进行异常写入,因此排除了这种可能。pstNewTestSpace给全局变量赋值情况如下:

  • pstNewTestSpace指向的内存已经通过全局变量被其他模块释放并再次分配出去使用,导致内存异常

        当pstNewTestSpace指向的内存通过全局变量被其他模块引用且释放后,该部分内存若再次被分配出去将重新使用。新获得该部分内存的模块将改写里面数据,但pstNewTestSpace并感知不到该部分内存已经被释放并分配给其他模块了使用了,仍然继续使用导致出现了问题。

        上面步骤我们已经知道局部变量指针pstNewTestSpace确实已经赋值给全局变量g_stTestDevice的成员pstTestSpace了,然后我们继续分析查看该全局变量是否存在被释放的地方。结果发现确实存在对全局变量g_stTestDevice的成员pstTestSpace释放内存的情况。释放全局变量指向的内存情况如下:

继续查看g_stTestDevice变量特别是成员pstTEstSpace指针被释放的情况,发现该变量只在函数Test_ReInitDevice中调用,而该函数被两个线程TestThread1、TestThread2分别调用,且也都是传入同一个全局变量g_stTestDevice,具体调用情况如下: 

因此,我们怀疑由于线程 TestThread1、TestThread2先后调用函数TEST_ReInitDevice,且都是使用同一个全局变量g_stTestDevice作为入参,但在函数中一个先申请内存,并将申请的内存地址赋值给全局变量g_stTestDevice的结构体成员pstTestSpace,然后当后一个线程也调用该接口后将释放另一个线程刚刚申请的内存。当该内存块被再次分配出去并使用后,里面原有数据将大量改写,而先申请内存的线程仍然在使用,导致出现数据异常,产生coredump。

为了验证该猜想,我做了如下操作:

  1. 我们先打印出TEST_ReInitDevice函数调用过程中入参pstObjDevice(实际为全局变量g_stTestDevice)的结构体成员值pstTestSpace,发现该成员值和局部变量值pstNewTestSpace不相等,这说明pstObjDevice->pstTestSpace在被pstNewTestSpace赋值后又被修改了。而入参pstObjDevice为全局变量,刚好存在被两个线程TestThread1、TestThread2都调用的情况,符合预期。
  2. 为了进一步验证猜想,我们使用thread命令先后切换到线程TestThread1、TestThread2并查看栈回溯,发现确实两个线程都在调用函数TEST_ReInitDevice,且函数的入参地址相同。进一步查看两个线程栈中pstNewTestSpace局部变量值,发现线程TestThread2(后调用函数TEST_ReInitDevice)的局部变量pstNewTestSpace值和线程TestThread1(先调用函数TEST_ReInitDevice)的入参成员值pstObjDevice->pstTestSpace相等,这进一步验证了上面的猜想,即先申请内存的线程TestThread1在申请了内存并赋值给全局变量g_stTestDevice成员pstTestSpace后被线程TestThread2释放了。在线程TestThread1继续使用它的变量pstNewTestSpace指向的内存过程中,该部分内存被再次分配出去并使用,导致该部分内存数据完全被改写,从而造成线程TestThread1访问异常数据,出现coredump

修改方案

        通过上面分析,我们知道问题的核心是由于线程TestThread1在使用申请的内存时,线程TestThread2通过全局变量g_stTestDevice释放了该部分内存,导致野指针访问,进而出现coredump。因此我们的修改思路是添加锁对全局变量g_stTestDevice进行保护,避免出现资源的竞争产生意想不到的结果。修改方案如下:

总结

        至此,由一个内存异常引发的coredump问题的解析、定位、解决算是圆满结束了。在这个案例中,我们根据一个内存异常问题coredump的常规分析步骤完整地走了一遍,其中分析了涉及的各个可能情形,最后我们再总结一下一个内存异常导致的coredump完整分析步骤:

  1. 首先判断出现内存异常访问的指针变量地址是否是一个合法的堆内存地址。若观察到该地址明显异常(比如地址值较小),或者使用x /50wx 命令查看该内存块的头结构(struct malloc_chunk)数据明显异常(如mchunk_size不符合业务申请的内存块大小),则说明该地址是一个非法值,此时继续步骤2;否则进行步骤3。
  2. 保存地址的指针变量有可能是全局变量,也有可能是局部变量。若是全局变量,则使用x /500a命令打印该全局变量附近的内存,查看是否有越界问题。若为局部变量,则有两种可能情况:一个是该局部变量栈中附近的其他局部变量产生了越界改写导致;另一个是在函数调用压栈出栈过程中越界导致。
  3. 当指针变量本身保存的地址正常时,我们就怀疑是指针指向的内存被破坏导致,具体可能情况也有两种:一个是被前一个内存块越界;另一个是该部分内存被其他模块释放后导致原有模块流程访问了空指针或者被其他模块异常改写。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值