中斷與異常模型圖

內(nèi)中斷
內(nèi)中斷是由 CPU 內(nèi)部事件引起的中斷,通常是在程序執(zhí)行過程中由于 CPU 自身檢測到某些異常情況而產(chǎn)生的。例如,當執(zhí)行除法運算時除數(shù)為零,或者訪問了不存在的內(nèi)存地址,CPU 就會產(chǎn)生內(nèi)中斷。
故障Fault
故障是在指令執(zhí)行過程中檢測到的錯誤情況導(dǎo)致的內(nèi)中斷,比如空指針,除0異常,缺頁中斷等
自陷Trap
這是一種有意的內(nèi)中斷,是由軟件預(yù)先設(shè)定的特殊指令或操作引起的。比如syscall,int 3這種故意設(shè)定的陷阱
終止abort
終止是一種比較嚴重的內(nèi)中斷,通常是由于不可恢復(fù)的硬件錯誤或者軟件嚴重錯誤導(dǎo)致的,比如內(nèi)存硬件損壞、Cache 錯誤等
硬件異常
CPU內(nèi)部產(chǎn)生的異常事件
用戶異常
軟件模擬出的異常,比如操作系統(tǒng)的SEH,.NET的OutOfMemoryException
外中斷
外中斷是由 CPU 外部的設(shè)備或事件引起的中斷。比如鍵盤,鼠標,主板定時器。這些外部設(shè)備通過向 CPU 發(fā)送中斷請求信號來通知 CPU 需要處理某個事件。外中斷是計算機系統(tǒng)與外部設(shè)備進行交互的重要方式,使得 CPU 能夠及時響應(yīng)外部設(shè)備的請求,提高系統(tǒng)的整體性能和響應(yīng)能力。
NMI(Non - Maskable Interrupt,非屏蔽中斷)
NMI 是一種特殊類型的中斷,它不能被 CPU 屏蔽。與普通中斷(可以通過設(shè)置中斷屏蔽位來阻止 CPU 響應(yīng))不同,NMI 一旦被觸發(fā),CPU 必須立即響應(yīng)并處理。這種特性使得 NMI 通常用于處理非常緊急且至關(guān)重要的事件,這些事件的優(yōu)先級高于任何其他可屏蔽中斷。
INTR(Interrupt Request,中斷請求)
INTR 是 CPU 用于接收外部中斷請求的引腳(在硬件層面)或者信號機制(在軟件層面)。外部設(shè)備(如磁盤驅(qū)動器、鍵盤、鼠標等)通過向 CPU 的 INTR 引腳發(fā)送信號來請求 CPU 中斷當前任務(wù),為其提供服務(wù)。這是計算機系統(tǒng)實現(xiàn)設(shè)備交互和多任務(wù)處理的關(guān)鍵機制之一。
用戶異常
C#的異常,在Windows平臺下是完全圍繞SEH處理框架來展開。在Linux上則是圍繞signal模擬成SEH結(jié)構(gòu),因為都會進入內(nèi)核態(tài),所以其開銷并不低,內(nèi)部走了很多流程。
static void Main(string[] args)
{
try
{
var num = Convert.ToInt32("a");
}
catch (Exception ex)
{
Debugger.Break();
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}

眼見為實:用戶Execption的調(diào)用棧

硬件異常
硬件異常指CPU執(zhí)行機器碼出現(xiàn)異常后,由CPU通知操作系統(tǒng),操作系統(tǒng)再通知進程觸發(fā)的異常。
比如:
內(nèi)核模式切換:syscall
訪問違例:AccessViolationException
visual studio中F9中斷:int 3
static void Main(string[] args)
{
try
{
string str = null;
var len = str.Length;
Console.WriteLine(len);
}
catch (Exception ex)
{
Debugger.Break();
Console.WriteLine(ex.ToString());
}
Console.ReadLine();
}

與用戶異常不同的是,異常的發(fā)起點在CPU上,并且CLR為了統(tǒng)一處理。會先將硬件異常轉(zhuǎn)換成用戶異常。以此來復(fù)用后續(xù)邏輯。所以相比用戶異常,硬件異常的開銷更大
眼見為實:硬件Execption的調(diào)用棧

硬件異常如何與用戶異常綁定?
上面說到,CLR會先將硬件異常轉(zhuǎn)換成用戶異常。那么在拋出異常的時候,如何正確拋出一個托管堆認識的異常呢?
以空指針異常為例

核心邏輯在ProcessCLRException中,它會判斷 Thread 是否掛了異常?沒有的話就會通過MapWin32FaultToCOMPlusException來轉(zhuǎn)換,然后通過 pThread.SafeSetThrowables 塞入到線程里。從而實現(xiàn)了硬件異常在托管堆上的映射。
眼見為實
上源碼
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/excep.cpp

.NET 異常處理流程
對.NET Runtime來說,主要實現(xiàn)以下四個操作
捕獲異常并拋出異常的位置
通過線程棧空間獲取異常調(diào)用棧
線程的棧空間維護了整個調(diào)用棧,掃描整個棧空間即可獲取。
windbg的k系列命令就是參考此原理。
- 獲取元數(shù)據(jù)的異常處理表
一旦方法中有try-catch語句塊時,JIT會將try-catch的適用范圍記錄下來,并整理成異常處理表(Execption Handling Table , EH Table)
C# 代碼
public class ExceptionEmample
{
public static void Example()
{
try
{
Console.WriteLine("Try outer");
try
{
Console.WriteLine("Try inner");
}
catch (Exception)
{
Console.WriteLine("Catch Expception inner");
}
}
catch (ArgumentException)
{
Console.WriteLine("Catch ArgumentException outer");
}
catch (Exception)
{
Console.WriteLine("Catch Exception outer");
}
finally
{
Console.WriteLine("Finally outer");
}
}
}
?IL代碼
.method public hidebysig static void Example() cil managed
{
// Code size 96 (0x60)
.maxstack 1
IL_0000: nop
IL_0001: nop
IL_0002: ldstr "Try outer"
IL_0007: call void [System.Console]System.Console::WriteLine(string)
IL_000c: nop
IL_000d: nop
IL_000e: ldstr "Try inner"
IL_0013: call void [System.Console]System.Console::WriteLine(string)
IL_0018: nop
IL_0019: nop
IL_001a: leave.s IL_002c
IL_001c: pop
IL_001d: nop
IL_001e: ldstr "Catch Expception inner"
IL_0023: call void [System.Console]System.Console::WriteLine(string)
IL_0028: nop
IL_0029: nop
IL_002a: leave.s IL_002c
IL_002c: nop
IL_002d: leave.s IL_004f
IL_002f: pop
IL_0030: nop
IL_0031: ldstr "Catch ArgumentException outer"
IL_0036: call void [System.Console]System.Console::WriteLine(string)
IL_003b: nop
IL_003c: nop
IL_003d: leave.s IL_004f
IL_003f: pop
IL_0040: nop
IL_0041: ldstr "Catch Exception outer"
IL_0046: call void [System.Console]System.Console::WriteLine(string)
IL_004b: nop
IL_004c: nop
IL_004d: leave.s IL_004f
IL_004f: leave.s IL_005f
IL_0051: nop
IL_0052: ldstr "Finally outer"
IL_0057: call void [System.Console]System.Console::WriteLine(string)
IL_005c: nop
IL_005d: nop
IL_005e: endfinally
IL_005f: ret
IL_0060:
// Exception count 4
.try IL_000d to IL_001c catch [System.Runtime]System.Exception handler IL_001c to IL_002c
.try IL_0001 to IL_002f catch [System.Runtime]System.ArgumentException handler IL_002f to IL_003f
.try IL_0001 to IL_002f catch [System.Runtime]System.Exception handler IL_003f to IL_004f
.try IL_0001 to IL_0051 finally handler IL_0051 to IL_005f
} // end of method ExceptionEmample::Example
IL代碼中最后4行就代表了方法的異常處理表。
1. IL_000d to IL_001c 之間代碼發(fā)生的Exception異常由IL_001c to IL_002c 之間的代碼處理
2. IL_0001 to IL_002f 之間發(fā)生的ArgumentException異常由IL_002f to IL_003f之間的代碼處理
3. IL_0001 to IL_002f 之間發(fā)生的Exception異常由IL_003f to IL_004f之間的代碼處理
4. IL_0001 to IL_0051 之間無論發(fā)生什么,結(jié)束后都要執(zhí)行IL_0051 to IL_005f之間的代碼
枚舉異常處理表,調(diào)用對應(yīng)的catch塊與finally塊
當異常發(fā)生時,Runtime會枚舉EH Table,找出并調(diào)用對應(yīng)的catch塊與finally塊。
核心方法為ProcessManagedCallFrame:

https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/exceptionhandling.cpp
需要注意的是,一旦CLR找到catch塊,就會先執(zhí)行內(nèi)層所有finally塊中的代碼,再等到當前catch塊中的代碼執(zhí)行完畢f(xié)inally才會執(zhí)行
重新拋出異常
在執(zhí)行catch,finally的過程中,如果又拋出了異常。程序會再次進入ProcessCLRException中走重復(fù)流程。
但是調(diào)用鏈會消失,如果想要防止調(diào)用鏈丟失,需要特殊處理。
static void Main(string[] args)
{
try
{
Test();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
} private static void Test()
{
try
{
throw new Exception("test");
}
catch (Exception ex)
{
}
}
我在這里踩過大坑,使用throw ex重新拋出異常,結(jié)果丟失了異常真正的觸發(fā)點,日志跟沒記一樣。
finally一定會執(zhí)行嗎?
常規(guī)情況下,finally是保證會執(zhí)行的代碼,但如果直接用win32函數(shù)TerminateThread殺死線程,或使用System.Environment的Failfast殺死進程,finally塊不會執(zhí)行。
先執(zhí)行return還是先執(zhí)行finally?
C#代碼
~~~
public static int Example2()
{
try
{
return 100+100;
}
finally
{
Console.WriteLine("finally");
}
}
~~~
IL代碼
.method public hidebysig static int32 Example2() cil managed
{
.maxstack 1
.locals init (int32 V_0)
IL_0000: nop
IL_0001: nop
IL_0002: ldc.i4.1
IL_0003: stloc.0
IL_0004: leave.s IL_0014
IL_0006: nop
IL_0007: ldstr "finally"
IL_000c: call void [System.Console]System.Console::WriteLine(string)
IL_0011: nop
IL_0012: nop
IL_0013: endfinally
IL_0014: ldloc.0
IL_0015: ret
IL_0016:
.try IL_0001 to IL_0006 finally handler IL_0006 to IL_0014
}
從IL中可以看到,當try中包含return語句時,編譯器會生成一個臨時變量將返回值保存起來。然后再執(zhí)行finally塊。最后再return 臨時變量。這個過程稱為局部展開(local unwind)
再舉一個例子
C#代碼
public static int Test()
{
int result = 1;
try
{
return result;
}
finally
{
result = 3;
}
}
IL代碼
.method public hidebysig static int32 Test() cil managed
{
.maxstack 1
.locals init (int32 V_0,
int32 V_1)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0
IL_0003: nop
IL_0004: ldloc.0
IL_0005: stloc.1
IL_0006: leave.s IL_000d
IL_0008: nop
IL_0009: ldc.i4.3
IL_000a: stloc.0
IL_000b: nop
IL_000c: endfinally
IL_000d: ldloc.1
IL_000e: ret
IL_000f:
.try IL_0003 to IL_0008 finally handler IL_0008 to IL_000d
}
雖然在finally塊中修改了result的值,但是return語句已經(jīng)確定了要返回的值,finally塊中的修改不會改變這個返回值。不過,如果返回的是引用類型),在finally塊中修改引用類型對象的內(nèi)容是會生效的異常對性能的影響
引用別人的數(shù)據(jù),自己就不班門弄斧了
大佬的研究
https://www.cnblogs.com/huangxincheng/p/12866824.html
<.NET Core底層入門>

總體來說,只要進入內(nèi)核態(tài)。就沒有開銷低的。
CLS與非CLS異常(歷史包袱)
在CLR的2.0版本之前,CLR只能捕捉CLS相容的異常。如果一個C#方法調(diào)用了其他編程語言寫的方法,且拋出一個非CLS相容的異常。那么C#無法捕獲到該異常。
在后續(xù)版本中,CLR引入了RuntimeWrappedException類。當非CLS相容的異常被拋出時,CLR會自動構(gòu)造RuntimeWrappedException實例。使之與與CLS兼容
public static void Example2()
{
try
{
}
catch(Exception)
{
}
catch
{
}
}
.NET 9 的改進
.NET 9 重寫了異常處理機制,新實現(xiàn)基于 NativeAOT Runtime的異常處理模型。
https://learn.microsoft.com/zh-cn/dotnet/core/whats-new/dotnet-9/runtime#faster-exceptions
轉(zhuǎn)自https://www.cnblogs.com/lmy5215006/p/18604440
該文章在 2024/12/19 10:40:06 編輯過