导读
C++反常处理机制trycatch在快手App内突然失效,引发很多未捕获反常导致的溃散。本文介绍稳定性团队排查此次问题的进程,问题的根本原因以及修正躲避计划,最后梳理反常处理流程,知其然,知其所以然,方能在问题出现时镇定应对。
本文初次发表在 快手大前端技术,请各位观众老爷在微信内翻开并关注点赞,给您拜年啦。
布景
快手 App iOS 端在最近的一个版别上线前, 自动化测验流程上报了十分多 C++ 未捕获反常(invalid_argument 、out_of_range 等)导致的溃散,溃散仓库在版别周期内并没有修正记载,而且在溃散的代码途径上存在 try catch 句子,catch 句子声明的反常类型是 exception。
invalid_argument 和 out_of_range 都是 logic_error 的子类,logic_error 是 exception 的子类。
依据 try catch 的工作原理,catch 句子声明 exception 能够捕获子类 out_of_range 和 invalid_argument。
catch @ExcType This clause means that the landingpad block should be entered if the exception being thrown is of type @ExcType or a subtype of @ExcType. For C++, @ExcType is a pointer to the std::type_info object (an RTTI object) representing the C++ exception type.
以 mmkv 的溃散仓库为例,readString 办法抛出了 std::out_of_range ,这个反常应该在 decodeOneMap 办法内被捕获,而不应该触发溃散。
mmkv::CodedInputData::readString 抛反常代码:
mmkv::MiniPBCoder::decodeOneMap 中捕获反常代码: debug 线上的版别,124 行能够正常输出过错日志,在 mmkv 没有任何改动的情况下,自动化测验版别 catch 句子不能正常捕获反常了。
二、排查进程
本人王者荣耀钻五选手,对这个游戏有着相当深刻的了解,接下来的排查进程就以一局游戏为例吧。(真实原因是起一个阶段标题和函数命名相同困难!)
全军反击
同样深化了解游戏的人脑海中已经有了画面,游戏局面,小兵慢慢抵达了战场,小鲁班 A 了一下兵线,first blood 人没了……
mmkv 里边的溃散仓库简化后如下所示:
void throw_exception() {
throw std::out_of_range("out_of_range");
}
int catch_exception() {
auto block = []() {
throw_exception();
};
try {
block();
} catch (std::exception& e) {
std::cout << "Caught exception." << e.what() << std::endl;
}
return 0;
}
这段代码在 demo 工程里边运转,运用 exception 类型能够 catch 子类型 out_of_range。可是把相同的代码复制到快手 App,catch 句子不会履行。其时置疑和快手 App 的编译选项改动有关,找到架构那边的同学,承认编译参数近期没有任何改动。
21 年年底处理过一次 try catch 失效导致的溃散,这种超乎常理的问题总是令人印象深刻。前次的原因是 hook 系统办法 objc_msgSend 后没有增加 CFI 指令,导致 unwind 回溯到 objc_msgSend 后中止,无法持续向上查找调用栈中的 catch 句子。所以在排查这个问题时首要想到的是判别 unwind 流程是否正常。这个判别用代码比较容易完结,测验用例里边,新增一个 catch 句子,捕获详细的子类,然后在快手 App 运转。
try {
block();
} catch (std::exception& e) {
std::cout << "Caught exception" << e.what() << std::endl;
} catch (std::out_of_range& e) {
std::cout << "Caught out_of_range" << e.what() << std::endl; << ---- 会履行
}
运转上述代码后,履行了第二个 catch 块,out_of_range 捕获了 out_of_range,阐明 unwind 流程是正常的,能够回溯到 try catch 句子。
测验用例中运转成果一同表明:
- out_of_range 实例 is type of out_of_range 建立。
- out_of_range 实例 a subtype of exception 不建立。
第二条明显不符合预期,所以在快手 App 内反常没有被捕获的原因是判别 exception 和 out_of_range 的继承关系时存在过错。测验运用 is_base_of 在快手 App 内判别 out_of_range 是否是 exception 的子类,返回的成果 rv 是 true。
bool rv = std::is_base_of<std::logic_error, std::out_of_range>::value
&& std::is_base_of<std::exception, std::logic_error>::value
&& std::is_base_of<std::exception, std::out_of_range>::value;
if (rv) {
abort(); << ---- 会履行
}
然而,在反常处理流程中,判别 catch 的 exception 类型是否能匹配抛出子类型 out_of_range 反常时是经过 is_base_of 办法吗?这个问题现在来看比较初级,可是在其时缺少对整个反常处理流程的认知,不知从何处开端调试,只能暂时放下这个问题,开端其它方向的排查。
请求打野支援
一顿操作猛如虎,一看战绩 0-5。打野,速速来 gank!
这是一个新增而且能够稳定复现的溃散,因而必定能够查找到是哪个 MR 引进的。稳定性组的两个搭档,从出现溃散的 commit 开端二分查找之前一天的 MR,终究锁定了动态库改静态库这个提交 ,这个提交 merge 之后结构的测验用例能够复现溃散。之后依据自动化流程上报的仓库,修正 mmkv readString 办法,调用即抛出 out_of_range 反常,在 decodeOneMap 办法内反常没有被 catch,实锤了是这个 MR 引进的问题。
这个 MR 并不杂乱,修正点不多,将部分动态库改为静态库集成到快手 App。里边删去了一些动态库的编译选项,和 C++ 相关的只要一个 CLANG_CXX_LANGUAGE_STANDARD,用于指定 Clang 编译器在编译 C++ 代码时所运用的言语规范。
尽管定位了问题引进的 MR,可是此时依据代码 diff 还是看不出详细的原因。
集合预备团战
不是一个人的王者,而是团队的荣耀!
动态库改静态库是最近一次活动必须要上的需求,不然会存在 ANR 影响活动效果,所以定位到的 MR 不能被直接回滚。周四就要提审,这个问题不被处理必定会阻塞提审流程,影响到活动版别的覆盖率。
周三晚上,稳定性组的负责人开端组织整个组同学参与进来一同评论处理计划。在这次评论中,首要排除了一个方向: 代码 diff 中删去动态库的 C++ 版别对快手 App编译环境无影响。之后初步梳理了 C++ exception handling 的逻辑,明确溃散场景下运用 __gxx_personality_v0 routine 办法来判别栈帧是否能 catch 反常。之后 debug __gxx_personality_v0 得出了一个十分要害的信息,导致 try catch 失效的直接原因是快手 App 内多了一份 exception 的 type_info:
std::exception 的 type info 对不上,name 都是相同的,可是一个在 libc++abi.dylib, 一个在快手 App内,正常应该都会在 libc++abi.dylib,也就是说快手 App多了一份 std::exception 信息。
在定位到直接原因后,接下来开端查找 std::exception 的 type_info 是被哪个编译 target 引进快手 App的。image lookup 能够检查 type_info 指针地址详细的信息:
(lldb) image lookup -a 0x000000010396c970
Address: Example[0x0000000101498970] (Example.__DATA.__const + 39952)
Summary: Example`typeinfo for std::exception
0x000000010396c970 存储在 __DATA.__const 段, __DATA.__const 是一个特别的 section,用于存储只读的常量数据,在一般情况下,__const section 中存储常量的次序是按照它们在源代码中出现的次序来排列的。测验检查 0x000000010396c970 这个地址邻近存储的信息。
(lldb) image lookup -a 0x000000010396c978
Address: Example[0x0000000101498978] (Example.__DATA.__const + 39960)
Summary: Example`typeinfo for std::exception + 8
(lldb) image lookup -a 0x000000010396c980
Address: Example[0x0000000101498980] (Example.__DATA.__const + 39968)
Summary: Example`typeinfo for std::bad_alloc
(lldb) image lookup -a 0x000000010396c960
Address: Example[0x0000000101498960] (Example.__DATA.__const + 39936)
Summary: Example`vtable for std::__1::__function::__base<bool (Runtime::JSState&)> + 56
(lldb) image lookup -a 0x000000010396c950
Address: Example[0x0000000101498950] (Example.__DATA.__const + 39920)
Summary: Example`vtable for std::__1::__function::__base<bool (Runtime::JSState&)> + 40
在 0x000000010396c970 – 0x10 的方位找到了 Runtime::JSState。Runtime 这个符号在组件 dummy 内界说。取 dummy 的编译产物 libdummy.a,发现 .a 里边 const 段,存在 exception 的 type info。
00000000000070b0 (__DATA,__const) weak private external __ZTISt9exception
00000000000070c0 (__DATA,__const) weak private external __ZTISt9bad_alloc
00000000000070d8 (__DATA,__const) weak private external __ZTISt20bad_array_new_length
➜ Exception git:(main) ✗ c++filt __ZTISt9exception
typeinfo for std::exception
测验 demo 工程依靠组件 dummy 后编译,运用 exception 无法 catch out_of_range ,实锤了是这个组件引进的问题。在 podspec 里边检查 dummy 的编译选项,发现禁用了 RTTI,在 Xcode 里边将这个选项修正为 YES 之后,try catch 失效导致的未捕获反常溃散不再复现。
dummy 是这次动态库改静态库的需求中,改动的动态库直接依靠的静态库,主可履行文件之前不会直接依靠 dummy。宿主动态库以静态库办法集成到快手 App后,dummy 同样以静态库的办法集成到快手 App,导致 std::exception 的 type_info 被引进主可履行文件。定位到了引进的子库 dummy 和 try catch 失效的原因后,接下来就是查找对应的处理计划。
VICTORY
敌方水晶已被击破!
计划 1
最快速的修正是将 dummy 编译选项 GCC_ENABLE_CPP_RTTI 修正为 YES,可是由于其特别的事务场景不答应被修正。
计划 2
dummy 删去 std::exception 的依靠。终究以失利告终,libdummy.a 依然存在 exception type_info,其时应该是没有删去干净,依然存在 exception 的子类。
计划 3
这个计划和计划 2 在同步进行。溃散的直接原因是动改静之后,将 dummy 集成到了快手 App,导致主可履行文件多出了一份 std::exception。尽管不能将全量的动态库回滚,独自将 dummy 回滚为动态库也能处理问题。
计划 4
反向修正。在检查 libdummy.a 符号时,发现这个库一同存在 exception 的子类 std::bad_alloc 的 type_info,在快手 App内运用 exception 能够 catch bad_alloc,阐明父类和子类都在主可履行文件时,try catch exception 也能够捕获子类。假如 dummy 包括了一切的子类,try catch 失效的问题也能处理。这个计划尽管能修正咱们遇到的问题,可是咱们无法评价这样的修正是否会产生额定的影响。
计划 5
过后我手搭档又提供一个处理计划,增加如下cflags:set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -D_LIBCPP_TYPEINFO_COMPARISON_IMPLEMENTATION=2”) ,增加之后,即使存在两份 type_info,type_info 的判等能够走到 strcmp 的逻辑里边。
终究挑选了计划 3,风险最低,不会影响事务逻辑,而且修正的时刻成本较低。计划 3 并不是把 try catch 失效的影响规模缩小到了dummy 里边,依据计划 4 的原理,计划 3 并不会导致 dummy 内的 try catch 失效。终究由架构组同学负责修正,修正后验证可行。
三、复盘
段位在钻五之上的人必定是懂复盘的,要从之前成功的对局中吸取经验,这样在以后的对局中才干一向赢,一向赢,守住钻五的牌面。
try catch 失效的问题尽管被处理了,可是排查进程中遇到的一些问题还没有找到答案。从稳定性的视点动身,首要问题是理清 C++ 反常处理机制,exception handing 如何查找 catch 块以及判别 catch 块是否匹配正在抛出的反常?在问题处理后,查阅了一些材料,对 C++ 反常处理机制有了一些根本的了解。
在函数调用栈帧中每个函数都对应 1 个或许 0 个 exception table,里边包括了这个函数内不同的 PC 区间能够 catch 的反常类型,以及 catch 后的跳转地址,在反常抛出时,查找调用栈中能够捕获抛出反常的栈帧时,会顺次运用调用栈中不同的栈帧对应的 exception table,依据栈帧跳转时的 PC,匹配栈帧 exception table 内记载的地址区间,找到这个区间能够 catch 的反常类型,运用这个类型来判别是否能 catch 抛出的反常,假如能够 catch 则跳转到区间对应的跳转地址,持续履行 catch 块的代码,不然持续查找上一个栈帧。
接下来将结合详细的 demo 用例、编译产物、源码和咱们一同分享下反常处理流程。
throw
从 throw 说起,编译时 throw 会被替换为 __cxa_throw, __cxa_throw 会调用 _Unwind_RaiseException, 假如_Unwind_RaiseException 未找到捕获当时 exception 的 landing pad 或许在查找进程中出现过错,会 return。return 后 __cxa_throw 办法持续履行 failed_throw,failed_throw 内履行__terminate。查找到能够捕获反常的 landing pad 之后会跳转到对应的地址,不会 return 也就不会触发溃散。
这儿的 landing pad 能够了解为当反常捕获时持续履行的函数调用进口,这个函数接纳两个参数 exception structure 和 exception type_info 在 typetable 中的索引。在 landing pad 函数内会依据 type_info 的索引值来决定详细履行的 catch 块。landing pad 的另一个语义是 cleanup 调用进口。
_Unwind_RaiseException
_Unwind_RaiseException 包括了反常处理的两个核心流程 phase1 和 phase2,对应 search 和 cleanup。
在反常抛出时,需求遍历栈帧,查找能够捕获反常的 catch 句子。search 阶段运用 libunwind 顺次回退栈帧,康复寄存器信息,并依据 PC 二分查找 __unwind_info,获取栈帧对应的 personality 函数,以及履行 personality 函数依靠的 exception table — LSDA(Language Specific Data Area)。之后调用 personality 解析 LSDA 来判别当时栈帧是否能 catch 反常,假如能够会记载栈帧相应的信息。
search 阶段假如没有查找到能够处理反常的栈帧,会返回到 __cxa_throw 办法内,履行 terminate,查找成功会持续履行 cleanup 阶段。当反常发生时,从 throw 到 catch 之间的函数履行中止,cleanup 等价于在函数退出时履行的整理操作。以下面的代码为例,A 调用 B,B 调用 C,A catch C 抛出的反常,在 B 调用 C 之前界说了 m_cls。未发生反常时 m_cls 在函数结尾触发析构办法,反常发生时 B 函数履行中止,m_cls 在 cleanup 阶段履行析构办法。
void funcC() {
// 抛出反常
}
void funcB() {
MyClass m_cls;
funcC();
}
void funcA() {
// catch 反常
}
cleanup 会再次回退栈帧,并履行局部变量的整理,保证资源能够正常释放,当回退到 search 阶段记载的栈帧,会运用 search 缓存的跳转地址,履行 resume,完结 throw 到 catch 块的跳转。
反常处理流程为什么会拆分为 search 和 cleanup 呢?官方给出的解说如下:
A two-phase exception-handling model is not strictly necessary to implement C++ language semantics, but it does provide some benefits. For example, the first phase allows an exception-handling mechanism to dismiss an exception before stack unwinding begins, which allows resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised). While C++ does not support resumptive exception handling, other languages do, and the two-phase model allows C++ to coexist with those languages on the stack.
两个阶段的反常处理模型对于 C++ 并不是严厉必需的,可是它能够带来一些优点。比如,第一阶段答应反常处理机制在栈帧打开之前消除反常,这样能够进行康复式反常处理(对反常情况进行修正,然后在抛出反常的当地持续履行),尽管 C++ 不支撑康复式的反常处理,但其它言语支撑,两阶段模型答应 C++ 与那些言语在仓库上共存。
在 search 和 cleanup 阶段,都需求依靠 LSDA 判别当时栈帧是否能 catch 反常,了解 LSDA 的数据结构对于了解反常处理流程至关重要。
LSDA
LSDA 包括 header, call site table, action table 和一个可选的 type table。
判别当时栈帧是否能 catch 反常时,涉及到三次查表的进程,
-
依据当时栈帧的 PC 查找 call site table,获取地址区间匹配的的 call site。
-
依据 call site 记载的 action 索引值在 action table 取 action。
-
依据 action 中 type_info 的索引值在 type table 中取 type_info。
之后依据 type_info 判别是否能 catch 反常,是则记载(phase1)或许跳转(phase2)到 call site 中 lpad 字段记载的的 landing pad address。不然持续向上回溯栈帧,并重复上述进程。
LSDA 的数据结构如下图所示:
header
LPStart: 默认是函数的开始方位。
TTBase: 记载 type table 的相对方位。
call site record
start & len: 记载了或许会抛出反常的 PC 区间,这个区间是相对于函数开始方位的偏移量。
lpad: 记载匹配之后跳转的“函数地址” landing pad address 的相对方位。
action_offset: 记载 action table 中的索引值,action 用于查找 call site 能够捕获的反常类型。
action record
filter:记载了 catch 块中反常类型的 typeinfo 在 type table 中的索引。
next_ar:指向下一个 action 或许 end,假如 filter 类型不匹配,则持续查找 next_ar。遍历到 end 未找到匹配的 filter 表明当时 call site 所包括的 catch 块都不能处理 exception。
type table
编译器会将 catch 块反常类型的 typeinfo 信息存储在 type table 中。typeinfo是 C++规范库提供的类,它包括了与类型相关的运转时信息。
举个例子
以下面的代码为例,抛出反常并在当时函数内捕获反常:
简化后判别是否能 catch 反常的进程:
catch_exception 将函数的地址区间拆分为不同的 call site,21 行 try 块地点的 call site ,记载了这个区间能 catch 的反常类型 out_of_range 和 exception,以及 catch 后的跳转地址 22 行,在 22 行依据抛反常的类型判别详细履行的 catch 句子。try 块内抛出的反常类型 out_of_range 和 call site 记载的反常类型匹配,反常被捕获,依据匹配的类型持续履行第一个 catch 句子。假如不匹配,会持续在调用栈向上查找,假如上一个栈帧跳转到 catch_exception 的地址也在 try 块内,则持续运用对应的 call site 判别,假如依然不能匹配或许跳转地址自身就不在 try 块内则持续查找上一个栈帧。
接下来依据 catch_exception 办法生成的实际数据推演运转时捕获 exception 的流程。
catch_exception 办法内 catch 两种类型的反常 out_of_range 和 exception, 对应的 type table 如下所示, 其间 0 表明会 catch 一切的反常:
Catch TypeInfos | type_info |
---|---|
TypeInfo 3 | 0 |
TypeInfo 2 | __ZTISt12out_of_range@GOT-Ltmp28 |
TypeInfo 1 | __ZTISt9exception@GOT-Ltmp29 |
action table 如下所示,action table entry 中 filter 表明上述 type table 中的索引值,以 Action Record 4 为例表明运用 type table 中索引 1 对应的 std::exception 判别是否能 catch 反常,判别履行的逻辑是 std::exception 是否和抛出的反常类型是同一个类型或许有相同的基类。而 Action Record 5 ,next_ar 指向 action 4,表明的是一个链表,会先运用 action 5 中的索引值 2 对应的 std::out_of_range 判别,假如不能 catch 反常,会持续履行 action 4 的判别逻辑,两者恣意一个类型匹配抛出反常的类型都表明反常能够被捕获。
Action Record | TypeInfo(索引) | Next Action |
---|---|---|
Action Record 1 | 0(Cleanup) | No further actions |
Action Record 2 | 1 | Continue to action 1 |
Action Record 3 | 2 | Continue to action 2 |
Action Record 4 | 1 | No further actions |
Action Record 5 | 2 | Continue to action 4 |
catch_exception 办法在编译时生成的 call site table 如下,其间的 Lfunc_beginX,LtmpX 表明汇编代码的标签,能够了解为代码段中地址的别名。
以 Call Site 3 为例,表明当 PC 处于 Ltmp3 和 Ltmp4 地址区间内,会依据上述 action table 中的 action 5 判别是否能 catch 反常,是则会跳转到 call site 记载的 landing pad 地址 Ltmp5 处,履行 catch 句子处理反常。
Call Site 1 2 和 4 记载的 action 都是 0, 0 表明需求履行 cleanup,cleanup 只会在 phase2 阶段触发,在 phase1 命中 cleanup,表明当时栈帧无法 catch 反常,会持续履行 unwind。1 和 4 的 lpad 也是 0 表明不存在履行 cleanup 的函数进口,2 不为 0,实际上也只要 Call Site 2 会在阶段 2 跳转到 Ltmp2 地址处履行 cleanup。
Call Site | start(代码标签) | len | lpad | action(索引) |
---|---|---|---|---|
Call Site 1 | Lfunc_begin0 | Ltmp0-Lfunc_begin0 | no landing pad | 0(cleanup) |
Call Site 2 | Ltmp0 | Ltmp1-Ltmp0 | jumps to Ltmp2 | 0 |
Call Site 3 | Ltmp3 | Ltmp4-Ltmp3 | jumps to Ltmp5 | 5 |
Call Site 4 | Ltmp4 | Ltmp11-Ltmp4 | no landing pad | 0 |
抛出反常代码 throw std::out_of_range(“out of range”) 对应代码标签 Ltmp3:
Ltmp3:
.loc 1 0 15 ; CPPException/test.cpp:0:15
ldr x0, [sp, #24] ; 8-byte Folded Reload
adrp x1, __ZTISt12out_of_range@GOTPAGE
ldr x1, [x1, __ZTISt12out_of_range@GOTPAGEOFF]
adrp x2, __ZNSt12out_of_rangeD1Ev@GOTPAGE
ldr x2, [x2, __ZNSt12out_of_rangeD1Ev@GOTPAGEOFF]
.loc 1 21 9 ; CPPException/test.cpp:21:9
bl ___cxa_throw // <<<<<<<< 在这儿抛出反常
Ltmp3 在 Call Site 3 规模内 。
Call Site 3 | Ltmp3 | Ltmp4-Ltmp3 | jumps to Ltmp5 | 5 |
---|
Call Site 3 的 action 字段值为 5, 对应 Action Record 5:
Action Record 5 | 2 | Continue to action 4 |
---|
Action Record 5 运用 out_of_range 判别是否能 catch 反常。抛出反常类型为 out_of_range,action 5 能够 catch 反常,search 阶段履行完结。
Call Site 3 对应的 landing pad address 为 Ltmp5,在 phase2 跳转到 Ltmp5 依据 type_info 判别详细履行的 catch 句子,Ltmp5 标签处的代码:
Ltmp5:
.loc 1 27 1 ; CPPException/test.cpp:27:1
stur x0, [x29, #-8]
mov x8, x1
stur w8, [x29, #-12]
b LBB0_4
LBB0_4:
.loc 1 22 5 ; CPPException/test.cpp:22:5
ldur w8, [x29, #-12]
str w8, [sp, #12] ; 4-byte Folded Spill
subs w8, w8, #2
cset w8, ne
tbnz w8, #0, LBB0_8
b LBB0_5
LBB0_8 终究会履行第一个 catch 句子:
.loc 1 25 9 ; CPPException/test.cpp:25:9
bl _printf
LBB0_5 终究会履行第二个 catch 句子:
.loc 1 23 9 ; CPPException/test.cpp:23:9
bl _printf
源码剖析
对 LSDA 的内存布局和反常处理流程有必定了解之后,再去阅览反常处理流程的源码,相对就比较容易了。接下来回到问题自身,从源码的视点剖析一下在快手 App 内为什么 exception 无法 catch out_of_range。
反常处理流程会先获取 catch 句子中 exception 的 type_info:
const __shim_type_info* catchType =
get_shim_type_info(static_cast<uint64_t>(ttypeIndex),
classInfo, ttypeEncoding,
native_exception, unwind_exception,
base);
catchType == 0 表明 catch (…) 会捕获一切反常。不为 0 时调用 can_catch 办法判别 catch 块中声明的类型和抛出反常的类型是否匹配。
if (catchType->can_catch(excpType, adjustedPtr)) {
}
can_catch 有两个判别,其间恣意一个建立都表明能够 catch 反常。
-
调用 is_equal 判别 catch 块类型是否和抛反常类型持平。
-
调用 has_unambiguous_public_base 判别两者是否有相同的 base,判别 base 是否相一同也会调用 is_equal 办法。(unambiguous: 明确的)。
bool
__class_type_info::can_catch(const __shim_type_info* thrown_type,
void*& adjustedPtr) const
{
// bullet 1
if (is_equal(this, thrown_type, false))
return true;
const __class_type_info* thrown_class_type =
dynamic_cast<const __class_type_info*>(thrown_type);
if (thrown_class_type == 0)
return false;
// bullet 2
__dynamic_cast_info info = {thrown_class_type, 0, this, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
info.number_of_dst_type = 1;
thrown_class_type->has_unambiguous_public_base(&info, adjustedPtr, public_path);
if (info.path_dst_ptr_to_static_ptr == public_path)
{
adjustedPtr = const_cast<void*>(info.dst_ptr_leading_to_static_ptr);
return true;
}
return false;
}
is_equal 的判别逻辑如下,use_strcmp 传入的是 false,履行第 8 行:
static inline
bool
is_equal(const std::type_info* x, const std::type_info* y, bool use_strcmp)
{
// Use std::type_info's default comparison unless we've explicitly asked
// for strcmp.
if (!use_strcmp) //
return *x == *y;
// Still allow pointer equality to short circut.
return x == y || strcmp(x->name(), y->name()) == 0;
}
std::type_info 重载了 == 办法:
bool operator==(const type_info& __arg) const _NOEXCEPT
{
return __impl::__eq(__type_name, __arg.__type_name);
}
默认 __impl 中 __eq 完结如下:
static bool __eq(__type_name_t __lhs, __type_name_t __rhs) _NOEXCEPT {
if (__lhs == __rhs)
return true;
if (__is_type_name_unique(__lhs) || __is_type_name_unique(__rhs))
// Either both are unique and have a different address, or one of them
// is unique and the other one isn't. In both cases they are unequal.
return false;
return __builtin_strcmp(__type_name_to_string(__lhs), __type_name_to_string(__rhs)) == 0;
}
结合源码信息, 再次回忆下咱们这次遇到的问题,直接原因是 out_of_range 遍历到 base std::exception 时和 catch 句子声明的 std::exception 在履行 is_equal 时返回了 false,便于区分咱们把前者称之为 thrown_exception, 后者称之为 catch_exception。
第一个判别条件 ==:
== 判别的是 type_info 中 __type_name 的地址,__type_name 的类型是 const char *。 thrown_exception 的 __type_name 存储在 libc++abi.dylib 的 __TEXT.__const 段。catch_exception 的 __type_name 存储在主可履行文件的 __TEXT.__const 段。所以地址判等为 false。
第二个判别 is_unique:
thrown_exception 的 type_info 是 unique 类型的,unique 表明 type_info 在程序中只存在一份副本,因而对于地址不同的 type_info 必定是不持平的,不需求判别 name 是否持平, 所以在第二个 if 句子返回了 false。
第三个判别 strcmp:
尽管两个 type_info 的 name 都是 St9exception,但在第二个判别返回 false ,并没有走到 strcmp 的逻辑里边。
对于 type_info 的判等,实际上存在三种办法:
1. __unique_impl::__eq
static bool __eq(__type_name_t __lhs, __type_name_t __rhs) _NOEXCEPT {
return __lhs == __rhs;
}
在遵循 Itanium ABI 的编译器中,对于给定类型的 RTTI,只要一个仅有的副本存在,因而能够经过比较类型称号的地址来判别 type_info 是否持平,无需运用字符串,能够进步功能并简化代码。
2. __non_unique_impl::__eq
static bool __eq(__type_name_t __lhs, __type_name_t __rhs) _NOEXCEPT {
return __lhs == __rhs || __builtin_strcmp(__lhs, __rhs) == 0;
}
由于各种原因,链接器或许没有合并一切类型的 RTTI(例如:-Bsymbolic 或 llvm.org/PR37398)。在这种情况下,假如两个 type_info 的地址持平或许它们的称号字符串持平,这两个 type_info 被以为是持平的。
修正计划中的计划 5 经过设置 cflag 把 __impl 类型修正为 __no_unique_impl,运用 strcmp 办法判别主可履行文件中的 type_info 和 libc++abi 中的持平。
3.__non_unique_arm_rtti_bit_impl::__eq 默认完结
这种办法是 Apple ARM64 的特定完结,给定类型的 RTTI 或许存在多个副本。在结构 type_info 时,编译器将类型称号的指针存储在 uintptr_t 类型中,指针的最高位表明 non_unique 默以为 0(false)。假如最高位被设置为 1,表明 type_info 在程序中不是仅有的。假如最高位没有被设置,表明 type_info 是仅有的。
这个设计的目的是为了避免运用 weak 符号。它将本来会被作为弱符号生成的默认可见性的 type_info,转而运用隐藏可见性的 type_info,并把 non_unique bit 位设置为 1 ,表明非仅有。这样做的优点是,在链接镜像内,hidden 可见性的 type_info 依然能够以为是仅有的,能够持续经过 linker 进行去重,而在不同的镜像间,会被视为不同的类型,避免了 weak 符号被重定向,导致 RTTI 类型信息混乱。
EH & -fno-rtti
这次问题的直接原因是主可履行文件多了一份 type_info, 那为什么禁用 RTTI 之后会从头生成一份 type_info 呢?
反常处理流程依靠 type_info 完结 can catch 的判别逻辑,在禁用 RTTI 之后,为了能持续获取反常类型的 type_info 信息,编译器会从头生成一份。
llvm ItaniumCXXABI.cpp 文件 BuildTypeInfo 办法内能够检查生成 type_info 的逻辑。
其间判别是否运用外部的 type_info 代码如下:
// Check if there is already an external RTTI descriptor for this type.
if (IsStandardLibraryRTTIDescriptor(Ty) ||
ShouldUseExternalRTTIDescriptor(CGM, Ty))
return GetAddrOfExternalRTTIDescriptor(Ty);
条件1: IsStandardLibraryRTTIDescriptor
判别是否是基础类型,比如 int bool float double。
条件2: ShouldUseExternalRTTIDescriptor
判别 type_info 是否已经存在于其他方位,假如是在当时的编译单元中就不需求再生成 type_info。
在 ShouldUseExternalRTTIDescriptor 办法内判别了 RTTI 的状况,禁用后直接返回了 false。
// If RTTI is disabled, assume it might be disabled in the
// translation unit that defines any potential key function, too.
if (!Context.getLangOpts().RTTI) return false;
禁用 RTTI 后上述两个条件都不满意,会持续履行生成反常类型的 type_info,一同也会生成 base 的 type_info。
///Record 表明 Structure/Class descriptor
case Type::Record: {
const CXXRecordDecl *RD =
cast<CXXRecordDecl>(cast<RecordType>(Ty)->getDecl());
if (!RD->hasDefinition() || !RD->getNumBases()) {
// We don't need to emit any fields.
break;
}
if (CanUseSingleInheritance(RD))
BuildSIClassTypeInfo(RD);
else
BuildVMIClassTypeInfo(RD);
break;
}
四、总结
局面 3 分钟,战至二塔下猥琐发育的鲁班自言自语到: 有人需求技术支撑吗? 鲁班大师,智商二百五,崇拜,极度崇拜。
检查 mmkv 的 issue 列表,发现咱们并不孤单。iOS 工程通常运用 cocoapods 集成不同的组件,这些组件在编译时会作为一个独立的 target,恣意一个 target 的编译选项禁用 RTTI 后都会影响到宿主 App 的反常处理流程,继而或许引发 try catch 失效。
为了处理此类问题,给咱们提供两个躲避计划:
-
禁用 RTTI 的一同禁用 exception handling,即 -fno-rtti 和 -fno-exceptions 一同运用,这样独自的 target 不会影响到宿主 App 的 exception handing。
-
假如禁用 RTTI 后想保留 exception handing,增加 cflag -D_LIBCPP_TYPEINFO_COMPARISON_IMPLEMENTATION=2,在损耗一点功能的前提下保证 exception handling 正常的处理流程。
各位看官老爷,假如同样也遇到过不同的编译选项引发的问题,欢迎在评论区留言评论~
参考材料
[2] itanium-cxx-abi.github.io/cxx-abi/abi…
[3] itanium-cxx-abi.github.io/cxx-abi/exc…