📜 ⬆️ ⬇️

Exceptions in Windows x64. How it works. Part 4

Based on the material described in the first , second and third parts of this article, we will continue the discussion of the topic of exception handling in Windows x64.

The described material requires knowledge of basic concepts, such as the prologue, epilogue, frame functions and understanding of basic processes, such as the actions of the prologue and epilogue, the transfer of function parameters and the return of the function result. If the reader is not familiar with the above, then before reading it is recommended to read the material from the first part of this article. If the reader is not familiar with the PE image structures that are involved in the processing of an exception, then before reading it is recommended to read the material from the second part of this article. Also, if the reader is not familiar with the process of searching and invoking 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, one should not assume that the implementation of this mechanism attached to the article will exactly coincide with it, although there is no conceptual difference. Details of the attached implementation in the article will not be considered, if this is not stated explicitly. Therefore, it is assumed that these details, if necessary, should be studied independently.

The article is accompanied by the implementation of the mechanism, which is located in the exceptions folder of the git repository at this address .
')

1. Promotion stack


During error handling, a situation may arise when it is necessary to directly return control of one of the previous functions, bypassing intermediate functions. Those. the control will not be returned by a normal return from the function to the calling function, which in turn must also perform such a return, but by changing the processor state 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 growth of the stack.


Picture 1

In the example above, the stack consists of frames of four functions, 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 the user space and the ntoskrnl.exe module in the 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 up to which the stack should be unwound. The TargetIp parameter accepts the address of the instruction from which execution will continue after promotion. The ExceptionRecord parameter takes a pointer to an EXCEPTION_RECORD structure that will be passed to handlers during promotion. The ReturnValue parameter is written to the RAX register of the processor, i.e. immediately after transferring 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 spinning functions and determining the target state of the processor after spinup. The HistoryTable parameter takes a pointer to the structure that is used to cache the search. The format of this structure can be found in winnt.h.

The TargetFrame parameter is optional. If its value is NULL, then the RtlUnwindEx function performs the so-called exit exit (exit unwind), where the frames of all stack functions are unwound. 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 the STATUS_UNWIND value, the ExceptionRecord field will contain NULL, the ExceptionAddress field will contain a pointer to the program instruction RtlUnwindExample, a program will be set to NULL, the ExceptionAddress field will contain a pointer to the RtlUnwindEx program, which will be NULL. The HistoryTable parameter is optional.

The prototype of the RtlUnwind function differs only in that it does not accept the last two parameters.

Below, in Figure 3, an example of the operation of the RtlUnwind function is shown.


Figure 3

The figure above shows an example of a program consisting of four functions: _tmain, Func1, Func2, Func3. The _tmain function calls Func1, Func1 calls Func2, and Func2 calls Func3. Functions Func1, Func2, Func3 return a boolean value. The Func3 function performs 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 for calling the function Func1. To the right of the source code is the _tmain and Func3 assembler code of functions whose instruction addresses are absolute. To the right of the assembler code, the processor states and call stacks are shown for three cases: the top shows the processor state and the call stack immediately before the Func1 function call; the middle shows the state of the processor and the call stack immediately before the RtlUnwind function call; The bottom shows the processor status after executing the RtlUnwind function. The instruction pointers of these states are mapped to assembler instructions using unique numbers. Attention should be paid 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. The function frames Func1, Func2 and Func3 no longer exist on the stack. Since the RAX value after promotion is not zero, the _tmain function will display a message on the screen. In the ordinary case, i.e. if promotion was not performed, this message will not be displayed, because Func3 returns false. You should also pay attention to the fact that the _tmain function frame pointer search cycle performs four iterations, when there are only three spinning 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 take the appropriate values ​​for the function for which virtual promotion was performed, when the ContextRecord parameter reflects the processor state immediately after calling the untwisted function. Consequently, at the third iteration of the loop, the RtlVirtualUnwind function returns the Establishment Frame parameter for the Func1 function to the EstablisherFrame parameter, when the ContextRecord parameter reflects the processor state immediately after the Func1 function call. Therefore, an additional iteration is required to determine the _tmain function frame pointer.

The RtlUnwind / RtlUnwindEx function, also, prior to stack promotion, sequentially calls handlers to unwind all functions, starting with itself and to a function that is a 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, it is overhead, because To find the frame of the function that caused the function RtlUnwind / RtlUnwindEx, you need to perform an additional virtual promotion. The process of calling handlers and changing the state of the processor in order to transfer control of one of the previous functions is the so-called promotion.

The following figure 4 shows the flowchart of the RtlUnwindEx function.


Figure 4

