用戶內核模式切換(Windows內核學習筆記)

Inter x86將處理器執行模式分爲4層環,我們平時說的用戶層程序處於3環(Ring3),內核層代碼處於0環(Ring0),而Ring3的地址空間是各進程之間獨立的,Ring0層是進程之間共享,不同進程會由進程上下背景文的切換來實現對Ring0層訪問。Windows中並沒有使用Ring1和Ring2層。
我們的用戶層進程通過系統服務來完成我們需要完成的工作,而系統服務是在內核層實現的,那麼就需要CPU就要從Ring3層的調用切入Ring0層,那麼這個過程是如何實現的?
進入內核一般有中斷、異常、自陷三種手段,而如果是我們平時函數正常系統調用,會通過自陷指令(int 0x2e)讓CPU主動進入系統空間,對於應用程序而言,相當於執行了一次函數調用。還有一種稱爲快速調用約定的sysenter和sysexit指令,也是調用系統服務,併爲此配備了SYSENTER_CS_MSR、SYSENTER_EIP_MSR、SYSENTER_ESP_MSR三個MSR(Mode Specific Register)寄存器。
接下來說一說這二者分別是怎麼進行執行的,首先來說傳統的Windows系統調用自陷指令int 0x2e:
以經典的ReadFile函數(因爲我看的書是拿這個函數舉例的)來進行解釋,我們調用一個WinAPI ReadFile函數,其位於kernel32.dll的導出函數中,在用戶層這個函數內部調用的是ntdll下的Nt系列的函數NtReadFile,該函數就是一個Windows的系統調用,內核中也有與之同名的函數。衆所周知,ntdll下有兩個函數,一個叫做ZwReadFile,一個叫做NtReadFile,二者指向同一片地址:
在這裏插入圖片描述
其會將eax傳入系統服務索引,調用KiIntSystemCall:

.text:77F062F8 ZwReadFile      proc near               ; CODE XREF: RtlGetSetBootStatusData+72p
.text:77F062F8                                         ; RtlGetSetBootStatusData:loc_77F4E917p ...
.text:77F062F8
.text:77F062F8 FileHandle      = dword ptr  4
.text:77F062F8 Event           = dword ptr  8
.text:77F062F8 ApcRoutine      = dword ptr  0Ch
.text:77F062F8 ApcContext      = dword ptr  10h
.text:77F062F8 IoStatusBlock   = dword ptr  14h
.text:77F062F8 Buffer          = dword ptr  18h
.text:77F062F8 Length          = dword ptr  1Ch
.text:77F062F8 ByteOffset      = dword ptr  20h
.text:77F062F8 Key             = dword ptr  24h
.text:77F062F8
.text:77F062F8                 mov     eax, 111h       ; NtReadFile
.text:77F062FD                 mov     edx, 7FFE0300h ;此處存儲着一個函數指針指向KiIntSystemCall或者KiFastSystemCall
.text:77F06302                 call    dword ptr [edx]
.text:77F06304                 retn    24h
.text:77F06304 ZwReadFile      endp

(Ring3對Ring0的調用通過堆棧傳遞參數),需要利用內核中的函數真正完成操作,將edx指向堆棧參數塊起點,就需要調用int 0x2e自陷指令切入內核,
這條指令放在KiIntSystemCall中:
在這裏插入圖片描述
當CPU通過自陷指令進入內核時,CPU會自動將用戶空間的CS\ESP\EFLAGS\CS\EIP寄存器的值,壓入內核堆棧,這些寄存器的值是從內核返回用戶層所必須的:
在這裏插入圖片描述
我們通過Windbg來看看中斷服務描述表的0x2e處的索引到底是何方神聖:
在這裏插入圖片描述
在這裏插入圖片描述
一連串的push指令是用來保存寄存器中的值,保護用戶層切換到內核層時的數據,以便返回用戶層時可以順利返回。在這裏的push 0需要注意,它的作用是在堆棧中保存一個位置,返回時該位置將被設置爲返回值。當CPU時因爲異常進入內核時,會把一個異常碼壓入堆棧,但是通過自陷或者中斷進入時,就沒有這個異常碼,所以此處設置0是爲了保證框架有一個統一的大小。
還有就是fs段寄存器在此處賦值爲0x30h,30h轉換即0011(3)0000(0),段選擇碼的低2位表示運行在哪一環,0表示0環,3表示3環,倒數第三位表示GDT(0)或者LDT(1),剩下的位數表示GDT或者IDT中的下標,所以此處表示0環的GDT表中的索引爲6的數據。此時fs在內核中指向的是一個KPCR的內核結構體,接下來又有語句:
848426d7 648b3524010000 mov esi,dword ptr fs:[124h]
其中的0x124是在fs段中的偏移,實際指向了當前內核線程體_KTHREAD:

