-fno-rtti導致的慘案(object has invalid vptr)
PS:要轉載請注明出處,本人版權所有。
PS: 這個只是基于《我自己》的理解,
如果和你的原則及想法相沖突,請諒解,勿噴。
環境說明
- Ubuntu 24.04.2 LTS \n \l
- gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04)
前言
??對于C++程序開發來說,MemoryLeek/UndefinedBehavior 等問題,簡直就是大型開發過程必定會出現的問題。那么我們怎么嘗試減少這些問題,在我的日常開發中,大概有以下方案:
- 對于開發過程中來說,在c++11以后,標準庫引入了智能指針,然后增強開發者內存所有權意識等,可以有效減少MemoryLeek問題。
- 對于測試發布流程來說,我們常常引入了valgrind/sanitizer減少MemoryLeek/UndefinedBehavior 等問題。
??尤其是對于新的編譯器來說,sanitizer還是比較好用的。最近遇到了一個不是那么常見的sanitizer ub錯誤,我覺得非常有趣,可以分享一下。
-fno-rtti導致的慘案
??下面的圖片是出現的問題現場截圖:

??上圖一看就是ub錯誤,具體是什么原因,還要分析一番。
問題最小用例復現
??下面是最小的復現用例:
//l.cpp
#include <memory>
#include "A.hpp"
void B::p(){printf("p(): class B\n");}
void B::p1(){printf("p1(): class B\n");}
void A::p(){printf("p(): class A\n");}
void A::p1(){printf("p1(): class A\n");}
std::shared_ptr<A> my_A(new A());
void i(){
my_A->p1();
}
//l.hpp
void i();
//l.hpp
void i();
//A.hpp
#include <cstdio>
class B{
public:
virtual void p();
virtual void p1();
};
class A:public B{
public:
void p();
void p1();
};
//t.cpp
#include <memory>
#include <cstdio>
#include "A.hpp"
#include "l.hpp"
std::shared_ptr<A> my_AA(new A());
int main(int argc, const char* argv[])
{
my_AA->p();
i();
return 0;
}
g++ -c l.cpp -O3 -fPIC -fsanitize=undefined
g++ -shared -o libA.so l.o -O3
g++ t.cpp -o t -O3 -I. -L . -l A -fno-rtti -fsanitize=undefined -Wl,-rpath=.
# ./t 運行就會得到如上的錯誤
注意上述例子用到了多態類,這和我原始工程中類似,但是實際情況中,一個普通的類也會有同樣的問題,具體原因,見如下分析。
問題分析
??首先我們看看出現的_Sp_counted_base/_Sp_counted_ptr是什么,這個通過報錯,看起來像shared_ptr引用計數相關,我們看看其實際的代碼大致關系如下:
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
//...
}
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
//...
__shared_count<_Lp> _M_refcount; // Reference counter.
}
template<_Lock_policy _Lp>
class __shared_count
{
template<typename _Ptr>
explicit
__shared_count(_Ptr __p) : _M_pi(0)
{
__try
{
_M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
}
__catch(...)
{
delete __p;
__throw_exception_again;
}
}
//...
_Sp_counted_base<_Lp>* _M_pi;
}
template<typename _Ptr, _Lock_policy _Lp>
class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
{
//...
}
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
//...
}
??我們使用如下命令,看一下t和libA.so的_Sp_counted_ptr符號,我們發現對于相同的符號來說,其大小不一樣。
# readelf -sW libA.so |grep counted_ptr
24: 0000000000005160 54 OBJECT WEAK DEFAULT 16 _ZTSSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE
25: 0000000000003970 338 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED1Ev
27: 0000000000003970 338 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED2Ev
30: 0000000000003790 7 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE14_M_get_deleterERKSt9type_info
33: 0000000000006d80 56 OBJECT WEAK DEFAULT 22 _ZTVSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE
40: 0000000000006cf0 24 OBJECT WEAK DEFAULT 22 _ZTISt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE
44: 0000000000003c30 489 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE10_M_destroyEv
48: 0000000000003880 225 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE10_M_disposeEv
53: 0000000000003ad0 350 FUNC WEAK DEFAULT 14 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED0Ev
# readelf -sW t |grep counted_ptr
20: 0000000000002700 172 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED2Ev
22: 0000000000002700 172 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED1Ev
23: 0000000000002870 233 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE10_M_destroyEv
24: 00000000000027b0 188 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EED0Ev
26: 00000000000026a0 92 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE10_M_disposeEv
30: 0000000000002610 7 FUNC WEAK DEFAULT 16 _ZNSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE14_M_get_deleterERKSt9type_info
32: 0000000000005ca0 56 OBJECT WEAK DEFAULT 24 _ZTVSt15_Sp_counted_ptrIP1ALN9__gnu_cxx12_Lock_policyE2EE
??這個時候我們想一下-fno-rtti的作用,其作用是禁用typeinfo+dynamic_cast,某些情況下可以提升執行性能。然后根據錯誤中的提示(object has invalid vptr),必定和其虛表有關系,那就意味著_Sp_counted_base/_Sp_counted_ptr的虛表存在異常。
??用ida查看t和libA.so中std::_Sp_counted_base的虛表內容,他們如下圖:


??注意,一般的虛表結構如下:
+-------------------+
| Offset-to-Top | (通常為負數,用于多重繼承)
+-------------------+
| type_info 指針 | (用于 RTTI)
+-------------------+
| 虛函數1 的地址 |
+-------------------+
| 虛函數2 的地址 |
+-------------------+
| ... |
??從上面的圖和虛表結構可知,就是兩個同名的vtable內容不一樣,導致了此問題。解決方法也很簡單,大家使用同樣的編譯參數即可。
后記
??c++的一些錯誤是非常有趣的,值得細看。
參考文獻
- 無

PS: 請尊重原創,不喜勿噴。
PS: 要轉載請注明出處,本人版權所有。
PS: 有問題請留言,看到后我會第一時間回復。
浙公網安備 33010602011771號