ReactOS源码中,通过系统调用/异常/中断进入内核,首先会遇到SYSCALL_PROLOG/TRAP_PROLOG之类的入口函数;当调KiServiceExit退出内核空间时,又会调用SYSCALL_PROLOG之类的出口函数。这些代码在R3-R0切换期间起到什么作用?本文旨在注释这部分及相关代码作用。
先看下在什么语境下会调用这些宏:
1).系统调用时调用该宏:
.func KiSystemService
TRAP_FIXUPS kss_a, kss_t, DoNotFixupV86, DoNotFixupAbios
_KiSystemService:
/* Enter the shared system call prolog */
SYSCALL_PROLOG kss_a, kss_t
/* Jump to the actual handler */
/*整个SYSCALL_PROLOG没修改eax*/
jmp SharedCode
.endfunc
再看看谁调用了_KiSystemService,以在用户空间中调用ReadFile函数为例。
ReadFile内部调用桩函数NtReadFile [Ring3],然后桩函数通过int 0x2E进入内核 [Ring0],而int 0x2E对应的idt入口点为_KiSystemService。进入内核后先调用SYSCALL_PROLOG形成调用栈,然后再跳到ShareCode中去执行API调用分发,最终执行内核中的NtReadFile。这个流程中调用了_KiSystemService,点题一次!而从r3进入r0的整个过程远不是几行文字能描述完整,预计会另辟一文注释其中过程。
2).发生异常时调用该宏:
.func KiTrap14
TRAP_FIXUPS kite_a, kite_t, DoFixupV86, DoNotFixupAbios
_KiTrap14:
/* Enter trap */
;生成KTRAP_FRAME框架保存类似中断框架
;最先push的,在KTRAP_FRAME结构的位置越靠后
;进入TRAP_PROLOG时,用esp为当前中断/异常开辟KTRAP_FRAME所需的栈空间,
;mov ebp,esp 使ebp指向KTRAP_FRAME,同时保存本次中断/异常的ebp于KTRAP_FRAME!ebp中
;由于可嵌套系统调用,将上一次发生中断/异常的ebp保存在当前KTRAP_FRAME!edx中
TRAP_PROLOG kite_a, kite_t
/* Check if we have a VDM alert */
cmp dword ptr PCR[KPCR_VDM_ALERT], 0
jnz VdmAlertGpf
...
发生缺页异常时,CPU到idt中寻找缺页异常的处理函数,该处理函数是_kiTrap14。_KiTrap14第一条语句也是TRAP_PROLOG。
关于异常处理的流程,也得另辟一文注释其中过程。
3).从内核退出时:
.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
先检测是否有待处理的APC,然后调用TRAP_EPILOG
综上所述,这几个宏在r3与r0之间的切换起到了重要的作用。在展开介绍宏之前,先看个结构,这里简单注释一下:
// Trap Frame Definition
//
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; //这个域也比较重要,会保存前一次进入中断时保存的KTRAP_FRAME栈顶,从而形成堆栈链
ULONG Ecx;
ULONG Eax;
ULONG PreviousPreviousMode; //前一次mode,cs段寄存器低2位决定
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList; //异常处理
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
//以下几个域比较重要,当中断发生前CPU为R3,则CPU会自动压入R3模式下的ss/esp,如果中断发生在R0模式CPU不会压入ss/esp的值,但是仍然会以此压入eflags cs ip的
//值,至于ErrCode,对于有些异常,CPU会自动压入,有些则不会。为了统一处理,对于不会产生ErrCode的异常,由系统(OS)自动往这个域填入0替代
ULONG ErrCode; //呼应下文观察点1
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
//V86模式,不知道干啥用的
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
经过SYSCALL_PPROLOG操作,就是在内核栈上开辟这个结构,然后把进入异常/中断的值依次填入这个结构的各个域中。
其中注释edx的功能,用来形成堆栈链,怎么理解?
先看个例子:
int StackChain2()
{
return 0;
}
int StackChain1()
{
StackChain2();
return 0;
}
int main()
{
StackChain1();
}
真是铁索连环般的嵌套调用,看看反汇编:
1: int StackChain2()
2: {
00401020 push ebp
00401021 mov ebp,esp
6: int StackChain1()
7: {
00401050 push ebp
00401051 mov ebp,esp
00401053 sub esp,40h
12: int main()
13: {
0040D480 push ebp
0040D481 mov ebp,esp
每个函数的入口处,通过push ebp保存调用者的栈底ebp,然后用本函数堆栈esp初始化本函数的栈帧ebp,形成堆栈链。这么看堆栈链也许还不是很明了,还是看下内存值:
图中每个函数可以通过保存在本函数栈底的前一个函数的栈帧,找到前一个函数的堆底。图上最左边是嵌套最深处的函数ChainStack2,往右依次是该函数的调用者ChainStack1 main。StackChain2栈底为12FED8,从此找到StackChain1的栈为12FF2C,而从12FF2C可以找到main函数的栈:12FF80。再从12FF80找到main函数的调用者的栈:12FFC0。什么main函数的调用者?淡定,这是crt库的事,初始化程序运行的堆栈,然后把运行权交给我们的程序。这是题外话,详见看雪的加密与解密。这样通过函数堆栈回溯可以找到最开始时的堆栈,如果调试器支持的话。如果仔细观察,栈顶一直是往减小的方向生长,开始时栈顶是FF80 然后FF2C 最后FED8。
至此,堆栈链的概念应该建立,然后,来看看ReactOS中怎么建立堆栈链,上代码:
//
// @name SYSCALL_PROLOG
//
// This macro creates a system call entry prologue.
// It should be used for entry into any fast-system call (KiGetTickCount,
// KiCallbackReturn, KiRaiseAssertion) and the generic system call handler
// (KiSystemService)
//
// @param Label
// Unique label identifying the name of the caller function; will be
// used to append to the name of the DR helper function, which must
// already exist.
//
// @remark None.
//
.macro SYSCALL_PROLOG Label EndLabel
/* Create a trap frame */
push 0 //观察点1
push ebp
push ebx
push esi
push edi
push fs //观察点1结束
/* Load PCR Selector into fs */
mov ebx, KGDT_R0_PCR
.byte 0x66
mov fs, bx
/* Get a pointer to the current thread */
/*#define PCR fs*/
mov esi, PCR[KPCR_CURRENT_THREAD]
/* Save the previous exception list */
/*
_EXCEPTION_REGISTRATION struc
prev dd ;下一个_EXCEPTION_REGISTRATION结构
handler dd ;异常处理函数地址
_EXCEPTION_REGISTRATION ends
保存当前SEH节点
*/
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 */
/*ebp指向KTRAP_FRAME顶部*/
mov ebp, esp //=====a)
/* Save the old trap frame pointer where EDX would be saved */
/*
后面第五条指令是mov [esi+KTHREAD_TRAP_FRAME], ebp 即本线程fs:[KTHREAD_TRAP_FRAME]用以
保存进入系统调用时的ebp的值;在没有运行mov [esi+KTHREAD_TRAP_FRAME], ebp 前,[esi+KTHREAD_TRAP_FRAME]
保存上面mov ebp,esp后新生成的ebp。由于win支持嵌套系统调用,每次fs:[KTHREAD_TRAP_FRAME]都保存ebp,如果
没有一个指针保存前一次的[esi+KTHREAD_TRAP_FRAME],从系统调用退出后,会无法恢复上次一堆栈
因此用[ebp+KTRAP_FRAME_EDX]作为指针,保存上一个ebp,形成一条链表,以此为依据搜索上一层堆栈框架
*/
mov ebx, [esi+KTHREAD_TRAP_FRAME] //=====b)
/*用ebp+KTRAP_FRAME_EDX中edx域保存前一个堆栈框架*/
mov [ebp+KTRAP_FRAME_EDX], ebx //=====c)
/* 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 ebp, esp 保存此次系统调用时的堆栈*/
mov [esi+KTHREAD_TRAP_FRAME], ebp //=====d)
cld
/* Save DR registers if needed */
jnz Dr_&Label
/* Set the trap frame debug header */
Dr_&EndLabel:
SET_TF_DEBUG_HEADER
/* Enable interrupts */
sti //=====e)
.endm
代码中用abcde)标示处即是生成堆栈链的。why?
假设一个场景,用户通过ReadFile进入内核空间,通过中断异常进入R0时,都是关中断的,能保证这个宏在一个CPU上完整执行,直到执行到e)处,开中断。期间,在a)mov ebp,esp使ebp指向R0上的KTRAP_FRAME结构;然后在d)处mov [esi+KTHREAD_TRAP_FRAME],ebp使得线程TrapFrame指针指向这个刚形成的结构。这时,CPU接收到一个中断或者执行遇到一个异常,CPU又会执行该宏,当再次执行到a)处时,内核上已经有个新的KTRAP_FRAME结构,但尚未保存,如果直接存放到KTHREAD!TrapFrame域中,会覆盖原本指向系统调用的KTRAP_FRAME结构,因此用本次栈帧中的edx域保存上次进入内核时的TRAP_FRAME结构指针,如此就不会丢失前后关系。当从中断退出,就能回退到上次系统调用的语境中。
这里有个疑问,代码中没有看到显式的分配一个KTRAP_FRAME结构,系统是如何保存这些寄存器值到KTRAP_FRAME中的?
回到代码开始部分,我加了观察点1的位置。仔细看push入栈的顺序依次是0 ebp ebx esi edi和fs。这和KTRAP_FRAME结构定义中注释有呼应观察点1处有点相似?只是顺序好像反了。好,仔细想想KTRAP_FRAME结构中
{ ...
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
ULONG ErrCode;
...
}
这几个域地址值是不是依次增大?而宏入口处push 0 ebp ebx esi edi和fs 地址值是不是依次在减小?如果把KTRAP_FRMAE结构每一项当做一个函数局部变量,那么进入函数时,要通过减小esp来一次性分配这儿多变量。减小esp的值最直接的办法是sub esp 4*n,如果换个方式,怎么表达这个减小的过程?执行push操作n次是不是也能达到等价的效果?为了在地址方向上保持结构中域增长关系,应该先分配结构尾部域然后逐渐减小esp的值分配结构中前部域。
回过头来看下ReactOS中的代码,push 0 push ebp 。。。正好对应这个关系。因此在ReactOS中,是通过这种方式在内核栈上分配KTRAP_FRAME结构变量。可是光push 0只填充了ErrCode,谁填充了Eip/SegCs/Eflags域?这是经过idt门时用CPU自行压入的。
前奏看完了,再看看后记TRAP_EPILOG:
这部分代码是PROLOG的逆操作:
.macro TRAP_EPILOG SystemCall, RestorePreviousMode, RestoreSegments, RestoreVolatiles, RestoreAllRegs
//进入本宏前,通过mov esp,ebp 用进入异常/中断时保存的调用帧(ebp)恢复esp,使得esp指向KTRAP_FRAME
#ifdef DBG
/* Assert the flags */
pushfd
pop edx
test edx, EFLAGS_INTERRUPT_MASK
jnz 6f
/* Assert the stack */
cmp esp, ebp
jnz 6f
/* Assert the trap frame */
#endif
5:
#ifdef DBG
sub dword ptr [esp+KTRAP_FRAME_DEBUGARGMARK], 0xBADB0D00
jnz 0f
/* Assert FS */
mov bx, fs
cmp bx, KGDT_R0_PCR
jnz 1f
/* Assert exception list */
cmp dword ptr PCR[KPCR_EXCEPTION_LIST], 0
jnz 2f
1:
push -1
call _KeBugCheck@4
#endif
2:
/* Get exception list */
mov edx, [esp+KTRAP_FRAME_EXCEPTION_LIST]
#ifdef DBG
/* Assert the saved exception list */
or edx, edx
jnz 1f
UNHANDLED_PATH
1:
#endif
/* Restore it */
/*
异常处理过程中,可能会添加新的SEH到fs:[0]中,这里截断新生成的SEH节点,恢复以前的SEH链表
*/
mov PCR[KPCR_EXCEPTION_LIST], edx
.if \RestorePreviousMode
/*发生系统调用时RestorePreviousMode==1*/
/* Get previous mode */
mov ecx, [esp+KTRAP_FRAME_PREVIOUS_MODE]
#ifdef DBG
/* Assert the saved previous mode */
cmp ecx, -1
jnz 1f
UNHANDLED_PATH
1:
#endif
/* Restore the previous mode */
mov esi, PCR[KPCR_CURRENT_THREAD]
mov byte ptr [esi+KTHREAD_PREVIOUS_MODE], cl
.else
#ifdef DBG
/* Assert the saved previous mode */
mov ecx, [esp+KTRAP_FRAME_PREVIOUS_MODE]
cmp ecx, -1
jz 1f
UNHANDLED_PATH
1:
#endif
.endif
/* Check for debug registers */
test dword ptr [esp+KTRAP_FRAME_DR7], ~DR7_RESERVED_MASK
jnz 2f
/* Check for V86 */
4:
test dword ptr [esp+KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK
jnz V86_Exit
/* Check if the frame was edited */
test word ptr [esp+KTRAP_FRAME_CS], FRAME_EDITED
jz 7f
.if \RestoreAllRegs
/* Check the old mode */
cmp word ptr [esp+KTRAP_FRAME_CS], KGDT_R3_CODE + RPL_MASK
bt word ptr [esp+KTRAP_FRAME_CS], 0
cmc
ja 8f
.endif
.if \RestoreVolatiles
/* Restore volatiles */
mov edx, [esp+KTRAP_FRAME_EDX]
mov ecx, [esp+KTRAP_FRAME_ECX]
mov eax, [esp+KTRAP_FRAME_EAX]
.endif
/* Check if we were called from kernel mode */
cmp word ptr [ebp+KTRAP_FRAME_CS], KGDT_R0_CODE
jz 9f
.if \RestoreSegments
/* Restore segment registers */
/*跳过调试寄存器,直接从GS处恢复*/
lea esp, [ebp+KTRAP_FRAME_GS]
pop gs
pop es
pop ds
.endif
/* Restore FS */
3:
/*[ebp+KTRAP_FRAME_FS]存入栈顶,然后pop fs*/
lea esp, [ebp+KTRAP_FRAME_FS]
pop fs
9:
/* Skip debug information and unsaved registers */
/*跳过段,直接从GS处恢复*/
lea esp, [ebp+KTRAP_FRAME_EDI]
pop edi
pop esi
pop ebx
pop ebp
/* Check for ABIOS */
cmp word ptr [esp+8], 0x80
ja AbiosExit
/* Pop error code */
/*从Pop error code看出,至此,堆栈已经是发生系统调用时的堆栈:eflag eip cs*/
add esp, 4
.if \SystemCall
/* Check if previous CS is from user-mode */
test dword ptr [esp+4], 1
/* It is, so use Fast Exit */
jnz FastExit
/* Jump back to stub */
/*
上文Pop error code->add esp, 4,堆栈上还剩eip cs eflag,
下面有条popf指令,猜测pop edx应该保存发生调用的地址
*/
pop edx //=====a)
pop ecx
popf
/*
结合注释Jump back to stub,jmp edx就是跳回到调用发生处
*/
jmp edx
.ret:
.endif
//异常 就从此退出?
iret