1. Call and return of C functions
To understand the implementation of the C + + exception mechanism, first understand the call and return mechanism of a function, which involves the ESP and EBP registers. Let's take a look at the function call and the return process.
The following is the call convention __stdcall calling function test (intP1,intp2) Assembly code assumes that the function is executed before the stack pointer esp is nnpush p2; parameter 2 into the stack, esp-= 4h, ESP = NN-4hpush p1; parameter 1 into the stack, ESP-= 4h, ESP = NN-8hcall test; Press-in return address ESP-= 4h, ESP = NN-0Ch{push ebp; protect previous EBP pointer, ebp into stack, ESP-=4h, ESP = NN-10hmov EBP, esp; set EBP pointer to stack top NN-10hmov eax, DWORD ptr [EBP+0CH]; ebp+0ch for nn-4h, which is the position of the parameter 2 mov ebx, DWORD ptr [EBP+08H]; ebp+08h for nn-8h, which is the position of the parameter 1 sub ESP,8Esp-= of space occupied by local variables8, ESP = nn-18h...add ESP,8; release local variables, esp+=8, ESP = nn-10hpop Ebp; Out of the stack, recovering ebp, ESP+=4, ESP = nn-0Chret8; ret return, popup return address, esp+=4, esp=nn-08h, followed by operand 8 for the balance stack, esp+=8, esp=NN, restore the stack before entering the function.}
The function stack architecture mainly carries the following parts:
1. Pass parameters: Typically, the function's invocation parameters are always at the top of the stack frame.
2. Delivery return address: Tells the callee where the return statement should return to, usually pointing to the next statement of the function call (the offset in the code snippet).
3. Hold the caller's current stack pointer: It is easy to clean up all local variables of the callee and restore the caller's scene.
4. Store all local variables within the current function: remember? I just said that all local and temporary variables are stored on the stack.
2. C + + function call
Let's first clarify that the "C + + function" here refers to:
1, the function may throw an exception directly or indirectly: that is, the definition of the function is stored in a C + + compilation (rather than a traditional C) cell, and the function does not use the "throw ()" Exception filter
2. A try block is used within the definition of the function.
Both of these meet one of them. In order to be able to catch exceptions successfully and to complete stack fallback correctly (stack unwind), the compiler must introduce some additional data structures and corresponding processing mechanisms. Let's start by looking at what the stack frame, which introduces the exception handling mechanism, might look like:
As you can see from Figure 2, there is something more in the stack frame of each C + + function. If you look closely, you will find that the extra thing is exactly an EXP type structure. Further analysis will reveal that this is a typical unidirectional-linked-list structure:
The Piprev member points to the previous node of the list, which is used primarily to search for matching catch blocks in the function call stack, and to complete the stack fallback work.
The Pihandler member points to the data structures necessary to complete the exception capture and stack fallback (mainly two tables with key data: "Try" block table: Tbltryblocks and "stack fallback table": Tblunwind).
The Nstep member is used to locate the try block and to find the correct entry in the stack fallback table.
It is necessary to note that the compiler defines a EHDL structure for each C + + function, but only the Tbltryblocks member is defined for the function that contains the "try" block. In addition, the exception handler maintains a pointer to the current exception-handling framework for each thread. The pointer points to the end of the chain of the exception handler, usually in a TLS slot or in a similar role.
3. Stack fallback (stack unwind)
"Stack fallback" is a new concept introduced in C + + with exception handling, primarily to ensure that after an exception is thrown, captured, and processed, all objects whose lifetimes have ended are properly refactored and the space they occupy is properly reclaimed. Let's take a look at how the compiler implements the stack fallback mechanism:
In the "Funcunwind" function in the figure, all real code is marked in black and blue, and the compiler-generated code is indicated by the gray and orange fonts. In this case, the Nstep variables and Tblunwind members given in the diagram are very obvious.
Nstep variables are used to track the construction and destruction stages of local objects within a function. With the Tblunwind table generated by the compiler for each function, the fallback mechanism can be completed. The Pfndestroyer field in the table records the destructor (destructor pointer) that should be performed at the corresponding stage, and the PObj field records the object this pointer offset that corresponds to it. By adding the offset value referred to by POBJ to the current stack frame base (EBP), it is necessary to substitute the this pointer of the Pfndestroyer destructor to complete the destruction of the object. The Nnextidx field points to the next row (subscript) where the destructor is needed.
In the event of an exception, the exception handler first checks the Nstep value within the framework of the current function stack and obtains the tblunwind[] table through Pihandler. It then takes Nstep as a subscript into the table, executes the destructor for the row definition, and then turns to the next line that is pointed to by Nnextidx until Nnextidx is 1. After the end of the stack fallback of the current function, the exception handler can repeat the above operation along the Piprev value in the current function stack frame back to the previous node in the exception processing chain until all the fallback work is complete.
It is worth mentioning that the value of the Nstep is fully determined at compile time, and the runtime only needs to perform several simple integer assignments (usually directly assigned to a register in the CPU). In addition, for all internal types and types that use the default construct, the Destructor method (and all of its members and base classes also use the default method), the value of nstep is not affected by its creation and destruction.
Note: If, in the process of a stack fallback, the exception is thrown again due to a call to the destructor (exception in the exception), it is considered a serious failure of the exception handling mechanism. At this point the process will be forcibly banned. To prevent this, use the "std::uncaught_exception ()" method in all destructors that may throw exceptions to determine whether a stack fallback is currently in progress (that is, there is an uncaught or incomplete exception). If so, then the exception should be suppressed for re-throwing.
4. Abnormal capture
When an exception is thrown, a C + + exception capture mechanism is immediately thrown:
In the previous section, we have seen the role of the Nstep variable in tracking object construction and destruction. In fact, in addition to being able to track the object creation and destruction phases, Nstep is able to identify whether the current execution point is in a try block, and in which try block (if the current function has more than one try block). This is done by assigning a unique ID value to each of the entry and exit Nstep of each try block, and ensuring that the nstep changes within the corresponding try block are within this range.
In implementing exception trapping, first, the C + + exception handler checks to see if the location where the exception occurred is within a try block of the current function. This work can be done by using the Nstep value of the current function in the entry in the Pihandler point to tbltryblocks[] table in the range of [Nbeginstep, Nendstep].
For example: If the FUNCB in Figure 4 has an exception in nstep = = 2 O'Clock, 2∈[1, 3 is found by comparison to the tbltryblocks[] table in the FUNCB, so the exception occurs in the first try block within the FUNCB.
Second, if the exception occurs in a try block in the current function, try to match the tblcatchblocks[] table in the corresponding entry for that tbltryblocks[]. The tblcatchblocks[] Table records information about all catch blocks that are associated with the specified try block, including information such as the type of exception that the catch block can catch and its start address.
If a matching catch block is found, the current exception object is copied to this catch block and then jumps to its entry address to execute the in-block code.
Otherwise, the occurrence of the exception is not within the try block of the current function, or there is no catch block in the try block that matches the current exception, and the process is repeated over the Piprev address in the function stack frame (that is, the previous node in the exception handling chain) until a match is found Catch block or the first node of the exception handling chain. For the latter, we call an uncaught exception, and for C + + exception handlers, an uncaught exception is a critical error that will cause the current process to be forced to end.
5. Throw an exception
Next, we discuss the last link in the whole C + + exception handling mechanism, throwing the exception:
When compiling a piece of C + + code, the compiler replaces all throw statements with a specified function in its C + + runtime library, where we call it __cxxrtthrowexp (as with all other data structures and property names mentioned in this article, which can be any name in the actual application). The function receives a compiler-approved internal structure (we call it the EXCEPTION structure). This structure contains the starting address of the exception object to be thrown, the destructor used to destroy it, and its type_info information. For exception class hierarchies that do not have the RTTI mechanism enabled (the compiler disables the RTTI mechanism or the virtual table is not used in the class hierarchy), you may also want to include type_info information for all of its base classes in order to match the corresponding Catch block.
In the dark gray block diagram of the diagram, we use C + + pseudocode to show the "Throw Myexp (1)" In the function Funca, and the statement will be translated by the compiler eventually. In fact, in most cases, the __CXXRTTHROWEXP function is the "exception handler" that we have mentioned many times before, and the important work of exception capture and stack fallback is done by it.
__cxxrtthrowexp first receives (and saves) the EXCEPTION object, and then finds the Pihandler, Nstep, and other exception handling data corresponding to the current function from the Tls:current exphdl. and complete the exception capture and stack fallback as described in the previous article. This completes the whole set of exception handling mechanisms, including "throw", "capture", "fallback" and so on.
6. Summary
The above is the implementation of the C + + exception principle, of course, other language anomaly capture mechanism is the same idea to implement exception processing.
Implementation mechanism of C + + exception mechanism