Exceptions on Windows x64. How it works. Part 4
Based on the material described in the first , second, and third parts of this article, we continue the discussion of the topic of exception handling in Windows x64.
The described material requires knowledge of basic concepts, such as a prologue, an epilogue, a frame of a function, and an understanding of basic processes, such as the actions of a prologue and an epilogue, the transfer of function parameters and the return of a function result. If the reader is not familiar with the above, then before reading it is recommended to familiarize yourself with the material from the first part of this article. If the reader is not familiar with the structures of the PE image that are involved in the process of processing the exception, then before reading it is recommended to familiarize yourself with the material from the second part of this article. Also, if the reader is not familiar with the process of finding and calling exception handlers, it is recommended that you read the third part of this article.
The given description refers to the implementation in Windows, and therefore, it should not be assumed that the implementation of this mechanism attached to the article will exactly coincide with it, although there are no conceptually differences. The details of the attached implementation will not be considered in the article unless this is explicitly stated. Therefore, it is assumed that these details, if necessary, should be studied independently.
The implementation of the mechanism, which is located in the exceptions folder of the git repository at this address , is attached to the article .
In the process of error handling, a situation may arise when it is necessary to directly return control to one of the previous functions, bypassing the intermediate functions. Those. the control will be returned not by the usual return from the function to the calling function, which in turn will also have to perform such a return, but by changing the state of the processor so that immediately after the change it continues to perform the target function. Figure 1 shows an example of such a situation where the arrow indicates the direction of stack growth.

Figure 1
In the example above, the stack consists of four function frames, where the Main function called Func1, Func1 called Func2, and Func2 called Func3. Therefore, for example, if the Func3 function needs to return control to the Main function, then it will use the RtlUnwind / RtlUnwindEx function, which is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The prototype of the RtlUnwindEx function is shown below, in Figure 2.

Figure 2
The TargetFrame parameter takes the frame address of the function to which the stack should be untwisted. The TargetIp parameter accepts the address of the instruction from which execution will continue after promotion. The ExceptionRecord parameter accepts a pointer to the EXCEPTION_RECORD structure, which will be passed to the handlers during promotion. The ReturnValue parameter is written in the RAX register of the processor, i.e. immediately after the transfer of control to the corresponding function, the RAX register will contain the value of this parameter. The ContextRecord parameter contains a pointer to the CONTEXT structure, which is used by the RtlUnwindEx function when promoting functions and determining the target state of the processor after promotion. The HistoryTable parameter takes a pointer to a structure that is used to cache searches. You can find the format of this structure in winnt.h.
The TargetFrame parameter is optional. If its value is NULL, then the RtlUnwindEx function performs the so-called exit unwind, where the frames of all the stack functions are untwisted. In this case, the TargetIp parameter is ignored. The ExceptionRecord parameter is optional, and if it is NULL, then the RtlUnwindEx function initializes its EXCEPTION_RECORD structure, where the ExceptionCode field will contain a STATUS_UNWIND value, the ExceptionRecord field will contain NULL, the ExceptionAddress field will contain a pointer to the instruction of the RtlUnwindEx function, and the NumberParameters field. The HistoryTable parameter is optional.
The prototype of the RtlUnwind function differs only in that it does not accept the last two parameters.
Figure 3 below shows an example of how the RtlUnwind function works.

Figure 3
The figure above shows an example of a program consisting of four functions: _tmain, Func1, Func2, Func3. The _tmain function calls Func1, the Func1 function calls Func2, and the Func2 function calls Func3. Func1, Func2, Func3 functions return a Boolean value. The Func3 function performs the virtual promotion of the three previous functions in order to: find the frame address of the _tmain function; find the address of the instruction from which execution will continue, and in this example, the address will point to the instruction immediately after the instruction to call the Func1 function. To the right of the source code is the assembler code _tmain and Func3 of functions whose instruction addresses are absolute. To the right of the assembler code are the processor states and call stacks for three cases: the processor status and the call stack immediately before the Func1 function call are shown on top; in the middle the processor status and the call stack are shown immediately before the call to the RtlUnwind function; The processor status after executing the RtlUnwind function is shown below. The instruction pointers of these states are mapped to assembler instructions by means of unique numbers. You should pay attention to the latter case, where the RAX register took the value of the ReturnValue parameter, and the call stack was reduced to one function, i.e. Func1, Func2, and Func3 function frames no longer exist on the stack. Since the value of RAX after spin-up is not zero, the _tmain function will display a message on the screen. In the usual case, i.e. if the promotion was not performed, this message will not be displayed, because Func3 function returns false. You should also pay attention to the fact that the search loop of the frame pointer of the _tmain function performs four iterations, when there are only three untwisted functions. This is due to the previously discussed features of the RtlVirtualUnwind function. The fact is that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function.
The RtlUnwind / RtlUnwindEx function also, prior to stack promotion, sequentially invokes the promotion processors of all functions, starting with itself and up to the function that is the target, inclusive. Since the RtlUnwind / RtlUnwindEx function does not have exception / promotion handlers, it will simply be skipped during the virtual promotion process and, therefore, there will be no side effects. On the other hand, this is overhead, because to find the frame of the function that called the RtlUnwind / RtlUnwindEx function, you need to perform additional virtual promotion. The process of calling handlers and changing the state of the processor in order to transfer control to one of the previous functions is the so-called promotion.
Figure 4 below shows a block diagram of the RtlUnwindEx function.

