用户态进入系统态有以下几种方式:
中断(Interrupt)。在允许中断的情况下,有外部设备的中断请求到达,CPU自动转入系统空间,并从预定地址执行指令。中断只发生在两条指令之间,不会打断正在执行的指令。对CPU而言,进入中断是被动的,所以中断的发生可以看作是“异步”的。
异常(Exception)。不管是用户空间或系统空间,执行指令失败都会引起一次“异常”,CPU因此转入系统空间,并从预定地址执行指令。异常在形式上与中断十分相似,只是异常发生在执行指令的过程中,发生异常后当前指令半途而废。异常同样也是被动无法预知的,除了那些故意引发异常的情况。
自陷(Trap)。为了让CPU主动进入系统空间,绝大多数CPU都设有专门的“自陷”指令。自陷与中断相似,就像一次由CPU主动发出的中断请求。自陷是主动的,是成功执行了一条自陷指令的结果。
“调用门”机制,但基本无人采用;“快速系统调用”机制,使用专门的sysenter、sysexit指令以及新增的三个寄存器。
早期Windows(Win2K及以前)使用自陷指令“int 0x2e”来实现系统调用。执行“int 0x2e”时,CPU首先从用户态切换成系统态,然后从“任务状态段”TSS(TR寄存器指向当前任务的TSS,ltr、str特权指令可设置TR寄存器)中装入本线程系统空间的SS寄存器和ESP寄存器,接着依次将用户空间的SS寄存器、ESP寄存器、EFLAGS标志寄存器、CS、EIP压入系统空间堆栈,关闭中断,最后从“中断向量表”IDT中以0x2e为“中断向量”,找到入口地址开始执行。系统调用返回则通过“中断返回”iret指令,实现上述的逆过程。
XP及以后,使用“快速系统调用”机制来实现系统调用(若CPU支持)。“快速系统调用”指令不涉及堆栈操作,执行sysenter指令时,CPU进入系统态,并把SYSENTER_CS_MSR(0x174编号)寄存器加载到CS中、SYSENTER_EIP_MSR(0x176编号)寄存器加载到EIP中、SYSENTER_CS_MSR寄存器内容+8加载到SS中、SYSENTER_ESP_MSR(0x175编号)寄存器加载到ESP中。MSR(Mode Specific Register)寄存器只能通过特权指令rdmsr和wrmsr进行读写。SYSENTER_CS_MSR被预先设为0x8(KGDT_R0_CODE),则SS段寄存器的取值就为0x10(KGDT_R0_DATA)。ENTER_ESP_MSR被设为KPRCB.DpcStack,这是一个不属于任何线程的独立堆栈,ENTER_EIP_MSR被设为KiFastCallEntry函数的地址。执行sysexit指令时,CPU进入用户态,并把SYSENTER_CS_MSR内容+16加载到CS中(KGDT_R3_CODE | RPL_MASK)、edx内容加载到EIP中、SYSENTER_CS_MSR内容+24加载到SS中(KGDT_R3_DATA | RPL_MASK)、ecx内容加载到ESP中。
以ReadFile函数为例,ReactOS系统调用的实现过程:
应用程序调用ReadFile(kernel32.dll中)函数读取文件内容,查看ReadFile函数源码(dll\win32\kernel32\file\rw.c),在检查验证相关参数后函数内部调用NtReadFile,NtReadFile在内核模块ntoskrnl.exe和ntdll.dll中都存在,内核模块中是真正的系统服务实现,ntdll.dll中的则是用户模式下的存根代码(从ring3进ring0调用系统服务),显然此处ReadFile函数中调用的是ntdll.dll的NtReadFile。
ntdll.dll所有系统服务的存根代码都是在编译时通过nci.exe工具自动生成的,具体详见tools\nci\ncitool.c,如NtReadFile部分为:
public _NtReadFile@36
_NtReadFile@36:
mov eax, 0beh
mov ecx, KUSER_SHARED_SYSCALL
call [ecx]
ret 24h
KUSER_SHARED_SYSCALL被定义为0x7FFE0300(include\ndk\asm.h),显然这里保存着一个函数指针,系统在初始化时(ntoskrnl\ps\psmgr.c)根据CPU是否支持快速系统调用,将此函数指针指向KiIntSystemCall或KiFastSystemCall。
先看KiIntSystemCall的实现(dll\ntdll\main\i386\dispatch.S):
_KiIntSystemCall@0:
/* Set stack in EDX and do the interrupt */
lea edx, [esp+8]
int 0x2E
/* Return to caller */
ret
KiIntSystemCall使用int 2e自陷指令进入内核调用系统服务的,其中eax存放着系统服务号,edx存放着参数地址。
再看KiFastSystemCall的实现(dll\ntdll\main\i386\dispatch.S):
_KiFastSystemCall@0:
/* Put ESP in EDX and do the SYSENTER */
mov edx, esp
sysenter
_KiFastSystemCallRet@0:
/* Just return to caller */
ret
KiFastSystemCall是使用sysenter快速系统调用指令进入内核调用系统服务的,其中eax存放着系统服务号,edx放着当前用户堆栈地址(根据此值可以得到如参数、返回地址等信息),相应的内核服务使用sysexit指令返回到KiFastSystemCallRet处。
中断描述符表(IDT)每项占8个字节,其地址被设置保存在IDTR寄存器中(线性地址),在include\ndk\i386\ketypes.h中定义为:
typedef struct _KIDTENTRY
{
USHORT Offset;
USHORT Selector;
USHORT Access;
USHORT ExtendedOffset;
} KIDTENTRY, *PKIDTENTRY;
Offset(低16位)和ExtendedOffset(高16位)组成一个32位的中断处理例程地址,Selector为段选择子,Access描述一些权限属性。在ntoskrnl\ke\i386\trap.s中定义了一张中断描述符表KiIdt(ntoskrnl\ke\i386\trap.s),其格式为方便人类阅读,和上述CPU要求的格式并不相同,因此系统初始化过程中会调用KeInitExceptions函数(ntoskrnl\ke\i386\exp.c)将其转换为intel定义的格式。
KiIdt中断表第2e项的中断例程函数为KiSystemService,就是系统服务的内核实现,由之前描述CPU执行自陷指令时自动压栈的一些动作可知,此时系统空间的堆栈存的内容为:
用户空间EIP值 --------- 低地址
用户空间CS值
EFLAGS寄存器值
用户空间ESP值
用户空间SS值 ---------- 高地址
看一下函数的实现(ntoskrnl\ke\i386\trap.s):
_KiSystemService:
/* Enter the shared system call prolog */
SYSCALL_PROLOG kss_a, kss_t
/* Jump to the actual handler */
jmp SharedCode
SYSCALL_PROLOG是个宏,其定义(ntoskrnl\include\internal\i386\asmmacro.S):
.macro SYSCALL_PROLOG Label EndLabel
/* Create a trap frame */
push 0
push ebp
push ebx
push esi
push edi
push fs
/* Load PCR Selector into fs */
mov ebx, KGDT_R0_PCR
.byte 0x66
mov fs, bx
/* Get a pointer to the current thread */
mov esi, PCR[KPCR_CURRENT_THREAD]
/* Save the previous exception list */
push PCR[KPCR_EXCEPTION_LIST]
/* Set the exception handler chain terminator */
mov dword ptr PCR[KPCR_EXCEPTION_LIST], -1
/* Save the old previous mode */
push [esi+KTHREAD_PREVIOUS_MODE]
/* Skip the other registers */
sub esp, 0x48
/* Set the new previous mode based on the saved CS selector */
mov ebx, [esp+0x6C]
and ebx, 1
mov byte ptr [esi+KTHREAD_PREVIOUS_MODE], bl
/* Go on the Kernel stack frame */
mov ebp, esp
/* Save the old trap frame pointer where EDX would be saved */
mov ebx, [esi+KTHREAD_TRAP_FRAME]
mov [ebp+KTRAP_FRAME_EDX], ebx
/* Flush DR7 */
and dword ptr [ebp+KTRAP_FRAME_DR7], 0
/* Check if the thread was being debugged */
test byte ptr [esi+KTHREAD_DEBUG_ACTIVE], 0xFF
/* Set the thread's trap frame and clear direction flag */
mov [esi+KTHREAD_TRAP_FRAME], ebp
cld
/* Save DR registers if needed */
jnz Dr_&Label
/* Set the trap frame debug header */
Dr_&EndLabel:
SET_TF_DEBUG_HEADER
/* Enable interrupts */
sti
.endm
SYSCALL_PROLOG首先构建一个栈帧,这是一个系统调用约定的框架结构,所有自陷、中断、异常处理过程都使用这样一个相同的栈帧用来恢复返回到用户空间。接着把KGDT_R0_PCR(0x30)加载到fs段寄存器中,因为Windows规定运行在内核时fs选择子必须指向KPCR结构,0x30表示为RPL0环、GDT表中的第6项。KPCR定义为(include\ddk\winddk.h):
typedef struct _KPCR {
KPCR_TIB Tib; /* 00 */
struct _KPCR *Self; /* 1C */
struct _KPRCB *Prcb; /* 20 */
KIRQL Irql; /* 24 */
ULONG IRR; /* 28 */
ULONG IrrActive; /* 2C */
ULONG IDR; /* 30 */
PVOID KdVersionBlock; /* 34 */
PUSHORT IDT; /* 38 */
PUSHORT GDT; /* 3C */
struct _KTSS *TSS; /* 40 */
USHORT MajorVersion; /* 44 */
USHORT MinorVersion; /* 46 */
KAFFINITY SetMember; /* 48 */
ULONG StallScaleFactor; /* 4C */
UCHAR SpareUnused; /* 50 */
UCHAR Number; /* 51 */
} KPCR, *PKPCR; /* 54 */
其中KPCR_TIB定义为(include\ddk\winddk.h):
typedef struct _KPCR_TIB {
PVOID ExceptionList; /* 00 */
PVOID StackBase; /* 04 */
PVOID StackLimit; /* 08 */
PVOID SubSystemTib; /* 0C */
_ANONYMOUS_UNION union {
PVOID FiberData; /* 10 */
DWORD Version; /* 10 */
} DUMMYUNIONNAME;
PVOID ArbitraryUserPointer; /* 14 */
struct _KPCR_TIB *Self; /* 18 */
} KPCR_TIB, *PKPCR_TIB; /* 1C */
继续往下看,把edi赋值为PCR[KPCR_CURRENT_THREAD],PCR是个宏被定义为“fs:”,而KPCR_CURRENT_THREAD定义为0x124,实际上从KPCR开始位置偏移0x120处保存着另一个名为KPRCB的数据结构(include\ndk\i386\ketypes.h),KPRCB结构定义很大,在其0x04处是一个KTHREAD*的CurrentThread指针成员。
KPCR_EXCEPTION_LIST定义为0,KTHREAD_PREVIOUS_MODE定义为0xD7,代码中把KPCR.Tib. ExceptionList和KPRCB.CurrentThread->PreviousMode压入栈保存,并将KPCR.Tib. ExceptionList置-1,即链表终止。esp减0x48是给调试寄存器等留空间,然后栈帧中cs段最低位赋值给KPRCB.CurrentThread->PreviousMode,先前模式0为内核模式,1为用户模式。
用户模式下调用系统服务,cs段CPL一定为3,赋值后先前模式就为1了。但如果在内核中调用系统服务,cs段CPL为0,则先前模式就变为0,系统服务在返回时根据先前模式的不同会有不同的动作。举例来说,NtReadFile和ZwReadFile,在用户存根(ntdll.dll)代码里,这两个函数没区别,而内核中的NtReadFile和ZwReadFile,内核NtReadFile是具体的实现是被系统服务所调用的,内核ZwReadFile则是模拟自陷指令构建一个栈帧,从而进入系统服务入口(KiSystemService),自然这里的先前模式就为0了。
到这里可知自陷、中断、异常所共同使用的栈帧结构的完整框架定义为(ndk\i386\ketypes.h):
typedef struct _KTRAP_FRAME
{
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;
ULONG Ecx;
ULONG Eax;
ULONG PreviousPreviousMode; // 0x48
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
ULONG ErrCode;
ULONG Eip; // 0x68
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
接着看下面的代码,KTHREAD_TRAP_FRAME定义为0x0110,KTRAP_FRAME_EDX定义为0x3C,将KPRCB.CurrentThread->TrapFrame保存到栈帧的edx寄存器(进入前edx指向参数地址,返回后此寄存器无关紧要,所以可以拿来使用),并将KPRCB.CurrentThread->TrapFrame指向当前已构建的栈帧地址,KTRAP_FRAME_DR7定义为0x2C,即把栈帧的dr7寄存器清0。DF标志位复位,esi、edi串操作时向高地址自增。
检测KPRCB.CurrentThread->DispatcherHeader.DebugActive值,如果调试状态下跳转至Dr_kss_a标签处,非调试继续往下走,是个SET_TF_DEBUG_HEADER宏,此宏定义为:
.macro SET_TF_DEBUG_HEADER
/* Get the Debug Trap Frame EBP/EIP */
mov ebx, [ebp+KTRAP_FRAME_EBP]
mov edi, [ebp+KTRAP_FRAME_EIP]
/* Write the debug data */
mov [ebp+KTRAP_FRAME_DEBUGPOINTER], edx
mov dword ptr [ebp+KTRAP_FRAME_DEBUGARGMARK], 0xBADB0D00
mov [ebp+KTRAP_FRAME_DEBUGEBP], ebx
mov [ebp+KTRAP_FRAME_DEBUGEIP], edi
.endm
主要针对栈帧中调试寄存器做一些赋值,最后sti开中断。
消化一下,然后看KiSystemService的后一段:jmp SharedCode,SharedCode标签的定义(ntoskrnl\ke\i386\trap.s):
SharedCode:
/*
* Find out which table offset to use. Converts 0x1124 into 0x10.
* The offset is related to the Table Index as such: Offset = TableIndex x 10
*/
mov edi, eax
shr edi, SERVICE_TABLE_SHIFT
and edi, SERVICE_TABLE_MASK
mov ecx, edi
/* Now add the thread's base system table to the offset */
add edi, [esi+KTHREAD_SERVICE_TABLE]
/* Get the true syscall ID and check it */
mov ebx, eax
and eax, SERVICE_NUMBER_MASK
cmp eax, [edi+SERVICE_DESCRIPTOR_LIMIT]
/* Invalid ID, try to load Win32K Table */
jnb KiBBTUnexpectedRange
/* Check if this was Win32K */
cmp ecx, SERVICE_TABLE_TEST
jnz NotWin32K
/* Get the TEB */
mov ecx, PCR[KPCR_TEB]
/* Check if we should flush the User Batch */
xor ebx, ebx
ReadBatch:
or ebx, [ecx+TEB_GDI_BATCH_COUNT]
jz NotWin32K
/* Flush it */
push edx
push eax
//call [_KeGdiFlushUserBatch]
pop eax
pop edx
NotWin32K:
/* Increase total syscall count */
inc dword ptr PCR[KPCR_SYSTEM_CALLS]
#ifdef DBG
/* Increase per-syscall count */
mov ecx, [edi+SERVICE_DESCRIPTOR_COUNT]
jecxz NoCountTable
inc dword ptr [ecx+eax*4]
#endif
/* Users's current stack frame pointer is source */
NoCountTable:
mov esi, edx
/* Allocate room for argument list from kernel stack */
mov ebx, [edi+SERVICE_DESCRIPTOR_NUMBER]
xor ecx, ecx
mov cl, [eax+ebx]
/* Get pointer to function */
mov edi, [edi+SERVICE_DESCRIPTOR_BASE]
mov ebx, [edi+eax*4]
/* Allocate space on our stack */
sub esp, ecx
/* Set the size of the arguments and the destination */
shr ecx, 2
mov edi, esp
/* Make sure we're within the User Probe Address */
cmp esi, _MmUserProbeAddress
jnb AccessViolation
CopyParams:
/* Copy the parameters */
rep movsd
/* Do the System Call */
call ebx
AfterSysCall:
#ifdef DBG
/* Make sure the user-mode call didn't return at elevated IRQL */
test byte ptr [ebp+KTRAP_FRAME_CS], MODE_MASK
jz SkipCheck
mov esi, eax /* We need to save the syscall's return val */
call _KeGetCurrentIrql@0
or al, al
jnz InvalidIrql
mov eax, esi /* Restore it */
/* Get our temporary current thread pointer for sanity check */
mov ecx, PCR[KPCR_CURRENT_THREAD]
/* Make sure that we are not attached and that APCs are not disabled */
mov dl, [ecx+KTHREAD_APC_STATE_INDEX]
or dl, dl
jnz InvalidIndex
mov edx, [ecx+KTHREAD_COMBINED_APC_DISABLE]
or edx, edx
jnz InvalidIndex
#endif
SkipCheck:
/* Deallocate the kernel stack frame */
mov esp, ebp
KeReturnFromSystemCall:
/* Get the Current Thread */
mov ecx, PCR[KPCR_CURRENT_THREAD]
/* Restore the old trap frame pointer */
mov edx, [ebp+KTRAP_FRAME_EDX]
mov [ecx+KTHREAD_TRAP_FRAME], edx
.endfunc
.func KiServiceExit
_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
.endfunc
不得不说好长啊,慢慢看不急,一想到神功即成,难掩兴奋之情:)。
分段详解,前面知道由int 0x2e自陷进入KiSystemService系统服务时,eax存放着系统调用号,edx指向参数地址。这里先将调用号右移8位再与0x10相与,结果存入ecx,ecx不是等于0x00就是等于0x10。合起来看的意思是,根据调用号范围,0x1000以下或以上,ecx有不同的偏移量。接着KPRCB.CurrentThread->ServiceTable加上偏移量,ServiceTable指向系统服务描述表,结构定义为(include\ndk\ketypes.h):
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
PULONG_PTR Base;
PULONG Count;
ULONG Limit;
#if defined(_IA64_)
LONG TableBaseGpOffset;
#endif
PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
X86系统有两组服务描述表,分别为KeServiceDescriptorTable[2]和KeServiceDescriptorTableShadow[2]( ntoskrnl\ke\procobj.c),两组表第0项的基址都指向基本系统调用函数表MainSSDT(obj-i386\ntoskrnl\include\internal\napi.h),调用号0x1000以下的函数地址均在此表中。不同的是KeServiceDescriptorTable[1]内容为空,而KeServiceDescriptorTableShadow[1].Base指向扩充的Win32k系统调用函数表Win32kSSDT(obj-i386\subsystems\win32\win32k\include\napi.h),即调用号在0x1000及以上的函数地址表。
KPRCB.CurrentThread->ServiceTable不是指向KeServiceDescriptorTable就是指向KeServiceDescriptorTableShadow。若调用号小于Limit并且ecx偏移不等0x10,则跳转NotWin32K标签处。
KPRCB.KeSystemCalls引用计数加1,得到系统调用的参数字节,得到函数表中的函数地址,在检测edx参数地址不越系统空间界后便开始在当前栈分配空间,拷贝用户栈参数内容。之后调用服务实现函数。
返回后,esp恢复到栈帧地址,从栈帧中恢复原先的KPRCB.CurrentThread->TrapFrame,关中断,后面是CHECK_FOR_APC_DELIVER和TRAP_EPILOG两个宏,CHECK_FOR_APC_DELIVER宏暂时先略过,它的作用是检查是否有执行用户空间APC的请求,为其做好准备。
TRAP_EPILOG又是一个长篇大宏,完整定义在(ntoskrnl\include\internal\i386\asmmacro.S)中,主要操作有:恢复先前的KPCR.Tib. ExceptionList;恢复先前的KPRCB.CurrentThread->PreviousMode;恢复先前的fs段寄存器;恢复先前的edi、esi、ebx、ebp寄存器;栈帧cs段CPL为用户态时跳转至FastExit:
.ret:
iret
FastExit:
/* Is SYSEXIT Supported/Wanted? */
cmp dword ptr ss:[_KiFastSystemCallDisable], 0
jnz .ret
test dword ptr [esp+8], EFLAGS_TF
jnz .ret
/* Restore FS to TIB */
mov ecx, KGDT_R3_TEB + RPL_MASK
mov fs, ecx
/* We will be cleaning up the stack ourselves */
pop edx /* New Ring 3 EIP */
add esp, 4 /* Skip Ring 3 DS */
and dword ptr [esp], 0xfffffdff /* Remove EFLAGS_INTERRUPT_MASK from EFLAGS */
popf /* Restore old EFLAGS */
pop ecx /* Old Ring 3 SS:ESP */
/*
* At this point:
* ECX points to the old User Stack.
* EDX points to the instruction to execute in usermode after the sysenter
*/
sti
sysexit
如果CPU不支持快速系统调用或单步调试TF标志有效,则通过iret指令返回。否则把fs赋值为0x3B(KGDT_R3_TEB+RPL_MASK),即RPL为3,GDT表的第7项。加载栈帧的EIP到edx寄存器中、加到栈帧的EFLAGS并清除IF标志(中断允许位),加到栈帧的ESP到ecx寄存器中,开中断,执行sysexit返回。
看完了int 0x2e具体的调用过程,再来看sysenter的流程,前面已述,执行sysenter后便跳转到了KiFastCallEntry(ntoskrnl\ke\i386\trap.s),其定义为:
_KiFastCallEntry:
/* Enter the fast system call prolog */
FASTCALL_PROLOG FastCallDrSave, FastCallDrReturn
SharedCode:
…………
SharedCode已经很熟悉了,是和前面KiSystemService系统服务入口跳转共用的部分,重点看一下FASTCALL_PROLOG宏的定义(ntoskrnl\include\internal\i386\ asmmacro.S):
.macro FASTCALL_PROLOG Label EndLabel
/* Set user selector */
mov ecx, KGDT_R3_DATA | RPL_MASK
/* Set FS to PCR */
push KGDT_R0_PCR
pop fs
/* Set DS/ES to User Selector */
mov ds, cx
mov es, cx
/* Set the current stack to Kernel Stack */
mov ecx, PCR[KPCR_TSS]
mov esp, [ecx+KTSS_ESP0]
/* Set up a fake INT Stack. */
push KGDT_R3_DATA + RPL_MASK
push edx /* Ring 3 SS:ESP */
pushf /* Ring 3 EFLAGS */
push 2 /* Ring 0 EFLAGS */
add edx, 8 /* Skip user parameter list */
popf /* Set our EFLAGS */
or dword ptr [esp], EFLAGS_INTERRUPT_MASK /* Re-enable IRQs in EFLAGS, to fake INT */
push KGDT_R3_CODE + RPL_MASK
push dword ptr ds:KUSER_SHARED_SYSCALL_RET
/* Setup the Trap Frame stack */
push 0
push ebp
push ebx
push esi
push edi
push KGDT_R3_TEB + RPL_MASK
/* Save pointer to our PCR */
mov ebx, PCR[KPCR_SELF]
/* Get a pointer to the current thread */
mov esi, [ebx+KPCR_CURRENT_THREAD]
/* Set the exception handler chain terminator */
push [ebx+KPCR_EXCEPTION_LIST]
mov dword ptr [ebx+KPCR_EXCEPTION_LIST], -1
/* Use the thread's stack */
mov ebp, [esi+KTHREAD_INITIAL_STACK]
/* Push previous mode */
push UserMode
/* Skip the other registers */
sub esp, 0x48
/* Make space for us on the stack */
sub ebp, 0x29C
/* Write the previous mode */
mov byte ptr [esi+KTHREAD_PREVIOUS_MODE], UserMode
/* Sanity check */
cmp ebp, esp
jnz BadStack
/* Flush DR7 */
and dword ptr [ebp+KTRAP_FRAME_DR7], 0
/* Check if the thread was being debugged */
test byte ptr [esi+KTHREAD_DEBUG_ACTIVE], 0xFF
/* Set the thread's trap frame */
mov [esi+KTHREAD_TRAP_FRAME], ebp
/* Save DR registers if needed */
jnz Dr_&Label
/* Set the trap frame debug header */
Dr_&EndLabel:
SET_TF_DEBUG_HEADER
/* Enable interrupts */
sti
.endm
开头依旧是把指向KPCR的选择子赋给fs段寄存器,并把用户态的数据段选择子赋值给ds、es。将esp赋值为KPCR.TSS->Esp0,即当前线程的系统空间堆栈。然后手动构建一个自陷、中断、异常共同约定的栈帧框架,并把当前EFLAGS标志清0(位1保留位必须为1),edx+8修正为指向用户堆栈的参数,而栈帧中的返回地址EIP设为ds:KUSER_SHARED_SYSCALL_RET(0x7FFE0000+0x304),即KiFastSystemCallRet。最后把栈帧Dr7清0,不在调试状态下,开启中断,继续执行之后的SharedCode。
从内核中发起系统调用:
在内核中进行系统调用前,也就是调用KiSystemService函数前,需在当前堆栈构建栈帧框架,但不必是完全的,如栈帧的SS、ESP等就可不要,因为内核调用后还是返回内核状态,就不再需要进行堆栈切换了。eax存入系统服务号,edx指向参数地址,EFLAGS压栈,KGDT_R0_CODE压入,call _KiSystemService就OK了。
最终SharedCode返回时,有如下代码 :
test dword ptr [esp+4], 1
/* It is, so use Fast Exit */
jnz FastExit
/* Jump back to stub */
pop edx
pop ecx
popf
jmp edx
[esp+4]为栈帧的CS内容,显然,如果先前模式为0,即在内核中发起系统服务调用的,不会跳转至FastExit,而是直接清理残余栈帧后,返回到调用处。