Location>code7788 >text

NET Core Exception (Exception) underlying principles of the discussion

Popularity:239 ℃/2024-12-17 22:20:39

Interrupt and Exception Model Diagram

image

  1. internal interrupt
    Internal interrupts are interrupts caused by events internal to the CPU, and are usually generated during program execution when the CPU itself detects some abnormal conditions. For example, the CPU generates an internal interrupt when a division operation is performed and the divisor is zero, or when a memory address is accessed that does not exist.

    1. hardware anomaly
      Exceptional events generated within the CPU
      1. Fault Fault
        Faults are internal interrupts caused by error conditions detected during instruction execution, such as null pointers, divide-by-0 exceptions, missing page interrupts, and so on.
      2. Self Trapped Trap
        This is an intentional internal interrupt that is caused by a special instruction or operation that is predetermined by the software. For example, syscall,int 3 is an intentional trap.
      3. Terminate abort
        Termination is a more serious type of internal interrupt, usually caused by an unrecoverable hardware error or a serious software error, such as memory hardware corruption, Cache errors, and so on
    2. user anomaly
      Exceptions simulated by software, such as SEH for operating systems, OutOfMemoryException for .
  2. external interruptions
    External interrupts are interrupts caused by devices or events external to the CPU. Examples include keyboards, mice, and motherboard timers. These external devices send interrupt request signals to the CPU to notify the CPU that it needs to handle a certain event. External interrupts are an important way for computer systems to interact with external devices, enabling the CPU to respond to requests from external devices in a timely manner and improving the overall performance and responsiveness of the system.

    1. NMI (Non - Maskable Interrupt)
      NMI is a special type of interrupt that cannot be masked by the CPU. Unlike normal interrupts, which can be blocked by setting the interrupt mask bit, NMIs must be responded to and handled by the CPU as soon as they are triggered. This feature makes NMIs often used to handle very urgent and critical events, which have a higher priority than any other maskable interrupt.
    2. INTR (Interrupt Request)
      INTR is a pin (at the hardware level) or signaling mechanism (at the software level) used by the CPU to receive external interrupt requests. External devices (e.g., disk drives, keyboards, mice, etc.) request the CPU to interrupt the current task by sending signals to the CPU's INTR pin to provide services to them. This is one of the key mechanisms that enable device interaction and multitasking in computer systems.

user anomaly

C# exceptions, on the Windows platform are completely centered around the SEH handling framework. Its overhead is not low and a lot of processes go on internally.

        static void Main(string[] args)
        {
            try
            {
                var num = Convert.ToInt32("a");
            }
            catch (Exception ex)
            {
                ();
                ();
            }

            ();
        }

image

Seeing is believing: the call stack of user Execption

image

hardware anomaly

Hardware exceptions refer to exceptions triggered by the CPU executing machine code with an exception, which is notified by the CPU to the operating system, which in turn notifies the process.
For example:

  1. Kernel mode switching: syscall
  2. Access Violation: AccessViolationException
  3. F9 interrupt in visual studio: int 3
        static void Main(string[] args)
        {
            try
            {
                string str = null;
                var len = ;

                (len);
            }
            catch (Exception ex)
            {
                ();
                (());
            }

            ();
        }

image

Unlike user exceptions, exceptions are initiated on the CPU and are handled by the CLR in a uniform manner. Hardware exceptions are first converted to user exceptions. In order to reuse the subsequent logic. So compared to user exceptions, theHardware anomalies have more overhead

Seeing is believing: Hardware Execption call stacks

image

How are hardware anomalies tied to user anomalies?

As mentioned above, the CLR converts hardware exceptions to user exceptions first. So when throwing an exception, how do you properly throw an exception that is recognized by the managed heap?
Take the null pointer exception as an example
image

The core logic is in ProcessCLRException, which determines if the Thread has a hung exception? If not, it is converted by MapWin32FaultToCOMPlusException and stuffed into the thread. This enables the mapping of hardware exceptions to the managed heap.

seeing is believing

source code
/dotnet/runtime/blob/main/src/coreclr/vm/
image

NET Exception Handling Process