Figure 4
At the beginning of its work, the function receives the lower and upper limits of the stack. Next, the function captures the current state of the processor by calling the RtlCaptureContext function. Thus, the CONTEXT structure will reflect the state of the processor immediately after calling the RtlCaptureContext function. The same structure is used as the initial state of the processor, from which the virtual promotion of functions begins. The RtlUnwindEx function in the course of its work uses two CONTEXT structures: one reflects the state of the processor at the time of execution of the function for which the handler is called (hereinafter - the current context); the other reflects the state of the processor immediately after returning from this function (hereinafter, the previous context). This is necessary due to the previously discussed features of the RtlVirtualUnwind function. Also an RtlUnwindEx function,
Next, the function forms the initial value of the ExceptionFlags field for the EXCEPTION_RECORD structure. This value is stored in a local variable and is not initially stored in the field of the structure itself. The function sets the EXCEPTION_UNWINDING flag, and if the address of the frame of the target function has not been passed to the function, then the function also sets the EXCEPTION_EXIT_UNWIND flag. Thus, the EXCEPTION_UNWINDING flag for handlers means that promotion is in progress, and the EXCEPTION_EXIT_UNWIND flag means that frames of all functions are untwisted.
Next, the function uses the RtlLookupFunctionEntry function to get the address of the PE image and a pointer to the RUNTIME_FUNCTION structure of the function of this image, the handler of which must be called (hereinafter, the current function). The address of one of the instructions for this function is retrieved from the current context. At the first iteration, this will be the address of the instruction of the RtlUnwindEx function itself. If the RtlLookupFunctionEntry function did not return a pointer, then it is considered that the current function for which an attempt was made to call its handler is simple, and therefore, the function has no frame. Because simple functions do not allocate memory on the stack, their RSP value will point to the return address, therefore, for such functions, the RtlUnwindEx function extracts this address, copies its value to the current context and increases the value of the Rsp field of the current context by 8. Now the current context reflects the state of the processor at the time of execution of the next function on the stack above. Then the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
For HR functions, the RtlLookupFunctionEntry function will return a pointer to the RUNTIME_FUNCTION structure. In this case, the RtlVirtualUnwind function is called to determine the frame pointer of the current function, as well as the address of its handler and a pointer to the data for this handler. Before calling the RtlVirtualUnwind function, the RtlUnwindEx function copies the current context to the previous one. This is done in order to preserve the state of the processor, which describes the moment of execution of the current function in case the function is targeted. It has already been mentioned many times that the RtlVirtualUnwind function returns the frame address of the function that was executed in the transferred state of the processor when, upon returning from the RtlVirtualUnwind function, the state will describe the next function on the stack above. Consequently, when the RtlUnwindEx function needs to resume execution of the target function, it will not be possible to use the processor state that the RtlVirtualUnwind function returned, because it will reflect the execution of the function that caused the target function. Immediately after calling the RtlVirtualUnwind function, the RtlUnwindEx function will check the frame pointer of the untwisted function to go beyond the stack limit. The function will also check whether the frame of the current function is higher on the stack than the frame of the target function, which in turn will mean that the RtlUnwindEx function skipped the frame of the target function due to stack damage, damage to the .pdata section, etc. In both cases, the function will throw a STATUS_BAD_STACK exception. Otherwise, if the RtlVirtualUnwind function did not return the address of the handler, then the RtlUnwindEx function will swap the current and previous contexts if the current function was not the target. Thus, the next function on the stack above will become current. Further, the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
If the RtlVirtualUnwind function returned a handler address for the current function, then its handler must be called. Before calling it, the RtlUnwindEx function will set the EXCEPTION_TARGET_UNWIND flag if the current function is the target. Thus, the handler of this function will be able to determine that its corresponding function is the function whose control is transferred. The RtlUnwindEx function will then update the contents of the ExceptionFlags field of the EXCEPTION_RECORD structure from its local copy. The exception handler was first discussed in Section 3 of the second part of this article, and its prototype is shown in Figure 5. Before calling the handler, the function, like the RtlDispatchException function, discussed in section 2.2 of the third part of this article, prepares the DISPATCHER_CONTEXT structure. which is actively used in cases of nested exception and active promotion (collided unwind). The definition of the structure itself is also shown in Figure 17 in section 2.2 of the third part of this article. The fields of this structure are initialized in the same way as in the case of the RtlDispatchException function, with the exception that the TargetIp field will contain the value of the corresponding parameter of the passed RtlUnwindEx function, i.e. the address of the instruction from which execution will resume after promotion; the ContextRecord field will contain a pointer to the CONTEXT structure, which describes the state of the processor at the time the current function was executed, and not the next one on the stack above; the ScopeIndex field contains the current value of the local variable and will be discussed in more detail when discussing the try / except and try / finally constructs.
The handler, as in the case of the RtlDispatchException function, is not called directly, and instead the helper function RtlpExecuteHandlerForUnwind is used, which takes the same parameters as the handler itself and also returns the same value. This function is actually a wrapper over the function of the promotion processor and is used to catch exceptions that have occurred during the execution of the processor itself. The assembler representation of the function is shown below in Figure 5.

Figure 5
As shown in the figure, first the function allocates memory on the stack for register variables and one variable, stores a pointer to the structure passed to DISPATCHER_CONTEXT in this variable and calls an exception handler whose address is stored in the LanguageHandler field of the DISPATCHER_CONTEXT structure. Also pay attention to the presence of a placeholder function body. Its role is the same as for the RtlpExecuteHandlerForException function. The assembler representation of the exception handler function is shown below in Figure 6.

Figure 6
As shown in the figure, the handler copies the context of the previous promotion process into the DISPATCHER_CONTEXT structure of the current process of searching for the processor or promotion. This allows you to continue the search for the handler from the place where the promotion was previously interrupted, or to continue the previously interrupted promotion. It also allows you to skip the call of the handlers of those functions for which such a call was already made during the previous promotion. It should also be noted that the call to the handlers resumes with the function on which the promotion process was interrupted. Those. for such functions, the handler will be called again. A more detailed explanation of this will be given during a discussion of the try / except and try / finally constructs.
After the DISPATCHER_CONTEXT structure has been prepared, the RtlUnwindEx function calls the appropriate handler. Immediately after calling the handler, the function resets the EXCEPTION_COLLIDED_UNWIND and EXCEPTION_TARGET_UNWIND flags.
If the handler returned ExceptionContinueSearch, then the function will swap the current and previous contexts if the current function was not the target. Thus, the next function on the stack above will become current. Further, the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
If the handler returned ExceptionCollidedUnwind, this means that during the promotion process another active promotion was detected, in the context of which an exception occurred. In this case, the DISPATCHER_CONTEXT structure of the RtlUnwindEx function will contain the context of the interrupted promotion, since It was copied by the RtlpExecuteHandlerForUnwind function handler. Therefore, the function will update the current context from the ContextRecord field of the DISPATCHER_CONTEXT structure, using the RtlVirtualUnwind function, get the previous one, set the EXCEPTION_COLLIDED_UNWIND flag and call the handler in the context of which an exception has previously occurred, and depending on its return result, will perform the previously described actions.
In all other cases, the RtlUnwindEx function will throw a STATUS_INVALID_DISPOSITION exception.
At each iteration, before receiving the PE image address and the pointer to the RUNTIME_FUNCTION structure, the function checks with the RtlpIsFrameInBounds function that the frame pointer of the function for which an attempt was made to call its handler is within the stack limit and is not a frame pointer of the target function. If such a check gives a positive result, then the function continues. Otherwise, if the frame pointer goes beyond the limit and the pointer is not the address of the target function frame, it means either the promotion was performed upon exit, and during the promotion, none of the handlers stopped this process, or the function frame pointer was not found due to stack damage, damage .pdata sections, etc. In this case, the RtlUnwindEx function will throw an exception to allow debugging, but not for the purpose of processing it. In all other cases, the function will end because found frame of the objective function. In this case, the value of the passed parameter ReturnValue will be written in the Rax field of the current context, and the value of the passed TargetIp parameter will be written in the Rip field of the same context, if the exception code is not the STATUS_UNWIND_CONSOLIDATE code. Because this case is not directly related to the topic under discussion, this code will not be discussed in this article. It should only be noted here that for the promotion with such code, the RtlRestoreContext function will call the handler before resuming work, and if the Rip field is updated, the handler will get an incorrect idea about the state of the processor. Next, the RtlUnwindEx function calls the RtlRestoreContext function, to which it passes two parameters: the current context and a pointer to the EXCEPTION_RECORD structure, which was either passed to the RtlUnwindEx function, or a pointer to a locally formed structure is passed. By the time the RtlRestoreContext function was called, the promotion handlers for all functions on the stack, starting from its top and up to the target function, were called. The RtlRestoreContext function does not return control since Applies a new state to the processor.
It is worth noting that checking the frame pointer at each iteration is an implementation error, because you should check the stack pointer from the current context, not the frame pointer. First of all, this check is performed immediately after the virtual promotion of the current function. And if the test result is negative, then the function will throw an exception. Therefore, this check at each iteration will never give a negative result, and since the stack pointer is not checked, the function may go beyond its limits in the course of its work. This error still persists.
From the point of view of the operating system, as was already considered in the description of exception handling and stack promotion, the processing itself is always a normal call to the corresponding function. The try / except and try / finally constructs are a mechanism that allows you to place exception handling code directly in the function body during development. Therefore, since the processing code is located directly in the body, these parts of the code cannot be directly called by the operating system. To ensure the correct functioning of these constructions, the compiler generates auxiliary information used by the exception handlers called by the operating system. It was mentioned earlier that all exception handling can be conditionally divided into two phases. The search phase and the transfer of control to the exception handlers of the constructions under discussion is the second phase. This separation is necessary because different programming languages handle exceptions differently; Thus, the operating system itself is abstracted from understanding the variety of mechanisms of different programming languages.
The C / C ++ compiler reserves the __C_specific_handler function. This function is responsible for the search and transfer of control of the corresponding design. The function itself must be implemented by the programmer. This approach allows you to abstract the compiler from an understanding of the operating system itself and adapt the executable image to any runtime environment, for example, to the Win32 subsystem, to the Windows kernel runtime environment, or to any other environment. Also, the implementation of this function is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The supplied Windows SDK and WDK contain libraries that import this function from the corresponding module. The ExceptionHandlerAddress field of the EXCEPTION_HANDLER structure will contain a pointer to this function. when the LanguageSpecificData field of the same structure will contain the SCOPE_TABLE structure, which describes the location of all structures in the function body. The prototype of the function is shown in Figure 5 in section 3 of the second part of this article. The definition of the SCOPE_TABLE structure is presented below in Figure 7.

