运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。多个驱动程序在内核模式下运行,但某些驱动程序在用户模式下运行。
当启动用户模式的应用程序时,Windows 会为该应用程序创建“进程”。进程为应用程序提供专用的“虚拟地址空间”和专用的“句柄表格”。用户模式下程序只能访问2GB或者3GB的虚拟地址,而内核代码能够访问当前进程的整个4G虚拟空间。由于应用程序的虚拟地址空间为专用空间,一个应用程序无法更改属于其他应用程序的数据。每个应用程序都孤立运行,如果一个应用程序损坏,则损坏会限制到该应用程序。其他应用程序和操作系统不会受该损坏的影响。
用户模式应用程序的虚拟地址空间除了为专用空间以外,还会受到限制。在用户模式下运行的处理器无法访问为该操作系统保留的虚拟地址。限制用户模式应用程序的虚拟地址空间可防止应用程序更改并且可能损坏关键的操作系统数据。
在内核模式下运行的所有代码都共享单个虚拟地址空间。这表示内核模式驱动程序未从其他驱动程序和操作系统自身独立开来。如果内核模式驱动程序意外写入错误的虚拟地址,则属于操作系统或其他驱动程序的数据可能会受到损坏。如果内核模式驱动程序损坏,则整个操作系统会损坏。
此图说明了用户模式组件与内核模式组件之间的通信。
用户态与核心态驱动程序的区别
♦ 用户态的驱动程序运行在非特权处理机模式(nonprivileged processor mode)上,其他一些被保护的子系统代码也运行在该模式上。用户态的驱动程序不能获得系统数据的存取权,除非调用Win32 API或者系统服务。
♦ 核心态驱动程序作为操作系统的一个组成部分被执行——支持一个或多个受保护的子系统的操作系统底层组件。
用户态和核心态驱动程序有不同的结构,不同的入口点和不同的系统接口。一个设备是需要一个用户态驱动程序还是需要一个核心态驱动程序依赖于该设备的类型和操作系统对它提供的支持。
用户模式与内核模式是如何交互的呢
当用户模式程序需要读取设备数据时,它就调用Win32 API函数,如 ReadFile 。Win32子系统模块(如KERNEL32.DLL)通过调用平台相关的系统服务接口实现该API,而平台相关的系统服务将调用内核模式支持例程。在 ReadFile 调用中,调用首先到达系统DLL(NTDLL.DLL)中的一个入口点, NtReadFile 函数。然后这个用户模式的NtReadFile函数接着调用 系统服务接口 ,最后由系统服务接口调用 内核模式中的服务例程 ,该例程同样名为 NtReadFile 。
系统中还有许多与NtReadFile相似的服务例程,它们同样运行在内核模式中,为应用程序请求提供服务,并以某种方式与设备交互。它们首先检查传递给它们的参数以保护系统安全或防止用户模式程序非法存取数据,然后创建一个称为“I/O请求包(IRP)”的数据结构,并把这个数据结构送到某个驱动程序的入口点。在刚才的ReadFile调用中,NtReadFile将创建一个主功能代码为IRP_MJ_READ(DDK头文件中的一个常量)的IRP。实际的处理细节可能会有不同,但对于NtReadFile例程,可能的结果是,用户模式调用者得到一个返回值,表明该IRP代表的操作还没有完成。用户模式程序也许会继续其它工作然后等待操作完成,或者立即进入等待状态。不论哪种方式,设备驱动程序对该IRP的处理都与应用程序无关。
驱动程序完成一个I/O操作后,通过调用一个特殊的内核模式服务例程来完成该IRP。完成操作是处理IRP的最后动作,它使等待的应用程序恢复运行。
一般有三种手段,使CPU进入系统态(即转入系统空间执行):
①中断:来自于外部设备的中断请求。当有中断请求到来时,CPU自动进入系统态,并从某个预定地址开始执行指令。中断只发生在两条指令之间,不影响正在执行的指令。
②异常:无论是在用户空间或系统空间,执行指令失败时都会引起异常,CPU会因此进入系统态(如果原先不在系统空间),从而在系统空间中对异常做出处理。异常发生在执行一条指令的过程中,所以当前执行的指令已经半途而废了。
③自陷:以上两种都CPU被动进入系统态。而自陷是CPU通过自陷指令主动进入系统态。多数CPU都有自陷指令,系统调用函数一般都是靠自陷指令实现的。一条自陷指令的作用相当于一次子程序调用,子程序存在于系统空间。
3、通过自陷指令调用系统服务流程:
windows系统通过自陷指令"int 0x2e"进入系统空间实现系统调用。
①CPU执行int 0x2e,CPU运行状态切换为系统态
②从任务状态段TSS装入本线程的系统空间的SS和ESP
③依次把用户空间的SS、ESP、EFLAGS、CS、EIP的内容压入系统空间堆栈
每个线程都有自己的系统空间堆栈,其堆栈段寄存器SS和堆栈指针ESP的内容保存在一个称为"任务状态段"既TSS的数据结构里面.
与此相应,CPU中有个 称为"任务寄存器"既TR的段寄存器.每当从用户空间进入系统空间时,CPU就自动根据TR的指引从TSS中获取当前进程的SS和ESP两个寄存器的值. 然后在把上面提到的几个寄存器中的内容压入这个堆栈.当然,还要根据IDTR即中断描述符表寄存器的指引获取CS和EIP. 这个过程所涉及的"时钟周期"显然不会少(这就是为什么在
④从中断向量表中(Interrupt Descriptor Table)以0x2e为中断向量,开始执行系统空间中的程序。
⑤程序执行后,通过iret(中断返回)指令实现上述过程的逆过程
__declspec(naked)__stdcall
NtReadFile(int dummy0,int dummy2,int dummy3)
{
__asm{
push ebp
mov ebp,esp
mov eax,152 //将NtReadFile()的系统调用号存入EAX
lea edx,8[ebp] //使EDX指向堆栈上参数块的起点,返回地址+Filehandle = 8 bytes
int 0x2E //进入内核
pop ebp
ret 9 //在堆栈上共有9个参数(win32汇编应该是9*4 :ret 36,)
}
}
4、windows中介函数详解
所有windows基本系统调用的中介函数存在于ntdll.dll中。这些中介函数拥有相同的样式(都是执行自陷指令,然后调用相应的内核函数),所以reactOS用工具自动生成这些中介函数。
函数名、系统调用号、参数个数,来自文件sysfuncs.lst中,例如:
- NtAcceptConnectPort 6
-
- NtAccessCheck 8
-
- NtAccessCheckAndAuditAlarm 11
-
- NtAccessCheckByType 11
-
- NtAccessCheckByTypeAndAuditAlarm 16
-
- NtAccessCheckByTypeResultList 11
-
- NtAccessCheckByTypeResultListAndAuditAlarm 16
-
- NtAccessCheckByTypeResultListAndAuditAlarmByHandle 17
-
- NtAddAtom 3
-
这里,NtAcceptConnerPort就是调用号为0的系统调用函数,它有六个参数。其他的调用函数类似。
5、何谓CPU进入系统空间?
①CPU的运行状态从用户态转为系统态,拥有了执行"特权指令"的能力。
②CPU进入系统态后,可以访问内存中的系统区(内核所在的区域),而在用户态下是无法访问的。
③当前进程使用的堆栈,从用户态切换到系统态。堆栈原先的内容(用户态),以及用户空间的堆栈指针,被压入系统空间堆栈。同时被压入的,还有EFLAGS、CS、EIP的内容。
④关闭中断,依照中断向量从IDT中找到相应的表项,并根据表项提供的程序入口进入相应的中断服务程序。
6、windows提供的自陷指令
- int 3:程序入口为_KiTrap3()。用于debug时设置断点。
- int 0x2e:程序入口为_KiSystemService()。用于实现windows系统调用。
- int 0x2a:程序入口为_KiGetTickCount()。让用户空间程序获取高精度时钟计数。
- int 0x2b:程序入口为_KiCallbackReturn()。特殊自陷入口。用于在内核中回调用户空间中的子程序,执行完后通过0x2b回系统空间。
- int 0x2c/int 0x2d:用于dubug。
7、_KiSystemService()
_KiSystemService()是所有的系统调用入口函数。对应于中断向量0x2e。所以,当中间函数执行int 0x2e时,CPU就进入系统空间中的这个函数。
进入系统空间时,CPU自动将下列信息压入系统空间堆栈:
①用户空间的堆栈位置,包括SS和ESP的内容
②EFLAGS的内容
③用户空间指令位置,包括CS和EIP的内容
这些信息是将来返回用户空间所必须的。
8、快速系统调用
x86系列从P2开始增设了两条指令和三个寄存器用来用来实现快速系统调用。分别是:
- sysenter和sysexit(对应于自陷方式中的int/call和iret/ret)
- sysenter_cs_msr
- sysenter_ip_msr
- sysenter_esp_msr
快速系统调用实现更为高效。
采用快速系统调用的总入口时_KiFastCallEntry(对应于_KiSystemService)。
快速系统调用代码摘要:
其中,KUSER_SHARED_SYSCALL指向用户空间的一个地址(0x7ffe0300),这个位置存储了一个函数指针,指向KiIntSystemCall()或KiFastSystemCall()两个函数其中之一。系统在初始化的时候根据CPU是否支持快速系统调用而使该指针指向KiIntSystemCall()或KiFastSystemCall()。
快速系统调用流程图:
9、从内核中发起系统调用
在内核中不能直接调用NtReadFile之类的实现函数。一般也是通过中介函数进行调用。
以NtReadFile为例,其在内核中的中介函数由工具生成,其名称为ZwReadFile。
代码:
__declspec(naked) _stdcall
ZwReadFile(int dummy0, int dummy1, int dummy2)
{
asm{
mov eax 152
lea edx,[esp+4]
push KGDT_R0_CODE
call _KiSystemservice
ret 36
}
}