For the .NET Runtime, the following four main operations are implemented

  1. Location where exceptions are caught and thrown

  2. Getting the Exception Call Stack via Thread Stack Space
    The thread's stack space maintains the entire call stack, which can be obtained by scanning the entire stack space.

windbg's k series of commands refer to this principle.

  1. Exception handling table for getting metadata
    Once a method has a try-catch block, the JIT records the scope of the try-catch and organizes it into an Exception Handling Table (EH Table).
C# Code
    public class ExceptionEmample
    {
        public static void Example()
        {
			try
			{
                ("Try outer");
				try
				{
                    ("Try inner");
                }
				catch (Exception)
				{ 
                    ("Catch Expception inner");
                }
            }
			catch (ArgumentException)
			{
                ("Catch ArgumentException outer");
            }
            catch (Exception)
            {
                ("Catch Exception outer");
            }
            finally
            {
                ("Finally outer");
            }
        }
    }
IL code
.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 []::WriteLine(string)
  IL_000c:  nop
  IL_000d:  nop
  IL_000e:  ldstr      "Try inner"
  IL_0013:  call       void []::WriteLine(string)
  IL_0018:  nop
  IL_0019:  nop
  IL_001a:      IL_002c
  IL_001c:  pop
  IL_001d:  nop
  IL_001e:  ldstr      "Catch Expception inner"
  IL_0023:  call       void []::WriteLine(string)
  IL_0028:  nop
  IL_0029:  nop
  IL_002a:      IL_002c
  IL_002c:  nop
  IL_002d:      IL_004f
  IL_002f:  pop
  IL_0030:  nop
  IL_0031:  ldstr      "Catch ArgumentException outer"
  IL_0036:  call       void []::WriteLine(string)
  IL_003b:  nop
  IL_003c:  nop
  IL_003d:      IL_004f
  IL_003f:  pop
  IL_0040:  nop
  IL_0041:  ldstr      "Catch Exception outer"
  IL_0046:  call       void []::WriteLine(string)
  IL_004b:  nop
  IL_004c:  nop
  IL_004d:      IL_004f
  IL_004f:      IL_005f
  IL_0051:  nop
  IL_0052:  ldstr      "Finally outer"
  IL_0057:  call       void []::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 [] handler IL_001c to IL_002c
  .try IL_0001 to IL_002f catch [] handler IL_002f to IL_003f
  .try IL_0001 to IL_002f catch [] handler IL_003f to IL_004f
  .try IL_0001 to IL_0051 finally handler IL_0051 to IL_005f
} // end of method ExceptionEmample::Example

The last 4 lines of the IL code represent the method's exception handling table.

1. The Exception that occurs in the code between IL_000d and IL_001c is handled by the code between IL_001c and IL_002c.
2. ArgumentException that occurs between IL_0001 to IL_002f is handled by the code between IL_002f to IL_003f.
3. Exception that occurs between IL_0001 and IL_002f is handled by code from IL_003f to IL_004f.
4. whatever happens between IL_0001 and IL_0051, the code between IL_0051 and IL_005f is executed at the end.
  1. Enumerate the exception handler table and call the corresponding catch and finally blocks.
    When an exception occurs, the Runtime enumerates the EH Table, finds and calls the corresponding catch and finally blocks.
    The core method is ProcessManagedCallFrame:
    image

/dotnet/runtime/blob/main/src/coreclr/vm/

Note that once the CLR finds the catch block, it executes all the code in the inner finally block first, and then waits until the code in the current catch block has finished executing before executing finally.

  1. Re-throw the exception
    During the execution of catch,finally, if another exception is thrown. The program will enter the ProcessCLRException again to repeat the process.
    But the call chain will disappear, and special handling is needed if you want to prevent the call chain from being lost.
        static void Main(string[] args)
        {
            try...
            test(); test(); test(); test(); test(); test()
                Test();
            }
            catch (Exception ex)
            catch (Exception ex); {
                (ex); } catch (Exception) {
            }
        }

private static void Test()
{
            private static void Test() {
            throw new Exception("test"); {
                throw new Exception("test"); }
            }
            catch (Exception ex)
            } catch (Exception ex)
                //throw ex; //loses the call chain and doesn't find the real exception.
                //throw; //the call chain is complete.
                //(ex).Throw(); //the call chain is more complete, showing where the exception was rethrown.
            }
        }