At the beginning of its work, the function receives the lower and upper limits of the stack. The function then 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 the call to 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 its work uses two CONTEXT structures: one reflects the state of the processor at the moment of executing the function for which the handler is called (here and below, the current context); the other one reflects the state of the processor immediately after returning from this function (hereinafter, the previous context). This is necessary because of the previously discussed features of the RtlVirtualUnwind function. Also, the RtlUnwindEx function, as previously indicated, initializes the EXCEPTION_RECORD structure for later transfer to a spinup handler, if the corresponding parameter was not passed when the function was called.

Next, the function generates 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 frame address 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 performed, and the EXCEPTION_EXIT_UNWIND flag means that the frames of all functions are unwound.

Next, the function, using the function RtlLookupFunctionEntry, obtains 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 of 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 does not have a frame. Since simple functions do not allocate memory in the stack, their RSP value will point to the return address, therefore, for such functions, RtlUnwindEx function retrieves this address, copies its value to the current context and increases the value of the Rsp field of the current context by 8. The current context now reflects the state of the processor at the time of execution of the next function in the stack above. Then the function will continue its work, starting with obtaining the address of the PE image and a pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function higher in the stack.

For personnel 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 the 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 with the aim to save the state of the processor, describing the moment of execution of the current function in case the function turns out to be the target. It has been repeatedly mentioned that the RtlVirtualUnwind function returns the frame address of the function that was executed in the transmitted state of the processor, when the return status of the RtlVirtualUnwind function describes the next function in the stack above. Therefore, 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, since it will reflect the execution of the function that called the target function. Immediately after calling the RtlVirtualUnwind function, the RtlUnwindEx function will check the frame pointer of the untwisted function to exit the stack limit. The function also checks whether the current function's frame is located higher in the stack than the target function frame, which in turn means that the RtlUnwindEx function missed the target function frame due to stack damage, damage to the .pdata section, etc. In both cases, the function will raise an 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 function. Thus, the next function in the stack above becomes the current one. Further, the function will continue its work, starting with obtaining the address of the PE image and a pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function above the stack.

