[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();
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/161535.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/161535.shtml
英文地址,請注明出處:http://en.pswp.cn/news/161535.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C/C++未定義行為的例子匯總

一、什么是未定義行為&#xff1f; 未定義行為&#xff08;Undefined Behavior&#xff09;是指C語言標準未做規定的行為。同時&#xff0c;標準也從沒要求編譯器判斷未定義行為&#xff0c;所以這些行為有編譯器自行處理&#xff0c;在不同的編譯器可能會產生不同的結果&#…

ElasticSearch之cat aliases API

執行aliases命令&#xff0c;如下&#xff1a; curl -X GET "https://localhost:9200/_cat/aliases?pretty&vtrue" --cacert $ES_HOME/config/certs/http_ca.crt -u "elastic:ohCxPHQBEs5*lo7F9"執行結果輸出如下&#xff1a; alias index …

在 VSCode 中使用 GDB 進行 C/C++ 程序調試(圖文版)

(??? )&#xff0c;Hello我是祐言QAQ我的博客主頁&#xff1a;C/C語言&#xff0c;數據結構&#xff0c;Linux基礎&#xff0c;ARM開發板&#xff0c;網絡編程等領域UP&#x1f30d;快上&#x1f698;&#xff0c;一起學習&#xff0c;讓我們成為一個強大的攻城獅&#xff0…

webpack loader

1、分類 2、執行順序 配置類型 執行順序是 loader1>loader2>loader3 3、使用方式 自己的第一個loader 同步loader /*** loader 就是一個函數* 當webpack 解釋資源時&#xff0c; 會調用相應的loader去處理* loader 接收到文件內容作為參數&#xff0c;返回文件內容* p…

Nginx 開源版安裝

下載 tar.gz安裝包&#xff0c;上傳。 解壓 [rootlocalhost ~]# tar zxvf nginx-1.21.6.tar.gz nginx-1.21.6/ nginx-1.21.6/auto/ nginx-1.21.6/conf/ nginx-1.21.6/contrib/ nginx-1.21.6/src/ ... ...安裝gcc [rootlocalhost nginx-1.21.6]# yum install -y gcc 已加載插件…

ios qt開發要點

目前關于ios qt的開發資料比較少&#xff0c;這里整理了幾個比較重要的開發要點&#xff0c;基于MacOS14 Xcode15 Qt15.5 cmake iphone真機。 cmake報錯&#xff0c;報錯信息如下 CMake Error at /Users/user/Qt/5.15.5/ios/lib/cmake/Qt5Core/Qt5CoreConfig.cmake:91 (m…

C#Wpf關于日志的相關功能擴展

目錄 一、日志Sink(接收器) 二、Trace追蹤實現日志 三、日志滾動 一、日志Sink(接收器) 安裝NuGet包&#xff1a;Serilog Sink有很多種&#xff0c;這里介紹兩種&#xff1a; Console接收器&#xff08;安裝Serilog.Sinks.Console&#xff09;; File接收器&#xff08;安裝…

CSM32RV003:國產高精度16位ADC低功耗RISC-V內核MCU

目錄 高精度ADC工業應用工業數據采集應用CSM32RV003簡介主要特性 高精度ADC工業應用 高精度ADC即高精度模數轉換器&#xff0c;是一種能夠將輸入模擬信號轉換為數字信號的芯片&#xff0c;在多種消費電子、工業、醫療和科研領域都有廣泛應用。高精度ADC的主要特點是能夠提供高…

深度學習圖像修復算法 - opencv python 機器視覺 計算機競賽

文章目錄 0 前言2 什么是圖像內容填充修復3 原理分析3.1 第一步&#xff1a;將圖像理解為一個概率分布的樣本3.2 補全圖像 3.3 快速生成假圖像3.4 生成對抗網絡(Generative Adversarial Net, GAN) 的架構3.5 使用G(z)生成偽圖像 4 在Tensorflow上構建DCGANs最后 0 前言 &#…

前端 HTML 的 DOM 事件相關知識有哪些?

HTML 的 DOM 事件是指在網頁上發生的各種事件&#xff0c;如點擊、鼠標移動、鍵盤輸入等。 通過 JavaScript 腳本可以對這些事件進行監聽和處理&#xff0c;以實現交互效果。以下是一些常見的 DOM 事件及其相關知識點&#xff1a; 1、click&#xff1a;點擊事件&#xff0c;在…

vue3引入vuex基礎

一&#xff1a;前言 使用 vuex 可以方便我們對數據的統一化管理&#xff0c;便于各組件間數據的傳遞&#xff0c;定義一個全局對象&#xff0c;在多組件之間進行維護更新。因此&#xff0c;vuex 是在項目開發中很重要的一個部分。接下來讓我們一起來看看如何使用 vuex 吧&#…

linux文件I/O:文件鎖的概念、函數以及代碼實現

文件鎖是一種用來保證多個進程對同一個文件的安全訪問的機制。文件鎖可以分為兩種類型&#xff1a;建議性鎖和強制性鎖。建議性鎖是一種協作式的鎖&#xff0c;它只有在所有參與的進程都遵守鎖的規則時才有效。強制性鎖是一種強制式的鎖&#xff0c;它由內核或文件系統來強制執…

使用Pytorch從零開始構建RNN

在這篇文章中&#xff0c;我們將了解 RNN&#xff08;即循環神經網絡&#xff09;&#xff0c;并嘗試通過 PyTorch 從頭開始??實現其中的部分內容。是的&#xff0c;這并不完全是從頭開始&#xff0c;因為我們仍然依賴 PyTorch autograd 來計算梯度并實現反向傳播&#xff0c…

Apache訪問控制

服務器相關的訪問控制 Options指令 Options指令是Apache服務器配置文件中的一個重要指令,它可以用于控制特定目錄啟用哪些服務器特性。Options指令可以在Apache服務器的核心配置、虛擬主機配置、特定目錄配置以及.htaccess文件中使用。 以下是一些常用的服務器特性選項: N…

Django(九、cookie與session)

文章目錄 一、cookie與session的介紹HTTP四大特性 cookiesession Django操作cookie三板斧基于cookie的登錄功能 一、cookie與session的介紹 在講之前我們先來回憶一下HTTP的四大特性 HTTP四大特性 1.基于請求響應 2.基于TIC、IP作用于應用層上的協議 3.無狀態 保存…

二叉查找(排序)樹你需要了解一下

簡介 二叉排序樹&#xff08;Binary Sort Tree&#xff09;&#xff0c;又稱二叉查找樹&#xff08;Binary Search Tree&#xff09;&#xff0c;亦稱二叉搜索樹&#xff0c;是一種重要的數據結構。 它有以下特性&#xff1a; 若左子樹不空&#xff0c;則左子樹上所有結點的…

目標檢測YOLO系列從入門到精通技術詳解100篇-【圖像處理】目標檢測

目錄 幾個高頻面試題目 如何在超大分辨率的圖片中檢測目標? 1當超大分辨率圖像邂逅目標檢測任務 2You Only Look Twice

邊緣計算多角色智能計量插座 x 資產顯示標簽:實現資產追蹤與能耗管理的無縫結合

越來越多智慧園區、智慧工廠、智慧醫院、智慧商業、智慧倉儲物流等企業商家對精細化、多元化智能生態應用場景的提升&#xff0c;順應國家節能減排、環保的時代潮流&#xff0c;設計一款基于融合以太網/WiFi/藍牙智能控制的智能多角色插座應運而生&#xff0c;賦予智能插座以遙…

大數據學習(23)-hive on mapreduce對比hive on spark

&&大數據學習&& &#x1f525;系列專欄&#xff1a; &#x1f451;哲學語錄: 承認自己的無知&#xff0c;乃是開啟智慧的大門 &#x1f496;如果覺得博主的文章還不錯的話&#xff0c;請點贊&#x1f44d;收藏??留言&#x1f4dd;支持一下博主哦&#x1f91…

uniapp實現表單彈窗

uni.showModal({title: 刪除賬戶,confirmColor:#3A3A3A,cancelColor:#999999,confirmText:確定,editable:true,//顯示content:請輸入“delete”刪除賬戶,success: function (res) {console.log(res)if(res.confirm){if(res.contentdelete){console.log(123123123213)uni.setSto…