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內核原理與實現》(潘愛明)