windows C++ 异常调用栈简析

楔子

以win11 + vs2022运行VC++ 编译观察的结果。
如果安装了Visual Studio 2022,比如安装在D盘,则路径:

D:Visual StudioIDEVCToolsMSVC14.33.31629 

下面包含了vcruntime.dll的源码,主要VC编译器和ntdll.dll 以及KernelBase.dll交互。
注:本篇不叙述正常的windows用户态和内核态异常处理,仅看用户态下偏角的运作方式。

代码

void main() { 	char* pStr = NULL; 	try 	{ 		throw pStr; 	} 	catch (char* s) 	{ 		printf("Hello S"); 	} 	getchar(); } 

try里面抛出一个异常,异常调用堆栈如下

分析

红色箭头,throw抛出异常之后,调用了_CxxThrowException函数,这个函数刚好在vcruntime.dll里面。
windows C++ 异常调用栈简析
_CxxThrowException函数源码在VS路径:

D:Visual StudioIDEVCToolsMSVC14.33.31629crtsrcvcruntimethrow.cpp 
extern "C" __declspec(noreturn) void __stdcall _CxxThrowException(     void *pExceptionObject, // The object thrown     _ThrowInfo *pThrowInfo  // Everything we need to know about it ) {     //为了方便观看,此处省略一万字     RaiseException(EH_EXCEPTION_NUMBER, EXCEPTION_NONCONTINUABLE, _countof(parameters), parameters); } 

_CxxThrowException又调用了RaiseException函数。RaiseException函数会进入到内核里面分别调用如下:

ntdll.dll!KiUserExceptionDispatch-》 ntdll.dll!RtlDispatchException-》 ntdll.dll!RtlpExecuteHandlerForException-》 

windows异常分为内核态和用户态处理过程,RtlpExecuteHandlerForException则刚好是用户态处理过程。这些过程过于复杂,此处为了避免无端枝节,不赘述。

RtlpExecuteHandlerForException是调用异常处理的函数,通俗点就是跳转到catch地址,然后执行catch后面的代码。

在VS2022里面,异常处理函数是__CxxFrameHandler4(此函数在vcruntime.dll里面)
源码在路径:

D:Visual StudioIDEVCToolsMSVC14.33.31629crtsrcvcruntimerisctrnsctrl.cpp 

__CxxFrameHandler4后面的调用函数是:

__CxxFrameHandler4-》 vcruntime140_1d.dll!__InternalCxxFrameHandler-》 vcruntime140_1d.dll!FindHandler-》 vcruntime140_1d.dll!CatchIt-》 vcruntime140_1d.dll!__FrameHandler4::UnwindNestedFrames-》 ntdll.dll!RtlUnwindEx-》 ntdll.dll!RtlGuardRestoreContext-》 ntdll.dll!RtlRestoreContext-》 ntdll.dll!RtlpExecuteHandlerForUnwind-》 vcruntime140_1d.dll!__CxxFrameHandler4-》 

到了这里看似已经接近完成了,但是实际上还远不止如此。如果再继续调用,会直接跳到函数

ntdll.dll!RcConsolidateFrames -》 vcruntime140_1d.dll!__FrameHandler4::CxxCallCatchBlock 

从__CxxFrameHandler4到RcConsolidateFrames经历什么?会发现跟上面的对不上。堆栈也没有显示。
为此,还需要继续跟踪

汇编

为了能看到从__CxxFrameHandler4到RcConsolidateFrames经历什么,我们跟踪下汇编
windows C++ 异常调用栈简析
__CxxFrameHandler4调用了RtlGuardRestoreContext,继续单步F11,RtlGuardRestoreContext里面调用了函数RtlRestoreContext
windows C++ 异常调用栈简析
RtlRestoreContext里面有个跳转指令jmp rdx。看下图:
windows C++ 异常调用栈简析
jmp指令调到了如下
windows C++ 异常调用栈简析
而call rax的rax就是CxxCallCatchBlock函数的指针。
因为RcConsolidateFrames函数是在ntdll.dll里面没有被开源,所以两次跳转(jmp 和 call 应该是这个函数里面所做的动作)
如此一来就对上上面的那个函数调用顺序(从上到下),但是还有一个问题,这个try里面抛出了异常,那么catch是何时被执行的呢?

Catch

理顺了RcConsolidateFrames函数调用顺序,RcConsolidateFrames自己则调用了函数CxxCallCatchBlock。这个函数里面调用了catch处理异常。
CxxCallCatchBlock函数源码地址:

D:Visual StudioIDEVCToolsMSVC14.33.31629crtsrcvcruntimeframe.cpp(1344行) 

源码:

void * RENAME_EH_EXTERN(__FrameHandler4)::CxxCallCatchBlock(     EXCEPTION_RECORD *pExcept     ) {           //为了方便观看,此处省略一万行             continuationAddress = RENAME_EH_EXTERN(_CallSettingFrame_LookupContinuationIndex) } 
RENAME_EH_EXTERN(_CallSettingFrame_LookupContinuationIndex) 这段的原型是: 

windows C++ 异常调用栈简析

windows C++ 异常调用栈简析

注意的点:

CxxCallCatchBlock函数不会返回,直接跳转到catch大括号下面的代码里面继续执行后面的代码段。

void * RENAME_EH_EXTERN(__FrameHandler4)::CxxCallCatchBlock(     EXCEPTION_RECORD *pExcept     ) {    //为了便于观察, 此处省略一万字,     return continuationAddress; } 

windows C++ 异常调用栈简析

总结下:

堆栈的调用如下:

	vcruntime140_1d.dll!__FrameHandler4::CxxCallCatchBlock 	(jmp rdx)ntdll.dll!RcConsolidateFrames 	ntdll.dll!RtlRestoreContext 	ntdll.dll!RtlGuardRestoreContext	  	ntdll.dll!RtlUnwindEx	  	vcruntime140_1d.dll!__FrameHandler4::UnwindNestedFrames  	vcruntime140_1d.dll!CatchIt  	vcruntime140_1d.dll!FindHandler  	vcruntime140_1d.dll!__InternalCxxFrameHandler  	vcruntime140_1d.dll!__CxxFrameHandler4	  	ntdll.dll!RtlpExecuteHandlerForException()	  	ntdll.dll!RtlDispatchException  	ntdll.dll!KiUserExceptionDispatch()  	KernelBase.dll!RaiseException()  	vcruntime140d.dll!_CxxThrowException  	ConsoleApplication2.exe!main	 

作者:江湖评谈
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
windows C++ 异常调用栈简析

发表评论

相关文章