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