1: kd> dt _KPCR
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Used_StackBase   : Ptr32 Void
   +0x008 Spare2           : Ptr32 Void
   +0x00c TssCopy          : Ptr32 Void
   +0x010 ContextSwitches  : Uint4B
   +0x014 SetMemberCopy    : Uint4B
   +0x018 Used_Self        : Ptr32 Void
   +0x01c SelfPcr          : Ptr32 _KPCR
   +0x020 Prcb             : Ptr32 _KPRCB
   +0x024 Irql             : UChar
   +0x028 IRR              : Uint4B
   +0x02c IrrActive        : Uint4B
   +0x030 IDR              : Uint4B
   +0x034 KdVersionBlock   : Ptr32 Void
   +0x038 IDT              : Ptr32 _KIDTENTRY
   +0x03c GDT              : Ptr32 _KGDTENTRY
   +0x040 TSS              : Ptr32 _KTSS
   +0x044 MajorVersion     : Uint2B
   +0x046 MinorVersion     : Uint2B
   +0x048 SetMember        : Uint4B
   +0x04c StallScaleFactor : Uint4B
   +0x050 SpareUnused      : UChar
   +0x051 Number           : UChar
   +0x052 Spare0           : UChar
   +0x053 SecondLevelCacheAssociativity : UChar
   +0x054 VdmAlert         : Uint4B
   +0x058 KernelReserved   : [14] Uint4B
   +0x090 SecondLevelCacheSize : Uint4B
   +0x094 HalReserved      : [16] Uint4B
   +0x0d4 InterruptMode    : Uint4B
   +0x0d8 Spare1           : UChar
   +0x0dc KernelReserved2  : [17] Uint4B
   +0x120 PrcbData         : _KPRCB
   //查看_KPRCB結構
1: kd> dt _KPRCB
nt!_KPRCB
   +0x000 MinorVersion     : Uint2B
   +0x002 MajorVersion     : Uint2B
   +0x004 CurrentThread    : Ptr32 _KTHREAD//這就是0x124的偏移地址
   +0x008 NextThread       : Ptr32 _KTHREAD
   +0x00c IdleThread       : Ptr32 _KTHREAD
.............省略

最終設置好了三個寄存器的值:eax爲系統服務號,edx爲調用者的當前棧指針,esi爲當前線程的地址。然後處理當前的系統服務,首先要根據傳入的參數個數拉開內核堆棧空間,接着將位於用戶層的參數拷貝到內核層,然後調用對應的系統服務例程(NtReadFile),待系統調用返回後,調用KiExitService結束系統調用,返回Ring3層:

1: kd> u kiserviceexit l 40
nt!KiServiceExit:
8484291c fa              cli
8484291d f6457202        test    byte ptr [ebp+72h],2
84842921 7506            jne     nt!KiServiceExit+0xd (84842929)
84842923 f6456c01        test    byte ptr [ebp+6Ch],1
84842927 7467            je      nt!KiServiceExit+0x74 (84842990)
84842929 648b1d24010000  mov     ebx,dword ptr fs:[124h]
84842930 f6430202        test    byte ptr [ebx+2],2
84842934 7408            je      nt!KiServiceExit+0x22 (8484293e)
84842936 50              push    eax
84842937 53              push    ebx
84842938 e8aa570a00      call    nt!KiCopyCounters (848e80e7)
8484293d 58              pop     eax
8484293e c6433a00        mov     byte ptr [ebx+3Ah],0
84842942 807b5600        cmp     byte ptr [ebx+56h],0
84842946 7448            je      nt!KiServiceExit+0x74 (84842990)
84842948 8bdd            mov     ebx,ebp
8484294a 894344          mov     dword ptr [ebx+44h],eax
8484294d c743503b000000  mov     dword ptr [ebx+50h],3Bh
84842954 c7433823000000  mov     dword ptr [ebx+38h],23h
8484295b c7433423000000  mov     dword ptr [ebx+34h],23h
84842962 c7433000000000  mov     dword ptr [ebx+30h],0
84842969 b901000000      mov     ecx,1
8484296e ff155c618084    call    dword ptr [nt!_imp_KfRaiseIrql (8480615c)]
84842974 50              push    eax
84842975 fb              sti
84842976 53              push    ebx
84842977 6a00            push    0
84842979 6a01            push    1
8484297b e8f52f0700      call    nt!KiDeliverApc (848b5975)
84842980 59              pop     ecx
84842981 ff1558618084    call    dword ptr [nt!_imp_KfLowerIrql (84806158)]
84842987 8b4344          mov     eax,dword ptr [ebx+44h]
8484298a fa              cli
8484298b eb9c            jmp     nt!KiServiceExit+0xd (84842929)
8484298d 8d4900          lea     ecx,[ecx]
84842990 8b54244c        mov     edx,dword ptr [esp+4Ch]
84842994 64891500000000  mov     dword ptr fs:[0],edx
8484299b 8b4c2448        mov     ecx,dword ptr [esp+48h]
8484299f 648b3524010000  mov     esi,dword ptr fs:[124h]
848429a6 888e3a010000    mov     byte ptr [esi+13Ah],cl
848429ac f744242cff23ffff test    dword ptr [esp+2Ch],0FFFF23FFh
848429b4 0f857e000000    jne     nt!KiSystemCallExit2+0x1c (84842a38)
848429ba f744247000000200 test    dword ptr [esp+70h],20000h
848429c2 0f85340a0000    jne     nt!Kei386EoiHelper+0x134 (848433fc)
848429c8 66f744246cf9ff  test    word ptr [esp+6Ch],0FFF9h
848429cf 0f84b9000000    je      nt!KiSystemCallExit2+0x72 (84842a8e)
848429d5 66837c246c1b    cmp     word ptr [esp+6Ch],1Bh
848429db 660fba64246c00  bt      word ptr [esp+6Ch],0
848429e2 f5              cmc
848429e3 0f8793000000    ja      nt!KiSystemCallExit2+0x60 (84842a7c)
848429e9 66837d6c08      cmp     word ptr [ebp+6Ch],8
848429ee 7405            je      nt!KiServiceExit+0xd9 (848429f5)
848429f0 8d6550          lea     esp,[ebp+50h]
848429f3 0fa1            pop     fs
848429f5 8d6554          lea     esp,[ebp+54h]
848429f8 5f              pop     edi
848429f9 5e              pop     esi
848429fa 5b              pop     ebx
848429fb 5d              pop     ebp
848429fc 66817c24088000  cmp     word ptr [esp+8],80h
84842a03 0f870f0a0000    ja      nt!Kei386EoiHelper+0x150 (84843418)
84842a09 83c404          add     esp,4


