欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

.NET Core 異常(Exception)底層原理淺談

freeflydom
2024年12月19日 10:40 本文熱度 297

中斷與異常模型圖

  1. 內中斷
    內中斷是由 CPU 內部事件引起的中斷,通常是在程序執行過程中由于 CPU 自身檢測到某些異常情況而產生的。例如,當執行除法運算時除數為零,或者訪問了不存在的內存地址,CPU 就會產生內中斷。

    1. 故障Fault
      故障是在指令執行過程中檢測到的錯誤情況導致的內中斷,比如空指針,除0異常,缺頁中斷等

    2. 自陷Trap
      這是一種有意的內中斷,是由軟件預先設定的特殊指令或操作引起的。比如syscall,int 3這種故意設定的陷阱

    3. 終止abort
      終止是一種比較嚴重的內中斷,通常是由于不可恢復的硬件錯誤或者軟件嚴重錯誤導致的,比如內存硬件損壞、Cache 錯誤等

    4. 硬件異常
      CPU內部產生的異常事件

    5. 用戶異常
      軟件模擬出的異常,比如操作系統的SEH,.NET的OutOfMemoryException

  2. 外中斷
    外中斷是由 CPU 外部的設備或事件引起的中斷。比如鍵盤,鼠標,主板定時器。這些外部設備通過向 CPU 發送中斷請求信號來通知 CPU 需要處理某個事件。外中斷是計算機系統與外部設備進行交互的重要方式,使得 CPU 能夠及時響應外部設備的請求,提高系統的整體性能和響應能力。

    1. NMI(Non - Maskable Interrupt,非屏蔽中斷)
      NMI 是一種特殊類型的中斷,它不能被 CPU 屏蔽。與普通中斷(可以通過設置中斷屏蔽位來阻止 CPU 響應)不同,NMI 一旦被觸發,CPU 必須立即響應并處理。這種特性使得 NMI 通常用于處理非常緊急且至關重要的事件,這些事件的優先級高于任何其他可屏蔽中斷。

    2. 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通知操作系統,操作系統再通知進程觸發的異常。
比如:

  1. 內核模式切換:syscall

  2. 訪問違例:AccessViolationException

  3. 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來說,主要實現以下四個操作

  1. 捕獲異常并拋出異常的位置

  2. 通過線程棧空間獲取異常調用棧
    線程的棧空間維護了整個調用棧,掃描整個棧空間即可獲取。

windbg的k系列命令就是參考此原理。

  1. 獲取元數據的異常處理表
    一旦方法中有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之間的代碼
  1. 枚舉異常處理表,調用對應的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才會執行

  1. 重新拋出異常
    在執行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; //會丟失調用鏈,找不到真正的異常所在
                //throw; //調用鏈完整
                //ExceptionDispatchInfo.Capture(ex).Throw();//調用鏈更完整,顯示了重新拋出異常所在的位置。
            }
        }

我在這里踩過大坑,使用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
{
  // Code size       22 (0x16)
  .maxstack  1
  .locals init (int32 V_0)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  ldc.i4.1  //將100+100的值,壓入Evaluation Stack
  IL_0003:  stloc.0   //從Evaluation Stack出棧,保存到序號為0的本地變量
  IL_0004:  leave.s   IL_0014 //退出代碼保護區域,并跳轉到指定內存區域IL_0014, 指令 leave.s 清空計算堆棧并確保執行相應的周圍 finally 塊。
  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 //讀取序號0的本地變量并存入Evaluation Stack
  IL_0015:  ret  //從方法返回,返回值從Evaluation Stack中獲取
  IL_0016: 
  // Exception count 1
  .try IL_0001 to IL_0006 finally handler IL_0006 to IL_0014
} // end of method ExceptionEmample::Example2

從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
{
  // 代碼大小       15 (0xf)
  .maxstack  1
  .locals init (int32 V_0,
           int32 V_1)
  IL_0000:  nop
  IL_0001:  ldc.i4.1  //將常量1壓棧
  IL_0002:  stloc.0   //將序號0出棧,賦值給result
  IL_0003:  nop
  IL_0004:  ldloc.0  //將當前方法序號0的變量,也就是result,壓入棧中。
  IL_0005:  stloc.1  //將序號1的值出棧,保存到一個臨時變量中。也就是return的值
  IL_0006:  leave.s    IL_000d   //跳轉到對應行, 指令 leave.s 清空計算堆棧并確保執行相應的周圍 finally 塊。
  IL_0008:  nop
  IL_0009:  ldc.i4.3   
  IL_000a:  stloc.0
  IL_000b:  nop
  IL_000c:  endfinally
  IL_000d:  ldloc.1  //將return的值 入棧
  IL_000e:  ret  //執行return
  IL_000f:  
  // Exception count 1
  .try IL_0003 to IL_0008 finally handler IL_0008 to IL_000d
} // end of method Class1::Test
雖然在finally塊中修改了result的值,但是return語句已經確定了要返回的值,finally塊中的修改不會改變這個返回值。不過,如果返回的是引用類型),在finally塊中修改引用類型對象的內容是會生效的

異常對性能的影響

引用別人的數據,自己就不班門弄斧了

  1. 大佬的研究
    https://www.cnblogs.com/huangxincheng/p/12866824.html

  2. <.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)
            {
                //c# 2.0之前這個塊只能捕捉CLS相容的異常
            }
            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 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved