在潘老师《Windows内核原理与实现》一书,解析了Windows应用程序发出的系统调用。图示如下。
从图可看出,系统调用所提供的服务(函数)是运行在内核中的,也就是说,在"系统空间"中。
用户空间与系统空间所在的内存区间不一样,同样,对于这两种区间,CPU的运行状态也不一样。
在用户空间中,CPU处于"用户态";在系统空间中,CPU处于"系统态"。
CPU从系统态进入用户态是容易的,因为可以执行一些系统态特有的特权指令,从而进入用户态。
而相反,用户态进入系统态则不容易,因为用户态是无法执行特权指令的。
所以,一般有三种手段,使CPU进入系统态(即转入系统空间执行):中断、异常、自陷。
关于指令Int 2e。
Intel X86处理器在IDT(中断描述服务表)查找2e项,IDTEntry包含了一个段选择符和中断历程的段内偏移,所以处理器还需要在GDT(全局描述表)中再查一次,得到段选择符指定的段的虚拟地址。段基地址加上中断例程偏移,最终得到中断例程的虚拟地址。
处理器在不同模式下使用不同的栈,用户模式代码使用用户栈,内核模式代码使用内核栈,因此当从用户模式切入到内核时,必定伴随着堆栈的变化。
Int 2e与Sysenter最主要的区别就在于堆栈切换方式的不同。但是不管是哪种方式本质上都是先获得内核栈然后保存用户模式下的ss,esp,eflags.cs.eip到内核栈,执行完系统调用后在通过保存的值进行用户空间的还原。
首先我们先分析下Int 2e/iret的栈切换。
首先如何获得内核栈的ss和esp?
处理器的任务寄存器指向当前任务环境的TSS,其中RING0的esp位于TSS+4的位置。WINDOWS每次切换线程时,总会设置好TSS中RING0的esp。
完成中断处理后通过iret从内核栈中弹出eip,cs,eflags,esp,ss,然后将控制权交给eip所指向的用户模式代码。
显然,通过int 2e/iret进行模式切换过程中涉及很多流程,开销很大。所以在XP后引入了Sysenter/Sysexit,快速系统调用。
然后看下Sysenter/Sysexit栈切换。
为了避免过多的内存访问,Sysenter增加了三个MSR寄存器来指定跳转目标和栈位置。
下面我们以XP下的Nt为例试着跟踪一下
kd> u Ntdll!NtWriteFile
ntdll!ZwWriteFile:
7c92df60 b812010000 mov eax,112h
7c92df65 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92df6a ff12 call dword ptr [edx]
7c92df6c c22400 ret 24h
7c92df6f 90 nop
首先看到Ntdll中NtWriteFile把索引保存进eax中,然后调用了ShareUserData结构中的CallSystemStub函数指针。
我们继续跟踪下ShareUserData结构(_KUSER_SHARED_DATA)
kd> dt _KUSER_SHARED_DATA 0x7ffe0000
ntdll!_KUSER_SHARED_DATA
.
.
.
+0x300 SystemCall : 0x7c92e4f0
+0x304 SystemCallReturn : 0x7c92e4f4
.
.
这样我们可以清楚的看到0X300处的函数指针了。
kd> u 0x7c92e4f0
ntdll!KiFastSystemCall:
7c92e4f0 8bd4 mov edx,esp ;注意此时的ESP代表的是参数块
7c92e4f2 0f34 sysenter
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
然后就sysenter指令进入内核的KiFastCallEntry() 函数了。
kd> u KiFastCallEntry l 50
nt!KiFastCallEntry:
.
.
.
8053e612 8bf2 mov esi,edx ;用户空间的ESP
8053e614 8b5f0c mov ebx,dword ptr [edi+0Ch]
8053e617 33c9 xor ecx,ecx
8053e619 8a0c18 mov cl,byte ptr [eax+ebx]
8053e61c 8b3f mov edi,dword ptr [edi]
8053e61e 8b1c87 mov ebx,dword ptr [edi+eax*4]
8053e623 c1e902 shr ecx,2
8053e626 8bfc mov edi,esp
8053e628 3b35d4995580 cmp esi,dword ptr [nt!MmUserProbeAddress (805599d4)]
8053e62e 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (8053e7dc)
以上是关顾从Ring3层发起的系统调用过程,有趣的是Ring0层如果调用Zw函数则会递归进入Kifastcallentry,然后调用Nt函数。
这就是内核Zw函数与Nt函数最大的区别!(Ntdll中的Zw函数与Nt函数没有区别)
所以在返回Kifastcallentry时会进行很繁琐,主要原因是通过先前模式判断发起系统调用的堆栈是用户控件或者系统空间,然后进行不同的处理。
发起系统调用 入口内核例程 返回指令 返回内核例程
int 2e KiSystemService iret KiSystemCallExit
sysenter(Intel) KiFastCallEntry SYSEXIT KiSytemcallExit2
syscall (AMD) KiFastCallEntry SYSRETURN KiSystemCallExit3