导读

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 句子居然失效了?

依据 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 办法内被捕获,而不应该触发溃散。

震动!try catch 句子居然失效了?

mmkv::CodedInputData::readString 抛反常代码:

震动!try catch 句子居然失效了?

mmkv::MiniPBCoder::decodeOneMap 中捕获反常代码:

震动!try catch 句子居然失效了?
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 句子。

测验用例中运转成果一同表明:

  1. out_of_range 实例 is type of out_of_range 建立。
  2. 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++ 代码时所运用的言语规范。

震动!try catch 句子居然失效了?
震动!try catch 句子居然失效了?

尽管定位了问题引进的 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 也就不会触发溃散。

震动!try catch 句子居然失效了?

这儿的 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 反常时,涉及到三次查表的进程,

  1. 依据当时栈帧的 PC 查找 call site table,获取地址区间匹配的的 call site。

  2. 依据 call site 记载的 action 索引值在 action table 取 action。

  3. 依据 action 中 type_info 的索引值在 type table 中取 type_info。

之后依据 type_info 判别是否能 catch 反常,是则记载(phase1)或许跳转(phase2)到 call site 中 lpad 字段记载的的 landing pad address。不然持续向上回溯栈帧,并重复上述进程。

LSDA 的数据结构如下图所示:

震动!try catch 句子居然失效了?

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++规范库提供的类,它包括了与类型相关的运转时信息。

举个例子

以下面的代码为例,抛出反常并在当时函数内捕获反常:

震动!try catch 句子居然失效了?

简化后判别是否能 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 反常。

  1. 调用 is_equal 判别 catch 块类型是否和抛反常类型持平。

  2. 调用 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 失效。

震动!try catch 句子居然失效了?

为了处理此类问题,给咱们提供两个躲避计划:

  1. 禁用 RTTI 的一同禁用 exception handling,即 -fno-rtti 和 -fno-exceptions 一同运用,这样独自的 target 不会影响到宿主 App 的 exception handing。

  2. 假如禁用 RTTI 后想保留 exception handing,增加 cflag -D_LIBCPP_TYPEINFO_COMPARISON_IMPLEMENTATION=2,在损耗一点功能的前提下保证 exception handling 正常的处理流程。

各位看官老爷,假如同样也遇到过不同的编译选项引发的问题,欢迎在评论区留言评论~

参考材料

[1] llvm.org/docs/Except…

[2] itanium-cxx-abi.github.io/cxx-abi/abi…

[3] itanium-cxx-abi.github.io/cxx-abi/exc…

[4] www.hexblog.com/wp-content/…

[5] github.com/Tencent/MMK…