Figure 7
The Count field contains the number of constructions in the function body and, therefore, the number of ScopeRecord elements in the structure. The compiler generates a structure element corresponding to ScopeRecord for each structure, which in turn describes the location of the corresponding structure in the function body, as well as the location of its handlers. ScopeRecord elements are sorted in the following order: nested constructions follow each other in the order they appear in the code, when nested constructions always follow the construct in which they are nested. The BeginAddress field of the ScopeRecord element contains the start address of the try block. The EndAddress field contains the address of the instruction following the last instruction enclosed in a try block. The JumpTarget field, if non-zero, contains the address of the first code instruction enclosed in an except block. The except code of the block immediately follows the code enclosed in the try block. The HandlerAddress field contains the address of the except filter function of the block. Despite the fact that the exclusion filter is enclosed in brackets after the except expression, the filter code is generated by the compiler as a separate function, the prototype of which is shown below, in Figure 8.

Figure 8
The function accepts two parameters. The first parameter contains a pointer to the structure, the definition of which is given below, in Figure 9. The second parameter contains a pointer to the frame of the function in which the corresponding structure is located. This pointer is used by the filter if during filtering it is necessary to access the local variables of the function in which the corresponding structure is located.

Figure 9
As reflected in the figure above, the structure contains two pointers. The first indicates a structure that describes the cause of the exception, the second indicates the structure that describes the state of the processor at the time the exception occurred.
The filter function returns the following values: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_CONTINUE_EXECUTION. The first value means that you want to transfer control to the exception handler for which the filter function was called. Also, this value can be encoded directly in the HandlerAddress field. In this case, the construct does not have a filter, and control transfer to the exception handler of this construct is always performed. The second value indicates that the search for the exception handler should continue. The third value means that the search should be interrupted and the interrupted stream should resume execution.
If the JumpTarget field is zero, then this construct is finally a construct, and the code enclosed in the finally block immediately follows the code enclosed in the try block. In this case, the HandlerAddress field contains the address of the function, which in its contents repeats the code enclosed in the finally block. The prototype of this function is shown in Figure 10.

Figure 10
Since the code enclosed in the finally block is executed regardless of whether an exception occurred or not, then if the exception occurred, this code cannot be called directly, because it is located in the body of the function. And since it is necessary to call this code during promotion, the compiler duplicates the code enclosed in the finally block as a separate function. The first parameter is a Boolean value, which means that the finally code of the block is executed due to the abnormal completion of the code enclosed in the try block (i.e., an exception occurred during its execution). The second parameter contains the frame pointer of the function in which the corresponding structure is located. This pointer is used by the function in the same way as the exception filter function - access to local variables of the function in which the corresponding construct is located.
In those cases when no exception occurred during the execution of the code enclosed in the try block, the finally code of the block is executed, which immediately follows the try code of the block.
All addresses in the SCOPE_TABLE structure are addresses relative to the start of the image.
Figure 11 below shows an example of the SCOPE_TABLE structure that the compiler will generate.

Figure 11
The figure above shows an example of a program whose _tmain function includes try / except and try / finally constructs. To the left of the source code is an assembler representation of the functions: _tmain, filter functions of the lower try / except constructs, and functions that duplicate code enclosed in a finally block. The functions are listed from bottom to top. The addresses of assembly instructions are absolute. Green markers match code enclosed in blocks with its assembler equivalents. It should be noted that the code block with marker 2 in the assembler representation occurs twice: in the body of the _tmain function and in the topmost function. The latter is a duplicate of code enclosed in a finally block. Also note the presence of the nop instructionafter the instruction to call the FailExecution function in the code block with marker 1. This instruction is also a placeholder, as in the case of gateway functions, the RtlpExecuteHandlerForException function and the RtlpExecuteHandlerForUnwind function. If there is no placeholder, then when checking the instructions for belonging to a particular construction, an erroneous assumption about its belonging can be made. In this case, an erroneous assumption will be made that the instruction to call the FailExecution function does not belong to the code block with marker 1, because After the promotion, the RtlVirtualUnwind function will return the address not to the FailExecution function call instruction, but to the instruction immediately after it. For this reason, the compiler adds a placeholder after the function call statement, if that in turn is the last instruction in the block.
The left part of the figure shows the structures that the compiler will generate. The element of the function table is shown at the top, the UNWIND_INFO structure referenced by this element is shown below. Despite the fact that the EXCEPTION_HANDLER structure is not part of the UNWIND_INFO structure, in the figure it is presented as part of this structure, as if present, then immediately after the UNWIND_INFO structure. Below the UNWIND_INFO structure, a more detailed representation of the EXCEPTION_HANDLER structure is shown, below it is a more detailed representation of the LanguageSpecificData field of this structure, in which the SCOPE_TABLE structure is located. At the very bottom, the ScopeRecord elements of an array of this structure are sequentially displayed. All addresses in the generated structures are relative.
It is worthwhile to dwell on the elements of the ScopeRecord array in more detail. Element 0 describes the location of the block with marker 1. The HandlerAddress field of this element contains the address of the function that duplicates the finally code of the block with marker 2. The JumpAddress field contains 0, because this is finally a block. Element 1 describes the location of the block with marker 3. The HandlerAddress field of this element contains the value 1, which in turn means that the design does not have a filter, and if an exception occurs, you should always transfer control to the block code with marker 4. The JumpAddress field contains the address of the beginning of the block with marker 4. Element 2 describes the location of the block with marker 5. The HandlerAddress field of this element contains the address of the filter function, the code of which is enclosed in brackets after the except keyword. The assembler representation of the filter function is in the middle, between the _tmain function and the function that duplicates the finally block. As shown in the figure, the filter function calls the ExceptionFilter function, which takes a pointer to a structure that describes the context of the exception. The JumpAddress field contains the address of the beginning of the block with marker 6.
Despite the fact that the __C_specific_handler function is not shown, the ExceptionHandlerAddress field of the EXCEPTION_HANDLER structure contains the address of this function. This function will be called by the operating system during the search for the exception handler or during the promotion of the stack. Therefore, the implementation of this function is responsible for interpreting the SCOPE_TABLE structure, calling filters, finally calling blocks, and passing control to except blocks.
The block diagram of the __C_specific_handler function is shown below in Figure 12.

