有趣 | 最近遇到一个狡猾的bug,复盘分享
关注「Linux大陆」,选择「星标公众号」一起进步!
最近遇到一个看似青铜、实则王者的bug。
事情是这样的,某个进程有数据解析处理、算法融合。
数据来源是gps模块,我负责这个程序的开发维护、与算法对接。
下面看看从这个bug的定位、分析、解决过程,一波三折~
机器之前一直正常在跑,但近两天做了一些特殊测试,发现机器走到某个位置之后基本上必会出现段错误,因为与位置相关的就是数据了,所以刚开始的时候我怀疑可能是数据解析出问题了。
但是之前解析有长时间测试过,没什么问题,特殊位置也有测过没什么问题。暂时排除了数据解析的问题。
定位问题
遇到死机问题,当然得先定位问题,才能去分析、解决问题。定位段错误的方法有很多:
1、log打印定位
可以把所有打印调试信息打开,一些段错误问题可以通过打印的方法就可以大致定位到某一块代码出现问题。
打印方式只是定位段错误的一个小尝试而已,不要对其结果抱有太大希望。有时候确实可以很快就定位到问题的根源,但针对本次的bug,通过打印的方式定位出的结果反而给我们带来了一些迷惑。
本次的bug通过打印的方式也锁定到了出问题的代码,在某个算法函数里的某两个个三角函数的算式。问题就是屏蔽掉这个算式,程序就没出现段错误,打开这两句代码就必出现段错误,这让我的注意力集中在了这个地方。
但反反复复看了好多次没发现这两个算式有什么不妥的地方,而且看了前后两层函数,也没发现有什么不妥。最后定位出了问题,这里确实不是问题的根源,但却在这浪费了很多时间。
应该有很多小伙伴跟我有同样的习惯,喜欢通过打印法来查找bug,打印法基本能定位到大多数问题。但对于一些藏得很深的bug,通过打印法有时候只看到了表象,而我们有时候会被这表象给迷惑了。
所以在分析问题的时候,尽量头脑清醒些,分析遇到不太合理的地方,要不断的推敲,不断地推翻不合理地分析。
当然,有好的定位问题的方法也很重要,下面看第二种定位段错误的方法:
2、远程调试
远程GDB是一种适合嵌入式系统的调试手段。它使用目标机端的gdbserver和主机端的gdb调试器协同进行调试,再搭配vscode可以很方便地进行调试。vscode+gdb+gdbserver远程调试的教程见:干货 | 远程gdb调试
远程GDB的原理是:
❝有一小段驻留在目标机上的代码,它被称为调试桩,也称为调试代理。调试代理负责目标机上实现由主机上的调试器发送过来的调试命令,例如:读写内存、读写寄存器、设置断点及运行被调试程序等。调试代理还要向主机调试器报告目标机上的异常事件。
❞
启动远程调试,全速运行,当程序出现段错误时可以很快知道出现段错误的代码行号。本次的这个bug也是使用这种方法来快速定位出来的。
除此之外还有其它很多方法来定位段错误,如使用strace工具跟踪、gdb调试core文件等方法,后续有机会再写使用分享。
分析、解决问题
通过远程调试的方法可以快速定位到本次的段错误出现在一个串口读函数里的下面这一句代码:
FD_SET(fd,&fs_read);
通过打印发现在出现段错误时这个fd的值是一个很大的数,显然是不对的。
在Linux中,一个进程默认可以打开的文件数为1024个,fd的范围为0~1023。可以通过设置,改变最大值。
我们代码里的fd是一个静态全局变量,fd突然出现一个异常的值,通过分析可能会出现两种情况:
❝1、串口的操作不当,没有正确打开、关闭;多个线程、进程操作了同一个串口。
2、fd的值被非法篡改了。
❞
代码中对串口的操作都比较合理,所以第二种情况的可能会大些。所以开始着手确认与fd前后相邻的变量的操作。这可以通过map文件来查看,在CMakeLists.txt中生成map文件的代码如:
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=output.map")
但Linux下的map文件默认不会显示static变量的一些地址信息。
刚开始时,我们为了查看fd的地址,把fd前面的static暂时先去掉,就可以在map文件中找到它的地址信息,但这时候再测试并不会出现段错误的情况了,这让我们更加肯定了fd的是被篡改掉了。
把fd前面的static去掉没有出现段错误的原因是由无static修饰,fd存放的区域不同,没有出现段错误是因为去掉static之后被篡改的就是不fd的值,所以暂时不会出现段错误。
所以,要找到问题的根源必须得把static给加回去,然后找到fd前后相邻地址的变量。问题是有static修饰时的fd的地址信息并不会存放到map文件中。这可如何是好?
当时没什么其它的办法,网上也查了,相关资料很少。只能把整个工程的static修饰的变量的地址挨个打印出来,这是个体力活。。。
还好这份并不是复杂,核心文件就那么几个,但查找过程也耗费了很多时间。最后锁定了某个源文件,然后依次把这个源文件里的static变量的值及地址给打印出来。终于,找到了fd的前一个变量,那是一个int类型的cnt计数变量:
当查到cnt的地址正好是fd的前一个地址时,别提有多开心。
当查看代码发现cnt除了忘了进行清零操作之外,没有其它操作异常的地方了。不清零也似乎不会影响到后面的fd,cnt不断递增,顶多会发生上溢。
这时候又陷入了沉思~莫非是思考的方向错了?
在下班后饿了自己一个多小时,突然想到程序才运行一小会,cnt竟然已经计数到了一个十位数的数,再往前多打印几个相邻变量的值也竟然都是十位数的数。终于,一切好像比较明了了,这是被篡改了一块连续内存,绝对是哪里有数组越界进行写操作了。
检查了一遍代码,果然,在一个算法函数里有对一个数组进行写操作越界了:
这里有一个含有5个元素的float类型的数组arr,但其因为逻辑设计不当,导致有对arr[5]、arr[6]、arr[7]、arr[8]进行了写操作,而arr[8]的地址正好是fd的地址,所以对arr[8]进行写操作就篡改了fd的值,从而导致段错误死机。到了这一步一切都明了了,之前打印调试屏蔽掉的那两句算式的与这个算法函数有一定的联系,一层套一层。
但是,后来,我同事鱼鹰(公众号:鱼鹰谈单片机)钻研出了Linux下static变量地址输出到map文件的方法。在CMakeLists.txt文件中设置编译参数,如:
set(CMAKE_C_FLAGS "-fdata-sections")
set(CMAKE_CXX_FLAGS "-fdata-sections")
如果本次的bug问题定位能使用这种方法,就可以很快地查找fd的相邻地址变量了。
总结
这次的bug藏得真够深的,从定位、分析到根本性耗费了一天半的时间。有时候我们通过屏蔽、打开某些代码来定位问题有可能只是bug的表象,并没有看到本质,所以应多思考问题的根源,从根本性地解决才是真正的解决。
虽然这次“浪费”了不少时间在这,但这些bug的解决不正是经验积累吗,下次再遇到类似的bug就可以很快的挖出来。
另外,Linux下的开发坑很多,多掌握一些调试工具、方法,对于我们快速解决问题有很大的帮助。
平时大家都有遇到哪些狡猾的bug呢?
欢迎留言分享
历史文章:
一文读懂apt、deb与背后的知识