[crash] cxa_pure_virtual 崩溃分析与原理

2023-11-23 08:12

本文主要是介绍[crash] cxa_pure_virtual 崩溃分析与原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  摘要:工作过程中处理线上的崩溃时发现了一例cxa_pure_virtual相关的crash,直接看堆栈基本山很容易确认是有异步调用导致出发了ABI的异常。但是对于为什么会触发cxa_pure_virtual虽然有大致的猜测但是没有直接的证据,因此本文主要描述触发该类型崩溃的原理。
  关键字:cxxabi,llvm,cxa_pure_virtual,vptr

  首先我们看一下崩溃的现象,线上的崩溃堆栈大概类似于下面形式:

0x********* abort()
0x********* std::terminate()
0x********* cxxabi::__cxa_pure_virtual()
0x********* ******::*******

  上面的崩溃我们看实际的代码基本上能够判断出当前类已经被析构的情况下当前类却尝试访问虚函数导致了cxa_pure_virtual,要修复该问题直接排查哪里导致的异步调用即可。但是为了更加输入的理解,我这边查阅了一些资料,如下。  摘要:工作过程中处理线上的崩溃时发现了一例cxa_pure_virtual相关的crash,直接看堆栈基本山很容易确认是有异步调用导致出发了ABI的异常。但是对于为什么会触发cxa_pure_virtual虽然有大致的猜测但是没有直接的证据,因此本文主要描述触发该类型崩溃的原理。
  关键字:cxxabi,llvm,cxa_pure_virtual,vptr

  首先我们看一下崩溃的现象,线上的崩溃堆栈大概类似于下面形式:

0x********* abort()
0x********* std::terminate()
0x********* cxxabi::__cxa_pure_virtual()
0x********* ******::*******

  上面的崩溃我们看实际的代码基本上能够判断出当前类已经被析构的情况下当前类却尝试访问虚函数导致了cxa_pure_virtual,要修复该问题直接排查哪里导致的异步调用即可。

  __cxa_pure_virtual的描述如下:

The __cxa_pure_virtual function is an error handler that is invoked when a pure virtual function is called.
If you are writing a C++ application that has pure virtual functions you must supply your own __cxa_pure_virtual error handler function.

  当调用一个纯虚函数时被调用,看llvm中cxxabi的实现可以看到该函数被调用时会直接abort。那就比较奇怪,如果我们调用的是一个纯虚函数按理说编译都无法通过,但是查看代码发现对应的函数是被重写的。那我们此时可能怀疑的一个点便是,虚基类的虚函数表构造和销毁问题。可能是因为子类被销毁是基类的虚函数表被改回基类的虚函数表,而基类中对应虚函数指针就是编译器指定的cxa_pure_virtual

_LIBCXXABI_FUNC_VIS _LIBCXXABI_NORETURN void __cxa_pure_virtual(void) {abort_message("Pure virtual function called!");
}

  怀疑到这一点,我这边开始找资料(类似的问题印象中标准中是不管的,那大概率在ABI中定义的,那我们去看ABI的定义)。从ABI的定义中找到如下的描述:

An implementation shall provide a standard entry point that a compiler may reference in virtual tables to indicate a pure virtual function. Its interface is:extern "C" void __cxa_pure_virtual ();
This routine will only be called if the user calls a non-overridden pure virtual function, which has undefined behavior according to the C++ Standard. Therefore, this ABI does not specify its behavior, but it is expected that it will terminate the program, possibly with an error message.if C::f is a pure virtual function, no specific requirement is made for the corresponding virtual table entry. It may point to __cxa_pure_virtual (see 3.2.6 Pure Virtual Function API) or to a wrapper function for __cxa_pure_virtual (e.g., to adapt the calling convention). It may also simply be null in such cases.

  上面这一段描述了cxa_pure_virtual实际的意义。下面再看一下CXXABI中关于对象以及虚函数表构造的过程的描述:

     // Sub-VTT for D (embedded in VTT for its derived class X):static vtable *__VTT__1D [1+n+m] ={ D primary vtable,// The sub-VTT for B-in-D in X may have further structure:B-in-D sub-VTT (n elements),// The secondary virtual pointers for D's bases have elements// corresponding to those in the B-in-D sub-VTT,// and possibly others for virtual bases of D:D secondary virtual pointer for B and bases (m elements) }; D ( D *this, vtable **ctorvtbls ){// (The following will be unwound, not a real loop):for ( each base A of D ) {// A "boring" base is one that does not need a ctorvtbl:if ( ! boring(A) ) {// Call subobject constructors with sub-VTT index// if the base needs it -- only B in our example:A ( (A*)this, ctorvtbls + sub-VTT-index(A) ); } else {// Otherwise, just invoke the complete-object constructor:A ( (A*)this );}}// Initialize virtual pointer with primary ctorvtbls address// (first element):this->vptr = ctorvtbls+0;	// primary virtual pointer// (The following will be unwound, not a real loop):for ( each subobject A of D ) {// Initialize virtual pointers of subobjects with ctorvtbls// addresses for the bases if ( ! boring(A) ) {((A*)this)->vptr = ctorvtbls + 1+n + secondary-vptr-index(A);// where n is the number of elements in the sub-VTTs} else {// Otherwise, just use the complete-object vtable:((A *)this)->vptr = &(A-in-D vtable);}}// Code for D constructor....}

  从上面的描述中我们能够看到:

  1. 当前类的虚函数表指针的确定是在执行具体的构造函数代码之前的;
  2. 构建当前类之前会搜索当前类的继承图,找到基类按照继承图的先序序列构造基类;
  3. 基类构造完成后开始调用当前类的构造函数的代码。

  析构函数的顺序相反。对于一个具有直接继承关系的虚基类A和B(B继承自A)的构造顺序为:

