sjlj (setjump/longjump)与dwarf-2为mingw32两种异常处理模型的实现。sjlj有着开销,而随linux发行的mingw32开发库包都是用sjlj版编译的,而Qt却采用dwarf-2版,那么两者之间有多少差异,本文就这问题对两版的异常代码的反汇编进行分析比较。
我使用mingw-w65-i686-810的sjlj与dwarf-2两个版本对下面异常代码编译。
__attribute__((dllimport)) int dllfunc(); int main() { dllfunc(); //_create_locale(LC_ALL, "C"); printf("abc"); //return 0; try { try { throw std::exception(); } catch(std::exception&) { std::rethrow_exception(std::current_exception()); } } catch(int) { } catch(std::exception& e) { std::cout << e.what() << std::endl; } catch(...) { std::cout << "unknown" << std::endl; } return 0; }
代码逻辑:
两层 try/catch,
1. 里层 try/catch
1.1 try块, throw 异常
1.2 catch块, rethrow
2. 外层 try/catch
2.1 有三catch分支。
开刷前,先定义一下。
如果将 try/catch 去除 c++语言特性后,基本就是一种由c++库还有c++编译器共同管理的 goto。
throw相当于goto, catch相当于label(一种以类型区分的)。
那么c++编译器与c++库为我们提供了什么样的管理呢?
c++编译器
0. 利用c++支持对象析构进行try块保护。
1. 将 throw 关键字生成汇编 call __cxa_throw,调用 c++库的函数。
2. 为每个catch块生成代码片断,只能通过jmp跳转进来。
2.1 开头 call __cxa_begin_catch。
2.2 结尾 call __cxa_end_catch。
2.3 最后跳出到 try/catch块逻辑代码的下条执行指令。
3. 为同一try/catch块的所有catch块产生分支控制代码。
4. 为try块的析构代码产生跳转入口。
5. 为每一层try/catch块生成 uncaught 代码块,调用 _Unwind_Resume。
c++库:
1. __cxa_throw,马上_Unwind_RaiseException。跳转到当前最里面一层 try/catch的支路控制代码片断。
2. _Unwind_Resume,向上继续展开。
3. std::rethrow_exception,调用 __gcclibcxx_demangle_callback,
3.1 要么有 catch可达跳回到原来代码的控制流,直接离开std::rethrow_exception的调用上下文。
3.2 要么从__gcclibcxx_demangle_callback返回,执行terminate结束进程。
sjlj 版的反汇编代码比 dwarf-2 版的多了50行。
先来看dwarf-2的反汇编代码
1 <+0>: lea 0x4(%esp),%ecx 2 <+4>: and $0xfffffff0,%esp 3 <+7>: pushl -0x4(%ecx) 4 <+10>: push %ebp 5 <+11>: mov %esp,%ebp 6 <+13>: push %esi 7 <+14>: push %ebx 8 <+15>: push %ecx 9 <+16>: sub $0x2c,%esp 10 <+19>: call 0x401890 <__main> 11 <+24>: mov 0x4071a4,%eax 12 <+29>: call *%eax 13 <+31>: movl $0x404045,(%esp) 14 <+38>: call 0x4027c4 <printf> 15 <+43>: movl $0x4,(%esp) 16 <+50>: call 0x4017ac <__cxa_allocate_exception> 17 <+55>: mov %eax,%ebx 18 <+57>: mov %ebx,%ecx 19 <+59>: call 0x402890 <std::exception::exception()> 20 <+64>: movl $0x4017d4,0x8(%esp) 21 <+72>: movl $0x4042a8,0x4(%esp) 22 <+80>: mov %ebx,(%esp) 23 <+83>: call 0x401794 <__cxa_throw> 24 <+88>: mov $0x0,%eax 25 <+93>: jmp 0x401723 <main()+355> 26 <+98>: mov %edx,%ecx 27 <+100>: cmp $0x2,%ecx 28 <+103>: je 0x40162b <main()+107> 29 <+105>: jmp 0x401663 <main()+163> 30 <+107>: mov %eax,(%esp) 31 <+110>: call 0x4017a4 <__cxa_begin_catch> 32 <+115>: mov %eax,-0x1c(%ebp) 33 <+118>: lea -0x28(%ebp),%eax 34 <+121>: mov %eax,(%esp) 35 <+124>: call 0x4017cc <_ZSt17current_exceptionv> 36 <+129>: lea -0x28(%ebp),%eax 37 <+132>: mov %eax,(%esp) 38 <+135>: call 0x4017c4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE> 39 <+140>: mov %eax,%esi 40 <+142>: mov %edx,%ebx 41 <+144>: lea -0x28(%ebp),%eax 42 <+147>: mov %eax,%ecx 43 <+149>: call 0x4017ec <_ZNSt15__exception_ptr13exception_ptrD1Ev> 44 <+154>: call 0x40179c <__cxa_end_catch> 45 <+159>: mov %esi,%eax 46 <+161>: mov %ebx,%edx 47 <+163>: cmp $0x1,%edx 48 <+166>: je 0x40166f <main()+175> 49 <+168>: cmp $0x2,%edx 50 <+171>: je 0x401683 <main()+195> 51 <+173>: jmp 0x4016ca <main()+266> 52 <+175>: mov %eax,(%esp) 53 <+178>: call 0x4017a4 <__cxa_begin_catch> 54 <+183>: mov (%eax),%eax 55 <+185>: mov %eax,-0x24(%ebp) 56 <+188>: call 0x40179c <__cxa_end_catch> 57 <+193>: jmp 0x401618 <main()+88> 58 <+195>: mov %eax,(%esp) 59 <+198>: call 0x4017a4 <__cxa_begin_catch> 60 <+203>: mov %eax,-0x20(%ebp) 61 <+206>: mov -0x20(%ebp),%eax 62 <+209>: mov (%eax),%eax 63 <+211>: add $0x8,%eax 64 <+214>: mov (%eax),%eax 65 <+216>: mov -0x20(%ebp),%edx 66 <+219>: mov %edx,%ecx 67 <+221>: call *%eax 68 <+223>: mov %eax,0x4(%esp) 69 <+227>: movl $0x6ff07a00,(%esp) 70 <+234>: call 0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc> 71 <+239>: movl $0x4017bc,(%esp) 72 <+246>: mov %eax,%ecx 73 <+248>: call 0x4017f4 <_ZNSolsEPFRSoS_E> 74 <+253>: sub $0x4,%esp 75 <+256>: call 0x40179c <__cxa_end_catch> 76 <+261>: jmp 0x401618 <main()+88> 77 <+266>: mov %eax,(%esp) 78 <+269>: call 0x4017a4 <__cxa_begin_catch> 79 <+274>: movl $0x404049,0x4(%esp) 80 <+282>: movl $0x6ff07a00,(%esp) 81 <+289>: call 0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc> 82 <+294>: movl $0x4017bc,(%esp) 83 <+301>: mov %eax,%ecx 84 <+303>: call 0x4017f4 <_ZNSolsEPFRSoS_E> 85 <+308>: sub $0x4,%esp 86 <+311>: call 0x40179c <__cxa_end_catch> 87 <+316>: jmp 0x401618 <main()+88> 88 <+321>: mov %eax,%ebx 89 <+323>: call 0x40179c <__cxa_end_catch> 90 <+328>: mov %ebx,%eax 91 <+330>: mov %eax,(%esp) 92 <+333>: call 0x402770 <_Unwind_Resume> 93 <+338>: mov %eax,%ebx 94 <+340>: call 0x40179c <__cxa_end_catch> 95 <+345>: mov %ebx,%eax 96 <+347>: mov %eax,(%esp) 97 <+350>: call 0x402770 <_Unwind_Resume> 98 <+355>: lea -0xc(%ebp),%esp 99 <+358>: pop %ecx 100 <+359>: pop %ebx 101 <+360>: pop %esi 102 <+361>: pop %ebp 103 <+362>: lea -0x4(%ecx),%esp 104 <+365>: ret
我们的主要代码逻辑只有20-30条指令
当 throw时,__cxa_throw函数是不会返回的, 如同goto最后是跳转到他处,若被本层catch处理完才会跳转回来<+88>。
然后看c++编译器为我们生成的异常代码 。
对于没有发生异常时,代码执行路径基本不会去涉及到异常代码支路,开销几近为0,只是代码量增大。
下面来看 sjlj 版的汇编代码,
1 function main(): 2 <+0>: lea 0x4(%esp),%ecx 3 <+4>: and $0xfffffff0,%esp 4 <+7>: pushl -0x4(%ecx) 5 <+10>: push %ebp 6 <+11>: mov %esp,%ebp 7 <+13>: push %edi 8 <+14>: push %esi 9 <+15>: push %ebx 10 <+16>: push %ecx 11 <+17>: sub $0x68,%esp 12 <+20>: movl $0x4017ac,-0x44(%ebp) 13 <+27>: movl $0x402958,-0x40(%ebp) 14 <+34>: lea -0x3c(%ebp),%eax 15 <+37>: lea -0x18(%ebp),%ebx 16 <+40>: mov %ebx,(%eax) 17 <+42>: mov $0x4015b4,%edx 18 <+47>: mov %edx,0x4(%eax) 19 <+50>: mov %esp,0x8(%eax) 20 <+53>: lea -0x5c(%ebp),%eax 21 <+56>: mov %eax,(%esp) 22 <+59>: call 0x402790 <_Unwind_SjLj_Register> 23 <+64>: call 0x4018b0 <__main> 24 <+69>: mov 0x406194,%eax 25 <+74>: movl $0xffffffff,-0x58(%ebp) 26 <+81>: call *%eax 27 <+83>: movl $0x404001,(%esp) 28 <+90>: call 0x4027e4 <printf> 29 <+95>: movl $0x4,(%esp) 30 <+102>: call 0x4017cc <__cxa_allocate_exception> 31 <+107>: mov %eax,-0x60(%ebp) 32 <+110>: mov %eax,%ecx 33 <+112>: call 0x4028b0 <std::exception::exception()> 34 <+117>: movl $0x4017f4,0x8(%esp) 35 <+125>: movl $0x404264,0x4(%esp) 36 <+133>: mov -0x60(%ebp),%eax 37 <+136>: mov %eax,(%esp) 38 <+139>: movl $0x1,-0x58(%ebp) 39 <+146>: call 0x4017b4 <__cxa_throw> 40 <+151>: mov $0x0,%eax 41 <+156>: mov %eax,-0x60(%ebp) 42 <+159>: jmp 0x401733 <main()+547> 43 <+164>: lea 0x18(%ebp),%ebp 44 <+167>: mov -0x54(%ebp),%edx 45 <+170>: mov -0x50(%ebp),%ecx 46 <+173>: mov -0x58(%ebp),%eax 47 <+176>: test %eax,%eax 48 <+178>: je 0x4015e6 <main()+214> 49 <+180>: sub $0x1,%eax 50 <+183>: test %eax,%eax 51 <+185>: je 0x40161b <main()+267> 52 <+187>: sub $0x1,%eax 53 <+190>: test %eax,%eax 54 <+192>: je 0x4016f8 <main()+488> 55 <+198>: sub $0x1,%eax 56 <+201>: test %eax,%eax 57 <+203>: je 0x401712 <main()+514> 58 <+209>: sub $0x1,%eax 59 <+212>: ud2 60 <+214>: mov %edx,%eax 61 <+216>: mov %ecx,%edx 62 <+218>: mov %edx,%ecx 63 <+220>: cmp $0x2,%ecx 64 <+223>: je 0x4015f3 <main()+227> 65 <+225>: jmp 0x401642 <main()+306> 66 <+227>: mov %eax,(%esp) 67 <+230>: call 0x4017c4 <__cxa_begin_catch> 68 <+235>: mov %eax,-0x1c(%ebp) 69 <+238>: lea -0x28(%ebp),%eax 70 <+241>: mov %eax,(%esp) 71 <+244>: call 0x4017ec <_ZSt17current_exceptionv> 72 <+249>: lea -0x28(%ebp),%eax 73 <+252>: mov %eax,(%esp) 74 <+255>: movl $0x2,-0x58(%ebp) 75 <+262>: call 0x4017e4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE> 76 <+267>: mov %edx,-0x60(%ebp) 77 <+270>: mov %ecx,-0x64(%ebp) 78 <+273>: lea -0x28(%ebp),%eax 79 <+276>: mov %eax,%ecx 80 <+278>: call 0x40180c <_ZNSt15__exception_ptr13exception_ptrD1Ev> 81 <+283>: mov -0x60(%ebp),%eax 82 <+286>: mov %eax,-0x60(%ebp) 83 <+289>: mov -0x64(%ebp),%esi 84 <+292>: mov %esi,-0x64(%ebp) 85 <+295>: call 0x4017bc <__cxa_end_catch> 86 <+300>: mov -0x60(%ebp),%eax 87 <+303>: mov -0x64(%ebp),%edx 88 <+306>: cmp $0x1,%edx 89 <+309>: je 0x40164e <main()+318> 90 <+311>: cmp $0x2,%edx 91 <+314>: je 0x401665 <main()+341> 92 <+316>: jmp 0x4016b3 <main()+419> 93 <+318>: mov %eax,(%esp) 94 <+321>: call 0x4017c4 <__cxa_begin_catch> 95 <+326>: mov (%eax),%eax 96 <+328>: mov %eax,-0x20(%ebp) 97 <+331>: call 0x4017bc <__cxa_end_catch> 98 <+336>: jmp 0x4015a7 <main()+151> 99 <+341>: mov %eax,(%esp) 100 <+344>: call 0x4017c4 <__cxa_begin_catch> 101 <+349>: mov %eax,-0x24(%ebp) 102 <+352>: mov -0x24(%ebp),%eax 103 <+355>: mov (%eax),%eax 104 <+357>: add $0x8,%eax 105 <+360>: mov (%eax),%eax 106 <+362>: mov -0x24(%ebp),%edx 107 <+365>: mov %edx,%ecx 108 <+367>: call *%eax 109 <+369>: mov %eax,0x4(%esp) 110 <+373>: movl $0x6ff29a00,(%esp) 111 <+380>: movl $0x3,-0x58(%ebp) 112 <+387>: call 0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc> 113 <+392>: movl $0x4017dc,(%esp) 114 <+399>: mov %eax,%ecx 115 <+401>: call 0x401814 <_ZNSolsEPFRSoS_E> 116 <+406>: sub $0x4,%esp 117 <+409>: call 0x4017bc <__cxa_end_catch> 118 <+414>: jmp 0x4015a7 <main()+151> 119 <+419>: mov %eax,(%esp) 120 <+422>: call 0x4017c4 <__cxa_begin_catch> 121 <+427>: movl $0x404005,0x4(%esp) 122 <+435>: movl $0x6ff29a00,(%esp) 123 <+442>: movl $0x4,-0x58(%ebp) 124 <+449>: call 0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc> 125 <+454>: movl $0x4017dc,(%esp) 126 <+461>: mov %eax,%ecx 127 <+463>: call 0x401814 <_ZNSolsEPFRSoS_E> 128 <+468>: sub $0x4,%esp 129 <+471>: movl $0xffffffff,-0x58(%ebp) 130 <+478>: call 0x4017bc <__cxa_end_catch> 131 <+483>: jmp 0x4015a7 <main()+151> 132 <+488>: mov %edx,-0x60(%ebp) 133 <+491>: call 0x4017bc <__cxa_end_catch> 134 <+496>: mov -0x60(%ebp),%eax 135 <+499>: mov %eax,(%esp) 136 <+502>: movl $0xffffffff,-0x58(%ebp) 137 <+509>: call 0x402788 <_Unwind_SjLj_Resume> 138 <+514>: mov %edx,-0x60(%ebp) 139 <+517>: movl $0x0,-0x58(%ebp) 140 <+524>: call 0x4017bc <__cxa_end_catch> 141 <+529>: mov -0x60(%ebp),%eax 142 <+532>: mov %eax,(%esp) 143 <+535>: movl $0xffffffff,-0x58(%ebp) 144 <+542>: call 0x402788 <_Unwind_SjLj_Resume> 145 <+547>: lea -0x5c(%ebp),%eax 146 <+550>: mov %eax,(%esp) 147 <+553>: call 0x402780 <_Unwind_SjLj_Unregister> 148 <+558>: mov -0x60(%ebp),%eax 149 <+561>: lea -0x10(%ebp),%esp 150 <+564>: pop %ecx 151 <+565>: pop %ebx 152 <+566>: pop %esi 153 <+567>: pop %edi 154 <+568>: pop %ebp 155 <+569>: lea -0x4(%ecx),%esp 156 <+572>: ret
下面的分析只列出不同的地方
上图的注释有误没有勘误过,lea是不访问内存,通常代替add指令做加法,应该是6条指令要访问内存。
支路控制代码:
可以看出,支路选路控制指令多而且复杂,还有就是跳转多。
最后是函数结束前。
可以看出在 sjlj 版本中,即使代码不发生异常,函数在进入与离开时都要为登记维护付出一此成本,当涉及异常代码时,支路选路控制更加复杂更多跳转。这里有一个成本比例,你的函数逻辑简单,上面的开销比重就越大,如果是频繁调用的轻量函数就要考虑不用exception这样的error handle。
还有就是当发生异常时,需要交给c++库去管理,不同异常处理模型的实现,有着不同的开销,本文并没有涉及到。只是单纯从c++库以外的代码进行分析,也足够看出他们之间有着一定的差别。