Figure 12
At the beginning of its work, the function receives: the address of the beginning of the PE image; the relative address of the instruction belonging to the body of the function for which the handler was called; pointer to the SCOPE_TABLE structure. Depending on the operation being performed (search for a handler or promotion), the operation of the function varies.
If a handler search is performed, the function prepares the EXCEPTION_POINTERS structure, a pointer to which is passed to the filters of the corresponding structures. Then the function sequentially scans the ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received instruction address of any construction. If it belongs, then it is also checked whether the particular try / except construct is a construct, and if not, it is simply ignored, and the next element is checked. Otherwise, the filter of this construct is called. If the filter returned EXCEPTION_CONTINUE_SEARCH, then this construct is ignored, and the next element is checked. If the filter returns EXCEPTION_CONTINUE_EXECUTION, then the function exits and returns ExceptionContinueExecution. to tell the operating system to stop the handler search and resume the interrupted thread. If the filter returned EXCEPTION_EXECUTE_HANDLER, then the function calls the RtlUnwind function, which indicates the frame of the function whose handler was called as the frame of the target function; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions. which handler was called; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions. which handler was called; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions.
If the promotion is performed, then the function behaves differently. First, the function receives the relative address of the instruction from which execution will resume. Then the function sequentially scans the ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received instruction address of any construction. If it does, then it also checks if the particular try / finally construct is a construct. If it is, then its handler is called, which is essentially a duplicate of the code enclosed in the finally block. Before calling, the function will increase the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one. The value of the AbnormalTermination parameter when calling this handler is always TRUE. Therefore, the AbnormalTermination macro will always return TRUE for these blocks called in this way. For the finally code of a block located in the body of the function itself, the same macro will always return FALSE. In these cases, the compiler explicitly substitutes this value. In other words, the AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13. The AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13. The AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13.

Figure 13
As can be seen from the figure, if such a check is not performed, then finally the block of the external structure will be called during the promotion. And this is unacceptable.
If the function performs the promotion for the objective function, i.e. the EXCEPTION_TARGET_UNWIND flag is set in the ExceptionFlags field of the EXCEPTION_RECORD structure, then it performs an additional check before calling the handler. The essence of the check is to determine whether the address from which execution will continue does not belong to the structure itself, and not to its processor. And if it belongs, then the function ends. A similar situation can only occur if goto statements are used within finally blocks that point outside these blocks. This situation is shown below, in figure 14.

Figure 14
As can be seen from the figure, if such a check is not performed, then finally the external structure block will be called during the promotion. And this is unacceptable. Also, if the function is not the target, then this check is not needed.
It should be noted that in both cases (both in the case of a handler search and in the case of promotion), the scanning of elements does not begin from the beginning, but from the element whose number is stored in the ScopeIndex field of the DISPATCHER_CONTEXT structure. As already noted, the function increases the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one before calling the try / finally constructor handler. It was mentioned earlier that if an incomplete promotion process is detected in the process of searching for a handler or performing a promotion, the continuation of the search for a processor or performing a promotion will be resumed from the interrupted location. In this case, the function handler that generated the exception will be called again when the handlers of the other functions are not called. In such a situation, it is unacceptable that handlers of structures that have already been called were called repeatedly. This situation is depicted in Figure 15 below.

Figure 15
The figure above shows the function call stack, on the left of which is shown the arrow of its growth direction, and on the right is a part of the Func1 function code. Although the RtlDispatchException and RtlUnwindEx functions call function handlers using the RtlpExecuteHandlerForException and RtlpExecuteHandlerForUnwind functions, these functions are not present in the call stack for brevity. The Func1 function called the Func2 function, which in turn called the Func3 function, which raised an exception. As soon as the RtlDispatchException function got control, it sequentially called handlers for the functions: first Func3, then Func2, and ultimately Func1. The function handler Func1 found a construct that can handle the exception, and called the RtlUnwind function to transfer control to the handler of this construct. The RtlUnwind function in turn called RtlUnwindEx, which sequentially called the handlers for the functions, first Func3, then Func2, and ultimately Func1. The function handler Func1 called the handler of the nested finally block itself, which in turn raised a new exception. As soon as the RtlDispatchException function got control, it sequentially called the handlers of the previous functions. One of these handlers will be the handler of the RtlpExecuteHandlerForUnwind function, which the RtlUnwindEx function calls when the control passes to the handler of the untwisted function. The handler of the RtlpExecuteHandlerForUnwind function will copy the promotion context from the RtlUnwindEx function into the RtlDispatchException function and, after returning to it, the search for the handler will continue from the point where the promotion was interrupted. Since the RtlUnwindEx function previously unwound Func3 and Func2 functions, their handlers will not be called. But since the Func1 function raised an exception, it was not promoted by the function, and therefore its handler will be called. Since the __C_specific_handler function increases the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one before invoking the construct handler, this field will be equal to 1 in the copied context. Therefore, when the __C_specific_handler function is called again for Func1, the construction search will begin with the construct with index 1. Therefore, the design that generated the exception will be skipped. Resume promotion is performed in a similar way. Despite the fact that the operating system and the compiler are abstracted from each other,
To conclude the discussion of try / except and try / finally constructions, it is worth describing how the GetExceptionCode macro works. Use of this macro is possible only in except blocks. This macro reads the contents of the RAX register, and when describing the __C_specific_handler function, it was mentioned that the transfer of control to a specific except block is performed using the RtlUnwind function, which takes a parameter whose value will be written to the RAX register after the transfer of control. This parameter is used to pass the code of the exception that occurred.
It is also worth describing how the GetExceptionInformation macro works. Use of this macro is only possible in expressions enclosed in brackets after the except keyword. Since in reality the expression enclosed in brackets (in other words, the filter) is a separate function that takes two parameters, this macro receives the value of the first parameter. When describing the __C_specific_handler function, it was mentioned that the filter function accepts two parameters, where the first parameter is a pointer to a structure describing the exception and the state of the processor at the time the exception occurred.
One of the drawbacks of this mechanism is the use of goto statements in finally blocks that point outside these blocks. In this case, the C / C ++ compiler, instead of directly transferring control, uses the reserved _local_unwind function, a prototype of which is shown below, in Figure 16.

Figure 16
The first parameter of the function accepts the frame address of the function to which the stack should be unwound, when the second accepts the address of the instruction to which control should be transferred after the unwinding. The function itself must be implemented by the programmer. Also, the implementation of this function is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The supplied Windows SDK and WDK contain libraries that import this function from the corresponding module. The implementation of the function itself is very simple, it calls the RtlUnwind function, which passes its two parameters. The remaining parameters of the RtlUnwind function are reset to zero when called.
The use of the _local_unwind function by the compiler instead of direct control transfer is primarily due to the inability to transfer control to an arbitrary place in the function if the finally block was called as a result of promotion. In this case, transferring control to the right place of the function is possible only through a new promotion process. This approach has side effects. The goto operator, in its essence, transfers direct control when the promotion causes finally blocks to be called. Therefore, before the actual transfer of control finally blocks will be called that can change the context of the function itself. Microsoft does not recommend using the goto statement in this way, and the compiler will issue a warning.
In this part of the article, we ended up discussing the exception handling mechanism. The need for its implementation came from practice. And it is primarily used in the boot-time hypervisor to simplify and speed up development. In the process of implementation, many problems arose that were eliminated, and the article itself was primarily aimed at facilitating the understanding of those who are also interested in such developments.
The described material requires knowledge of basic concepts, such as a prologue, an epilogue, a frame of a function, and an understanding of basic processes, such as the actions of a prologue and an epilogue, the transfer of function parameters and the return of a function result. If the reader is not familiar with the above, then before reading it is recommended to familiarize yourself with the material from the first part of this article. If the reader is not familiar with the structures of the PE image that are involved in the process of processing the exception, then before reading it is recommended to familiarize yourself with the material from the second part of this article. Also, if the reader is not familiar with the process of finding and calling exception handlers, it is recommended that you read the third part of this article.
The given description refers to the implementation in Windows, and therefore, it should not be assumed that the implementation of this mechanism attached to the article will exactly coincide with it, although there are no conceptually differences. The details of the attached implementation will not be considered in the article unless this is explicitly stated. Therefore, it is assumed that these details, if necessary, should be studied independently.
The implementation of the mechanism, which is located in the exceptions folder of the git repository at this address , is attached to the article .
1. Stack promotion
In the process of error handling, a situation may arise when it is necessary to directly return control to one of the previous functions, bypassing the intermediate functions. Those. the control will be returned not by the usual return from the function to the calling function, which in turn will also have to perform such a return, but by changing the state of the processor so that immediately after the change it continues to perform the target function. Figure 1 shows an example of such a situation where the arrow indicates the direction of stack growth.

