The user space exception mechanism is a simulation of the system space exception mechanism. In the kernel, not all exceptions are handled as soon as they enter "frame-based framework (seh)", but rather the entry of _ kitrap14 () and so on similar to vector interrupt, where it can be intercepted for priority processing, such as page switching and processing of copy-on-write pages. These processes are global rather than belong to a seh domain. This is equivalent to a global filtering. Only exceptions that do not belong to this level can be processed based on the seh framework.
To this end, each process in the user space also has a queue rtlpvectoredexceptionhead at the "vector Exception Handling" program entry. each node in the queue points to a specific exception handling function. Just like the "Register" interrupt handler in the kernel, you can also use rtladdvectoredexceptionhandler () to register with a volume exception handler in the user space, in this way, we can intercept the exception at the source. (However, in the Windows documents, we did not see the existence of user space vector Exception Processing, but it was found in wine code ). Here, rtlpexecutevectoredexceptionhand lers () is used to scan the queue first, so that each node in the queue can claim it in sequence. If it is claimed and processed, it will no longer be processed based on the seh framework, that is, the exceptionlist (the if statement condition in the code "! = "Suspected to be" = ", waiting for test). After processing, the system calls ntcontinue () to return to the kernel, after entering the kernel, the system will return the user space that was interrupted due to an exception. Therefore, the system call ntcontinue () is essentially a simulation of "interrupt return. When talking about the APC mechanism, the reader has seen that the APC function in the user space implements the "interrupt return" Through ntcontinue () (equivalent to the Linux system calling sigreturn ()).
If it is not processed in the vector Exception Processing queue, it is necessary to use rtldispatchexception () for Seh-based Exception Processing.
In the previous article, you have seen the rtldispatchexception () code, which is roughly as follows:
[Kiuserexceptiondispatcher ()> rtldispatchexception ()]
Rtldispatchexception(In pexception_record exceptionrecord,
In pcontext context)
{
......
/* Get the current stack limits and registration frame */
Rtlpgetstacklimits(& Stacklow, & stackhigh );
Registrationframe =Rtlpgetexceptionlist();
/* Now loop every frame */
While (registrationframe! = Prediction_chain_end)
{
.../* Scan exceptionlist until a node claims/processes this exception */
}
/* Unhandled, return false */
Return false;
}
However, it is in the kernel, as the first step of exception handling (firstchance), and is called by kidispatchexception () for exceptions occurring in the system space. Currently, this function is called in the user space. Can the functions in the kernel be directly called in the user space? No, this rtldispatchexception () is not the rtldispatchexception (). This is just "code reuse. After the rtldispatchexception () is compiled, it is connected to the ntdll. dll image (but not exported). The processed exceptionlist is in the user space, and the stack used is also the user space stack.
Predictionlist is obtained through rtlpgetexceptionlist.
_ Rtlpgetexceptionlist @ 0:
/* Return the exception list */
MoV eax, [FS: teb_exception_list]
RET
The constant teb_exception_list is defined as 0, so in short it is [FS: 0]. When the CPU runs in the system space, its segment register FS points to kpcr; when the CPU runs in the user space, it points to Teb. Therefore, rtlpgetexceptionlist () can always obtain the corresponding predictionlist pointer in system space or user space.
The same is true for rtlpgetstacklimits:
_ Rtlpgetstacklimits @ 8:
/* Get the stack limits */
MoV eax, [FS: teb_stack_limit]
MoV ECx, [FS: teb_stack_base]
/* Return them */
MoV edX, [esp + 4]
MoV [edX], eax
MoV edX, [esp + 8]
MoV [edX], ECx
/* Return */
RET 8
The constant teb_stack_limit is defined as 8, and teb_stack_base is defined as 4, which is the displacement of stacklimit and stackbase fields relative to the start point of Teb. In the kpcr data structure, the two fields are also in the same position. Of course, the former is used for the user space stack, and the latter is used for the system space stack. It can be seen that these data structures are indeed well-designed. On the other hand, the various data structures involved in processing the predictionlist are also used in both system space and user space.
In this way, the code of the same function rtldispatchexception () can be used both in the kernel and in the user space library. In this case, we do not have to read the rtldispatchexception () code again.
Return to kiuserexceptiondispatcher () and execute rtldispatchexception (). There are three possible results.
One is that the current exception is claimed by a node in the predictionlist, that is, it passes the filtering function smoothly and performs a long-range jump. Obviously, rtldispatchexception () will not be returned in this case. function call frameworks, including kiuserexceptiondispatcher (), are all crossed and discarded due to long-range jump. In addition, the two data structures copied to the user space Stack are also discarded due to the long-range jump.
The second type is the waste of time. () returns true, so the system calls ntcontinue () to complete the "interrupt return" operation ". Because the Context Data Structure on the stack contains the field reserved when the system space was originally entered due to an exception, it is finally returned to the location where the system space was interrupted due to the exception.
The third is that no node is claimed. The while loop in rtldispatchexception () consumes all nodes, so false is returned. This must be a problem. For example, the exceptionlist is damaged because it should not happen normally. As mentioned above, in the user space, the code of the entire thread is executed in a seh domain, so the exceptionlist must be non-empty and its last node (the earliest entry) that is the seh field entered in baseprocessstartup. The filter functions of this seh field generally return _ seh_execute_handler, so the visitor does not refuse. Therefore, since the exceptionlist has been exhausted, it must have encountered a serious problem. Therefore, you must call ntraiseexception () to cause a soft exception to go to the second step for the current exception. Note that when ntraiseexception () is called, the third parameter is false, indicating that this is not the first attempt.
Another possibility is that rtlraiseexception () has been called within rtldispatchexception (). For example, if rtlpexecutehandlerforexception () of a node returns predictioncontinueexecution, while the prediction_noncontinuable flag of exceptionflags in the exception record block is 1, it is necessary to use rtlraiseexception () to cause a soft exception of status_noncontinuable_exception.
Under normal circumstances, for call points, the system calls ntcontinue () and ntraiseexception () are not returned, and the return indicates the system call itself (rather than the exception processing) an error occurs, for example, a parameter error occurs. Therefore, if the two system calls are returned, the normal processing is exhausted, and you can only turn to soft exceptions. This is the call to rtlraiseexception.
The purpose of calling rtlraiseexception () is to simulate an exception. This "exception" is not the exception to be handled, but cannot be processed. At this time, it is not the problem that caused the original exception, but the problem that cannot be solved. Therefore, the original "Exception Code" exceptioncode may be status_access_violation, while the current exceptioncode is the error code status returned from ntraiseexception () or ntcontinue. Similarly, the status_noncontinuable_exception soft exception is initiated through rtldispatchexception. Because the exception to be initiated is not the exception to be handled, you need to prepare a new exception record block for rtlraiseexception.
On the other hand, the reason for initiating various types of soft exceptions through rtlraiseexception () is based on the premise that the exception will always be handled. This includes two aspects: first, the predictionlist should contain nodes that can claim/handle such exceptions, that is, the programs required to deal with such exceptions have been prepared. Second, even if no such node exists in the predictionlist, there is still a solution, that is, the three steps mentioned above, including debug, end the current thread, and even "dead. Note that "Dead Machine" can also be controlled or out of control, so that the CPU enters the stopped state is a controlled dead machine, which is better than letting the CPU jump in the program.
However, despite the difference between the exception and the original exception, it is related to the original exception, this should be known to the handler who claims the newly initiated exception. So there is a pointer exceptionrecord in the exception record block. In this case, let the new exception record block point to the original exception record block through this pointer. Therefore, a soft exception record block can be a string, and each record block can point to the cause of this exception. Only a hard exception record block will not point to another record block.
Apparently, rtlraiseexception () is an important function. Similarly, the code of this function is also kernel and user space amphibious. Note that the following call routes are only one of the many scenarios. In fact, there are many places to call this function.
[Kiuserexceptiondispatcher ()> rtlraiseexception ()]
Void ntapiRtlraiseexception(Pexception_record exceptionrecord)
{
Context context;
......
/* Capture the context */
Rtlcapturecontext(& Context );
/* Save the exception address */
Predictionrecord-> predictionaddress = rtlpgetexceptionaddress ();
/* Write the context flag */
Context. contextflags = context_full;
/* Check if we're being debugged (User-mode only )*/
If (! Rtlpcheckforactivedebugger (true ))
{
/* Raise an exception immediately */
Status =Zwraiseexception(Exceptionrecord, & context,True);
}
Else
{
/* Dispatch the exception and check if we shoshould continue */
If (Rtldispatchexception(Exceptionrecord, & context ))
{
/* Raise the exception */
Status =Zwraiseexception(Exceptionrecord, & context,False);
}
Else
{
/* Continue, go back to previous context */
Status =Zwcontinue(& Context, false );
}
}
/* If we returned, raise a status */
Rtlraisestatus(Status );
}
Call the exceptionrecord parameter to point to an exception record block. As mentioned above, this record block records the nature of exceptions and generally points to another exception record block through pointers.
First, get the context at that time through rtlcapturecontext (), and get the return address of rtlraiseexception () through rtlpgetexceptionaddress (), as the address where the exception occurred.
The specific operation varies with the result of rtlpcheckforactivedebugger. This function has two different versions: system space and user space. Currently, only the call parameters are returned for the system space version. Therefore, trye is always returned here. In the user space version, ntcurrentpeb ()-> beingdebugged is returned, so true is returned only during the test.
Therefore, if you call rtlraiseexception () in the user space, instead of being debugged, start the system to call zwraiseexception (). Note that the third parameter is true, indicating that this is the first effort. In this case, rtlraiseexception () is implemented by calling zwraiseexception. In addition, the function name used is zwraiseexception (), which can be called in both user space and system space.
If it is in the system space or the user space but debugging, call rtldispatchexception () to check whether there are nodes in the predictionlist of the space that can be claimed and processed. If a long-range jump is implemented, no result is returned. If the return value is true or false, the system starts to call zwraiseexception () or zwcontinue () respectively (). If rtldispatchexception () returns true, it indicates that a node in the predictionlist has been claimed, but a long-range jump is not executed, and the predictioncontinueexecution is returned. In this case, zwraiseexception () is used for the second effort, note that when zwraiseexception () is called, the 3rd parameter is false, indicating that the search for the predictionlist has failed. Next, we should take the second step. If rtldispatchexception () returns false, it indicates that no node in the root part of predictionlist can claim this exception. Therefore, zwcontinue () is used to directly return the obtained context, that is the location where rtlraiseexception () is called this time.
In our current scenario, rtlraiseexception () is called in kiuserexceptiondispatcher (), and there have been calls to zwraiseexception () or zwcontinue () before that. So now I want to call zwraiseexception () or zwcontinue () again? It is not repeated because different exception record blocks and different exception codes are used. This is a soft exception initiated for different reasons.
In addition, why should we directly call rtldispatchexception () to avoid debugging? The debugging tool is designed for exceptions under normal conditions, such as abnormal page access and Division 0, what happens now is a problem that should not have occurred in the process of handling exceptions. It is a system problem, not a problem that should be handled by a debugging tool. If a Soft Interrupt is initiated directly through zwraiseexception (), the program must be debugged first.
If this call to zwraiseexception () or zwcontinue () failed again (actually returned), the problem becomes more serious, so zwraiseexception () is used again () or the error code returned by zwcontinue () is the parameter call rtlraisestatus ().
[Kiuserexceptiondispatcher ()> rtlraiseexception ()> rtlraisestatus ()]
Void ntapiRtlraisestatus(Ntstatus status)
{
Prediction_record predictionrecord;
Context context;
Dprint1 ("rtlraisestatus (status 0x %. 08lx)/n", status );
/* Capture the context */
Rtlcapturecontext (& context );
/* Add one argument to ESP */
Context. ESP + = sizeof (pvoid );
/* Create an exception record */
Predictionrecord. exceptionaddress = rtlpgetexceptionaddress ();
Exceptionrecord. exceptioncode = status;
Predictionrecord. exceptionrecord = NULL;
Predictionrecord. numberparameters = 0;
Predictionrecord. exceptionflags = prediction_noncontinuable;
/* Write the context flag */
Context. contextflags = context_full;
/* Check if we're being debugged (User-mode only )*/
If (! Rtlpcheckforactivedebugger (true ))
{
/* Raise an exception immediately */
Zwraiseexception(& Exceptionrecord, & context,True);
}
Else
{
/* Dispatch the exception */
Rtldispatchexception (& exceptionrecord, & context );
/* Raise exception if we got here */
Status =Zwraiseexception(& Exceptionrecord, & context,False);
}
/* If we returned, raise a status */
Rtlraisestatus (Status );
}
The code of this function is similar to rtlraiseexception (), except that the new exception record block is created in the framework of this function and calls to zwcontinue () are excluded. It is particularly worth noting that this function may call itself recursively. If zwraiseexception () is returned again, it will call rtlraisestatus () again (). Obviously, the hope here lies in the difference between the call parameters of rtlraisestatus (), that is, the reason for the failure of zwraiseexception (), and the failure of zwraiseexception () is always handled properly once () returned. Note that the so-called "proper processing" also includes terminating or stopping the thread in step 3 of exception handling. If zwraiseexception () is always returned, it indicates that this problem occurs.
Readers may have a Gloomy impression on the outcome of exception handling. How can they always fail and fail? Note that this is the worst case, but the actual situation will not be so bad. As mentioned above, each thread runs in a seh domain, and the filter function of this seh domain is actually not rejected by the visitor. Besides, as long as there are no special cases, even if the processing based on predictionlist fails, steps 2 and 3 will be taken later. Under normal conditions, the last thread for a user space exception is to end the thread running through zwterminatethread (), and the last thread for a system space exception is to stop.
As mentioned above, zwraiseexception () means that the system calls ntraiseexception (). Generally, ntraiseexception () is used in the user space program, and zwraiseexception () is used in the kernel (). However, zwraiseexception () can also be used in the user space, which is set for "amphibious" code like rtlraiseexception (), in Ntdll. in the function table exported by DLL, ntraiseexception () and zwraiseexception () actually point to the same piece of code.
This system call simulates an exception. In a user space, an exception occurs in the user space. In a system space, an exception occurs in the system space. Readers may ask, we have previously seen the call to rtldispatchexception () in kiuserexceptiondispatcher (), scanning the exceptionlist with a new exception record block. Isn't this simulating an exception code? The difference is not small. Scanning exceptionlist is only one of the several steps for exception handling. The entire exception handling process can include three attempts. On the other hand, whether it is a real hard exception or a soft exception simulated by the system call, an exception framework will be formed on the system space stack (the trap framework formed by system calls is the same as this ). Directly calling rtldispatchexception () does not form a new exception framework.
The call interface for the system to call ntraiseexception () is as follows:
Ntsyscallapi ntstatus ntapi
Ntraiseexception(In pexception_record exceptionrecord,
In pcontext context, in Boolean searchframes );
The first two parameters are pointers to the exception record block and Context Structure respectively. The third parameter indicates whether to search for predictionlist. It is actually the firstchance we have seen before. If it is false, it indicates that the first attempt to handle this exception has failed.
The field predictioncode in the exception record block indicates the nature and type of the exception. This is a 32-bit unsigned Exception Code. It indicates an interrupt vector, such as status_integer_divide_by_zero and status_single_step. However, because it is not limited by hardware, the value range of the Exception Code is much wider than that of the interrupt vector. In essence, this is only an agreement between the exception initiator and the handler (filter function. Therefore, in addition to the Exception Code or status code defined by Microsoft, application developers can define new code as needed. For this reason, Microsoft has set the rules:
L Bit30-31 indicates severity: 0 = success, 1 = prompt, 2 = warning, 3 = error.
L bit29 indicates definition/User: 0 = defined by Microsoft, 1 = not defined by Microsoft.
L bit28 is not reserved. It must be 0.
L Bit16-27 represents a type.
L The Bit0-15 is the exception number, that is, the specific cause of the exception.
For example, status_network_session_expired is defined as 0xc000035c, that is, the severity is "error", defined by Microsoft, type code is 3, and the specific cause code is 0x5c.
Therefore, in the exception record block, the Exception Code indicates the nature and cause of the exception. Predictionflags provides the relevant status information in the form of a flag. Of course, for exceptions of different codes, these flag spaces can be explained differently.
The context is prepared for ntcontinue.
Whether it is ntraiseexception () or zwraiseexception (), the functions that implement this system call in the kernel are always ntraiseexception (). In the code of version 0.3.0 reactos, this is an assembly program:
[Ntraiseexception ()]
_ Ntraiseexception @ 12:
/* Note: We-must-be called by ZW * To have the right frame! */
/* Push the stack frame */
Push EBP
/* Get the current thread and restore its trap frame */
MoV EBX, [FS: kpcr_current_thread]
MoV edX, [EBP + ktrap_frame_edx]
MoV [EBX + kthread_trap_frame], EDX
/* Set up stack frame */
MoV EBP, ESP
/* Get the trap frame in EBX */
MoV EBX, [EBP + 0]
/* Get the exception list and restore */
MoV eax, [EBX + ktrap_frame_exception_list]
MoV [FS: kpcr_exception_list], eax
/* Get the parameters */
MoV edX, [EBP + 16]/* search frames */
MoV ECx, [EBP + 12]/* context */
MoV eax, [EBP + 8]/* exception record */
/* Raise the exception */
Push edX
Push EBX
Push 0
Push ECx
Push eax
Call_ Kiraiseexception @ 20
/* Restore trap frame in EBP */
Pop EBP
MoV ESP, EBP
/* Check the result */
Or eax, eax
JZ _ kiserviceexit2
/* Restore Debug Registers too */
JMP _ kiserviceexit
Before entering ntraiseexception (), a trap framework has been formed on the system space stack because of system calls. The "framework Pointer" EBP points to this framework. Here, we first get the pointer to the kthread data structure of the current thread through the kpcr structure, and then make the pointer trapframe in the kthread structure point to the current exception framework. In this way, you can use exgetpreviusmode () to obtain the correct "previous mode ".
But here is a question worth mentioning. The constant kpcr_current_thread is defined as 0x124, while the size of the kpcr structure is only 0x54. Therefore, the pointer to the kthread structure of the current thread is obviously not in kpcr. Originally, in addition to kpcr, each processor had a kprcb data structure (processor region control block ). The kpcr structure has a pointer prcb pointing to the paired kprcb data structure. However, the distance between the start point of the kprcb and kpcr data structures is fixed, that is, 0x120. The second long word in the kprcb structure is the pointer currentthread. Therefore, once the location of kpcr is determined, the location of the corresponding kprcb is determined. In this case, why not combine the two data structures into one? This is unknown. It may be a historical issue.
The actual processing is completed by kiraiseexception. After processing, if the returned value is STATUS_SUCCESS or 0, the returned value is returned from the exception through _ kiserviceexit2 or _ kiserviceexit respectively.
[Ntraiseexception ()> kiraiseexception ()]
Ntstatus ntapi
Kiraiseexception(Pexception_record exceptionrecord, pcontext context,
Pkexception_frame exceptionframe,
Pktrap_frame trapframe, Boolean searchframes)
{
Kprocessor_mode previusmode = kegetpreviusmode ();
Context localcontext;
Prediction_record localexceptionrecord;
Ulong parametercount, size;
Ntstatus status = STATUS_SUCCESS;
Dprint1 ("kiraiseexception/N ");
/* Set up seh */
_ Seh_try
{
/* Check the previous mode */
If (previusmode! = Kernelmode)
{
/* Validate the maximum parameters */
If (parametercount = predictionrecord-> numberparameters)>
Prediction_maximum_parameters)
{
/* Too large */
Status = status_invalid_parameter;
_ Seh_leave;
}
/* Probe the entire parameters now */
Size = (sizeof (prediction_record )-
(Exception_maximum_parameters-parametercount) * sizeof (ulong )));
Probeforread (exceptionrecord, size, sizeof (ulong ));
/* Now make copies in the stack */
Rtlmovememory(& Localcontext, context, sizeof (context ));
Rtlmovememory(& Localexceptionrecord, exceptionrecord, size );
Context = & localcontext;
Predictionrecord = & localexceptionrecord;
/* Update the parameter count */
Predictionrecord-> numberparameters = parametercount;
}
}
_ Seh_handle
{
Status = _ seh_getexceptioncode ();
}
_ Seh_end;
If (nt_success (Status ))
{
/* Convert the context record */
Kecontexttotrapframe(Context,
Predictionframe,
Trapframe,
Context-> contextflags,
Previusmode );
/* Dispatch the exception */
Kidispatchexception(Predictionrecord,
Predictionframe,
Trapframe,
Previusmode,
Searchframes );
}
/* Return the status */
Return status;
}
The predictionframe parameter is a pointer to the kexception_frame structure and is only used for PowerPC processors. Therefore, the passed real parameter is 0, and the trapframe parameter is a pointer to the trap framework on the stack. Other parameters are self-explanatory.
If this system call comes from the user space, the exception record block and Context Data Structure Used as the parameter are in the user space. Therefore, you must first use rtlmovememory () copy them to the temporary buffer of the system space. However, this may cause exceptions, so we need to have a seh domain to protect it.
After both the exception record block and the context data structure exist in the system space, you can call kidispatchexception (). The searchframes parameter becomes the firstchance parameter of kidispatchexception.
The code for kidispatchexception () does not need to be viewed again. In short, it is the aforementioned three steps and three efforts, mainly to search for predictionlist. However, which predictionlist is searched depends on the space in the "previous mode", that is, when zwraiseexception () is called:
L if the "previous mode" is the system mode, it is mainly used to search for the predictionlist of the system space. Under normal circumstances, a long-range jump will be performed and no return will be made, therefore, it will not be returned from this system call. If yes, it is returned to the place where zwraiseexception () is called.
L if "previous mode" is user mode, it is mainly to modify the return address in the exception framework on the stack, and then return, so that once it is returned to the user space, it enters kiuserexceptiondispatcher (), and scan the predictionlist of the user space. Under normal circumstances, the system will not return to the place where zwraiseexception () is called due to long-range jump. Alternatively, if you want to return to the place where zwraiseexception () is called without a long-range jump, you can use zwcontinue () to implement it through the system call.
After returning from kiraiseexception (), ntraiseexception () passes through _ kiserviceexit2 or _ kiserviceexit based on whether the returned value is 0:
_ Kiserviceexit:
/* Disable interrupts */
CLI
/* Check for, and deliver, user-mode APCs if needed */
Check_for_apc_deliver 1
......
/* Exit and cleanup */
Trap_epilog fromsystemcall, dorestorepreviousmode ,/
Donotrestoresegments, donotrestorevolatiles, dorestoreeverything
_ Kiserviceexit2:
/* Disable interrupts */
CLI
/* Check for, and deliver, user-mode APCs if needed */
Check_for_apc_deliver 0
/* Exit and cleanup */
Trap_epilog notfromsystemcall, dorestorepreviousmode ,/
Dorestoresegments, dorestorevolatiles, donotrestoreeverything
In the. s assembly code file, the format for defining and referencing macro operations is different. Fromsystemcall and notfromsystemcall are constants. The former is defined as 1, the latter is defined as 0, and so on.
The trap_epilog is obviously opposite to the trap_prolog, similar to the interrupt return, and the specific code is left to the reader.