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 Bn");} void B::p1(){printf("p1(): class Bn");} void A::p(){printf("p(): class An");} void A::p1(){printf("p1(): class An");} 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的虚表内容,他们如下图: