中斷與異常模型圖
內中斷
內中斷是由 CPU 內部事件引起的中斷,通常是在程序執行過程中由于 CPU 自身檢測到某些異常情況而產生的。例如,當執行除法運算時除數為零,或者訪問了不存在的內存地址,CPU 就會產生內中斷。
故障Fault
故障是在指令執行過程中檢測到的錯誤情況導致的內中斷,比如空指針,除0異常,缺頁中斷等
自陷Trap
這是一種有意的內中斷,是由軟件預先設定的特殊指令或操作引起的。比如syscall,int 3這種故意設定的陷阱
終止abort
終止是一種比較嚴重的內中斷,通常是由于不可恢復的硬件錯誤或者軟件嚴重錯誤導致的,比如內存硬件損壞、Cache 錯誤等
硬件異常
CPU內部產生的異常事件
用戶異常
軟件模擬出的異常,比如操作系統的SEH,.NET的OutOfMemoryException
外中斷
外中斷是由 CPU 外部的設備或事件引起的中斷。比如鍵盤,鼠標,主板定時器。這些外部設備通過向 CPU 發送中斷請求信號來通知 CPU 需要處理某個事件。外中斷是計算機系統與外部設備進行交互的重要方式,使得 CPU 能夠及時響應外部設備的請求,提高系統的整體性能和響應能力。
NMI(Non - Maskable Interrupt,非屏蔽中斷)
NMI 是一種特殊類型的中斷,它不能被 CPU 屏蔽。與普通中斷(可以通過設置中斷屏蔽位來阻止 CPU 響應)不同,NMI 一旦被觸發,CPU 必須立即響應并處理。這種特性使得 NMI 通常用于處理非常緊急且至關重要的事件,這些事件的優先級高于任何其他可屏蔽中斷。
INTR(Interrupt Request,中斷請求)
INTR 是 CPU 用于接收外部中斷請求的引腳(在硬件層面)或者信號機制(在軟件層面)。外部設備(如磁盤驅動器、鍵盤、鼠標等)通過向 CPU 的 INTR 引腳發送信號來請求 CPU 中斷當前任務,為其提供服務。這是計算機系統實現設備交互和多任務處理的關鍵機制之一。
用戶異常
C#的異常,在Windows平臺下是完全圍繞SEH處理框架來展開。在Linux上則是圍繞signal模擬成SEH結構,因為都會進入內核態,所以其開銷并不低,內部走了很多流程。
static void Main(string[] args)
{
try
{
var num = Convert.ToInt32("a");
}
catch (Exception ex)
{
Debugger.Break();
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
眼見為實:用戶Execption的調用棧
硬件異常
硬件異常指CPU執行機器碼出現異常后,由CPU通知操作系統,操作系統再通知進程觸發的異常。
比如:
內核模式切換: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();
}
與用戶異常不同的是,異常的發起點在CPU上,并且CLR為了統一處理。會先將硬件異常轉換成用戶異常。以此來復用后續邏輯。所以相比用戶異常,硬件異常的開銷更大
眼見為實:硬件Execption的調用棧
硬件異常如何與用戶異常綁定?
上面說到,CLR會先將硬件異常轉換成用戶異常。那么在拋出異常的時候,如何正確拋出一個托管堆認識的異常呢?
以空指針異常為例
核心邏輯在ProcessCLRException中,它會判斷 Thread 是否掛了異常?沒有的話就會通過MapWin32FaultToCOMPlusException來轉換,然后通過 pThread.SafeSetThrowables 塞入到線程里。從而實現了硬件異常在托管堆上的映射。
眼見為實
上源碼
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/excep.cpp
.NET 異常處理流程
對.NET Runtime來說,主要實現以下四個操作
捕獲異常并拋出異常的位置
通過線程棧空間獲取異常調用棧
線程的棧空間維護了整個調用棧,掃描整個棧空間即可獲取。
windbg的k系列命令就是參考此原理。
- 獲取元數據的異常處理表
一旦方法中有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 之間代碼發生的Exception異常由IL_001c to IL_002c 之間的代碼處理
2. IL_0001 to IL_002f 之間發生的ArgumentException異常由IL_002f to IL_003f之間的代碼處理
3. IL_0001 to IL_002f 之間發生的Exception異常由IL_003f to IL_004f之間的代碼處理
4. IL_0001 to IL_0051 之間無論發生什么,結束后都要執行IL_0051 to IL_005f之間的代碼
枚舉異常處理表,調用對應的catch塊與finally塊
當異常發生時,Runtime會枚舉EH Table,找出并調用對應的catch塊與finally塊。
核心方法為ProcessManagedCallFrame:
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/exceptionhandling.cpp
需要注意的是,一旦CLR找到catch塊,就會先執行內層所有finally塊中的代碼,再等到當前catch塊中的代碼執行完畢finally才會執行
重新拋出異常
在執行catch,finally的過程中,如果又拋出了異常。程序會再次進入ProcessCLRException中走重復流程。
但是調用鏈會消失,如果想要防止調用鏈丟失,需要特殊處理。
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重新拋出異常,結果丟失了異常真正的觸發點,日志跟沒記一樣。
finally一定會執行嗎?
常規情況下,finally是保證會執行的代碼,但如果直接用win32函數TerminateThread殺死線程,或使用System.Environment的Failfast殺死進程,finally塊不會執行。
先執行return還是先執行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語句時,編譯器會生成一個臨時變量將返回值保存起來。然后再執行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語句已經確定了要返回的值,finally塊中的修改不會改變這個返回值。不過,如果返回的是引用類型),在finally塊中修改引用類型對象的內容是會生效的異常對性能的影響
引用別人的數據,自己就不班門弄斧了
大佬的研究
https://www.cnblogs.com/huangxincheng/p/12866824.html
<.NET Core底層入門>
總體來說,只要進入內核態。就沒有開銷低的。
CLS與非CLS異常(歷史包袱)
在CLR的2.0版本之前,CLR只能捕捉CLS相容的異常。如果一個C#方法調用了其他編程語言寫的方法,且拋出一個非CLS相容的異常。那么C#無法捕獲到該異常。
在后續版本中,CLR引入了RuntimeWrappedException類。當非CLS相容的異常被拋出時,CLR會自動構造RuntimeWrappedException實例。使之與與CLS兼容
public static void Example2()
{
try
{
}
catch(Exception)
{
}
catch
{
}
}
.NET 9 的改進
.NET 9 重寫了異常處理機制,新實現基于 NativeAOT Runtime的異常處理模型。
https://learn.microsoft.com/zh-cn/dotnet/core/whats-new/dotnet-9/runtime#faster-exceptions
轉自https://www.cnblogs.com/lmy5215006/p/18604440
該文章在 2024/12/19 10:40:06 編輯過