Figure 1
In the example above, the stack consists of four function frames, where the Main function called Func1, Func1 called Func2, and Func2 called Func3. Therefore, for example, if the Func3 function needs to return control to the Main function, then it will use the RtlUnwind / RtlUnwindEx function, which is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The prototype of the RtlUnwindEx function is shown below, in Figure 2.

Figure 2
The TargetFrame parameter takes the frame address of the function to which the stack should be untwisted. The TargetIp parameter accepts the address of the instruction from which execution will continue after promotion. The ExceptionRecord parameter accepts a pointer to the EXCEPTION_RECORD structure, which will be passed to the handlers during promotion. The ReturnValue parameter is written in the RAX register of the processor, i.e. immediately after the transfer of control to the corresponding function, the RAX register will contain the value of this parameter. The ContextRecord parameter contains a pointer to the CONTEXT structure, which is used by the RtlUnwindEx function when promoting functions and determining the target state of the processor after promotion. The HistoryTable parameter takes a pointer to a structure that is used to cache searches. You can find the format of this structure in winnt.h.
The TargetFrame parameter is optional. If its value is NULL, then the RtlUnwindEx function performs the so-called exit unwind, where the frames of all the stack functions are untwisted. In this case, the TargetIp parameter is ignored. The ExceptionRecord parameter is optional, and if it is NULL, then the RtlUnwindEx function initializes its EXCEPTION_RECORD structure, where the ExceptionCode field will contain a STATUS_UNWIND value, the ExceptionRecord field will contain NULL, the ExceptionAddress field will contain a pointer to the instruction of the RtlUnwindEx function, and the NumberParameters field. The HistoryTable parameter is optional.
The prototype of the RtlUnwind function differs only in that it does not accept the last two parameters.
Figure 3 below shows an example of how the RtlUnwind function works.

Figure 3
The figure above shows an example of a program consisting of four functions: _tmain, Func1, Func2, Func3. The _tmain function calls Func1, the Func1 function calls Func2, and the Func2 function calls Func3. Func1, Func2, Func3 functions return a Boolean value. The Func3 function performs the virtual promotion of the three previous functions in order to: find the frame address of the _tmain function; find the address of the instruction from which execution will continue, and in this example, the address will point to the instruction immediately after the instruction to call the Func1 function. To the right of the source code is the assembler code _tmain and Func3 of functions whose instruction addresses are absolute. To the right of the assembler code are the processor states and call stacks for three cases: the processor status and the call stack immediately before the Func1 function call are shown on top; in the middle the processor status and the call stack are shown immediately before the call to the RtlUnwind function; The processor status after executing the RtlUnwind function is shown below. The instruction pointers of these states are mapped to assembler instructions by means of unique numbers. You should pay attention to the latter case, where the RAX register took the value of the ReturnValue parameter, and the call stack was reduced to one function, i.e. Func1, Func2, and Func3 function frames no longer exist on the stack. Since the value of RAX after spin-up is not zero, the _tmain function will display a message on the screen. In the usual case, i.e. if the promotion was not performed, this message will not be displayed, because Func3 function returns false. You should also pay attention to the fact that the search loop of the frame pointer of the _tmain function performs four iterations, when there are only three untwisted functions. This is due to the previously discussed features of the RtlVirtualUnwind function. The fact is that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. that after calling the RtlVirtualUnwind function, the HandlerData and EstablisherFrame parameters will take the corresponding values for the function for which the virtual promotion was performed, when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function. when the ContextRecord parameter will reflect the state of the processor immediately after calling the untwisted function. Therefore, at the third iteration of the cycle, the RtlVirtualUnwind function will return the frame pointer for the Func1 function to the EstablisherFrame parameter when the ContextRecord parameter will reflect the processor state immediately after the Func1 function is called. Therefore, an additional iteration is required to determine the frame pointer of the _tmain function.
The RtlUnwind / RtlUnwindEx function also, prior to stack promotion, sequentially invokes the promotion processors of all functions, starting with itself and up to the function that is the target, inclusive. Since the RtlUnwind / RtlUnwindEx function does not have exception / promotion handlers, it will simply be skipped during the virtual promotion process and, therefore, there will be no side effects. On the other hand, this is overhead, because to find the frame of the function that called the RtlUnwind / RtlUnwindEx function, you need to perform additional virtual promotion. The process of calling handlers and changing the state of the processor in order to transfer control to one of the previous functions is the so-called promotion.
Figure 4 below shows a block diagram of the RtlUnwindEx function.

