0. 前言

八月份快要结束了,这个月也没有啥输出,今天下班较早,赶一篇学了一年多C++后的一些思考,关于修Bug的一些想法和思路。
平时工作中,如果写代码花费一天时间,那调试解决Bug可能有时候会花好几天、或许更长。此外,线上部署的服务如果出现崩溃或者返回异常的问题,也会反馈给相应的开发负责人,毕竟自己挖的坑要自己填。大多数情况下,如果框架稳定且没有新需求开发,很多Bug就只是某些业务代码导致的小问题,也很好定位,但说实在的,长期写业务代码、一堆if else逻辑对个人能力提升很有限。如果想要学更深入的技术,还是要多写一些框架层面、算法设计方面的代码,不如如何设计多个类之间的调用关系、如何用基本数据结构组织一个适用于业务逻辑的结构体或者class。扯远了,这里只说一下我是如何在工程中调试分析bug的方法。

这里只说C++工程,其他语言的话也大同小异。首先,一个C++工程从0到1的实现流程一般流程是:①确定业务需求,即会提供什么样的输入,期望什么样的输出。后端C++工程一般向外提供的其实是一个类库,类库在运行期间的作用犹如一个函数,依据传入的数据流,做一些逻辑复杂的计算,然后返回出来一个结果数据流或者多个数据流。②基于需求,整理出详细的数据流图,即若要从输入数据计算出输出数据的话,需要经过哪几个步骤;③设计UML图。将上一步的数据流图转化为UML图,UML图不是单纯的将数据流图搬过来,这里面粒度要细到每个类及其public接口才可以。同时也要表明类之间的关系,主要有继承、实现、组合、聚合、关联、和 依赖这六种关系。之后就是④按照UML图进行相应实现,实现的时候也要注意,接口类单独在include文件夹内,实现类如果实现很庞大,一般也是要以同名的.cc和.h来组织,并置于源文件夹(src或source文件夹)下的对应模块内、具体嵌套多少级由类的作用域确定。当然如果某些类不需要虚接口类或者作用范围很小,那么也没有必要设置include内的头文件。

1. 如何修Bug

说上面这些,主要是想说,Bug就是在按照UML图写代码的时候引入的,这有可能是UML图设计的本来就有问题,比如会有调用纯虚函数的问题,这就是UML图的问题,或者说是类的实现问题。但实际开发中,也没有说非要先创建UML图作为参考来写代码,但我认为这是必要的,尤其是对于框架比较复杂的工程。

  • 通常遇到的Bug可以分为两类,一种是编译期就能报出来的,另一种是运行期出现的。编译期的Bug都是小问题,常见的就是只有声明没有定义导致链接失败、也有可能是重复定义导致二义性,这些问题可能是头文件没有引入或者重复引入头文件导致,也有可能是其他问题,但编译期大多数bug只需要关注报错位置直接相关的地方即可,同时也要明确编译过程是以每个cpp文件为基本编译单元的,从这一点出发,可以分析很多编译问题。

  • 第二种:运行期bug,这里我们只谈导致崩溃的bug,其实所有没有按照预期运行的异常动作或结果都可以称为bug,崩溃最常见的就是segmentation fault,当然还有其他各种各样的崩溃。工程一般都有log模块,第一步先查看日志,找找有价值的信息,说不定就直接找到问题了。此外,既然是运行期bug,那么最好调试运行起来看看究竟发什么什么错误。这就是运行期bug修复的第二步:bug复现,而且最好是最小复现,即能够在测试代码启动后最快时间复现出问题,毕竟可能要调试很多次,缩短复现时间是十分必要的。第三步:在崩溃现场查看堆栈。这里我还是建议使用IDE调试,堆栈看起来比较方便,可以摁鼠标跳转到对应源码,比命令行gdb调试方便。先在工程代码的最底层那层查找问题,因为再往下是标准库了,标准库一般是不会出错的。第四步:修复bug。比如一个越界访问bug,在越界访问的那层堆栈找到问题,但导致崩溃的原因可能并不在那儿,你可能需要在堆栈上层找找,确定这个变量是从哪里赋值的,在最合适的地方加以保护或其他处理方式来修复问题。对于多线程运行的程序,就不太好调了,可能需要分析更多的地方。

  • 此外,工程运行过程中可能出现卡死的情况,这种问题在多线程工程中出现的情况较多,而且还不好复现。这种问题最好是保护现场,然后attach到对应进程查看堆栈信息,看看是卡在哪里,是不是所有线程阻塞导致的。还有一种情况是死循环,这种情况表面上看是卡死,但你去监控cpu的时候,能发现cpu利用率是较高的,而且一般都是100的整数倍,表明有几个线程在死循环一直跑的状态,这种情况也是看堆栈就能发现问题。比较大的多线程工程查看threads是有成百上千线程的,在这里面优先去看那些条件变量阻塞的线程而不是那些系统拉起来的线程,条件变量阻塞的线程阻塞的原因也大概率不是在本线程,而是其他线程没有按预期发出唤醒信号导致的。

  • 还有,内存泄漏。这种问题如果不能通过分析源码找到的话,一般是用valgrind或asan这类工具分析,我习惯于用valgrind,但不一定能找到,只能是作为一个缩小排查范围的参考。

总之,编译期间的bug,都是小问题,主要还是对编译原理理解要到位,另外养成良好的工程组织习惯,形成自己的风格,可以避免大多数这类问题。另外,c++工程我们一般用cmake构建,所以cmake也要掌握,有些东西是需要在cmake文件里面设置的;而运行期bug,就是通过分析堆栈找问题,原因千奇百怪,不然要那么多程序员干嘛。