class A{
public:virtual void func() = 0;
};class B: public A{
public:virtual void func(){}
};
  1. B构造函数B::B被调用;
  2. 遍历B的基类构造调用基类的构造函数,这里就是A::A();
  3. 调用A的时候先将vfptr指向A的虚函数表,此表项中有基类偏移,typeinfo,__cxa_pure_virtual(因为func是纯虚函数因此该处的虚函数表指针以此填充);
  4. 调用A::A的用户代码,这里没有就不调用;
  5. A构造函数执行完后开始设置B的虚函数指针为B的虚函数表。

  析构顺序:

  1. 调用B::~B析构函数;
  2. 设置虚函数表指针为B的虚函数表;
  3. 执行B析构的用户代码;
  4. 调用基类A::~A(),该过程中先设置虚函数表指针为A的虚函数表再调用A的用户代码。

  从上面的过程中大概也能看出cxa_pure_virtual可能被调用的时机。当类被析构时,基类的析构稍微比较耗时时,第二个线程尝试访问当前类的一个被重写的纯虚函数,由于此时的虚函数表中的纯虚函数已经被修改为cxa_pure_virtual就会直接abort。那我们复现下:

class ClassA {
public:ClassA() {printf("Class A \n");}virtual ~ClassA() {std::this_thread::sleep_for(std::chrono::seconds(5));}virtual void func() = 0;
};class ClassB : public ClassA {
public:virtual ~ClassB() {printf("Class B \n");};virtual void func() override {printf("Class B func\n");}
};void func(ClassA *p) {while (1) {p->func();}
}int main(){std::cout << "Hello World!\n";ClassA* p = new ClassB;auto t = std::thread(func, p);std::this_thread::sleep_for(std::chrono::seconds(1));delete p;t.join();
}

  上面的代码中在析构函数中加了sleep函数来保证对象被析构过程中卡在基类的析构函数,第二个线程尝试访问该纯虚函数。
  再clang/gcc系列编译器上触发的是cxa_purer_virtual,而msvc触发的是_purecall