Figure 4
At the beginning of its work, the function receives the lower and upper limits of the stack. Next, the function captures the current state of the processor by calling the RtlCaptureContext function. Thus, the CONTEXT structure will reflect the state of the processor immediately after calling the RtlCaptureContext function. The same structure is used as the initial state of the processor, from which the virtual promotion of functions begins. The RtlUnwindEx function in the course of its work uses two CONTEXT structures: one reflects the state of the processor at the time of execution of the function for which the handler is called (hereinafter - the current context); the other reflects the state of the processor immediately after returning from this function (hereinafter, the previous context). This is necessary due to the previously discussed features of the RtlVirtualUnwind function. Also an RtlUnwindEx function,
Next, the function forms the initial value of the ExceptionFlags field for the EXCEPTION_RECORD structure. This value is stored in a local variable and is not initially stored in the field of the structure itself. The function sets the EXCEPTION_UNWINDING flag, and if the address of the frame of the target function has not been passed to the function, then the function also sets the EXCEPTION_EXIT_UNWIND flag. Thus, the EXCEPTION_UNWINDING flag for handlers means that promotion is in progress, and the EXCEPTION_EXIT_UNWIND flag means that frames of all functions are untwisted.
Next, the function uses the RtlLookupFunctionEntry function to get the address of the PE image and a pointer to the RUNTIME_FUNCTION structure of the function of this image, the handler of which must be called (hereinafter, the current function). The address of one of the instructions for this function is retrieved from the current context. At the first iteration, this will be the address of the instruction of the RtlUnwindEx function itself. If the RtlLookupFunctionEntry function did not return a pointer, then it is considered that the current function for which an attempt was made to call its handler is simple, and therefore, the function has no frame. Because simple functions do not allocate memory on the stack, their RSP value will point to the return address, therefore, for such functions, the RtlUnwindEx function extracts this address, copies its value to the current context and increases the value of the Rsp field of the current context by 8. Now the current context reflects the state of the processor at the time of execution of the next function on the stack above. Then the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
For HR functions, the RtlLookupFunctionEntry function will return a pointer to the RUNTIME_FUNCTION structure. In this case, the RtlVirtualUnwind function is called to determine the frame pointer of the current function, as well as the address of its handler and a pointer to the data for this handler. Before calling the RtlVirtualUnwind function, the RtlUnwindEx function copies the current context to the previous one. This is done in order to preserve the state of the processor, which describes the moment of execution of the current function in case the function is targeted. It has already been mentioned many times that the RtlVirtualUnwind function returns the frame address of the function that was executed in the transferred state of the processor when, upon returning from the RtlVirtualUnwind function, the state will describe the next function on the stack above. Consequently, when the RtlUnwindEx function needs to resume execution of the target function, it will not be possible to use the processor state that the RtlVirtualUnwind function returned, because it will reflect the execution of the function that caused the target function. Immediately after calling the RtlVirtualUnwind function, the RtlUnwindEx function will check the frame pointer of the untwisted function to go beyond the stack limit. The function will also check whether the frame of the current function is higher on the stack than the frame of the target function, which in turn will mean that the RtlUnwindEx function skipped the frame of the target function due to stack damage, damage to the .pdata section, etc. In both cases, the function will throw a STATUS_BAD_STACK exception. Otherwise, if the RtlVirtualUnwind function did not return the address of the handler, then the RtlUnwindEx function will swap the current and previous contexts if the current function was not the target. Thus, the next function on the stack above will become current. Further, the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
If the RtlVirtualUnwind function returned a handler address for the current function, then its handler must be called. Before calling it, the RtlUnwindEx function will set the EXCEPTION_TARGET_UNWIND flag if the current function is the target. Thus, the handler of this function will be able to determine that its corresponding function is the function whose control is transferred. The RtlUnwindEx function will then update the contents of the ExceptionFlags field of the EXCEPTION_RECORD structure from its local copy. The exception handler was first discussed in Section 3 of the second part of this article, and its prototype is shown in Figure 5. Before calling the handler, the function, like the RtlDispatchException function, discussed in section 2.2 of the third part of this article, prepares the DISPATCHER_CONTEXT structure. which is actively used in cases of nested exception and active promotion (collided unwind). The definition of the structure itself is also shown in Figure 17 in section 2.2 of the third part of this article. The fields of this structure are initialized in the same way as in the case of the RtlDispatchException function, with the exception that the TargetIp field will contain the value of the corresponding parameter of the passed RtlUnwindEx function, i.e. the address of the instruction from which execution will resume after promotion; the ContextRecord field will contain a pointer to the CONTEXT structure, which describes the state of the processor at the time the current function was executed, and not the next one on the stack above; the ScopeIndex field contains the current value of the local variable and will be discussed in more detail when discussing the try / except and try / finally constructs.
The handler, as in the case of the RtlDispatchException function, is not called directly, and instead the helper function RtlpExecuteHandlerForUnwind is used, which takes the same parameters as the handler itself and also returns the same value. This function is actually a wrapper over the function of the promotion processor and is used to catch exceptions that have occurred during the execution of the processor itself. The assembler representation of the function is shown below in Figure 5.

Figure 5
As shown in the figure, first the function allocates memory on the stack for register variables and one variable, stores a pointer to the structure passed to DISPATCHER_CONTEXT in this variable and calls an exception handler whose address is stored in the LanguageHandler field of the DISPATCHER_CONTEXT structure. Also pay attention to the presence of a placeholder function body. Its role is the same as for the RtlpExecuteHandlerForException function. The assembler representation of the exception handler function is shown below in Figure 6.

Figure 6
As shown in the figure, the handler copies the context of the previous promotion process into the DISPATCHER_CONTEXT structure of the current process of searching for the processor or promotion. This allows you to continue the search for the handler from the place where the promotion was previously interrupted, or to continue the previously interrupted promotion. It also allows you to skip the call of the handlers of those functions for which such a call was already made during the previous promotion. It should also be noted that the call to the handlers resumes with the function on which the promotion process was interrupted. Those. for such functions, the handler will be called again. A more detailed explanation of this will be given during a discussion of the try / except and try / finally constructs.
After the DISPATCHER_CONTEXT structure has been prepared, the RtlUnwindEx function calls the appropriate handler. Immediately after calling the handler, the function resets the EXCEPTION_COLLIDED_UNWIND and EXCEPTION_TARGET_UNWIND flags.
If the handler returned ExceptionContinueSearch, then the function will swap the current and previous contexts if the current function was not the target. Thus, the next function on the stack above will become current. Further, the function will continue its work, starting with obtaining the address of the PE image and the pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function on the stack above.
If the handler returned ExceptionCollidedUnwind, this means that during the promotion process another active promotion was detected, in the context of which an exception occurred. In this case, the DISPATCHER_CONTEXT structure of the RtlUnwindEx function will contain the context of the interrupted promotion, since It was copied by the RtlpExecuteHandlerForUnwind function handler. Therefore, the function will update the current context from the ContextRecord field of the DISPATCHER_CONTEXT structure, using the RtlVirtualUnwind function, get the previous one, set the EXCEPTION_COLLIDED_UNWIND flag and call the handler in the context of which an exception has previously occurred, and depending on its return result, will perform the previously described actions.
In all other cases, the RtlUnwindEx function will throw a STATUS_INVALID_DISPOSITION exception.
At each iteration, before receiving the PE image address and the pointer to the RUNTIME_FUNCTION structure, the function checks with the RtlpIsFrameInBounds function that the frame pointer of the function for which an attempt was made to call its handler is within the stack limit and is not a frame pointer of the target function. If such a check gives a positive result, then the function continues. Otherwise, if the frame pointer goes beyond the limit and the pointer is not the address of the target function frame, it means either the promotion was performed upon exit, and during the promotion, none of the handlers stopped this process, or the function frame pointer was not found due to stack damage, damage .pdata sections, etc. In this case, the RtlUnwindEx function will throw an exception to allow debugging, but not for the purpose of processing it. In all other cases, the function will end because found frame of the objective function. In this case, the value of the passed parameter ReturnValue will be written in the Rax field of the current context, and the value of the passed TargetIp parameter will be written in the Rip field of the same context, if the exception code is not the STATUS_UNWIND_CONSOLIDATE code. Because this case is not directly related to the topic under discussion, this code will not be discussed in this article. It should only be noted here that for the promotion with such code, the RtlRestoreContext function will call the handler before resuming work, and if the Rip field is updated, the handler will get an incorrect idea about the state of the processor. Next, the RtlUnwindEx function calls the RtlRestoreContext function, to which it passes two parameters: the current context and a pointer to the EXCEPTION_RECORD structure, which was either passed to the RtlUnwindEx function, or a pointer to a locally formed structure is passed. By the time the RtlRestoreContext function was called, the promotion handlers for all functions on the stack, starting from its top and up to the target function, were called. The RtlRestoreContext function does not return control since Applies a new state to the processor.
It is worth noting that checking the frame pointer at each iteration is an implementation error, because you should check the stack pointer from the current context, not the frame pointer. First of all, this check is performed immediately after the virtual promotion of the current function. And if the test result is negative, then the function will throw an exception. Therefore, this check at each iteration will never give a negative result, and since the stack pointer is not checked, the function may go beyond its limits in the course of its work. This error still persists.
2. The constructs try / except and try / finally
From the point of view of the operating system, as was already considered in the description of exception handling and stack promotion, the processing itself is always a normal call to the corresponding function. The try / except and try / finally constructs are a mechanism that allows you to place exception handling code directly in the function body during development. Therefore, since the processing code is located directly in the body, these parts of the code cannot be directly called by the operating system. To ensure the correct functioning of these constructions, the compiler generates auxiliary information used by the exception handlers called by the operating system. It was mentioned earlier that all exception handling can be conditionally divided into two phases. The search phase and the transfer of control to the exception handlers of the constructions under discussion is the second phase. This separation is necessary because different programming languages handle exceptions differently; Thus, the operating system itself is abstracted from understanding the variety of mechanisms of different programming languages.
The C / C ++ compiler reserves the __C_specific_handler function. This function is responsible for the search and transfer of control of the corresponding design. The function itself must be implemented by the programmer. This approach allows you to abstract the compiler from an understanding of the operating system itself and adapt the executable image to any runtime environment, for example, to the Win32 subsystem, to the Windows kernel runtime environment, or to any other environment. Also, the implementation of this function is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The supplied Windows SDK and WDK contain libraries that import this function from the corresponding module. The ExceptionHandlerAddress field of the EXCEPTION_HANDLER structure will contain a pointer to this function. when the LanguageSpecificData field of the same structure will contain the SCOPE_TABLE structure, which describes the location of all structures in the function body. The prototype of the function is shown in Figure 5 in section 3 of the second part of this article. The definition of the SCOPE_TABLE structure is presented below in Figure 7.

