之前想写一个关于分析、解决问题的一些方法讨论,后来一直为这个践行,力图总结出一套切实可行的理论,这次得空整理下。
背景:
在实际工作中,难免会遇到疑难问题,需要排查、分析、解决。很多时候并不是解决起来有多难,而往往是问题隐藏的比较深,导致表现出来的现象多变,又不容易复现;加上系统功能集成较多,程序员对整体不够了解,不是对每个部件度了解的情况下,无法立即根据经验、设计,推导出可能的原因来验证,导致无法较快定位问题,也就不能最后得到解决方法。相信很多人都遇到过这样的疑难杂症,下面来简单介绍些方法或者说是套路,以供讨论。
传统方法:
经验+运气:很多时候我们第一选择都喜欢依靠经验,来猜测问题的可能原因,然后调试、测试、验证。这种方法很大程度取决于程序员本身的经验、熟悉度,甚至还有些运气,经验多的人,经常能较快定位到问题点;或者运气好的时候,也可能较快的猜测到了问题点。
常用科学实验方法:
控制变量法:控制变量法是实验物理学中最常用的的探索问题、分析问题、解决问题的科学方法之一。通俗来讲是将某个测试因素(变量)以外的因素全部控制,保持其不变形,再比较这个可变的因子对实验结果的影响。
等效替换法:这种方法在实际工作中也很常用,当你怀疑系统中某个模块有问题时,就换一个与之相近的、具有共同特征的模型替代原模块测试实验,有时能很快确认、得出结论。
转换法:在物理学中,有些物理量不便于直接观察测量,可通过转换为容易测量与之相等或相关联的物理现象测量,从而获得结论。在软件调试中也存在这种方法:比如很多bug现象不容易重现,或者不容易观察现象,我们可以转换思维,比如:故意修改程序,试图让bug现象更容易出现;从而提高整体测试效率。
头脑风暴法:这个方法适合在调试者没有头绪的情况下使用,召集相关同事、特别是有些经验的讨论,大家一起激发联想和思路,罗列出所有可能性,并逐一分析优先级并实验分析排除。
类比推理法:这个方法也应用在各个学科中,其目的是比较两个甚至更多的事物相同属性,推出属性的相关程度,比如bug变现的现象多变,需要分析不同现象中的共性,这样才有足浴分析、定位问题。
PS:这里介绍的集中较常见的方法,有些相信很多大家平时都不自觉的在用,只是可能没哟给他们取名字而已,还有些涉及到概率统计之类的方法,可能比较复杂,需要大量的历史数据及模型,这种更适合分析较大型的问题。
当然也不是说传统方法不好,传统方法在经验充足情况下,效率往往是要高的,这里谈的是在传统方法碰壁后,如何有条理、有方向性、有顺序的推进问题的排查、定位。
这里也越来越发现,作为新兴学科的”软件工程”对传统学科的广泛借鉴,在模型、算法上源于数学,在软件设计、实践上借鉴建筑学,在软件测试、调试上很多有借鉴与实验物理学。
这里那两个实际案例来说明:
案例一:非法野指针导致的内存异常导致系统崩溃问题。
问题描述:
重复运行vo demo系统崩溃,具体现象如下:
a、每次都是固定重复运行在第23次出现。
b、崩溃位置在fatfs open的时候调用malloc出错,内存管理模块报错。
c、怀疑是akfat文件系统问题,换elm fat文件系统测试15476次没有出现,可能是akfat的问题?
d、使用60d_rgb硬件测试5600次没有出现。
问题分析、调试:
a、从c、d现象来看,问题比较发散,只能从宏观上分析,微观上很难确定。
b、决定从a、b入手,分析动态分配出错的原因来反推。
c、熟络slbc内存管理机制后,大概是将不同的内存大小分别挂在zone[72]链表数组上。
d、放开内存调试打印,发现每次都是最后一次zone[20]分配7次后异常。
e、参考问题程序,写一个与之相近的测试程序,也每次从zone[20]分配7次,重复执行23次,没有问题。
f、对比问题程序和测试程序的内存分配打印,分析内存管理的分配策略:初始化时得到157个连续块,优先分配这些连续的块,如果分配完,走另一个分支:从free链表上取。
这里可以解释为什么固定在第23次出错:157/7=22.说明第一次用free链表上的才会出错。
g、继续观察问题程序和测试程序的打印,发现其实第一次free就已经出错,free->c_next = 0;
h、所以不用分析之后的运行情况,专注排查c_next被清零的地方:最后逐步锁定到LCD驱动里将释放的内存指针(野指针)非法赋值,free(p_config_list);*p_config_list = NULL;
问题解释:
a、b已经在步骤f中解释,而从代码可以看出错误的代码只会在50d的mipi屏上出现,这里解释了现象d;现象c是因为不同的文件系统的内存使用不一致,elm文件系统没有使用到zone[20],这里的c现象很容易迷惑我们导致错误排查方向。
PS:这里关键用到了“等效替换法”,用一个测试程序,模拟问题程序,等效对比。
解决方案:
解决方案比较简单,但类似的问题如何避免才值得思考。
反思如何避免野指针的非法访问问题:
应用层面:对warning的重现,静态代码检查工具、程序修养上。
内存管理层面:牺牲内存,对每块内存增加边界检查,这个方法有很大的局限性,只能检查动态分配的。
编译器层面:这个是终极大法,拦截所有指针运算,检查指针是否为野指针。这样的工具软件有:boundschecker/purify/gcc bound checker等,各家技术都不一样,有时间再另外开一个帖子来研究。
合并另一个blog的问题调试案例:
案例2:ISP效果经验总结
基于最近平台独立分析排查宏视另一个强光下,ISP偏红震荡问题,最后由李志确认方案的案例。
结合平时解决问题的经验,个人认为在这种模块负责人没有时间情况下,有些时候问题负责人根据一定的套路可以尝试着解决。不合理的地方,还请多多指教。
对ISP效果问题总结如下:
1、收集:根据图像异常现象,打印常见ISP参数,观察参数的变化情况,这个过程要细心,不要放过任何蛛丝马迹。
2、猜测:确定某个参数异常变化的时间点是否和图像异常的时间点吻合,如果吻合,那么这个参数应重点跟踪。(这个猜测有了依据)
3、求证:阅读代码,梳理参数的变化过程,在异常的地方,临时调试性修正。查看结果,如果有效果,基本锁定了问题。如果没有效果,回到第1步。
4、修复:请模块负责人确认问题,如果负责人认为确实有问题并给出修复方案,问题负责人还需从正常程序设计逻辑上监控修复方案的合理性。 如果负责人认为不是问题,应该解释临时修正有效果的原因。
5、验证:合理性修复后,测试整个系统验证问题是否已修复。
ps:细心收集、大胆猜测、小心求证。对这个模块经验不足的人切记臆想型猜测,然后使用试错的方法解决。
案例3:rt-thread: vi动态模块线程被意外终止(内存异常)问题调试记录
1、追踪确定动态模块异常终止的原因:
动态模块创建的camera驱动线程的线程控制块出现异常,内核检测到后,将这个动态模块终止。
2、调试内存异常的过程:
a、在问题内存前后申请测试10K内存作为测试内存,问题依然存在,基本排除相邻内存越界。
b、怀疑是内存意外被线程释放(线程删除),放开内核调试打印,问题又不出现了,结合之前添加打印,有时异常,有时异常,随机性比较大,所以更可能是内存踩踏。 c、把不相关模块都剔除,缩小范围,重新梳理异常的现象,每次name被修改的比较随机,但是同一个结构体内的type却每次都被修改为0。
d、将该线程控制块每隔段时间打出来,基本可以缩小范围到函数DrvModule_Terminate_Task,只有在这个函数附近才会出现,屏蔽其中某些子函数可以恢复正常。但是不能确定是具体哪个子函数执行完才出现的,不能定位到具体那条语句。
e、那么就和线程调度有关,执行某个子函数被切出去踩踏了,回来就异常,全部查看其子函数是否有任务切换,手动添加线程调度打印,发现确实有线程调度。
f、阅读理解驱动框架drv_module,发现rt_drv_entry线程退出后,会调用DrvModule_Free,想起rtt开发手册有一条注:“在线程运行完成,自动结束的情况下,系统会自动删除线程,不需要再调用rt_thread_delete()函数接口”,但是手册没有说如果调用后会有什么后果。
g、为了验证这个猜测,在rt_drv_entry阻塞住,不退出,经过多次测试,问题没有再复现。
h、结合代码,确实是会在idle中将线程控制块中的type清零,name不会清零,所以才会出现c中描述的type每次都是0,name异常的比较随机。
i、想起剑平、兴建原来也遇到thread_dele接口问题,他们通过修改rtt代码绕过,通过验证也是这个问题,于是纠正原来的修复方式。
3、问题原理描述:
如果有子线程主动退出成为僵尸线程,在任务切换时又先执行了idle线程,idle线程会把僵尸线程的控制块释放,然后再切回应用主线程时,主线程又调用了rt_thread_delete接口释放资源,这时rt_thread_delete接口里仍然会意外访问该线程控制块,导致该内存访问异常。
4、解决方案:
修复驱动框架,避免子线程主动退出后,资源被idle线程释放,然后主线程又调用rt_thread_delete访问资源。