extern "C" int __cdecl _purecall()
{_purecall_handler const purecall_handler = _get_purecall_handler();if (purecall_handler){purecall_handler();// The user-registered purecall handler should not return, but if it does,// continue with the default termination behavior.}abort();
}

  __cxa_pure_virtual的描述如下:

The __cxa_pure_virtual function is an error handler that is invoked when a pure virtual function is called.
If you are writing a C++ application that has pure virtual functions you must supply your own __cxa_pure_virtual error handler function.

  当调用一个纯虚函数时被调用,看llvm中cxxabi的实现可以看到该函数被调用时会直接abort。那就比较奇怪,如果我们调用的是一个纯虚函数按理说编译都无法通过,但是查看代码发现对应的函数是被重写的。那我们此时可能怀疑的一个点便是,虚基类的虚函数表构造和销毁问题。可能是因为子类被销毁是基类的虚函数表被改回基类的虚函数表,而基类中对应虚函数指针就是编译器指定的cxa_pure_virtual

_LIBCXXABI_FUNC_VIS _LIBCXXABI_NORETURN void __cxa_pure_virtual(void) {abort_message("Pure virtual function called!");
}

  怀疑到这一点,我这边开始找资料(类似的问题印象中标准中是不管的,那大概率在ABI中定义的,那我们去看ABI的定义)。从ABI的定义中找到如下的描述:

An implementation shall provide a standard entry point that a compiler may reference in virtual tables to indicate a pure virtual function. Its interface is:extern "C" void __cxa_pure_virtual ();
This routine will only be called if the user calls a non-overridden pure virtual function, which has undefined behavior according to the C++ Standard. Therefore, this ABI does not specify its behavior, but it is expected that it will terminate the program, possibly with an error message.if C::f is a pure virtual function, no specific requirement is made for the corresponding virtual table entry. It may point to __cxa_pure_virtual (see 3.2.6 Pure Virtual Function API) or to a wrapper function for __cxa_pure_virtual (e.g., to adapt the calling convention). It may also simply be null in such cases.

  上面这一段描述了cxa_pure_virtual实际的意义。下面再看一下CXXABI中关于对象以及虚函数表构造的过程的描述:

     // Sub-VTT for D (embedded in VTT for its derived class X):static vtable *__VTT__1D [1+n+m] ={ D primary vtable,// The sub-VTT for B-in-D in X may have further structure:B-in-D sub-VTT (n elements),// The secondary virtual pointers for D's bases have elements// corresponding to those in the B-in-D sub-VTT,// and possibly others for virtual bases of D:D secondary virtual pointer for B and bases (m elements) }; D ( D *this, vtable **ctorvtbls ){// (The following will be unwound, not a real loop):for ( each base A of D ) {// A "boring" base is one that does not need a ctorvtbl:if ( ! boring(A) ) {// Call subobject constructors with sub-VTT index// if the base needs it -- only B in our example:A ( (A*)this, ctorvtbls + sub-VTT-index(A) ); } else {// Otherwise, just invoke the complete-object constructor:A ( (A*)this );}}// Initialize virtual pointer with primary ctorvtbls address// (first element):this->vptr = ctorvtbls+0;	// primary virtual pointer// (The following will be unwound, not a real loop):for ( each subobject A of D ) {// Initialize virtual pointers of subobjects with ctorvtbls// addresses for the bases if ( ! boring(A) ) {((A*)this)->vptr = ctorvtbls + 1+n + secondary-vptr-index(A);// where n is the number of elements in the sub-VTTs} else {// Otherwise, just use the complete-object vtable:((A *)this)->vptr = &(A-in-D vtable);}}// Code for D constructor....}

  从上面的描述中我们能够看到:

  1. 当前类的虚函数表指针的确定是在执行具体的构造函数代码之前的;
  2. 构建当前类之前会搜索当前类的继承图,找到基类按照继承图的先序序列构造基类;
  3. 基类构造完成后开始调用当前类的构造函数的代码。

  析构函数的顺序相反。对于一个具有直接继承关系的虚基类A和B(B继承自A)的构造顺序为:

class A{
public:virtual void func() = 0;
};class B: public A{
public:virtual void func(){}
};
  1. B构造函数B::B被调用;
  2. 遍历B的基类构造调用基类的构造函数,这里就是A::A();
  3. 调用A的时候先将vfptr指向A的虚函数表,此表项中有基类偏移,typeinfo,__cxa_pure_virtual(因为func是纯虚函数因此该处的虚函数表指针以此填充);
  4. 调用A::A的用户代码,这里没有就不调用;
  5. A构造函数执行完后开始设置B的虚函数指针为B的虚函数表。

  析构顺序:

  1. 调用B::~B析构函数;
  2. 设置虚函数表指针为B的虚函数表;
  3. 执行B析构的用户代码;
  4. 调用基类A::~A(),该过程中先设置虚函数表指针为A的虚函数表再调用A的用户代码。

  从上面的过程中大概也能看出cxa_pure_virtual可能被调用的时机。当类被析构时,基类的析构稍微比较耗时时,第二个线程尝试访问当前类的一个被重写的纯虚函数,由于此时的虚函数表中的纯虚函数已经被修改为cxa_pure_virtual就会直接abort。那我们复现下:

class ClassA {
public:ClassA() {printf("Class A \n");}virtual ~ClassA() {std::this_thread::sleep_for(std::chrono::seconds(5));}virtual void func() = 0;
};class ClassB : public ClassA {
public:virtual ~ClassB() {printf("Class B \n");};virtual void func() override {printf("Class B func\n");}
};void func(ClassA *p) {while (1) {p->func();}
}int main(){std::cout << "Hello World!\n";ClassA* p = new ClassB;auto t = std::thread(func, p);std::this_thread::sleep_for(std::chrono::seconds(1));delete p;t.join();
}

  上面的代码中在析构函数中加了sleep函数来保证对象被析构过程中卡在基类的析构函数,第二个线程尝试访问该纯虚函数。
  再clang/gcc系列编译器上触发的是cxa_purer_virtual,而msvc触发的是_purecall


extern "C" int __cdecl _purecall()
{_purecall_handler const purecall_handler = _get_purecall_handler();if (purecall_handler){purecall_handler();// The user-registered purecall handler should not return, but if it does,// continue with the default termination behavior.}abort();
}

这篇关于[crash] cxa_pure_virtual 崩溃分析与原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/416556

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

VMWare报错“指定的文件不是虚拟磁盘“或“The file specified is not a virtual disk”问题

《VMWare报错“指定的文件不是虚拟磁盘“或“Thefilespecifiedisnotavirtualdisk”问题》文章描述了如何修复VMware虚拟机中出现的“指定的文件不是虚拟... 目录VMWare报错“指定的文件不是虚拟磁盘“或“The file specified is not a virt

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Spring中Bean有关NullPointerException异常的原因分析

《Spring中Bean有关NullPointerException异常的原因分析》在Spring中使用@Autowired注解注入的bean不能在静态上下文中访问,否则会导致NullPointerE... 目录Spring中Bean有关NullPointerException异常的原因问题描述解决方案总结

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异