一剑破万法:noexcept与C++异常导致的coredump

news/2024/5/15 4:53:22/文章来源:https://blog.csdn.net/qq_40989769/article/details/133933533

作为C/C++程序员,最不想见到的就是coredump。导致coredump的原因有很多,今天我来谈一下其中一种十分常见的原因,那就是由于C++异常没有被catch导致的coredump。

从一篇知乎文章讲起

先看一位知友的文章:

C++11 std::thread异常coredump导致调用堆栈丢失问题的跟踪和解决( https://zhuanlan.zhihu.com/p/456536345)

这篇文章说是这位知友遇到一次std::thread执行时coredump,但经过gdb调试后却无法一眼看到问题代码位置。

有时候coredump不可怕,但是core栈不清晰最可怕。这次的问题的根因是在回调函数中抛出了异常但是没被catch导致,如果不是被std::thread回调,本身C++异常导致的coredump在gdb调试时是能直观看到出问题的代码行的。

然后作者使用了极其高深而琐细的方法,最终定位到了引发coredump的代码。不得不说作者其实很厉害,我也从中学到不少。但其实这有更简便的方法,请听我细细道来!

演示代码

借用一下这位知友后来写的demo验证代码:

#include <iostream>
#include <thread>
#include <vector>
void thread_func() {std::cout << "thread_func start ..." << std::endl;std::vector<int> vec;//vec.push_back(1);//vec.push_back(2);std::cout << vec.at(1) << std::endl;
}
int main (void) {std::thread th1(thread_func);th1.join();return 0;
}

该程序运行后会触发一个coredump。由于是demo代码,所以其实你一眼就能找到bug所在,但这不是重点,让我们假装不知,然后去排查。

典型的coredump堆栈

gdb打开coredump文件后,bt命令展示的堆栈信息如下:

Program terminated with signal 6, Aborted.
#0  0x00007fa9f0015387 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) bt
#0  0x00007fa9f0015387 in raise () from /lib64/libc.so.6
#1  0x00007fa9f0016a78 in abort () from /lib64/libc.so.6
#2  0x00007fa9f0b41a95 in __gnu_cxx::__verbose_terminate_handler() () from /lib64/libstdc++.so.6
#3  0x00007fa9f0b3fa06 in ?? () from /lib64/libstdc++.so.6
#4  0x00007fa9f0b3fa33 in std::terminate() () from /lib64/libstdc++.so.6
#5  0x00007fa9f0b963c5 in ?? () from /lib64/libstdc++.so.6
#6  0x00007fa9f03b4ea5 in start_thread () from /lib64/libpthread.so.0
#7  0x00007fa9f00ddb0d in clone () from /lib64/libc.so.6

这是一个非常典型的coredump文件。请记住不管你在实际生产过程中是多么复杂的C++程序,只要coredump文件中有signal 6、int raise()、int abort()这三个关键字,基本就可以大概率确认这是一起由于异常没有被catch而导致的coredump。

在实际生产过程中采用原作者的排查方法无疑比较繁琐的,而且未必有这样的条件(因为涉及到修改libstdc++的源码,重新编译,重新连接)。其实我说的简便的方法就是C++11开始引入的noexcept关键字!

修改演示代码

来给回调函数加上noexcept声明:

#include <iostream>
#include <thread>
#include <vector>
void thread_func() noexcept {std::cout << "thread_func start ..." << std::endl;std::vector<int> vec;//vec.push_back(1);//vec.push_back(2);std::cout << vec.at(1) << std::endl;
}
int main (void) {std::thread th1(thread_func);th1.join();return 0;
}

重新编译执行,然后gdb调试coredump文件。这次的core堆栈如下:

Program terminated with signal 6, Aborted.
#0  0x00007f35b2889387 in raise () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) bt
#0  0x00007f35b2889387 in raise () from /lib64/libc.so.6
#1  0x00007f35b288aa78 in abort () from /lib64/libc.so.6
#2  0x00007f35b33b5a95 in __gnu_cxx::__verbose_terminate_handler() () from /lib64/libstdc++.so.6
#3  0x00007f35b33b3a06 in ?? () from /lib64/libstdc++.so.6
#4  0x00007f35b33b29b9 in ?? () from /lib64/libstdc++.so.6
#5  0x00007f35b33b3624 in __gxx_personality_v0 () from /lib64/libstdc++.so.6
#6  0x00007f35b2e4c8e3 in ?? () from /lib64/libgcc_s.so.1
#7  0x00007f35b2e4cc7b in _Unwind_RaiseException () from /lib64/libgcc_s.so.1
#8  0x00007f35b33b3c46 in __cxa_throw () from /lib64/libstdc++.so.6
#9  0x00007f35b3408b17 in std::__throw_out_of_range(char const*) () from /lib64/libstdc++.so.6
#10 0x0000000000401595 in std::vector<int, std::allocator<int> >::_M_range_check (this=0x7f35b2851e60, __n=1) at /usr/include/c++/4.8.2/bits/stl_vector.h:794
#11 0x0000000000401313 in std::vector<int, std::allocator<int> >::at (this=0x7f35b2851e60, __n=1) at /usr/include/c++/4.8.2/bits/stl_vector.h:812
#12 0x0000000000400fde in thread_func () at demo.cpp:9
#13 0x000000000040262f in std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) (this=0xd32040) at /usr/include/c++/4.8.2/functional:1732
#14 0x0000000000402589 in std::_Bind_simple<void (*())()>::operator()() (this=0xd32040) at /usr/include/c++/4.8.2/functional:1720
#15 0x0000000000402522 in std::thread::_Impl<std::_Bind_simple<void (*())()> >::_M_run() (this=0xd32028) at /usr/include/c++/4.8.2/thread:115
#16 0x00007f35b340a330 in ?? () from /lib64/libstdc++.so.6
#17 0x00007f35b2c28ea5 in start_thread () from /lib64/libpthread.so.0
#18 0x00007f35b2951b0d in clone () from /lib64/libc.so.6

看#12的位置已经指出了demo.cpp的第9行,即:

std::cout << vec.at(1) << std::endl;
相关视频推荐

【C++】代码运行一段时间出现莫名其妙的coredump么?

C++异常处理-4个问题开始聊try/catch实现

7个方面讲解c/c++后端开发技术

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

bRPC社区的案例

通过前面的解读,我们可以发现发生在回调函数中未被catch的异常所引发的coredump,在不加noexcept声明的情况下,其堆栈信息颇为隐晦。在C++在线服务中,回调函数自然必不可少,不管是多线程或者是多协程的代码,都会用到回调函数。比如实现服务接口的函数都是被RPC框架所调用的回调函数。

前面的demo中,回调函数是极为简单的,但在实际生产环境中,业务逻辑十分复杂,存在大量的函数嵌套调用,稍不注意异常就会被连续抛到RPC框架的调度逻辑中,此时更难以觉察,甚至会误导业务程序员以为是RPC框架自身的bug。比如在bRPC社区中就多次出现这样的issue:

  • #2081:http_rpc_protocol.cpp中core掉了, 看起来不像是业务代码导致的(https://github.com/apache/brpc/issues/2081)

  • #1437:运行一段时间,就会core在brpc内部(https://github.com/apache/brpc/issues/1437)

  • #165:在brpc接口内部core,但是使用gdb分析时遇到问题(https://github.com/apache/brpc/issues/165)

这些对于bRPC的误解,其实才是本文写作的初衷。

以issue:#1437为例,看一下其中的core堆栈信息:

#0 0x00007f635e6fb597 in raise () from /lib64/libc.so.6
#1 0x00007f635e6fcdc8 in abort () from /lib64/libc.so.6
#2 0x00007f635f0029d5 in __gnu_cxx::__verbose_terminate_handler() () from /lib64/libstdc++.so.6
#3 0x00007f635f000946 in ?? () from /lib64/libstdc++.so.6
#4 0x00007f635efff909 in ?? () from /lib64/libstdc++.so.6
#5 0x00007f635f000574 in __gxx_personality_v0 () from /lib64/libstdc++.so.6
#6 0x00007f635ea99903 in ?? () from /lib64/libgcc_s.so.1
#7 0x00007f635ea99e37 in _Unwind_Resume () from /lib64/libgcc_s.so.1
#8 0x000056490472bc40 in operator() (this=, obj=) at incubator-brpc-0.9.7/src/brpc/destroyable.h:35
#9 ~unique_ptr (this=, __in_chrg=) at /usr/include/c++/4.8.2/bits/unique_ptr.h:184
#10 ~DestroyingPtr (this=, __in_chrg=) at incubator-brpc-0.9.7/src/brpc/destroyable.h:41
#11 brpc::policy::ProcessNsheadRequest (msg_base=) at incubator-brpc-0.9.7/src/brpc/policy/nshead_protocol.cpp:325
#12 0x00005649046e7eda in brpc::ProcessInputMessage (void_arg=void_arg@entry=0x5649087f8840) at incubator-brpc-0.9.7/src/brpc/input_messenger.cpp:135
#13 0x00005649046e8bf3 in operator() (this=, last_msg=0x5649087f8840) at incubator-brpc-0.9.7/src/brpc/input_messenger.cpp:141
#14 brpc::InputMessenger::OnNewMessages (m=0x7f5fa8f07040) at /usr/include/c++/4.8.2/bits/unique_ptr.h:184
#15 0x000056490479339d in brpc::Socket::ProcessEvent (arg=0x7f5fa8f07040) at incubator-brpc-0.9.7/src/brpc/socket.cpp:1017
#16 0x00005649046bcaca in bthread::TaskGroup::task_runner (skip_remained=skip_remained@entry=1) at incubator-brpc-0.9.7/src/bthread/task_group.cpp:296
#17 0x00005649046bcdcb in bthread::TaskGroup::run_main_task (this=this@entry=0x5649085aa4e0) at incubator-brpc-0.9.7/src/bthread/task_group.cpp:157
#18 0x00005649046b720e in bthread::TaskControl::worker_thread (arg=0x564907f066e0) at incubator-brpc-0.9.7/src/bthread/task_control.cpp:76
#19 0x00007f635f2b2dc5 in start_thread () from /lib64/libpthread.so.0
#20 0x00007f635e7c04dd in clone () from /lib64/libc.so.6

通过ProcessNsheadRequest()这个函数,可知这是一个nshead协议的bRPC服务,nshead是百度内部一个古老的RPC协议,bRPC也支持该协议。如果是更通用的baidu_std协议的bRPC服务,那么堆栈信息中应该是ProcessRpcRequest(),比如issue:#165。

直观来看,coredump确实是发在了bthread调度的链路上:run_main_task() -> task_runner() -> ProcessEvent() -> OnNewMessages()因此导致bRPC的使用者,将矛头直指bRPC,但真相并非如此。其本质是bRPC服务接口的业务逻辑中出现了未被catch的异常。

但具体是什么代码抛出的异常,其实仍然难以排查,如果是增量上线引入的代码,通过review本次上线的代码应该可以发现端倪。但若要避免线上经常出现此类问题,则需要我们养成一个好编码习惯,请继续阅读。

C++在线服务与异常的最佳实践

以下经验不止适用于bRPC服务,其他C++ RPC框架的使用者也能从中获益。

不在服务运行时抛异常

由于C++的异常规格与Java差异较大,对于是否该使用C++的异常,C++圈子内向来争论不休。

我个人的经验是:对于C++在线服务,不应当在服务运行时主动throw异常。这里的服务运行时主要指的是请求处理的业务代码中。虽然异常一般意味着本次请求已经不可能继续正常处理。但若主动抛出异常,并且整个调用链上都遗漏了对这种异常的catch,那么整个服务就会挂掉。从而导致同期其他能够正常处理的请求终止处理。

当然这里说的是单进程多线程/多协程的服务,对于多进程单线程处理请求的服务而言,单进程coredump该服务仍然可以继续工作。不过这种多进程的模式在在线服务中不太流行。

另外服务运行时不throw异常还包括一些background的异步线程。比如服务内有一个词典组件,该组件会定期热加载词典文件。加载过程在运行在一个单独的线程中的,这种线程内的函数也要避免throw异常。

当然凡事并无绝对,受限于业务场景,有些时候也存在一些workaround。

服务启动时可以抛异常

对于一个在线服务而言,除了运行时的请求处理代码,还有一部分代码是在服务启动时、开始处理外部请求之前执行的。比如各类资源的初始化操作,所以该阶段也可以称作初始化阶段。此时由于并不处理请求,因此在初始化异常的时候可以抛出异常,直截了当地终止服务。彼时通过查看coredump堆栈,可以快速发现是哪一处初始化失败了。

勤于给函数加上noexcept声明

即使遵守了前面的准则,我们不主动throw异常,但也未必能完全规避异常。比如在使用标准库或者某些第三方库的时候,它们仍然有可能抛出异常。这时就需要我们在可能抛异常的第一现场加上异常对应的catch逻辑或者给函数加上noexcept声明。从而避免其继续抛到上层调用的函数中。这对于偶发case的排查可能是比较有帮助的。

由于代码本身并不是一成不变的,维护人后续也可能变更,有时即便当下这个函数不会抛出异常,也无法保证某次需求会不会引入可能抛异常的代码,所以我们要勤于给函数加上noexcept声明。不可否认的是无差别的noexcept声明,确实会让代码略显冗余。确认哪些函数是必要添加的位置,从勤于变成善于,是一个长期目标,但这需要一些经验,当然前期无差别的添加noexcept也并非不可取。

lambda表达式添加noexcept声明

除了普通函数、成员函数外,lambda函数也可以添加noexcept声明。比如:

#include <iostream>
#include <thread>
#include <vector>
int main (void) {std::vector<int> vec;std::thread th1([&]() noexcept {std::cout << "thread_func start ..." << std::endl;//vec.push_back(1);//vec.push_back(2);std::cout << vec.at(1) << std::endl;});th1.join();return 0;
}

兜底:service函数加上noexcept

以bRPC的echo_server为例,下面是一个提供Echo接口的服务。实现该接口的执行逻辑就是自定义类型继承proto生成的Service父类,然后覆写虚函数Echo。这时我们可以给这个Echo函数加一下noexcept声明。

虽然抛出异常的代码未必就在Echo中,也可能是Echo层层调用的千里之外的某个函数中。但加上noexcept之后,当业务代码抛出异常时,也不会让人误以为是RPC框架的问题,从而干扰排查方向。故而算是一种兜底的做法。

class EchoServiceImpl : public EchoService {
public:EchoServiceImpl() {}virtual ~EchoServiceImpl() {}virtual void Echo(google::protobuf::RpcController* cntl_base,const EchoRequest* request,EchoResponse* response,google::protobuf::Closure* done) noexcept {...}}
};

是否应该使用标准库/第三方库中会抛出异常的函数?

我们需要熟悉哪些标准库的函数或者第三方库的函数会抛异常。比如STL容器中at()函数都是会做越界检查的,会抛异常。我个人建议程序员自己做边界检查,尽量避免使用at()。比如:

vector<int> v;
...
if (i < v.size()) {...  // 使用v[i]
}map<string, float> m;
...
auto it = m.find(key);
if (it != m.end()) {auto& value = it->second;...  // 使用value
}

当然这样严格的使用限制虽然避免了线上coredump的风险,但是可能会导致自己的业务逻辑的bug无法被及时发现。比如在你预期的逻辑中,使用v[i]或m[key]的时候永远不会越界。但是你在实现时对某些极少数的边界情况没有考虑周全,从而出现了越界。这时候由于做了边界检查,没有触发coredump,反而导致功能上线了很长时间,而未发现bug。这后果有时候可能更严重。

我们也可以给上面的if都补一个else去做日志打印或者报警之类的功能,但如果想更快发现bug,想要避免bug产生实际影响,那么我建议你在这种情况下,使用at(),并且给整个函数加上noexcept声明,从而让coredump快速定位。这也就是我前面提到的『不在服务时抛异常』的一些workaround情况。

noexcept specifier

基本介绍

前面我所提到的在函数声明中加入noexcept声明的用法,被称为noexcept specifier。其实这只是noexcept这个关键字的其中一个用法,还有另外一个用法,我们稍后会讲。先关注noexcept specifier。可以参考:https://en.cppreference.com/w/cpp/language/noexcept_spec

所谓noexcept,其实是noexcept(true)的简化,同样可以声明成noexcept(false)表示可能会抛异常。

void foo() noexcept(true);  // 等价于 void foo() noexcept;
void bar() noexcept(false); // 基本等价于 void bar();

自定义函数在没有加noexcept或noexcept(true)声明的时候,其默认是noexcept(false)。但对于一些特殊函数即使在没有显式添加noexcept声明时,也可能是noexcept(true)的。比如所有析构函数在C++11以后默认是noexcept的。这是语法规范的一部分。当然你也可以修改它:

class A {
public:~A() noexcept(false) {}
}

注意,本文以下内容中在未特殊指明的时候,所说的noexcept声明,都指的是noexcept(true)含义的noexcept声明。

noexcept与多态

如果在类中把某个虚函数声明成noexcept,那么在继承这个类的子类中,其同名函数必须也要声明成noexcept,否则编译直接失败。

这对于框架与组件库的设计者来说是一个极好的功能,方便限制住使用方在实现子类的时候不会漏掉noexcept,从而减少后续排查coredump的麻烦。但是请注意,对于框架和组件库中一些已有的函数如果之前没有加noexcept,后续就不要再加了。因为会破坏前向兼容性,将会导致使用者存量的代码无法编译通过!

因此作为框架与组件库的设计者应该在第一次释出新函数接口的时候,就考虑加上noexcept声明。

再说个题外话,当在子类中需要覆写的虚函数同时使用override和noexcept的时候,要保证noexcept在前。

class A {
public:A() {}~A() {}virtual void echo() noexcept {}
};
class B : public A {
public:~B() override {}void echo() noexcept override {}
};

noexcept与直接throw

通常当你给一个函数加上noexcept声明的时候,就不应该在这个函数中再显式地throw异常了。

void foo() noexcept {...throw runtime_error("... error");
}

对于高版本g++,编译的时候会出现编译警告:

In function ‘void foo()’:
warning: throw will always call terminate() [-Wterminate]throw runtime_error("... error");

此时如果开启g++的-Werror、-Werror=terminate编译选项,则会直接编译失败。

如果在noexcept(true)函数中调用了一个内部会throw异常的函数,这种情况是不会编译警告或编译失败的。

值得一提的是g++4.8是没有这个编译检查的,不会有编译警告或者导致编译失败。具体从哪个版本引入的这项检查,我没有去深究,至少我用过的g++7是有的。

noexcept与函数的声明与定义

在函数声明与定义分离的时候,如果在声明函数的头文件中的加入了noexcept声明。那么在定义函数的源文件中也要加上noexcept。而前面我们所提到的override关键字在函数声明与定义分离的时候,只能在函数声明的时候添加!

noexcept operator

前面提到的noexcept用法都是noexcept specifier,其实它还有另外一个用法是noexcept operator,用于判定一个表达式是否是noexcept的。

#include <iostream>
using namespace std;
void foo() noexcept {
}void bar() noexcept(false) {  // 或者 void bar() {
} int main() {cout<<"foo() check noexcept:" << noexcept(foo()) << endl;cout<<"bar() check noexcept:" << noexcept(bar()) << endl;return 0;
}

运行后输出:

foo() check noexcept:1
bar() check noexcept:0

注意,noexcept operator判断是一个表达式,不是函数。所以要使用noexcept(foo())而不是noexcept(foo)。其实这不难理解,因为foo本身可能存在重载:

#include <iostream>
using namespace std;
void foo() noexcept {
}void foo(int i) {
} int main() {cout<<"foo() check noexcept:" << noexcept(foo()) << endl;cout<<"foo(1) check noexcept:" << noexcept(foo(1)) << endl;return 0;
}

运行后输出:

foo() check noexcept:1
foo(1) check noexcept:0

另外尽管这个例子中我是用cout来输出的,但其实noexcept operator是在编译期间求值的,也就是说程序运行时noexcept operator是无开销的。

不信你可以这样来做一下测试:

#include <iostream>
using namespace std;
void foo() noexcept {
}void bar() noexcept(false) {  // 或者 void bar() {
} int main() {static_assert(noexcept(foo()), " foo is not noexcept");static_assert(noexcept(bar()), " bar is not noexcept");return 0;
}

这个代码在编译阶段就会失败,编译输出:

error: static assertion failed: bar is not noexceptstatic_assert(noexcept(bar()), " bar is not noexcept");

noexcept operator也可以用来检测类的成员函数,前面我们提到过类的析构函数默认都是noexcept(true)的,对于类中其他默认的函数,在不加声明的时候具体是noexcept(true)还是noexcept(false),是比较复杂的,在本文中就不过度展开了,有兴趣可以阅读:https://en.cppreference.com/w/cpp/language/noexcept

noexcept不是coredump万金油!

最后请注意虽然本文标题十分标题党地使用了『一剑破万法』的说法,但是这个『万法』仅仅指的是各类C++异常(Exception),对于其他原因导致的coredump,比如访问非法内存地址触发coredump,noexcept并不会有太大帮助!

所以noexcept并不是排查coredump的万金油,它只对异常没被catch导致的coredump有效。

高版本g++/gdb能解决问题吗?

高版本的g++和gdb对于回调函数中异常没被catch导致的coredump,其堆栈不清晰的问题有优化吗?来做个测试。

我这里有一个g++9.4,gdb9.2的环境。

先试一下前面提到的知友的那个未加noexcept声明的demo代码:

#include <iostream>
#include <thread>
#include <vector>
void thread_func() {std::cout << "thread_func start ..." << std::endl;std::vector<int> vec;//vec.push_back(1);//vec.push_back(2);std::cout << vec.at(1) << std::endl;
}
int main (void) {std::thread th1(thread_func);th1.join();return 0;
}

加上-g参数编译,重新触发coredump,进行gdb调试:

#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fa0eec91859 in __GI_abort () at abort.c:79
#2  0x00007fa0eef3d911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007fa0eef4938c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007fa0eef493f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007fa0eef496a9 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00007fa0eef403ab in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#7  0x00005621d5f5b8fc in std::vector<int, std::allocator<int> >::_M_range_check (this=0x7fa0eeb19de0, __n=1)at /usr/include/c++/9/bits/stl_vector.h:1070
#8  0x00005621d5f5b6ed in std::vector<int, std::allocator<int> >::at (this=0x7fa0eeb19de0, __n=1) at /usr/include/c++/9/bits/stl_vector.h:1091
#9  0x00005621d5f5b36a in thread_func () at zhiyou.cpp:9
#10 0x00005621d5f5c15e in std::__invoke_impl<void, void (*)()> (__f=@0x5621d6810eb8: 0x5621d5f5b309 <thread_func()>)at /usr/include/c++/9/bits/invoke.h:60
#11 0x00005621d5f5c0f6 in std::__invoke<void (*)()> (__fn=@0x5621d6810eb8: 0x5621d5f5b309 <thread_func()>) at /usr/include/c++/9/bits/invoke.h:95
#12 0x00005621d5f5c088 in std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul> (this=0x5621d6810eb8) at /usr/include/c++/9/thread:244
#13 0x00005621d5f5c045 in std::thread::_Invoker<std::tuple<void (*)()> >::operator() (this=0x5621d6810eb8) at /usr/include/c++/9/thread:251
#14 0x00005621d5f5c016 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run (this=0x5621d6810eb0)at /usr/include/c++/9/thread:195
#15 0x00007fa0eef75de4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#16 0x00007fa0eee69609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#17 0x00007fa0eed8e133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

可以看出 #7、#8 能看出是at()导致的越界异常,#9 指明了行号。看起来似乎不错,但在复杂的回调逻辑中表现如何呢?我们来试一下bRPC的echo_server的代码,原版代码:https://github.com/apache/brpc/blob/master/example/echo_c%2B%2B/server.cpp

在Echo接口函数的底部中加上一个会抛出异常的代码:

       virtual void Echo(google::protobuf::RpcController* cntl_base,const EchoRequest* request,EchoResponse* response,google::protobuf::Closure* done) noexcept {// This object helps you to call done->Run() in RAII style. If you need// to process the request asynchronously, pass done_guard.release().brpc::ClosureGuard done_guard(done);brpc::Controller* cntl =static_cast<brpc::Controller*>(cntl_base);// The purpose of following logs is to help you to understand// how clients interact with servers more intuitively. You should // remove these logs in performance-sensitive servers.LOG(INFO) << "Received request[log_id=" << cntl->log_id() << "] from " << cntl->remote_side() << " to " << cntl->local_side()<< ": " << request->message()<< " (attached=" << cntl->request_attachment() << ")";// Fill response.response->set_message(request->message());// You can compress the response by setting Controller, but be aware// that compression may be costly, evaluate before turning on.// cntl->set_response_compress_type(brpc::COMPRESS_TYPE_GZIP);if (FLAGS_echo_attachment) {// Set attachment which is wired to network directly instead of// being serialized into protobuf messages.cntl->response_attachment().append(cntl->request_attachment());}std::vector<int> v;std::cout<< v.at(0) << std::endl;  // 这里抛出越界异常!}

接着进行编译,启动echo_server。然后启动echo_client向echo_server发请求,echo_server出现了coredump。使用gdb进行调试,coredump堆栈如下:

#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007f8be87d5859 in __GI_abort () at abort.c:79
#2  0x00007f8be8baf911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007f8be8bbb38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007f8be8bba369 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007f8be8bbad21 in __gxx_personality_v0 () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00007f8be89b5bef in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#7  0x00007f8be89b65aa in _Unwind_Resume () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#8  0x00005609e57d96f3 in brpc::detail::Destroyer<brpc::policy::MostCommonMessage>::operator() (this=<optimized out>, obj=<optimized out>)at ./src/brpc/destroyable.h:35
#9  std::unique_ptr<brpc::policy::MostCommonMessage, brpc::detail::Destroyer<brpc::policy::MostCommonMessage> >::~unique_ptr (this=<optimized out>, __in_chrg=<optimized out>) at /usr/include/c++/9/bits/unique_ptr.h:292
#10 brpc::DestroyingPtr<brpc::policy::MostCommonMessage>::~DestroyingPtr (this=<optimized out>, __in_chrg=<optimized out>)at ./src/brpc/destroyable.h:41
#11 brpc::policy::ProcessRpcRequest (msg_base=<optimized out>) at src/brpc/policy/baidu_rpc_protocol.cpp:314
#12 0x00005609e59663db in brpc::ProcessInputMessage (void_arg=<optimized out>) at src/brpc/input_messenger.cpp:159
#13 0x00005609e5966f35 in brpc::InputMessenger::InputMessageClosure::~InputMessageClosure (this=<optimized out>, __in_chrg=<optimized out>)at src/brpc/input_messenger.cpp:194
#14 0x00005609e5967a6b in brpc::InputMessenger::OnNewMessages (m=0x7f8bc801aea0) at /usr/include/c++/9/bits/atomic_base.h:493
#15 0x00005609e5b39002 in brpc::Socket::ProcessEvent (arg=0x7f8bc801aea0) at src/brpc/socket.cpp:1093
#16 0x00005609e5aa682f in bthread::TaskGroup::task_runner (skip_remained=<optimized out>) at src/bthread/task_group.cpp:298
#17 0x00005609e5a9ddb1 in bthread_make_fcontext () at /usr/include/c++/9/bits/stl_iterator.h:803

发现并不清晰,coredump发生在bRPC框架中的brpc::DestroyingPtr中,这对于大多数业务程序员来说仍然颇具迷惑性。

现在我们给Echo接口函数加上noexcept声明,再重走一遍上述流程,看一下新的coredump堆栈:

#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007f987922d859 in __GI_abort () at abort.c:79
#2  0x00007f9879607911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007f987961338c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007f9879612369 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007f9879612d21 in __gxx_personality_v0 () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00007f987940dbef in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#7  0x00007f987940e281 in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#8  0x00007f987961369c in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#9  0x00007f987960a3ab in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#10 0x000055e51d9cf0d6 in std::vector<int, std::allocator<int> >::_M_range_check (__n=0, this=<synthetic pointer>)at /usr/include/c++/9/bits/stl_vector.h:1089
#11 std::vector<int, std::allocator<int> >::at (__n=0, this=<synthetic pointer>) at /usr/include/c++/9/bits/stl_vector.h:1091
#12 example::EchoServiceImpl::Echo (this=<optimized out>, cntl_base=0x7f9854027df0, request=0x7f9854028120, response=0x7f9854029950, done=<optimized out>) at server.cpp:73
#13 0x000055e51d9cd39a in example::EchoService::CallMethod (method=<optimized out>, done=<optimized out>, response=<optimized out>, request=<optimized out>, controller=<optimized out>, this=<optimized out>) at echo.pb.cc:671
#14 example::EchoService::CallMethod (this=<optimized out>, method=<optimized out>, controller=<optimized out>, request=<optimized out>, response=<optimized out>, done=<optimized out>) at echo.pb.cc:663
#15 0x000055e51dbf37a0 in brpc::policy::ProcessRpcRequest (msg_base=0x7f985401f460) at /usr/include/c++/9/bits/unique_ptr.h:381
#16 0x000055e51db3c37b in brpc::ProcessInputMessage (void_arg=<optimized out>) at src/brpc/input_messenger.cpp:159
#17 0x000055e51db3ced5 in brpc::InputMessenger::InputMessageClosure::~InputMessageClosure (this=<optimized out>, __in_chrg=<optimized out>)at src/brpc/input_messenger.cpp:194
#18 0x000055e51db3da0b in brpc::InputMessenger::OnNewMessages (m=0x7f986001aea0) at /usr/include/c++/9/bits/atomic_base.h:493
#19 0x000055e51dd0efa2 in brpc::Socket::ProcessEvent (arg=0x7f986001aea0) at src/brpc/socket.cpp:1093
#20 0x000055e51dc7c7cf in bthread::TaskGroup::task_runner (skip_remained=<optimized out>) at src/bthread/task_group.cpp:298
#21 0x000055e51dc73d51 in bthread_make_fcontext () at /usr/include/c++/9/bits/stl_iterator.h:803

可以看出 #10、#11、#12清晰的展示出了越界异常,以及问题代码的行号server.cpp:73,这一行就是std::cout<< v.at(0)<< std::endl;所在的行。

综上所述,高版本的g++、gdb对于本文所探讨的问题确实有优化,但对于复杂的回调链路仍旧捉襟见肘,因此noexcept依旧是我们的首选。当然更高版本的g++、gdb我就没有环境测试了,其他同学有其他版本的测试结论可以评论回复。不过对于生产环境而言,升级编译器版本往往并不是自己或者自己团队所能决定的了,这是一项十分浩大的变更工作。

原文地址:果冻虾仁:一剑破万法:noexcept与C++异常导致的coredump

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_186001.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Element UI 密码输入框--可切换显示隐藏,自定义图标

<el-form-item prop"password"><el-inputkeyup.enter.native"login"placeholder"密码"v-model"formData.password":type"showPassword ? text : password"><i slot"suffix" click"switchPas…

软件项目管理【UML-组件图】

目录 一、组件图概念 二、组件图包含的元素 1.组件&#xff08;Component&#xff09;->构件 2.接口&#xff08;Interface&#xff09; 3.外部接口——端口 4.连接器&#xff08;Connector&#xff09;——连接件 4.关系 5.组件图表示方法 三、例子 一、组件图概念…

[数据挖掘、数据分析] clickhouse在go语言里的实践

系列文章目录 [数据挖掘] clickhouse在go语言里的实践 [数据挖掘] 用户画像平台构建与业务实践 文章目录 系列文章目录前言一、clickhouse的起源二、OLAP/OLTP2.1、主流的OLAP/OLTP数据库 三、go语言开发实践3.1、安装配置go语言环境&#xff0c;配置IDE3.1.1、Go开发环境安装…

AI绘画的魅力与未来:人工智能如何重塑艺术创作

随着人工智能&#xff08;AI&#xff09;技术的不断进步&#xff0c;AI绘画已经成为艺术与技术交汇的新领域。通过深度学习、神经网络等先进技术&#xff0c;AI不仅能理解和模拟人类艺术家的创作风格&#xff0c;还能生成令人惊叹的原创艺术作品。本文旨在探讨AI绘画的现状、挑…

2023年中国人力资源咨询发展历程及市场规模前景分析[图]

人力资源咨询是企业借助外部智力资源提高自身管理水平和效率的重要路径&#xff0c;属于管理咨询业的一个重要分支, 一方面&#xff0c;人力资源咨询要为企业提供基础的人力资源外包服务&#xff1b;另一方面&#xff0c;人力资源咨询要为企业提供专业化、职业化现代人力资源管…

【linux kernel】linux的platform设备驱动框架分析

文章目录 一、简介二、platform总线三、platform设备和驱动的匹配过程四、platrom驱动和platform设备五、platform驱动设计六、代码示例 &#x1f53a;【linux内核系列文章】 &#x1f449;对一些文章内容进行了勘误&#xff0c;本系列文章长期不定时更新&#xff0c;希望能分享…

榜样力量激发青少年英语学习新活力

近日,在第二届iEnglish英语风采秀总决选中,12岁的天津女孩田丽雨以出色的英语能力和生动表演赢得了年度舞台之星的称号。田丽雨的英语学习方法引发了社会各界的广泛关注,尤其是她以身边榜样为动力,借助智能英语学习工具iEnglish实现了英语自由阅读和无障碍交流的目标。 据中国青…

2023年中国劳务派遣市场规模、竞争现状及行业趋势[图]

劳务派遣业务是指劳务派遣单位依据用工单位的需求将员工外派到用工单位工作&#xff0c;分别与用工单位和被外派人员签订派遣协议、劳动合同&#xff0c;以规范三方在派遣期间的权利和义务。 我国目前能够从事人力资源服务行业的主体包括民营人力资源服务企业、国有人力资源服务…

竞赛选题 深度学习乳腺癌分类

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…

支付宝小程序介入人脸识别(金融级--前端部分)

在这里只做前端部分说明: 详情参考文档:如何通过集成支付宝小程序唤起实人认证服务_实人认证-阿里云帮助中心 操作步骤 调用 API 发起认证。 发起认证服务。 调用 startBizService 接口请求认证。 function startAPVerify(options, callback) {my.call(startBizService, {n…

CentOS7安装MySQL8.0.28

CentOS7安装MySQL8.0.28 一、下载MySQL安装包二、安装配置mysql 一、下载MySQL安装包 点击以下链接可以自动跳转&#xff1a;MySQL官网 接下来按如图所示依次点击进入。 选择自己所需要版本 此处如需下载历史版本可以点击 二、安装配置mysql 1、登录ssh或其他相关软件上…

【数据结构】线性表(六)堆栈:顺序栈及其基本操作(初始化、判空、判满、入栈、出栈、存取栈顶元素、清空栈)

文章目录 一、堆栈1. 定义2. 基本操作 二、顺序栈0. 顺序表1. 头文件和常量2. 栈结构体3. 栈的初始化4. 判断栈是否为空5. 判断栈是否已满6. 入栈7. 出栈8. 查看栈顶元素9. 清空栈10. 主函数11. 代码整合 堆栈Stack 和 队列Queue是两种非常重要的数据结构&#xff0c;两者都是特…

laravel的默认首页怎么改-laravel框架默认欢迎页面如何修改

laravel的默认首页怎么改 搭建好的laravel的默认首页怎么改 我们有两种改动方式&#xff1a; 第一种修改默认路由&#xff1a; 下一步是要移除Laravel应用程序默认的欢迎页路由。这个路由可以在routes/web.php文件的顶部找到&#xff0c;看起来类似于以下代码&#xff1a; …

微信native-v3版支付对接流程及demo

1.将p12证书转为pem证书&#xff0c;得到商户私钥 openssl pkcs12 -in apiclient_cert.p12 -out apiclient_cert.pem -nodes 密码是&#xff1a;商户id 2.将获取到的apiclient_cert.pem证书&#xff0c;复制出这一块内容&#xff0c;其他的不要 3.下载这个工具包 https://gi…

Optuna:带仪表盘的可视化的超参数优化

1、引言 Optuna是一个由日本东京大学开发的自动化超参数优化库&#xff0c;用于机器学习和人工智能。它可用于自动优化神经网络、随机森林等模型的超参数&#xff0c;以改善模型的性能和准确性。Optuna还可以与其他流行的机器学习框架&#xff08;如TensorFlow和PyTorch&#…

https证书

SSL证书的作用是确保通过网站传输的数据在客户端和服务器之间是安全、私密的。 它通过建立安全的通信通道来防止数据泄露、中间人攻击以及篡改等安全威胁&#xff0c;提供了身份认证和数据加密的功能。 这样可以确保用户在网站上提交的敏感信息&#xff0c;如个人数据、公司信…

2023下半年信息系统集成设计师案例

案例题 重要的知识点容易忽略的知识点不错的小题合同管理配置管理变更管理成本管理招标管理人力资源管理质量管理风险管理沟通管理立项和需求 能说专业术语&#xff08;比喻十大领域管理不足&#xff09;就说&#xff0c;没法说的大白话也没问题 重要的知识点 如果题目没有说从…

2023年中国光模块行业研究报告

第一章 行业概况 1.1 行业简介 光模块行业是光纤通信技术发展的重要组成部分&#xff0c;作为连接光纤通信网络的基础设备&#xff0c;光模块为数据传输提供了必要的硬件支持。光模块是光纤通信系统核心器件之一&#xff0c;它包括多种模块类别&#xff0c;例如光接收模块、光…

短视频矩阵系统源码---开发

一、智能剪辑、矩阵分发、无人直播、爆款文案于一体独立应用开发 抖去推----主要针对本地生活的----移动端(小程序软件系统&#xff0c;目前是全国源头独立开发)&#xff0c;开发功能大拆解分享&#xff0c;功能大拆解&#xff1a; 7大模型剪辑法&#xff08;数学阶乘&#x…