Back to Home

Exceptions on Windows x64. How it works. Part 3 / Company blog Aladdin R.D.

c · c ++ · windows · system programming · UEFI · GitHub · exceptions · exceptions · try / except

Exceptions on Windows x64. How it works. Part 3

    Based on the material described in the first and second 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 function frame and an understanding of basic processes, such as the actions of a prologue and epilogue, the transfer of function parameters and the return of the result of a function. 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. Also, 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 also recommended to familiarize yourself with the material from the second part of this article.

    The description given 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 conceptually there are no 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. Exceptions and their handling


    In the following subsections, exception handling and everything that underlies it will be discussed in detail. The description given 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 conceptually there are no 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.

    1.1. Secondary functions


    Before you start discussing the process of handling exception, you should consider the RtlLookupFunctionEntry and RtlVirtualUnwind functions that are exported by the ntoskrnl.exe module in kernel space and the ntdll.dll library in user space.

    The RtlLookupFunctionEntry function, the prototype of which is shown in Figure 1, returns a pointer to the RUNTIME_FUNCTION structure and the start address of the PE image corresponding to the code whose address is passed in the ControlPc parameter.


    Figure 1

    The ImageBase parameter takes a pointer to a variable, where the function returns the address of the beginning of the PE image, and the HistoryTable parameter, which is optional, takes a pointer to the structure that is used for search caching. The structure format of the last parameter can be found in winnt.h. If the function returns NULL, either the corresponding PE image could not be found by the passed code pointer, or the corresponding record was not found in the function table, which may mean that the function does not have a frame.

    The RtlVirtualUnwind function, the prototype of which is shown in Figure 2, performs the virtual promotion of the function.


    Figure 2

    It is called virtual because the function does not change the state of the physical processor, and instead takes a parameter in the ContextRecord parameter to a structure that describes the processor context at a particular point in time. The processor context after promotion returns to the same structure. The CONTEXT structure itself is shown below in Figure 3.


    Figure 3

    Fields P1Home-P6Home are introduced for ease of use of the structure, for example, they can be used as a region of register and stack parameters. The ContextFlags field is a bit field and describes the state of the entire structure, i.e. which fields reflect the state of the corresponding processor registers, and which do not. The field may contain the following flags:

    • CONTEXT_CONTROL - if set, the SegSs, Rsp, SegCs, Rip and EFlags fields reflect the status of the corresponding processor registers;
    • CONTEXT_INTEGER - if set, the fields Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi and R8-R15 reflect the status of the corresponding processor registers;
    • CONTEXT_SEGMENTS - if set, then the SegDs, SegEs, SegFs and SegGs fields reflect the status of the corresponding processor registers;
    • CONTEXT_FLOATING_POINT - if set then fields Xmm0-Xmm15 and MxCsr reflect the states of the corresponding processor registers;
    • CONTEXT_DEBUG_REGISTERS - if set, the fields Dr0-Dr3 and Dr6-Dr7 reflect the status of the corresponding processor registers.

    It should be noted that the ContextFlags field is not interpreted by the RtlVirtualUnwind function in any way, and it always assumes that the structure contains the actual state of the processor at a particular moment in time. Since the fields VectorRegister, VectorControl, DebugControl, LastBranchToRip, LastBranchFromRip, LastExceptionToRip and LastExceptionFromRip are not directly related to the topic under discussion, their purpose will not be described here. The FltSave field is used when it is necessary to maintain the state of the full XMM context, including the state of the FPU.

    The ImageBase parameter accepts the address of the PE image whose code was running. The ControlPc parameter carries the address of the instruction where execution was interrupted, and the FunctionEntry parameter takes the address of the RUNTIME_FUNCTION structure corresponding to the address of the instruction.

    The HandlerType parameter accepts the type of expected handler. If the value UNW_FLAG_EHANDLER is accepted and the untwisted function has an exception handler, the function returns the address of this exception handler. If the value UNW_FLAG_UHANDLER is accepted and the untwisted function has a promotion handler, the function returns the address of this promotion processor. In all other cases, the function returns NULL. It should also be noted that if the execution of the untwisted function was interrupted at the time the prolog code or the epilogue code was executed, the function returns NULL. The philosophy here is that the exception and / or promotion handler is bound to the body of the function. If the function returns the address of one of the handlers, then it also returns the address of the data bound to this handler by the compiler of the corresponding programming language. The address is returned by the pointer passed in the HandlerData parameter. As described in section 3 of the second part of this article, the address of the exception handler and / or the promotion handler is stored in the ExceptionHandlerAddress field of the EXCEPTION_HANDLER structure, when the data associated with the processor is stored in the LanguageSpecificData field of the same structure.

    The EstablisherFrame parameter accepts a pointer to a variable where the function returns the frame pointer before the function unwinds. The ContextPointers parameter is optional and if used, it contains the address of the structure, which repeats the contents of general registers and XMM registers after promotion. It should be noted that only those registers that participated in the promotion get into the structure.

    Below, in figure 4, an example of the function is shown.


    Figure 4

    On the left is an example image that contains three functions: wmain, func2, and func1. The wmain function calls func2, which in turn calls func1. In the middle, the assembler representations of each function are shown, where the absolute address of the instruction in memory is shown on the left, and the RSP value in the middle until the instruction is executed, when the assembler mnemonic code of the instruction is on the right. RSP values ​​are not shown for all instructions, but only for those that were executed or for an instruction whose thread execution was interrupted. To the right of the assembler representations of the functions, the meaning of the processor registers is shown before calling the corresponding functions; for brevity, only general registers and the instruction pointer register (RIP) are shown. Labels denote the values ​​of parameters that are passed to the function or returned by it.

    The RtlVirtualUnwind function spins exactly one function, i.e. after the function is executed, the contents of the CONTEXT structure will become as if the untwisted function was not called, with the exception that the values ​​of the volatile registers will not be restored, and the RIP will not contain a pointer to the instruction to call the corresponding function, but to the instruction immediately after it. Also, the RtlVirtualUnwind function will return a frame pointer of the untwisted function, and, if required, a pointer to its handler and a pointer to the data of this handler.

    As reflected in the example, the ImageBase parameter will be 0x7FF6AEAF000 (label 1); the ControlPc parameter will be 0x7FF6AEAF104E (label 2); the FunctionEntry parameter will contain the address of the RUNTIME_FUNCTION structure (label 3) corresponding to the address of the instruction whose value is passed in the ControlPc parameter; the ContextRecord parameter will contain the register values ​​of the interrupted function (label 4). The pointer to the RUNTIME_FUNCTION structure can be obtained using the RtlLookupFunctionEntry function. The function RtlVirtualUnwind after execution will return: the values ​​of the registers after promotion (label 5); from the EXCEPTION_HANDLER structure will return the address on the LanguageSpecificData field in the HandlerData parameter and return a pointer to the handler that is extracted from the ExceptionHandlerAddress field of the same structure (label 6);

    The RtlVirtualUnwind function before the promotion determines whether the processor performed a prologue, epilogue, or the body of the untwisted function. If it was a body, then promotion is performed using the UNWIND_INFO structure. If it was a prologue, then the promotion is also performed, with the exception that the function first determines in which place the prolog was interrupted, and it is from this place that it performs the promotion. If it was an epilogue, then for version 2 of the UNWIND_INFO structure, promotion is performed from it. Also, as in the case of the prologue, before the promotion, the function determines in which place the execution of the epilogue was interrupted, and from this place the promotion is performed. For version 1 of the UNWIND_INFO structure, the promotion is performed by analyzing the subsequent instructions of the epilogue, since the UNWIND_INFO structure of this version does not contain any information about the epilogue. Section 1 of the first part of this article mentioned that the beginning of the epilogue of functions described by the UNWIND_INFO version 1 structures are considered add rsp, constant or lea rsp, [frame pointer + constant] instructions. The fact is that the analysis of XMM instructions complicates the code of the promotion function, and if an exception occurs before the conditional epilogue, then their values ​​will be restored from the UNWIND_INFO structure, so the integrity of these registers is not damaged after the promotion. The only side effect is when an exception occurs when XMM registers are restored, in which case the RtlVirtualUnwind function will return a pointer to the exception handler, which, therefore, will be called when the exception is processed. For the functions described by the UNWIND_INFO version 2 structures, this philosophy has changed a bit, and the beginning of the epilogue began to be considered instructions for pushing general registers from the stack, because add rsp instructions, constant and lea rsp, [frame pointer + constant] cannot raise exceptions in principle. Figure 5 shows an example code, to the right of which an assembler representation of its two functions is shown: func1 and func2. The addresses of the instructions are absolute, and for compactness their hexadecimal representation is absent. As an example, consider the stack promotion of the func1 function. Three cases are indicated in the figure: A, B, C. Each of them depicts the state of the function: A - prologue, B - body, C - epilogue. to the right of which is an assembler representation of its two functions: func1 and func2. The addresses of the instructions are absolute, and for compactness their hexadecimal representation is absent. As an example, consider the stack promotion of the func1 function. Three cases are indicated in the figure: A, B, C. Each of them depicts the state of the function: A - prologue, B - body, C - epilogue. to the right of which is an assembler representation of its two functions: func1 and func2. The addresses of the instructions are absolute, and for compactness their hexadecimal representation is absent. As an example, consider the stack promotion of the func1 function. Three cases are indicated in the figure: A, B, C. Each of them depicts the state of the function: A - prologue, B - body, C - epilogue.


    Figure 5

    Below, in figures 6-8, each case is considered separately. On the left are the processor registers before the spin, and on the right after. For compactness, only general purpose registers and instruction pointer register (RIP) are shown, because in the cases described, only these registers are subject to change.

    Case A is shown in Figure 6. In this case, the processor performed the prologue of the func1 function, the execution of which was interrupted by the push instruction of the RDI register. The stack promotion in this case will be performed using the data of the UNWIND_INFO structure. First, the value of the RSI register will be restored, then the return address will be restored. Therefore, the registers RSI, RSP, RIP will be changed.


    Figure 6

    Figure 7 shows case B. In this case, the processor executed the body of the func1 function, the execution of which was interrupted by instructions to read 8 bytes from the top of the stack to the RAX register. The stack promotion in this case will be performed using the data of the UNWIND_INFO structure. First, the memory from the stack allocated by the prolog for local function variables will be freed, then the values ​​of the RDI and RSI registers will be restored, then the return address will be restored. Therefore, the registers RSI, RDI, RSP, RIP will be changed.


    Figure 7

    Figure 8 shows case C. In this case, the processor performed the epilogue of the func1 function, the execution of which was interrupted by the instructions for pushing the RDI register. Depending on the version of the UNWIND_INFO structure, the promotion will be performed either using the structure itself, if it is a version 2 structure, or by analyzing the epilogue code if it is a version 1 structure. First, the values ​​of the RDI and RSI registers will be restored, then the return address will be restored. Therefore, the registers RSI, RDI, RSP, RIP will be changed.


    Figure 8

    ControlPc parameter for case A will be 0x7FF70C131036, for case B - 0x7FF70C13104E, for case C - 0x7FF70C131078. The ImageBase parameter for all three cases will be 0x7FF70C130000.

    The EstablisherFrame parameter for case A will be 0x6DE73AF6E0, for case B it will be 0x7E68EFF860, and for case C it will be 0x979BB9FAD8. In all three cases, this will be the value of the RSP register before promotion. Separately, consider the values ​​of EstablisherFrame for a function that has a frame pointer. Figure 9 shows an example of such a function where the addresses of the instructions are absolute, and instead of their hexadecimal representation, the stack pointer (RSP) is displayed before the instruction is executed.


    Figure 9

    If the execution of the function was interrupted before the instruction was executed at the address 0x7FF6D76C1101, then EstablisherFrame will assume the value 0x9E84B9FAA0. If the execution was interrupted before the instruction was executed at the address 0x7FF6D76C110A, then EstablisherFrame will assume the value 0x9E84B9FA90. If the execution was interrupted before the instruction was executed at the address 0x7FF6D76C1118, then EstablisherFrame will also take the value 0x9E84B9FA90. It should be noted that if the function has a frame pointer, as in this example, and it was installed before the function was interrupted, then EstablisherFrame will take the value of the stack pointer at the time the frame pointer was set, and not the current stack pointer. In this example, the installation of the frame pointer was performed by the instruction at the address 0x7FF6D76C1106.

    2. Processing


    The entire process of exception handling can be conditionally divided into two parts.

    The first part is finding and calling the exception handler. This part is performed by the operating system. A conceptual diagram of exception handling is depicted below in Figure 10.


    Figure 10

    The figure above shows the space of the entire process. User space is shown on the left, while kernel space is on the right. Each space contains modules. For any application, the ntdll.dll module is always displayed, which performs the necessary auxiliary tasks for user space. Ntoskrnl.exe, which is the core of Windows, is always present in the kernel space. The rest of the modules are for illustration purposes only. When an exception occurs, the processor calls the function from the corresponding gateway descriptor, which is an element of the Interrupt Descriptor Table. This function is a kernel function. See the Intel 64 and IA-32 Architectures Software Developer's Manual for more details on the interrupt table. Then this function, together with the KiExceptionDispatch function, prepares all the necessary data for processing, after which the KiDispatchException function is called, which performs additional actions before processing, one of which is that if an exception occurred in user space, then the processing of this exception is redirected to the user space. The ntdll.dll module is responsible for processing in user space. When all the necessary preparation for processing is completed, the RtlDispatchException function is called, which searches and calls the handler by scanning the .pdata sections of the images, and if the handler is found, the function calls it. It should also be noted that the function does not spin the stack, it only performs a handler search.

    The second part depends on the format of the LanguageSpecificData field of the EXCEPTION_HANDLER structure generated by the compiler of the corresponding programming language, and the implementation of the found exception handler that relies on this field.

    In this article, we consider the try / except and try / finally constructs of the C / C ++ languages, so the description of the second part will deal with the format of the LanguageSpecificData field of the EXCEPTION_HANDLER structure generated by the compiler for these constructs.

    The following subsections examine in more detail the entire process of preparing and searching for a handler. For the sake of definiteness and simplification of the explanation, the whole process will be considered on the example of processing the division by zero exception, and the code that generated this exception will be the kernel mode code. Despite the fact that the entire explanation will be limited to an example of a specific exception, the described will also be relevant for other exceptions, as if they do not repeat the behavior of eliminating division by zero, they are very similar and conceptually behave the same.

    2.1 Preparation for processing


    As previously indicated, at the time of the exception, the processor calls the function from the corresponding gate descriptor, which is an element of the interrupt descriptor table. The division by zero gateway function is the KiDivideErrorFault kernel function. Below, in figure 11, an assembler representation of the function is shown. For brevity, only that part of the code that is directly related to the topic under discussion is displayed.


    Figure 11

    As you can see from the figure, first the function simulates an empty error code, because to avoid division by zero, the processor does not push the code onto the stack. Next, the function pushes general registers, allocates memory on the stack, and sets the frame pointer. This ends the prologue of the function. Saving mutable general purpose registers and XMM registers is performed in the function body. The function also saves the type of the called handler in the stack variable. The value 1 is always set for exceptions, 0 for interrupts, and 2 for services, i.e. Calling kernel services from user space. The last action of the function is to call the KiExceptionDispatch function. Before calling, the function also resets the direction flag, saves the MXCSR register of the XMM block, and then loads it with the standard value. This will be described in more detail below. Note that the function does not have an epilogue. The fact is that the work of the stream, after handling the exception, does not resume in the usual way, i.e. KiExceptionDispatch does not return control, and therefore the epilogue is not needed. After the function call instruction, an idle instruction follows. This is the so-called placeholder. A special role is assigned to it, its presence allows the RtlVirtualUnwind function to reliably determine that an exception occurred during the execution of the function body. That is, if there is no such placeholder, then the RtlVirtualUnwind function, when promoting the KiExceptionDispatch function, will extract the return address for the retn instruction, instead of the nop instruction. And, therefore, at the next iteration of the promotion (i.e., when the KiDivideErrorFault function is already unwound), the RtlVirtualUnwind function will analyze whether the prologue, epilogue, or body was performed. As was already noted in Section 1 of the first part of this article, whether the epilogue was executed is determined by the code of the function itself (or using records of the UWOP_EPILOG type, structure UNWIND_INFO version 2, which does not make much difference, since in this case it already plays a role instruction address, not a stream of code bytes), and since the retn instruction is used only in epilogues, the RtlVirtualUnwind function will make an erroneous assumption that the epilogue was executed, and not the body. Therefore, this will lead to the fact that when the KiDivideErrorFault function is unwound, the prolog will not be untwisted, and the address of the frame following the stack above the function will be determined incorrectly. it is determined by the code of the function itself (or using records of the UWOP_EPILOG type, structure UNWIND_INFO version 2, which does not make much difference, because in this case the address of the instruction already plays a role, and not the stream of code bytes), and since the retn instruction is used only in epilogues, the RtlVirtualUnwind function will make the erroneous assumption that the epilogue was executed, and not the body. Therefore, this will lead to the fact that when the KiDivideErrorFault function is unwound, the prolog will not be untwisted, and the address of the frame following the stack above the function will be determined incorrectly. it is determined by the code of the function itself (or using records of the UWOP_EPILOG type, structure UNWIND_INFO version 2, which does not make much difference, because in this case the address of the instruction already plays a role, and not the stream of code bytes), and since the retn instruction is used only in epilogues, the RtlVirtualUnwind function will make the erroneous assumption that the epilogue was executed, and not the body. Therefore, this will lead to the fact that when the KiDivideErrorFault function is unwound, the prolog will not be untwisted, and the address of the frame following the stack above the function will be determined incorrectly. not the body. Therefore, this will lead to the fact that when the KiDivideErrorFault function is unwound, the prolog will not be untwisted, and the address of the frame following the stack above the function will be determined incorrectly. not the body. Therefore, this will lead to the fact that when the KiDivideErrorFault function is unwound, the prolog will not be untwisted, and the address of the frame following the stack above the function will be determined incorrectly.

    The MXCSR register of the XMM block was not mentioned in section 3 of the first part of this article, but its calling conventions also govern its use when calling functions. This register is divided into a constant and a non-constant part, as reflected below, in Figure 12.


    Figure 12 The

    non-constant part consists of 6 status flags, bits 0-5. The rest of the register, consisting of control bits from 6-15, is considered constant. If the called function changes the state of the constant part, then it must restore it before returning. Moreover, the calling function, before calling other functions, must load the constant values ​​into the constant part if it was changed by it. Standard values ​​of the fields of the constant part:

    • Bit 6 is 0: denormal operands are zeros;
    • Bits 7-12 are equal to 1: all exceptions are masked;
    • Bits 13-14 are equal to 0: rounding to the nearest;
    • Bit 15 is 0: Resets the result to zero at lower overflow.

    These rules can only be violated in two cases:

    1. if the purpose of the function is to change the constant part of the register;
    2. if violation of these rules does not lead to a change in the behavior of the program, i.e. the program will behave as if the rules were not violated.

    The state of the non-constant part should not be interpreted in any way at the boundary of functions, i.e. the called function should not rely on its values, but the calling function after returning control to it, unless explicitly indicated in the function description.

    As for the direction flag (DF), its default value is 0. If the flag was set, then it must be reset before calling the function or before returning from the function.

    KiExceptionDispatch takes 8 parameters. ECX contains the exception code; EDX number of parameters specific to this exception; R8 contains the address of the instruction that raised the exception; Registers R9, R10, R11 contain parameter values ​​specific to this exception; RBP and RSP are pointers to stored volatile registers. As noted earlier, the function does not return control. Below, in figure 13, an assembler representation of the function is shown. For brevity, only those sections of the code that are directly related to the topic under discussion are listed.


    Figure 13

    As can be seen from the figure, the prologue of the function first allocates memory on the stack, after which constant XMM and general purpose registers are saved. This ends the prologue of the function. Next, the function initializes the EXCEPTION_RECORD structure in the memory allocated on the stack and calls the KiDispatchException function. After returning from the function, the following are restored: general-purpose constant registers, constant XMM registers, MXCSR register, non-constant general-purpose registers and non-constant XMM registers. Next, the memory on the stack allocated by the gateway function is freed (in this example, the memory allocated by the KiDivideErrorFault function), and it returns to the interrupted stream. The structure of EXCEPTION_RECORD is defined below in Figure 14.


    Figure 14

    The ExceptionCode field contains the exception code. The ExceptionFlags field is a bit field that describes the type and state of exception handling. Its flags will be examined in detail during the discussion of searching and calling the handler, as well as in the discussion of stack promotion. The ExceptionRecord field in some cases contains a pointer to another structure of the same type. For example, if an invalid situation was found during the search for an exception handler or a promotion handler (for example, the handler returned an incorrect processing result), then a new exception will be thrown whose EXCEPTION_RECORD structure will contain a pointer to the EXCEPTION_RECORD structure for the exception in the context of which this situation occurred. In other cases, the field is NULL. It should be noted that this statement is true for 32-bit versions of Windows, and in 64 bit versions it is almost always NULL. The ExceptionAddress field contains the address of the instruction that raised the exception. The NumberParameters field contains the number of parameters in the ExceptionInformation array that are specific to a particular type of exception, and the EXCEPTION_MAXIMUM_PARAMETERS definition is 15, i.e. this is the maximum number of parameters for all types of exceptions.

    The KiDispatchException function takes 5 parameters: ExceptionRecord - a pointer to an EXCEPTION_RECORD structure describing the cause of the exception; NonvolatileRegisters - pointer to constant registers; VolatileRegisters - pointer to volatile registers; PreviousMode - the context of the thread in which the exception occurred (user or kernel context); FirstChance - the first processing attempt (TRUE or FALSE). The function does not return any value.

    ExceptionRecord describes the cause of the exception. VolatileRegisters is generated by the gateway function (in this example, the KiDivideErrorFault function). NonvolatileRegisters are generated by the KiExceptionDispatch function. It should also be noted that both structures contain not only the values ​​of the registers at the time of the exception, but also other miscellaneous information that will not be discussed in this article, because it is not directly related to the topic under discussion. PreviousMode carries information about the context in which the exception occurred, and is equal to either KernelMode or UserMode. FirstChance is a Boolean value indicating whether this attempt to handle the exception is the first.

    The KiDispatchException function is responsible for handling the exception without involving the exception handlers themselves, if possible. Also, if an exception occurs in user space, then the exception handling is redirected to it. A simplified block diagram of the function is shown below in Figure 15.


    Figure 15

    As shown in the figure, at the beginning of its work, the function forms a CONTEXT structure from structures at the NonvolatileRegisters and VolatileRegisters pointers, and also fields that reflect processor registers that are not contained in these structures (for example, segment registers) are initialized with standard values. Therefore, this structure will reflect the values ​​of the processor registers at the time of the exception.

    Next, the function tries to handle the exception using the KiPreprocessFault function, without involving the exception handlers. If the exception cannot be processed, then if it occurs in the kernel context, the function calls the RtlDispatchException function, which performs a search and a call to the handler.

    After the RtlDispatchException function has completed its work, and since the fields of the CONTEXT structure could be changed by exception handlers, the fields of this structure are copied back to the structures using the NonvolatileRegisters and VolatileRegisters pointers by means of the KeContextToKframes function, thereby modifying the context of the interrupted stream.

    If an exception occurs in the user context, then the function is not called by the function handlers for security reasons, and instead the function copies the RSP and RIP value at the time of the exception to the user stack, copies the EXCEPTION_RECORD and CONTEXT structures to the user stack and modifies the kernel machine frame so that when returning from the function, control was transferred to the user mode handler.

    A pointer to a user mode handler is registered at the time of system initialization. The function responsible for handling exceptions in the user context is located in the ntdll.dll library called KiUserExceptionDispatch. Despite the fact that a custom exception handler is called for the user space, it is very similar to the kernel-mode handler, and therefore, no further explanation of its operation is required.

    2.2 Search and call the handler


    As mentioned earlier, the RtlDispatchException function performs the search and call of the handler. The function takes two parameters: ExceptionRecord - a pointer to an EXCEPTION_RECORD structure that describes the cause of the exception; ContextRecord - a pointer to a CONTEXT structure that describes the state of processor registers at the time of the exception. The function returns a boolean value, TRUE if the exception was handled, and FALSE otherwise.

    The RtlDispatchException function performs a sequential scan on the stack of called functions. If the function has a handler, then the RtlDispatchException function calls it. If the handler returns ExceptionContinueExecution, then the RtlDispatchException function stops working, otherwise the search for the handler continues. Below, in figure 16, a block diagram of the function is shown.


    Figure 16

    At the beginning of the work, the function receives the lower and upper limits of the stack. Since when the exception handler is called, it is passed a pointer to a structure that describes the state of the processor at the time the exception was thrown, and since the function performs virtual stack unwinding during the search, the contents of the transmitted CONTEXT structure will change, and therefore the function will copy its contents to its local variable.

    Next, the function forms the initial value of the ExceptionFlags field for the EXCEPTION_RECORD structure. It should be noted that the field of the transmitted structure may contain the set EXCEPTION_NONCONTINUABLE flag, which indicates that the continuation of the interrupted stream is not possible. Therefore, when initializing the initial value, the function copies this flag from the passed structure to a local variable. Then the function nullifies the frame pointer of the function, the exception handler of which, during its execution, raised an exception (i.e., a nested exception) and copies the address of the instruction that generated the exception from the passed EXCEPTION_RECORD structure to a local variable.

    Next, the function, by means of the RtlLookupFunctionEntry function, receives the address of the PE image and a pointer to the RUNTIME_FUNCTION function structure of this image, during which an exception has occurred. If the function did not return a pointer, then it is considered that an exception occurred during the execution of a simple function, which, as discussed earlier, does not have any promotion information. Because simple functions do not allocate memory on the stack, their RSP value will point to the return address, therefore, for such functions, the RtlDispatchException function extracts this address, copies its value to the Rip field of the local CONTEXT structure and increases the value of the Rsp field of the same structure by 8, so simulating the promotion of a simple function. Now the contents of the local CONTEXT structure describes the execution status of the next function on the stack above. Next, the function from the local CONTEXT structure copies the address of the instruction belonging to the next function on the stack above to the local variable and checks by means of the RtlpIsFrameInBounds function that the new RSP pointer is within the stack limit. If the pointer goes beyond these limits, it means that the exception handler was not found, and therefore, the RtlDispatchException function will return FALSE. Otherwise, the function will continue its work, starting with receiving 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. that the new RSP pointer is within the stack limit. If the pointer goes beyond these limits, it means that the exception handler was not found, and therefore, the RtlDispatchException function will return FALSE. Otherwise, the function will continue its work, starting with receiving 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. that the new RSP pointer is within the stack limit. If the pointer goes beyond these limits, it means that the exception handler was not found, and therefore, the RtlDispatchException function will return FALSE. Otherwise, the function will continue its work, starting with receiving 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 frame functions, the RtlLookupFunctionEntry function will return a pointer to the RUNTIME_FUNCTION structure. In this case, the promotion of such functions is performed using the function RtlVirtualUnwind, which will return the frame pointer for the untwisted function. Immediately after the promotion, a check is made that the frame pointer is within the stack limit. If the frame pointer goes beyond these limits, the RtlDispatchException function sets the EXCEPTION_STACK_INVALID flag in the ExceptionFlags field of the passed CONTEXT structure and returns FALSE. Otherwise, if the RtlVirtualUnwind function did not return a pointer to an exception handler for the untwisted function, then the RtlDispatchException function will continue to unwind the next function on the stack above, after copying the address of the instruction belonging to this function.

    If the RtlVirtualUnwind function returned a pointer to an exception handler, then the RtlDispatchException function will call it. Before calling it, the function will update the contents of the ExceptionFlags field of the passed 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 prepares the DISPATCHER_CONTEXT structure, which is actively used in cases of nested exception and active promotion (collided unwind). The structure definition is shown below in Figure 17.


    Figure 17

    The ControlPc field contains the address belonging to the body of the function for which the handler was called. The ImageBase field contains the address of the beginning of the PE image, which contains the function and its handler. The FunctionEntry field contains the address RUNTIME_FUNCTION of the structure of the same function. The EstablisherFrame field contains the frame pointer of the function. The TargetIp field is used for promotion, and will be discussed in detail during its discussion. The ContextRecord field contains a pointer to a CONTEXT structure that reflects the current state of the handler search, i.e. Pointer to a local variable of the RtlDispatchException function. The LanguageHandler field contains the address of the called handler. The HandlerData field contains the address for data specific to the corresponding programming language. The HistoryTable field contains a pointer to the search cache table. The ScopeIndex field contains the current value of the local variable of the RtlDispatchException function, and its purpose will be discussed in detail in the discussion of promotion. The Fill0 field is not used at all and is present for alignment purposes.

    The RtlDispatchException function does not call the exception handler directly, and instead uses the RtlpExecuteHandlerForException helper function, 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 exception handler and is used to catch exceptions that occurred during the execution of the exception handler itself. The assembler representation of the function is shown below in Figure 18.


    Figure 18

    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. In addition to the previously described reasons for its necessity, one more is added to them: since the exception handler is bound to the body of the function, if there is no placeholder, it will not be called, and therefore, the operation of the RtlDispatchException function will be violated additionally for this reason. The assembler representation of the exception handler function is shown below in Figure 19.


    Figure 19

    As shown in the figure, the handler first checks whether the promotion is running, and if it is, the function returns an ExceptionContinueSearch, thereby giving the promotion functions an indication of the continuation of the search engine. Otherwise, an exception handler was searched for, during which another exception occurred and it was necessary to copy the frame pointer of the function whose handler generated the new exception into the DISPATCHER_CONTEXT structure of the current handler search process.

    After the DISPATCHER_CONTEXT structure has been prepared, the RtlDispatchException function throws an exception handler. Immediately after calling the handler, the function sets the EXCEPTION_NONCONTINUABLE flag in its local flag copy if it was set in the passed EXCEPTION_RECORD structure by the handler. Next, the function resets the EXCEPTION_NESTED_CALL flag in the local copy and resets the frame pointer for the function, the exception handler of which, during its execution, raised an exception if the frame pointer of this function matches the previously fixed one. The following describes the corresponding actions of the functions depending on the result.

    If the handler returned ExceptionContinueSearch, the function will continue to promote the next function on the stack above, after copying the address belonging to this function and checking the value of the Rsp field of the local CONTEXT structure to go beyond the stack limit.

    If the handler returned ExceptionContinueExecution, then the function will immediately stop its operation and return TRUE. Previously, the function will verify that the EXCEPTION_NONCONTINUABLE flag is not set, otherwise the function will throw a STATUS_NONCONTINUABLE_EXCEPTION exception.

    If the handler throws an ExceptionNestedException, this means that a different, incomplete search process for the exception handler was found in the search process, in the context of which a new exception occurred. In this case, the EstablisherFrame field of the DISPATCHER_CONTEXT structure will contain the frame pointer of the function whose exception handler raised the exception. As mentioned above, this value copies the RtlpExecuteHandlerForException function exception handler there. The RtlDispatchException function will set the EXCEPTION_NESTED_CALL flag for the ExceptionFlags field and also update the frame pointer of the function whose handler raised the exception. This value will be updated only if the current value of the pointer is 0 (there were no nested exceptions), or the EstablisherFrame field of the DISPATCHER_CONTEXT structure contains a function frame pointer,

    If the handler returned ExceptionCollidedUnwind, this means that an active promotion was detected in the search process, in the context of which an exception occurred. This case will be described in more detail when describing the promotion of the stack, here it is only necessary to indicate that in response to this result, the RtlDispatchException function will update the DISPATCHER_CONTEXT structure and the local CONTEXT structure so that the search for the handler will be resumed from the place where the promotion was interrupted.

    In all other cases, the RtlDispatchException function will throw a STATUS_INVALID_DISPOSITION exception.

    Conclusion


    As already mentioned in Section 2, the entire process can be conditionally divided into two parts, and we fully considered the first. In the next part of the article, the second part will be considered, which includes the promotion of the stack and the principle of operation of try / except and try / finally blocks.

    Read Next