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