日更系列:排查一次gcc的复杂core

2022-05-14 14:32:51 浏览数 (1)

线上服务出现了重启告警,而且重启后继续。排查线上出现了一堆core,core信息显示如下:

这个是一个线程抛出的异常,但是看不到线程本身的代码,只可看到core文件堆栈栈钉出现在libstdc 库上。唯一业务代码在第6个栈帧。catch方块里的throw e代码上。

这里有几个问题。这里的异常实际上是在线程调度的函数抛出了异常。也就是thread->m_Runner->Execute()的时候,当时在异常抛出的时候,我们其实想要看到的是那个时候的堆栈内存快照。但是这个core发生的时候,已经退出了那个Execute函数,这个实际函数已经退出了,当时core点的堆栈已经展开了。然后函数跳过Execute函数,走到catch模块,到throw e位置,由于外部再无补货这个exception e的函数,函数在此结束。core信息反应的是此时throw e的内存快照。

1. 问题

1.1 c 的throw或者abort机制

这里引入了一个知识点就是:C 程序,如果 代码throw了 exception ,但是外部又没有 catch,那么一般会产生 coredump。而如果外面catch住了异常,这个就不会产生coredump,但是你也可以继续选择向上级抛异常,也可以忽略打点异常日志出来e.what()。 gcc有个宏_GLIBCXX_THROW_OR_ABORT定义你的行为是throw还是abort

我记得谷歌c 规范也是建议大家不写异常。但是如果团队内有人不遵守怎么办,我们可以在makefile加上(-gcc的 -fno-exceptions ,在编译阶段禁用异常机制。这样写了异常的代码不会通过。

gcc有个宏_GLIBCXX_THROW_OR_ABORT 的宏,該宏定義為

代码语言:javascript复制
#ifndef _GLIBCXX_THROW_OR_ABORT
# if __cpp_exceptions
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (throw (_EXC))
# else
#  define _GLIBCXX_THROW_OR_ABORT(_EXC) (__builtin_abort())
# endif
#endif

1.2 线程执行机制

有了第一个问题的铺垫,我们知道上层代码如果catch住了调用函数的异常,会导致core信息显示的不是调用函数本身的堆栈。所以要么函数外面不cacth,要么让抛__cxa_throw的函数直接挂掉。

本文的第一个问题正是,在 gcc 4.x 版本(gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) )产生的 coredump 文件中,没有 throw 时候的堆栈信息,导致不知道是哪里 throw 的,没法查问题。

我们继续看gcc 4.x 的 /libstdc -v3/src/c 11/thread.cc:92 的代码就发现原因。这个gcc 4.x的线程执行的实现里面有个 catch(…),所以 stack unwind 了,就没了 throw 时候的 stack 。

以下是gcc4.x-6.x的实现

而gcc7.x以上已经重新实现了,以下是gcc 7.x以上的实现

这里就没有了cache。https://abcdabcd987.com/libstdc -bug/

二、解决办法

2.1 升级到gcc7以上

一个解决办法是可以升级 GCC 7 ,

2.2 强行改写throw方法

因为不是所有的项目都方便切换到新版本的gcc,历史代码不好兼容新版本gcc,可以用更简单的办法:

1.代码 hook __cxa_throw

一个解决办法是通过改代码,hook __cxa_throw() 让每次生成的 coredump 都带上堆栈:

https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/cxxabi.h#L616

vim /usr/include/c /4.8.2/cxxabi.h 看到__cxa_throw的声明。__cxa_throw() 是 libstdc /libc 用于实现 throw 的函数。

https://libcxxabi.llvm.org/spec.html

我们可以重写(或者叫hook)

这个函数要加到你调用线程的那个文件里。比如我这里的Util/CThread.cpp(项目代码中线程执行函数)。

效果如下:

0 人点赞