循迹追踪使人头秃的Crash,十倍程序员的Debug平常(2)

做者|陶建辉node

原文首发于:git

循迹追踪使人头秃的Crashgithub

咱们写 C 程序,常常碰到 Crash,绝大部分状况下都是空指针或野指针形成,从 call stack 来看,通常很容易找出问题。可是有一类 Crash 很难 debug,那就是内存溢出。溢出的部分的内存空间正好覆盖另一个线程访问的数据(好比一个结构体),那么另一个线程读取这块数据时,获取的数据就是无效的,每每致使不可预见的错误,甚至 Crash。但由于形成数据溢出的线程已经离开现场,致使问题很难定位。这是我在 2020 年 5 月写的一篇内部博客,以我当时碰到的一个错误为例,将解决这类问题的方法分享出来,供你们参考,触类旁通。

具体问题

在feature/query分支上,在community仓库,执行如下脚本,出现Crash。session

./test.sh -f general/parser/col_arithmetic_operation.sim

重现问题

我登陆到指定的机器,查看了 core dump, 确实如此。Call Stack 截图以下:async

第一步:看哪一个地方 crash。是 shash.c:250 行,使用 GDB 命令 “f 1″ 查看 stack 1,查看*pObj,一下就看到 hashFp 为 NULL,天然会 crash。但为何会设置为空?其余参数呢?dataSize 设为 0 也必定是错的。所以能够判定,这个结构体是不对的。咱们须要看上一层调用是否传入了正确的参数。函数

第二步:使用 GDB “f 2″查看 stack 2,rpcMain.c:605 行,查看*pRpc,这些结构体里的参数显得很正常,包括指向 hash 的指针值看不出有什么问题。那所以能够判定调用是 OK 的,调用 taosGetStrHashData 应该提供了正确的参数。工具

第三步:既然参数对的,看 shash.c 的程序,那只多是 SHashObj 这个结构体已经被释放,访问的时候,天然无效。再看代码,只有一个可能,函数 taosCleanUpStrHash 被调用,所以我在改函数里立刻加上一行打印日志(注意 TDengine 的日志输出控制,系统配置文件 taos.cfg 里参数 asyncLog 要设置为 1,不然 crash 时,有可能日志不打印出来)。从新运行脚本,查看日志,发现 taosCleanUpStrHash 没有被调用过。那么如今只有一个可能,这一块数据的内存被其余线程写坏。测试

第四步:万幸,咱们有很棒的运行时内存检查工具 valgrind, 能够经过运行它来找找蛛丝马迹。一运行(valgrind 有不少选项,我运行的是 valgrind –leak-check=yes –track-origins=yes taosd -c test/dnode1/cfg),一下就发现有 invalid write,截图以下:编码

第五步:一看 valgrind 输出,就知道 rpcMain.c:585 有 invalid write, 这里是 memcpy。从编码上来看,这应该没有问题,由于拷贝的是固定大小的结构体 SRpcConn,每次运行到这里都会执行的。那么惟一的可能就是 pConn 指向的是无效的内存区,那 pConn 怎么多是无效的?咱们看一下程序:spa

看 584 行,pConn = pRpc->connList + sid。这个 sid 是 taosAllocateId 分配出来的。若是 sid 超过 pRpc->sessions,那 pConn 毫无疑问就指向了无效的区域。那怎么确认呢?

第六步:加上日志 578 行,分配出的 ID 打印出来,编译,从新运行测试脚本。

第七步:crash,看日志,能够看到 sid 能输出到 99(max 是 100),还一切正常,但以后就崩溃。所以能够断言,就是因为分配的 ID 超过了 pRpc→session。

第八步:查看分配 ID 的程序 tidpool.c,一看就知道缘由,ID 分配是从 1 到 MAX,而 RPC 模块只能使用 1 到 Max-1。这样当 ID 返回为 max 时,RPC 模块天然会产生 invalid write。

解决方案

既然知道缘由,那就很好办,有两套方法:

  1. 在 tidpool.c,taosInitIdPool 里,将 maxId 减一,这样分配的 ID 只会是 1 到 max-1。
  2. 在 rpcMain.c 的 rpcOpen() 函数里,将
pRpc->idPool = taosInitIdPool(pRpc->sessions);

改成

pRpc->idPool = taosInitIdPool(pRpc->sessions-1);

若是应用要求最多 100 个 session,这么改,RPC 至多建立 99 个,为保证 100 个,再将

pRpc->sessions = pInit->sessions;

改成

pRpc->sessions = pInit→sessions+1;

验证

两种方法,都从新编译,运行测试脚本经过,crash 再也不发生。

经验总结

遇到内存被写坏的场景,必定要用 valgrind 跑一次,看是否有 invalid write。由于它是一个动态检查工具,报的错误都应该是对的。只有把 invalid write 先解决,再去看 crash 问题。

怎么避免相似问题

这个 BUG 的核心是因为 tidpool.c 分配的 ID 范围是 1 到 max,而 RPC 模块假设的 ID 分配是 1 到 max-1。所以是模块之间的约定出了问题。

怎么避免?每一个模块对外 API 要作详细说明,若是 API 作了调整,要通知你们,并且要运行完整的测试例,以避免破坏了某种约定。

本文为 TDengine 的一则真实案例,git commit id:89d9d62,欢迎你们重现。