楔子
前面一篇研究了下C++異常的,這篇來看下,CLR的異常內存模型,實際上都是一個模型,承繼自windows異常處理機制。不同的是,有VC編譯器(vcruntime.dll)接管的部分,被CLR里面的函數ProcessCLRException接管了。
注意:這里面省略了一部分不必要贅述的細節問題,版本號分別為(CLR PreView 7和vcruntime 14.0)
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
CLR異常棧
> coreclr.dll!ProcessCLRException C++ntdll.dll!RtlpExecuteHandlerForExceptionntdll.dll!RtlDispatchExceptionntdll.dll!KiUserExceptionDispatchKernelBase.dll!RaiseExceptioncoreclr.dll!`RaiseTheExceptionInternalOnly'::`53'::__Body::Runcoreclr.dll!RaiseTheExceptionInternalOnlycoreclr.dll!IL_Throw00007ffa6faf040c()
對比
CLR異常棧的地址00007ffa6faf040c()實際上就是C#的main函數入口。對比的是C異常棧的函數入口main。
其它的一一對應(C# ----C++):
1.IL_Throw-》_CxxThrowException
2.RaiseTheExceptionInternalOnly和RaiseTheExceptionInternalOnly以及RaiseException-》RaiseException()
3.KiUserExceptionDispatch-》KiUserExceptionDispatch()
4.RtlDispatchException-》RtlDispatchException
5.RtlpExecuteHandlerForException》RtlpExecuteHandlerForException
6.ProcessCLRException -》__CxxFrameHandler4
注意粗體部分對應的,這個地方開始,VC和CLR分道揚鑣了。各自實現了后面函數異常處理的實現。
下面是windows 異常
楔子
以win11 + vs2022運行VC++ 編譯觀察的結果。
如果安裝了Visual Studio 2022,比如安裝在D盤,則路徑:
D:\Visual Studio\IDE\VC\Tools\MSVC\14.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里面。
_CxxThrowException函數源碼在VS路徑:
D:\Visual Studio\IDE\VC\Tools\MSVC\14.33.31629\crt\src\vcruntime\throw.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 Studio\IDE\VC\Tools\MSVC\14.33.31629\crt\src\vcruntime\risctrnsctrl.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經歷什么,我們跟蹤下匯編
__CxxFrameHandler4調用了RtlGuardRestoreContext,繼續單步F11,RtlGuardRestoreContext里面調用了函數RtlRestoreContext
RtlRestoreContext里面有個跳轉指令jmp rdx。看下圖:
jmp指令調到了如下
而call rax的rax就是CxxCallCatchBlock函數的指針。
因為RcConsolidateFrames函數是在ntdll.dll里面沒有被開源,所以兩次跳轉(jmp 和 call 應該是這個函數里面所做的動作)
如此一來就對上上面的那個函數調用順序(從上到下),但是還有一個問題,這個try里面拋出了異常,那么catch是何時被執行的呢?
Catch
理順了RcConsolidateFrames函數調用順序,RcConsolidateFrames自己則調用了函數CxxCallCatchBlock。這個函數里面調用了catch處理異常。
CxxCallCatchBlock函數源碼地址:
D:\Visual Studio\IDE\VC\Tools\MSVC\14.33.31629\crt\src\vcruntime\frame.cpp(1344行)
源碼:
void * RENAME_EH_EXTERN(__FrameHandler4)::CxxCallCatchBlock(EXCEPTION_RECORD *pExcept)
{//為了方便觀看,此處省略一萬行continuationAddress = RENAME_EH_EXTERN(_CallSettingFrame_LookupContinuationIndex)
}
RENAME_EH_EXTERN(_CallSettingFrame_LookupContinuationIndex)
這段的原型是:
注意的點:
CxxCallCatchBlock函數不會返回,直接跳轉到catch大括號下面的代碼里面繼續執行后面的代碼段。
void * RENAME_EH_EXTERN(__FrameHandler4)::CxxCallCatchBlock(EXCEPTION_RECORD *pExcept)
{//為了便于觀察, 此處省略一萬字,return continuationAddress;
}
總結下:
堆棧的調用如下:
vcruntime140_1d.dll!__FrameHandler4::CxxCallCatchBlock(jmp rdx)ntdll.dll!RcConsolidateFramesntdll.dll!RtlRestoreContextntdll.dll!RtlGuardRestoreContext ntdll.dll!RtlUnwindEx vcruntime140_1d.dll!__FrameHandler4::UnwindNestedFramesvcruntime140_1d.dll!CatchItvcruntime140_1d.dll!FindHandlervcruntime140_1d.dll!__InternalCxxFrameHandlervcruntime140_1d.dll!__CxxFrameHandler4 ntdll.dll!RtlpExecuteHandlerForException() ntdll.dll!RtlDispatchExceptionntdll.dll!KiUserExceptionDispatch()KernelBase.dll!RaiseException()vcruntime140d.dll!_CxxThrowExceptionConsoleApplication2.exe!main
作者:江湖評談
版權:本作品采用「署名-非商業性使用-相同方式共享 4.0 國際」許可協議進行許可。