I've stepped in a big hole here, using throw ex to rethrow the exception, and ended up losing the real trigger for the exception, and the log is as good as not.

Will finally be executed?

Normally, finally is code that is guaranteed to execute, but if you kill the thread directly with the win32 function TerminateThread, or if you kill the process with Failfast, the finally block will not execute.

Whether to execute return or finally first.

C# code
~~~
        public static int Example2()
        {
            try
            {
                return 100+100;
            }
            finally
            {
                ("finally");
            }
        }
~~~
IL code
.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 // Press 100+100 values into Evaluation Stack
  IL_0003: stloc.0 //Save the value from the Evaluation Stack to the local variable with serial number 0.
  IL_0004: IL_0014 //Exit the code protection area and jump to the specified memory area IL_0014, the instruction empties the Evaluation Stack and ensures the execution of the corresponding surrounding finally block.
  IL_0006: nop
  IL_0007: ldstr "finally"
  IL_000c: call void []::WriteLine(string)
  IL_0011: nop
  IL_0012: nop
  IL_0013: endfinally
  IL_0014: ldloc.0 //Read the local variable with serial number 0 and store it in the Evaluation Stack.
  IL_0015: ret //return from method, get return value from 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

As you can see from IL, when a try contains a return statement, the compiler generates a temporary variable to hold the return value. Then the finally block is executed. Finally, the temporary variable is returned. This process is called local unwind.

One more example.

C# code
        public static int Test()
        {
			int result = 1;
			try
			{
				return result;
			}
			finally
			{
				result = 3;
			}
        }
IL code
.method public hidebysig static int32 Test() cil managed
{
  // Code size 15 (0xf)
  .maxstack 1
  .locals init (int32 V_0, int32 V_1)
           int32 V_0, int32 V_1)
  IL_0000: nop
  IL_0001: ldc.i4.1 // put constant 1 on the stack
  IL_0002: stloc.0 //put serial number 0 on the stack and assign it to result
  IL_0003: nop
  IL_0004: ldloc.0 //Press the variable with serial number 0, which is result, onto the stack.
  IL_0005: stloc.1 //Take the value of serial number 1 off the stack and save it to a temporary variable. That is, the value of return
  IL_0006: IL_000d //Jump to the corresponding line, the instruction empties the stack and ensures that the corresponding surrounding finally block is executed.
  IL_0008: nop
  IL_0009: ldc.i4.3
  IL_000a: stloc.0
  IL_000b: nop
  IL_000c: endfinally
  IL_000d: ldloc.1 //put the value of return on the stack
  IL_000e: ret //execute return
  IL_000f.
  // Exception count 1
  .try IL_0003 to IL_0008 finally handler IL_0008 to IL_000d
} // end of method Class1::Test


Although the value of result is modified in the finally block, the return statement has already determined the value to be returned, and the modification in the finally block does not change that return value. However, if the return is of a reference type), modifying the contents of the reference type object in the finally block will take effect

Performance Impact of Exceptions

Quoting other people's data and not making a fool of myself

  1. Big Brother's research
    /huangxincheng/p/
  2. <.NET Core Underlying Getting Started>.
    image

Overall, as soon as you enter the kernel state. There is no such thing as low overhead.

CLS vs. non-CLS exceptions (historical baggage)

Prior to version 2.0 of the CLR, the CLR could only catch CLS-compatible exceptions. If a C# method called a method written in another programming language and threw a non-CLS-compatible exception. Then C# could not catch that exception.
In subsequent versions, the CLR introduced the RuntimeWrappedException class. When a non-CLS-compatible exception is thrown, the CLR automatically constructs an instance of RuntimeWrappedException. Making it CLS-compatible

        public static void Example2()
        {
            try...
            {

            catch(Exception)
            catch(Exception)
            {
                //pre-c# 2.0 this block could only catch CLS-compatible exceptions
            }
            catch
            catch(Exception) { //pre-c# 2.0 this block can catch all exceptions } catch
                // This block catches all exceptions
            }
        }