If the function RtlVirtualUnwind returned the address of the handler 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 a target. Thus, the handler of this function will be able to determine that its corresponding function is a function whose control is being passed. Then, the RtlUnwindEx function will 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 exceptions (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 passed to the RtlUnwindEx function, i.e. address of the instruction from which execution will be resumed after promotion; The ContextRecord field will contain a pointer to a CONTEXT structure that describes the state of the processor at the time the current function is executed, and not the next one higher in the stack; The ScopeIndex field contains the current value of a local variable and will be discussed in more detail when discussing the try / except and try / finally constructs.

The handler, as is the case with the RtlDispatchException function, is not called directly, and the auxiliary function RtlpExecuteHandlerForUnwind is used instead, 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 handler and is used to catch exceptions that occurred during the execution of the handler itself. The assembler representation of the function is shown below in Figure 5.


Figure 5

As shown in the figure, the function first allocates memory in the stack for register variables and one variable, stores a pointer to the structure passed to DISPATCHER_CONTEXT in this variable and calls the exception handler whose address is stored in the LanguageHandler field of the DISPATCHER_CONTEXT structure. Also note the presence of a function body placeholder. 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 unwinding process into the DISPATCHER_CONTEXT structure of the current handler search or unwinding process. This allows you to continue searching for the handler from the place where promotion was previously interrupted, or continue the previously interrupted promotion. It also allows you to skip the call to the handlers of those functions for which such a call was made during the previous promotion. It should also be noted that the call to handlers is resumed from 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 the discussion of 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, the function will swap the current and previous contexts if the current function was not the target. Thus, the next function in the stack above becomes the current one. Further, the function will continue its operation, starting with obtaining the address of the PE image and a pointer to the RUNTIME_FUNCTION structure, for the address of the new instruction, already for the next function in the stack above.

If the handler returned ExceptionCollidedUnwind, it means that during the promotion another active promotion was detected, in the context of which an exception occurred. In this case, the structure of the DISPATCHER_CONTEXT RtlUnwindEx function will contain the context of the interrupted promotion, since it was copied by the RtlpExecuteHandlerForUnwind function handler. Consequently, the function will update the current context from the ContextRecord field of the DISPATCHER_CONTEXT structure, use the RtlVirtualUnwind function to get the previous one, set the EXCEPTION_COLLIDED_UNWIND flag and call a handler in the context of which an exception has previously occurred, and depending on its return result, perform the previously described actions.

In all other cases, the RtlUnwindEx function throws a STATUS_INVALID_DISPOSITION exception.

At each iteration, before obtaining the PE address of the image and a pointer to the RUNTIME_FUNCTION structure, the function, using the RtlpIsFrameInBounds function, checks that the frame pointer of the function for which the handler was attempted to be called is within the stack limit and is not the frame pointer of the target function. If such a test gives a positive result, then the function continues. Otherwise, if the frame pointer goes beyond the limit and the pointer is not the frame of the target function, then either spin was performed on exit, and none of the processors stopped this process during spin, 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 The target function frame was found. In this case, the Rax field of the current context will contain the value of the passed ReturnValue parameter, and the Rip field of the same context will contain the value of the passed TargetIp parameter, unless the exception code is STATUS_UNWIND_CONSOLIDATE. Since 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 that for promotion with such a code, the RtlRestoreContext function will be called by the handler before resuming work, and if the Rip field is updated, the handler will get an incorrect view of the processor status. 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 the locally generated structure. By the time the RtlRestoreContext function was called, the handlers for the promotion of all functions in the stack, starting from its top and up to the target function inclusive, were called. The RtlRestoreContext function does not return control, since applies a new state to the processor.

It should be noted that checking the frame pointer at each iteration is an error in the implementation, since 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, the function will generate 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 it in the course of its work. This error still persists.

2. try / except and try / finally constructs


From the point of view of the operating system, as was already considered when describing 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 placed directly in the body, these parts of the code cannot be directly invoked by the operating system. To ensure the correct functioning of these constructs, the compiler generates auxiliary information that is used by the operating system exception handlers. It was mentioned earlier that all exception handling can be divided into two phases. The search phase and the transfer of control to the exception handlers of the constructs discussed 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 diversity of the mechanisms of different programming languages.

The C / C ++ compiler reserves the function __C_specific_handler. 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 us to abstract the compiler from understanding the operation of the operating system itself and adapt the executable image to any execution environment, for example, to the Win32 subsystem, to the Windows kernel execution environment, or to any other environment. The implementation of this function is also exported by the ntdll.dll module in the user space and the ntoskrnl.exe module in the kernel space. The supplied Windows SDK and WDK contain libraries that import this feature 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 contains the SCOPE_TABLE structure, which describes the location of all the 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 structures in the function body and, therefore, the number of ScopeRecord elements in the structure. For each construction, the compiler generates the corresponding ScopeRecord structure element, which in turn describes the location of the corresponding construction in the function body, as well as the location of its handlers. ScopeRecord elements are sorted in the following order: non-nested constructions follow each other in the order in which they appear in the code, when nested constructions always follow the construction in which they are nested. The BeginAddress field of the ScopeRecord element contains the address of the beginning of the try block. The EndAddress field contains the address of the instruction following the last instruction enclosed in the try block. The JumpTarget field, if not zero, contains the address of the first instruction of the code contained in the except block. The code of the except block follows immediately after the code enclosed in the try block. The HandlerAddress field contains the address of the function for the filter of the except block. Although the exception 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 takes 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 the frame pointer 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 shown in the figure above, the structure contains two pointers. The first one points to the structure that describes the reason for the exception, the second one - to the structure that describes the processor state at the moment of the exception.

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. This value can also be encoded directly in the HandlerAddress field. In this case, the structure has no filter, and the transfer of control to the exception handler of this structure is always performed. The second value indicates that the search for the exception handler should continue. The third value means that you should interrupt the search and resume the execution of the interrupted thread.

If the JumpTarget field is zero, then this construct is a finally construct, and the code enclosed in the finally block follows immediately after the code enclosed in the try block. In this case, the HandlerAddress field contains the address of the function, which in its content repeats the code contained 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 an 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, meaning that the code of the finally block is executed due to an abnormal completion of the code enclosed in a try block (that is, an exception has 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 by the function of the exception filter — access to the local variables of the function in which the corresponding structure is located. The function returns no values.

In cases where, during the execution of the code enclosed in a try block, an exception did not occur, the finally code of the block that immediately follows the try code of the block is executed.

All addresses in the SCOPE_TABLE structure are addresses relative to the beginning of the image.

Below, in Figure 11, an example of the SCOPE_TABLE structure that the compiler generates is shown.


Figure 11

The figure above shows an example of a program, the _tmain function of which includes try / except and try / finally constructs. To the left of the source code, there is an assembler representation of the functions: _tmain, filter functions of the lower try / except construction and function, duplicating the code contained in the finally block. Functions are listed from bottom to top. The addresses of the assembler instructions are absolute. Green markers match the code enclosed in blocks with its assembly equivalents. It should be noted that the block of code with marker 2 in the assembly representation occurs twice: in the body of the _tmain function and in the topmost function. The latter is a duplicate of the code enclosed in the finally block. You should also pay attention to the presence of the nop instruction after the instruction to call the FailExecution function in the code block with marker 1. This instruction is also a placeholder, as is the case with gateway functions, the RtlpExecuteHandlerForException function and the RtlpExecuteHandlerForUnwind function. If the filler is missing, then when checking the instructions for belonging to a particular structure, an erroneous assumption about its belonging can be made. In this case, an erroneous assumption will be made that the instruction to call the function FailExecution does not belong to a block of code with marker 1, since the function RtlVirtualUnwind after promotion will return the address not on the instruction of calling the function FailExecution, but on the instruction immediately after it. For this reason, the compiler adds a placeholder after the function call instruction, if that in turn is the last instruction in the block. If the call instruction is not the last instruction in the block, then there will be no such placeholder.

The left side of the figure shows the structures that the compiler generates. At the top is an element of the function table, below is the structure of the UNWIND_INFO referenced by this element. Although the EXCEPTION_HANDLER structure is not part of the UNWIND_INFO structure, it is represented in the figure as part of this structure, since if it is present, it follows immediately after the UNWIND_INFO structure. Below the UNWIND_INFO structure is a more detailed representation of the EXCEPTION_HANDLER structure, below it is a more detailed representation of the LanguageSpecificData field of this structure, which houses the SCOPE_TABLE structure. At the very bottom, the ScopeRecord array elements of this structure are sequentially depicted. All addresses in the generated structures are relative. Also, these addresses are mapped to the addresses of the assembly code by means of unique numbers.

It is worthwhile to dwell on the elements of the ScopeRecord array. Element 0 describes the location of the block with marker 1. The field HandlerAddress of this element contains the address of the function, duplicating the code of the finally block with marker 2. The field JumpAddress contains 0, since this is finally a block. Element 1 describes the location of the block with marker 3. The field HandlerAddress of this element contains the value 1, which in turn means that the structure does not have a filter, and when an exception occurs, you should always transfer control to the block code with marker 4. The JumpAddress field contains the start address of the block with marker 4. Element 2 describes the location of the block with marker 5. The field HandlerAddress of this element contains the address of the filter function, the code of which is enclosed in brackets after the keyword except. The assembler representation of the filter function is located 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 start address of the block with marker 6.

Although the __C_specific_handler function is not represented in the figure, 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 stack promotion. Therefore, the implementation of this function is responsible for interpreting the SCOPE_TABLE structure, calling filters, calling finally blocks and passing control except to 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 (search handler or promotion), the function of the function varies.

If the handler is being searched for, the function prepares the EXCEPTION_POINTERS structure, the pointer to which is passed to the filters of the corresponding constructions. Then the function sequentially scans ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received address belongs to an instruction of any structure. If it belongs, then it is also checked whether the particular try / except construct is a construct, and if not, then it is simply ignored and the next element is checked. Otherwise, the filter of this construction 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, the function ends its work and returns ExceptionContinueExecution to tell the operating system to stop searching for the handler and resume the execution of the interrupted thread. If the filter returned EXCEPTION_EXECUTE_HANDLER, then the function calls the RtlUnwind function, which as the frame of the target function indicates the frame of the function whose handler was called; as the address of the instruction from which execution will be continued, the address of the first except block statement is transmitted; and an exception code is transmitted, which will be contained in the RAX register immediately after transferring control to the target function. The RtlUnwind function, before transferring control, will sequentially call handlers of all intermediate functions.

If promotion is performed, then the function behaves differently. First, the function receives the relative address of the instruction from which execution will be resumed. Then the function sequentially scans ScopeRecord elements of the SCOPE_TABLE structure and checks whether the previously received address belongs to an instruction of any structure. If it belongs, then it is also checked whether 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 code of the finally 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 promotion is performed. Practically due to an exception. If the construct is not try / finally, then it is checked whether the address of the beginning of the block is not the address,from which will continue execution. And, if it is, then the function ends. This check is necessary because the try / except construct can be nested in another try / finally construct, as shown in Figure 13 below.


Figure 13

As can be seen from the figure, if such a check is not performed, then during the unwinding, a finally external construction block will be called. And this is unacceptable.

If the function performs promotion for the target function, i.e. The EXCEPTION_TARGET_UNWIND flag is set to 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 the execution will be continued belongs to the construction itself, and not to its processor. And if it belongs, then the function ends. This situation can be only in the case of use within the finally blocks of goto statements that point outside of these blocks. This situation is depicted below in Figure 14.


Figure 14

As can be seen from the figure, if such a check is not performed, then during the unwinding, a finally external construction block will be called. And this is unacceptable. Also, if the function is not a target, then this check is not needed.

It should be noted that in both cases (both in the case of the search for the processor and in the case of promotion) the scanning of the 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 handler of the structure. It was mentioned earlier that if in the process of searching for a handler or performing a spinup, an incomplete spinup process is detected, then the continuation of the search for a hooker or the execution of spinup will be resumed from the interrupted place. In this case, the handler of the function that raised the exception will be called again, when the handlers of the remaining functions will not be called. In such a situation it is unacceptable that constructors that have already been called,turned out to be called again. This situation is depicted below in Figure 15.


15

The figure above shows the function call stack, to the left of which the arrow of its growth direction is shown, and to the right is part of the function code Func1. The RtlDispatchException and RtlUnwindEx functions call function handlers through the RtlpExecuteHandlerForException and RtlpExecuteHandlerForUnwind functions, although for the sake of brevity these functions are not present in the call stack. The function Func1 caused the function Func2, which in turn caused the function Func3, which caused the exception. As soon as the RtlDispatchException function received control, it sequentially called handlers for the functions: first Func3, then Func2, and eventually Func1. The function handler Func1 found a construct that could handle the exception, and called the RtlUnwind function to pass control to the handler for this construct.The RtlUnwind function in turn called RtlUnwindEx, which sequentially called the handlers for functions Func3 first, then Func2, and eventually Func1. The handler of the Func1 function called the handler of the nested finally block, which in turn raised a new exception. As soon as the RtlDispatchException function received control, it sequentially called the handlers of the previous functions. One of these handlers will be the handler of the RtlpExecuteHandlerForUnwind function, which is called by the RtlUnwindEx function when transferring control to the handler of the unwinding function. The handler of the RtlpExecuteHandlerForUnwind function copies the context of the promotion from the RtlUnwindEx function to the RtlDispatchException function and, after returning control to it, the search for the handler will continue from the place where the promotion was interrupted.Since the function RtlUnwindEx previously spun the functions Func3 and Func2, 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 ScopeIndex field of the DISPATCHER_CONTEXT structure by one before calling the construction handler, the field will be equal to 1 in the copied context. Consequently, when the __C_specific_handler function is called again for the Func1 function, the structure search starts with the construction with index 1. Thus, the construction that generated the exception will be skipped. Renewal promotion is performed in the same way. Although the operating system and the compiler are abstracted from each other,the presence of the ScopeIndex field in the DISPATCHER_CONTEXT structure is a violation of this abstraction.

At the end of the discussion of try / except and try / finally constructions, it’s 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 via the RtlUnwind function, which accepts a parameter whose value will be recorded in the RAX register after the transfer of control. Through this parameter is passed the code of the exception that occurred.

It is also worth describing how the GetExceptionInformation macro works. Using this macro is possible only in expressions enclosed in brackets after the except keyword. Since, in reality, an expression enclosed in brackets (in other words, a filter) is a separate function that takes two parameters, this macro gets the value of the first parameter. When describing the __C_specific_handler function, it was mentioned that the filter function takes two parameters, where the first parameter is a pointer to the structure describing the exception and the processor state at the moment the exception occurred.

3. Disadvantages of the mechanism implementation


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 direct control, uses the reserved _local_unwind function, the prototype of which is shown below in Figure 16.


Figure 16

The first parameter of the function takes the frame address of the function to which the stack should be spun, when the second receives the address of the instruction to which control should be transferred after the spinup. The function itself must be implemented by the programmer. The implementation of this function is also exported by the ntdll.dll module in the user space and the ntoskrnl.exe module in the kernel space. The supplied Windows SDK and WDK contain libraries that import this feature from the corresponding module. The implementation of the function itself is very simple, it calls the function RtlUnwind, which passes two of its parameters. The remaining parameters of the RtlUnwind function are zeroed on the call.

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 of the function if the finally block was called as a result of unwind. In this case, it is possible to transfer control to the right place of the function only through a new promotion process. This approach has side effects. The goto operator, in its essence, transfers direct control when promotion leads to the call of finally blocks. 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 have finished the discussion on the mechanism of exception handling. The need for its implementation came from practice. First and foremost, it is used in the boot-time hypervisor in order to simplify and speed up development. In the process of implementation, a lot of problems arose that were eliminated, and the article itself, first of all, aims to facilitate the understanding of those who are also interested in such developments.

Source: https://habr.com/ru/post/326878/


All Articles