windows C++ 異常調用棧簡析

楔子

以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裏面。
image
_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經歷什麼,我們跟蹤下彙編
image
__CxxFrameHandler4調用了RtlGuardRestoreContext,繼續單步F11,RtlGuardRestoreContext裏面調用了函數RtlRestoreContext
image
RtlRestoreContext裏面有個跳轉指令jmp rdx。看下圖:
image
jmp指令調到了如下
image
而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)
這段的原型是:

image

image



注意的點:

CxxCallCatchBlock函數不會返回,直接跳轉到catch大括號下面的代碼裏面繼續執行後面的代碼段。

void * RENAME_EH_EXTERN(__FrameHandler4)::CxxCallCatchBlock(
    EXCEPTION_RECORD *pExcept
    )
{
   //爲了便於觀察, 此處省略一萬字,
    return continuationAddress;
}

image

總結下:

堆棧的調用如下:

	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 國際」許可協議進行許可。
image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章