Figure 7
The Count field contains the number of constructions in the function body and, therefore, the number of ScopeRecord elements in the structure. The compiler generates a structure element corresponding to ScopeRecord for each structure, which in turn describes the location of the corresponding structure in the function body, as well as the location of its handlers. ScopeRecord elements are sorted in the following order: nested constructions follow each other in the order they appear in the code, when nested constructions always follow the construct in which they are nested. The BeginAddress field of the ScopeRecord element contains the start address of the try block. The EndAddress field contains the address of the instruction following the last instruction enclosed in a try block. The JumpTarget field, if non-zero, contains the address of the first code instruction enclosed in an except block. The except code of the block immediately follows the code enclosed in the try block. The HandlerAddress field contains the address of the except filter function of the block. Despite the fact that the exclusion filter is enclosed in brackets after the except expression, the filter code is generated by the compiler as a separate function, the prototype of which is shown below, in Figure 8.

Figure 8
The function accepts two parameters. The first parameter contains a pointer to the structure, the definition of which is given below, in Figure 9. The second parameter contains a pointer to the frame of the function in which the corresponding structure is located. This pointer is used by the filter if during filtering it is necessary to access the local variables of the function in which the corresponding structure is located.

Figure 9
As reflected in the figure above, the structure contains two pointers. The first indicates a structure that describes the cause of the exception, the second indicates the structure that describes the state of the processor at the time the exception occurred.
The filter function returns the following values: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_CONTINUE_EXECUTION. The first value means that you want to transfer control to the exception handler for which the filter function was called. Also, this value can be encoded directly in the HandlerAddress field. In this case, the construct does not have a filter, and control transfer to the exception handler of this construct is always performed. The second value indicates that the search for the exception handler should continue. The third value means that the search should be interrupted and the interrupted stream should resume execution.
If the JumpTarget field is zero, then this construct is finally a construct, and the code enclosed in the finally block immediately follows the code enclosed in the try block. In this case, the HandlerAddress field contains the address of the function, which in its contents repeats the code enclosed in the finally block. The prototype of this function is shown in Figure 10.

Figure 10
Since the code enclosed in the finally block is executed regardless of whether an exception occurred or not, then if the exception occurred, this code cannot be called directly, because it is located in the body of the function. And since it is necessary to call this code during promotion, the compiler duplicates the code enclosed in the finally block as a separate function. The first parameter is a Boolean value, which means that the finally code of the block is executed due to the abnormal completion of the code enclosed in the try block (i.e., an exception occurred during its execution). The second parameter contains the frame pointer of the function in which the corresponding structure is located. This pointer is used by the function in the same way as the exception filter function - access to local variables of the function in which the corresponding construct is located.
In those cases when no exception occurred during the execution of the code enclosed in the try block, the finally code of the block is executed, which immediately follows the try code of the block.
All addresses in the SCOPE_TABLE structure are addresses relative to the start of the image.
Figure 11 below shows an example of the SCOPE_TABLE structure that the compiler will generate.

Figure 11
The figure above shows an example of a program whose _tmain function includes try / except and try / finally constructs. To the left of the source code is an assembler representation of the functions: _tmain, filter functions of the lower try / except constructs, and functions that duplicate code enclosed in a finally block. The functions are listed from bottom to top. The addresses of assembly instructions are absolute. Green markers match code enclosed in blocks with its assembler equivalents. It should be noted that the code block with marker 2 in the assembler representation occurs twice: in the body of the _tmain function and in the topmost function. The latter is a duplicate of code enclosed in a finally block. Also note the presence of the nop instructionafter the instruction to call the FailExecution function in the code block with marker 1. This instruction is also a placeholder, as in the case of gateway functions, the RtlpExecuteHandlerForException function and the RtlpExecuteHandlerForUnwind function. If there is no placeholder, then when checking the instructions for belonging to a particular construction, an erroneous assumption about its belonging can be made. In this case, an erroneous assumption will be made that the instruction to call the FailExecution function does not belong to the code block with marker 1, because After the promotion, the RtlVirtualUnwind function will return the address not to the FailExecution function call instruction, but to the instruction immediately after it. For this reason, the compiler adds a placeholder after the function call statement, if that in turn is the last instruction in the block.
The left part of the figure shows the structures that the compiler will generate. The element of the function table is shown at the top, the UNWIND_INFO structure referenced by this element is shown below. Despite the fact that the EXCEPTION_HANDLER structure is not part of the UNWIND_INFO structure, in the figure it is presented as part of this structure, as if present, then immediately after the UNWIND_INFO structure. Below the UNWIND_INFO structure, a more detailed representation of the EXCEPTION_HANDLER structure is shown, below it is a more detailed representation of the LanguageSpecificData field of this structure, in which the SCOPE_TABLE structure is located. At the very bottom, the ScopeRecord elements of an array of this structure are sequentially displayed. All addresses in the generated structures are relative.
It is worthwhile to dwell on the elements of the ScopeRecord array in more detail. Element 0 describes the location of the block with marker 1. The HandlerAddress field of this element contains the address of the function that duplicates the finally code of the block with marker 2. The JumpAddress field contains 0, because this is finally a block. Element 1 describes the location of the block with marker 3. The HandlerAddress field of this element contains the value 1, which in turn means that the design does not have a filter, and if an exception occurs, you should always transfer control to the block code with marker 4. The JumpAddress field contains the address of the beginning of the block with marker 4. Element 2 describes the location of the block with marker 5. The HandlerAddress field of this element contains the address of the filter function, the code of which is enclosed in brackets after the except keyword. The assembler representation of the filter function is in the middle, between the _tmain function and the function that duplicates the finally block. As shown in the figure, the filter function calls the ExceptionFilter function, which takes a pointer to a structure that describes the context of the exception. The JumpAddress field contains the address of the beginning of the block with marker 6.
Despite the fact that the __C_specific_handler function is not shown, the ExceptionHandlerAddress field of the EXCEPTION_HANDLER structure contains the address of this function. This function will be called by the operating system during the search for the exception handler or during the promotion of the stack. Therefore, the implementation of this function is responsible for interpreting the SCOPE_TABLE structure, calling filters, finally calling blocks, and passing control to except blocks.
The block diagram of the __C_specific_handler function is shown below in Figure 12.