將切入內核時各種寄存器的值都進行pop,即對寄存器的值賦值爲用戶層的上下背景文數據,接下來用戶層調用線程獲得CPU的使用權。

接下來說一下sysenter(KiFastSystemCall)快速系統調用

0:004> u KifastSystemcall
ntdll!KiFastSystemCall:
775e70f0 8bd4            mov     edx,esp;將堆棧指針保存在edx中
775e70f2 0f34            sysenter ;進入內核

爲什麼會產生快速系統調用?因爲使用int 0x2e在切換過程中涉及到很多次的內存訪問,以及兩次查表(IDT GDT)操作,以及訪問權限的檢查,這導致模式切換時候的開銷很大,所以就產生了sysenter和sysexit快速調用指令。
sysenter指令與上面提到的三個MSR搭配使用,當執行sysenter指令時候,CS_MSR中的內容複製到CS段寄存器中,(CS_MSR+8)處的內容複製到SS段寄存器中,這就要求CS段與SS段緊挨相鄰,EIP_MSR指向內核調用例程KiFastCallEntry,ESP_MSR指向系統空間堆棧頂部:

1: kd> rdmsr 0x174  ;CS_MSR標號 其中標誌着段選擇子偏移
msr[174] = 00000000`00000008


1: kd> rdmsr 0x176   ;EIP_MSR的標號
msr[176] = 00000000`84842790
1: kd> u 84842790
nt!KiFastCallEntry:
84842790 b923000000      mov     ecx,23h
84842795 6a30            push    30h
84842797 0fa1            pop     fs
84842799 8ed9            mov     ds,cx
8484279b 8ec1            mov     es,cx
8484279d 648b0d40000000  mov     ecx,dword ptr fs:[40h]
848427a4 8b6104          mov     esp,dword ptr [ecx+4]
848427a7 6a23            push    23h

1: kd> rdmsr 0x175 ;ESP_MSR的標號
msr[175] = 00000000`807f4000

在通過sysenter進入內核時候,也不會自動保存那些用戶層的堆棧指針寄存器,所以提高了CPU的執行效率。
當執行完Ring0層的工作之後,通過sysexit返回到Ring3層的函數KiFastSystemCallRet()的入口。當CPU執行sysexit指令時,CS設置成CS_MSR+16,實際上是KGDT_R3_CODE,把EDX內容複製到EIP,把SS設置成CS_MSR+24,實際上是KGDT_R3_DATA,把寄存器ECX內容複製給ESP。至此返回Ring3層執行。
最後附上潘愛民老師在內核原理與實現中畫的調用過程圖,加深理解:
在這裏插入圖片描述

“日日行,不怕千萬裏;常常做,不怕千萬事。”
參考書籍:
《Windows內核情景分析》(毛德操)
《Windows內核原理與實現》(潘愛明)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章