Figure 12
At the beginning of its work, the function receives: the address of the beginning of the PE image; the relative address of the instruction belonging to the body of the function for which the handler was called; pointer to the SCOPE_TABLE structure. Depending on the operation being performed (search for a handler or promotion), the operation of the function varies.
If a handler search is performed, the function prepares the EXCEPTION_POINTERS structure, a pointer to which is passed to the filters of the corresponding structures. Then the function sequentially scans the ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received instruction address of any construction. If it belongs, then it is also checked whether the particular try / except construct is a construct, and if not, it is simply ignored, and the next element is checked. Otherwise, the filter of this construct is called. If the filter returned EXCEPTION_CONTINUE_SEARCH, then this construct is ignored, and the next element is checked. If the filter returns EXCEPTION_CONTINUE_EXECUTION, then the function exits and returns ExceptionContinueExecution. to tell the operating system to stop the handler search and resume the interrupted thread. If the filter returned EXCEPTION_EXECUTE_HANDLER, then the function calls the RtlUnwind function, which indicates the frame of the function whose handler was called as the frame of the target function; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions. which handler was called; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions. which handler was called; as the address of the instruction from which execution will continue, the address of the first except statement of the block is passed; and also an exception code is transmitted, which will be contained in the RAX register immediately after the transfer of control to the target function. The RtlUnwind function before passing control sequentially will call the handlers of all intermediate functions.
If the promotion is performed, then the function behaves differently. First, the function receives the relative address of the instruction from which execution will resume. Then the function sequentially scans the ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received instruction address of any construction. If it does, then it also checks if the particular try / finally construct is a construct. If it is, then its handler is called, which is essentially a duplicate of the code enclosed in the finally block. Before calling, the function will increase the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one. The value of the AbnormalTermination parameter when calling this handler is always TRUE. Therefore, the AbnormalTermination macro will always return TRUE for these blocks called in this way. For the finally code of a block located in the body of the function itself, the same macro will always return FALSE. In these cases, the compiler explicitly substitutes this value. In other words, the AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13. The AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13. The AbnormalTermination macro returns TRUE only when the promotion is running. In practice, due to the exception. If the construct is not try / finally, then it is checked whether the start address of the except block is the address from which execution will continue. And, if there is, then the function is completed. Such a check is necessary because the try / except construct can be nested in another try / finally construct, as shown below, in Figure 13.

Figure 13
As can be seen from the figure, if such a check is not performed, then finally the block of the external structure will be called during the promotion. And this is unacceptable.
If the function performs the promotion for the objective function, i.e. the EXCEPTION_TARGET_UNWIND flag is set in the ExceptionFlags field of the EXCEPTION_RECORD structure, then it performs an additional check before calling the handler. The essence of the check is to determine whether the address from which execution will continue does not belong to the structure itself, and not to its processor. And if it belongs, then the function ends. A similar situation can only occur if goto statements are used within finally blocks that point outside these blocks. This situation is shown below, in figure 14.

Figure 14
As can be seen from the figure, if such a check is not performed, then finally the external structure block will be called during the promotion. And this is unacceptable. Also, if the function is not the target, then this check is not needed.
It should be noted that in both cases (both in the case of a handler search and in the case of promotion), the scanning of elements does not begin from the beginning, but from the element whose number is stored in the ScopeIndex field of the DISPATCHER_CONTEXT structure. As already noted, the function increases the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one before calling the try / finally constructor handler. It was mentioned earlier that if an incomplete promotion process is detected in the process of searching for a handler or performing a promotion, the continuation of the search for a processor or performing a promotion will be resumed from the interrupted location. In this case, the function handler that generated the exception will be called again when the handlers of the other functions are not called. In such a situation, it is unacceptable that handlers of structures that have already been called were called repeatedly. This situation is depicted in Figure 15 below.

Figure 15
The figure above shows the function call stack, on the left of which is shown the arrow of its growth direction, and on the right is a part of the Func1 function code. Although the RtlDispatchException and RtlUnwindEx functions call function handlers using the RtlpExecuteHandlerForException and RtlpExecuteHandlerForUnwind functions, these functions are not present in the call stack for brevity. The Func1 function called the Func2 function, which in turn called the Func3 function, which raised an exception. As soon as the RtlDispatchException function got control, it sequentially called handlers for the functions: first Func3, then Func2, and ultimately Func1. The function handler Func1 found a construct that can handle the exception, and called the RtlUnwind function to transfer control to the handler of this construct. The RtlUnwind function in turn called RtlUnwindEx, which sequentially called the handlers for the functions, first Func3, then Func2, and ultimately Func1. The function handler Func1 called the handler of the nested finally block itself, which in turn raised a new exception. As soon as the RtlDispatchException function got control, it sequentially called the handlers of the previous functions. One of these handlers will be the handler of the RtlpExecuteHandlerForUnwind function, which the RtlUnwindEx function calls when the control passes to the handler of the untwisted function. The handler of the RtlpExecuteHandlerForUnwind function will copy the promotion context from the RtlUnwindEx function into the RtlDispatchException function and, after returning to it, the search for the handler will continue from the point where the promotion was interrupted. Since the RtlUnwindEx function previously unwound Func3 and Func2 functions, their handlers will not be called. But since the Func1 function raised an exception, it was not promoted by the function, and therefore its handler will be called. Since the __C_specific_handler function increases the value of the ScopeIndex field of the DISPATCHER_CONTEXT structure by one before invoking the construct handler, this field will be equal to 1 in the copied context. Therefore, when the __C_specific_handler function is called again for Func1, the construction search will begin with the construct with index 1. Therefore, the design that generated the exception will be skipped. Resume promotion is performed in a similar way. Despite the fact that the operating system and the compiler are abstracted from each other,
To conclude the discussion of try / except and try / finally constructions, it is worth describing how the GetExceptionCode macro works. Use of this macro is possible only in except blocks. This macro reads the contents of the RAX register, and when describing the __C_specific_handler function, it was mentioned that the transfer of control to a specific except block is performed using the RtlUnwind function, which takes a parameter whose value will be written to the RAX register after the transfer of control. This parameter is used to pass the code of the exception that occurred.
It is also worth describing how the GetExceptionInformation macro works. Use of this macro is only possible in expressions enclosed in brackets after the except keyword. Since in reality the expression enclosed in brackets (in other words, the filter) is a separate function that takes two parameters, this macro receives the value of the first parameter. When describing the __C_specific_handler function, it was mentioned that the filter function accepts two parameters, where the first parameter is a pointer to a structure describing the exception and the state of the processor at the time the exception occurred.
3. The disadvantages of implementing the mechanism
One of the drawbacks of this mechanism is the use of goto statements in finally blocks that point outside these blocks. In this case, the C / C ++ compiler, instead of directly transferring control, uses the reserved _local_unwind function, a prototype of which is shown below, in Figure 16.

Figure 16
The first parameter of the function accepts the frame address of the function to which the stack should be unwound, when the second accepts the address of the instruction to which control should be transferred after the unwinding. The function itself must be implemented by the programmer. Also, the implementation of this function is exported by the ntdll.dll module in user space and the ntoskrnl.exe module in kernel space. The supplied Windows SDK and WDK contain libraries that import this function from the corresponding module. The implementation of the function itself is very simple, it calls the RtlUnwind function, which passes its two parameters. The remaining parameters of the RtlUnwind function are reset to zero when called.
The use of the _local_unwind function by the compiler instead of direct control transfer is primarily due to the inability to transfer control to an arbitrary place in the function if the finally block was called as a result of promotion. In this case, transferring control to the right place of the function is possible only through a new promotion process. This approach has side effects. The goto operator, in its essence, transfers direct control when the promotion causes finally blocks to be called. Therefore, before the actual transfer of control finally blocks will be called that can change the context of the function itself. Microsoft does not recommend using the goto statement in this way, and the compiler will issue a warning.
Conclusion
In this part of the article, we ended up discussing the exception handling mechanism. The need for its implementation came from practice. And it is primarily used in the boot-time hypervisor to simplify and speed up development. In the process of implementation, many problems arose that were eliminated, and the article itself was primarily aimed at facilitating the understanding of those who are also interested in such developments.