win10系统调用架构分析
Posted liuyez123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了win10系统调用架构分析相关的知识,希望对你有一定的参考价值。
1. 操作系统模型
大多数操作系统中,都会把应用程序和内核代码分离运行在不同的模式下。内核模式访问系统数据和硬件,应用程序运行在没有特权的模式下(用户模式),只能使用有限的API,且不能直接访问硬件。当用户模式调用系统服务时,CPU执行一个特殊的指令以切换到内核模式(Ring0),当系统服务调用完成时,操作系统切换回用户模式(Ring3)。
Windows与大多数UNIX系统类似,驱动程序代码共享内核模式的内存空间,意味着任何系统组件或驱动程序都可能访问其他系统组件的数据。但是,Windows实现了一套内核保护机制,比如PatchGuard和内核模式代码签名。
内核模式的组件虽然共享系统资源,但也不会互相访问,而是通过传参数的方式来访问或修改数据结构。大多数系统代码用C写的是为了保证可移植性,C语言不是面向对象的语言结构,比如动态类型绑定,多态函数,类型继承等。但是,基于C的实现借鉴了面向对象的概念,但并不依赖面向对象。
2. 系统架构
下图是简化版的Windows系统架构实现:
首先注意那条横线将用户模式和内核模式分开两部分了。横线之上是用户模式的进程,下面是内核模式的系统服务。服务进程和用户程序之下的“子系统DLL”。在Windows下,用户程序不直接调用本地Windows服务,而是通过子系统DLL来调用。子系统DLL的角色是将文档化的函数翻译成调用的非文档化的系统服务(未公开的)。
内核模式的几个组件包括:
- Windows执行实体,包括基础系统服务,比如内存管理器,进程和线程管理器,安全管理,I/O管理,网络,进程间通信。
- Windows内核,包括底层系统函数,比如线程调度,中断,异常分发,多核同步。也提供了一些routine和实现高层结构的基础对象。
- 设备驱动,包括硬件设备驱动(翻译用户I/O到硬件I/O),软件驱动(例如文件和网络驱动)。
- 硬件抽象层,独立于内核的一层代码,将设备驱动与平台的差异性分离开。
- 窗口和图形系统,实现了GUI函数,处理用户接口和绘图。
下表中是Windoows系统核心组件的文件名:
文件名
组件
Ntoskrnl.exe
执行体和内核
Ntkrnlpa.exe(32位才有)
支持PAE
Hal.dll
硬件抽象层
Win32k.sys
子系统的内核模式部分
Ntdll.dll
内部函数
KERNEL32.DLL,KERNELBASE.dll,USER32.dll, GDI32.dll
核心子系统的组件
在一个安装完成的Windows操作系统中可见并有效的内核实现文件是:
C:\Windows\System32\ntoskrnl.exe
C:\Windows\System32\ntkrnlpa.exe
请注意有两个内核文件,其中第二个比第一个的名字少了os多了个pa,省去的os没有任何意义,但是多出来的pa所代表的意思是PAE(物理地址扩展),这是X86CPU的一个硬件特性,Windows启动之后根据当前系统设置是否开启了PAE特性会自动选择把其中一个作为内核加载到内存中。
为什么加了这么多限定词,因为ntoskrnl.exe这个文件名并不一定是这个文件的真实名称,可以从文件属性中看到:
ntoskrnl.exe原始文件名为可能为ntoskrnl.exe或者ntkrnlmp.exe
ntkrnlpa.exe原始文件名为可能为ntkrnlpa.exe或者ntkrpamp.exe
可以发现其中的不同之处就是mp,mp就是Multi-processor(多处理器,也可以理解为多核,因为IA-32架构对多核处理器的编程和多处理器的编程是相似的机制)。为什么会出现这中情况呢?因为这完全是由计算机硬件的不同配置导致的。当安装Windows操作系统的时候,Windows安装程序会自动检测机器的CPU特性,根据CPU的核心数来确定使用哪一套内核。如果是单核心就只复制ntkrnlpa.exe和ntoskrnl.exe到系统目录下,如果是多核心就复制ntkrnlpamp.exe和ntoskrnlmp.exe到系统目录下,所以如果你有一台单核心CPU的机器,有一天你换了双核的CPU却没有重新安装操作系统,那么你就不会在看到熟悉的Windows启动画面了。类似这两个文件的还有一个文件C:\Windows\System32\hal.dll,这是Windows的硬件抽象层程序文件,这个就不做具体介绍了。
注意:由于在跟踪分析系统内核调用的时候需要导入相应的符号文件以及对函数偏移位置等进行分析,因此需要知道自己系统上内核文件的原始文件名。
3. 系统服务调用机制
对于应用程序进程来说,操作系统内核的作用体现在一组可供调用的函数,称为系统调用(也成"系统服务")。
从程序运行的角度来看,进程是主动、活性的,是发出调用请求的一方;而内核是被动的,只是应进程要求而提供服务。从整个系统运行角度看,内核也有活性的一面,具体体现在进程调度。
系统调用所提供的服务(函数)是运行在内核中的,也就是说,在"系统空间"中。而应用软件则都在用户空间中,二者之间有着空间的间隔(CPU运行模式不同)。
综上所述,应用软件若想进行系统调用,则应用层和内核层之间,必须存在"系统调用接口",即一组接口函数,这组接口运行于用户空间。对于windows来说,其系统调用接口并不公开,公开是的一组对系统调用接口的封装函数,称为windowsAPI。
用户空间与系统空间所在的内存区间不一样,同样,对于这两种区间,CPU的运行状态也不一样。
在用户空间中,CPU处于"用户态";在系统空间中,CPU处于"系统态"。
CPU从系统态进入用户态是容易的,因为可以执行一些系统态特有的特权指令,从而进入用户态。
而相反,用户态进入系统态则不容易,因为用户态是无法执行特权指令的。
所以,一般有三种手段,使CPU进入系统态(即转入系统空间执行):
① 中断:来自于外部设备的中断请求。当有中断请求到来时,CPU自动进入系统态,并从某个预定地址开始执行指令。中断只发生在两条指令之间,不影响正在执行的指令。
② 异常:无论是在用户空间或系统空间,执行指令失败时都会引起异常,CPU会因此进入系统态(如果原先不在系统空间),从而在系统空间中对异常做出处理。异常发生在执行一条指令的过程中,所以当前执行的指令已经半途而废了。
③ 自陷:以上两种都是CPU被动进入系统态。而自陷是CPU通过自陷指令主动进入系统态。多数CPU都有自陷指令,系统调用函数一般都是靠自陷指令实现的。一条自陷指令的作用相当于一次子程序调用,子程序存在于系统空间。
4. Windows使用系统调用的方法
4.1. 通过自陷实现系统调用
Windows API如果设涉及到系统调用就要由RING3进入RING0,这就牵扯到了X86保护模式下有特权级变化的控制转移。在早期的CPU中(Pentium II之前),没有快速系统调用这个机制,所以能用来进行特权级变化的控制转移的机制只有通过自陷实现(很多书或网络上也经常称为中断方式),保护模式下的中断的实现方式是通过IDT表来实现,IDT表中存放的是一种特殊的X86段描述符——门描述符,门描述符的格式如下:
可以看到其中有一个Selector字段和一个Offset字段,并且是不连续的,这里只介绍这两个字段的含义,其他字段的含义这里不再赘述,有兴趣的话可以自己去看下保护模式相关资料。说到底这个门描述符的作用就是描述一个程序段,对我们来说重要的就是Selector和Offset字段了,因为Selector可以帮我们找到它所描述的程序的【段】,Offset就是程序在【段】内的【偏移】,有了【段】和【偏移】就可以确定程序的线性地址。
在Win10 X64操作系统中IDT表的结构又有些不一样,具体的结构可以用WinDbg获得,具体指令及结果如下:
kd> dt_KIDTENTRY64
ACPI!_KIDTENTRY64
+0x000 OffsetLow : Uint2B
+0x002 Selector : Uint2B
+0x004 IstIndex : Pos 0, 3 Bits
+0x004 Reserved0 : Pos 3, 5 Bits
+0x004 Type : Pos 8, 5 Bits
+0x004 Dpl : Pos 13, 2 Bits
+0x004 Present : Pos 15, 1 Bit
+0x006 OffsetMiddle : Uint2B
+0x008 OffsetHigh : Uint4B
+0x00c Reserved1 : Uint4B
+0x000 Alignment : Uint8B
kd> r idtr
idtr=fffff801b88ca070
kd> dt_KIDTENTRY64 fffff801b88ca070
ACPI!_KIDTENTRY64
+0x000 OffsetLow : 0x7500
+0x002 Selector : 0x10
+0x004 IstIndex : 0y000
+0x004 Reserved0 : 0y00000 (0)
+0x004 Type : 0y01110 (0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006OffsetMiddle : 0xb6d5
+0x008 OffsetHigh : 0xfffff801
+0x00c Reserved1 : 0
+0x000 Alignment : 0xb6d58e00`00107500
在使用这种机制的windows系统中,系统调用2E号中断,进入了系统内核。一般在中断调用前都会初始化一个系统服务号;也叫做分发 ID,该 ID 需要在执行 int 2Eh 前,加载到EAX 寄存器,以便在切换到内核模式的时候调用相应的内核函数来完成相应的功能。
粗略地讲,INT 指令在内部涉及如下几个操作:
1) 清空陷阱标志(TF),和中断允许标志(IF);
2) 依序把(E)FLAGS,CS,(E)IP 寄存器中的值压入栈上;
3) 转移到 IDT 中的中断门描述符记载的相应 ISR(中断服务例程)的起始地址;
4) 执行 ISR,直至遇到 IRET 返回。
最关键的第3步涉及“段间”转移,通过中断门描述符,能够引用一个 Ring0 权限代码段,该代码段对应的 64 位段描述符(存储在 GDT 中)中的 DPL 位,即特权级位等于0(0=Ring0;3=Ring3,即便由 Intel 规定的段描述符的 DPL 位有4种取值,但 Windows 仅使用了其中的最高特权级 Ring0 与最低特权级 Ring3,总体而言,用户模式应用程序位于 Ring3 代码或数据段;内核与设备驱动程序则位于 Ring0 代码或数据段 ),再结合段描述符中的“基址”与中断门描述符中的“偏移”,就能计算出 ISR在 Ring0 代码段中的起始地址。下表是64位段描述符的格式,取自 Intel 文档,自行添加了翻译:
我们知道了系统调用了2E号中断,从而进入了系统内核,知道了中断号下面我们要做的就是找到这个中断的服务程序,也就是RING3进入到RING0之后的第一条指令在哪里。下面就进入内核调试模式。由于IDT是由IDTR指定的,这里用WINDBG进行手工分析:
1) 在X86模式下:
0: kd> r idtr
idtr=8003f400
这个IDT有多大呢?
0: kd> r idtl
idtl=000007ff
其实大小就是这个数加一。地址找到了,大小找到了,关键是这个是啥结构,IDT长啥样呢?
0: kd> dt _KIDTENTRY
ntdll!_KIDTENTRY
+0x000Offset : Uint2B
+0x002Selector : Uint2B
+0x004Access : Uint2B
+0x006ExtendedOffset : Uint2B
就是这个结构的数组。
下面看看第一个成员。
0: kd> dt _KIDTENTRY 8003f400
ntdll!_KIDTENTRY
+0x000Offset : 0x3360
+0x002Selector : 8
+0x004Access : 0x8e00
+0x006ExtendedOffset : 0x8054
这个结构的具体的含义,请看前面对中断门描述符的解释或查看Intel的手册及者相关的资料。经过计算得出地址是:0x80543360
验证的方式之一:
|
|
看到了吧!显示的是正确的。
另一个办法是:
|
|
2) 在X64模式下:
首先查看IDTR和IDTL
kd> r idtr
idtr=fffff801b88ca070
kd> r idtl
idtl=0fff
在64位系统中使用的结构是_KIDTENTRY64
kd> dt _KIDTENTRY64
ACPI!_KIDTENTRY64
+0x000 OffsetLow : Uint2B
+0x002 Selector : Uint2B
+0x004 IstIndex : Pos 0, 3Bits
+0x004 Reserved0 : Pos 3, 5Bits
+0x004 Type : Pos 8, 5Bits
+0x004 Dpl : Pos 13,2 Bits
+0x004 Present : Pos 15,1 Bit
+0x006 OffsetMiddle : Uint2B
+0x008 OffsetHigh : Uint4B
+0x00c Reserved1 : Uint4B
+0x000 Alignment : Uint8B
kd> dt _KIDTENTRY64 fffff801b88ca070
ACPI!_KIDTENTRY64
+0x000 OffsetLow : 0x7500
+0x002 Selector : 0x10
+0x004 IstIndex : 0y000
+0x004 Reserved0 : 0y00000(0)
+0x004 Type : 0y01110(0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006 OffsetMiddle : 0xb6d5
+0x008 OffsetHigh :0xfffff801
+0x00c Reserved1 : 0
+0x000 Alignment :0xb6d58e00`00107500
查看IDT服务表
kd> !idt
Dumping IDT: fffff801b88ca070
00: fffff801b6d57500nt!KiDivideErrorFault
01: fffff801b6d57600nt!KiDebugTrapOrFault
02: fffff801b6d577c0nt!KiNmiInterrupt Stack =0xFFFFF801B88E5000
03: fffff801b6d57b40nt!KiBreakpointTrap
04: fffff801b6d57c40nt!KiOverflowTrap
05: fffff801b6d57d40nt!KiBoundFault
06: fffff801b6d57fc0nt!KiInvalidOpcodeFault
07: fffff801b6d58200nt!KiNpxNotAvailableFault
08: fffff801b6d582c0nt!KiDoubleFaultAbort Stack =0xFFFFF801B88E3000
09: fffff801b6d58380nt!KiNpxSegmentOverrunAbort
0a: fffff801b6d58440nt!KiInvalidTssFault
0b: fffff801b6d58500nt!KiSegmentNotPresentFault
0c: fffff801b6d58640nt!KiStackFault
0d: fffff801b6d58780nt!KiGeneralProtectionFault
0e: fffff801b6d58880nt!KiPageFault
10: fffff801b6d58c40nt!KiFloatingErrorFault
11: fffff801b6d58dc0nt!KiAlignmentFault
12: fffff801b6d58ec0nt!KiMcheckAbort Stack =0xFFFFF801B88E7000
13: fffff801b6d59540nt!KiXmmException
1f: fffff801b6d52890nt!KiApcInterrupt
20: fffff801b6d56c10nt!KiSwInterrupt
29: fffff801b6d59700nt!KiRaiseSecurityCheckFailure
2c: fffff801b6d59800nt!KiRaiseAssertion
2d: fffff801b6d59900nt!KiDebugServiceTrap
2f: fffff801b6d52b60nt!KiDpcInterrupt
30: fffff801b6d52d90nt!KiHvInterrupt
31: fffff801b6d530f0nt!KiVmbusInterrupt0
32: fffff801b6d53440nt!KiVmbusInterrupt1
33: fffff801b6d53790nt!KiVmbusInterrupt2
34: fffff801b6d53ae0nt!KiVmbusInterrupt3
35: fffff801b6d51718hal!HalpInterruptCmciService (KINTERRUPT fffff801b7425cb0)
50: fffff801b6d517f0USBPORT!USBPORT_InterruptService (KINTERRUPT ffffd001fee5c640)
60: fffff801b6d51870VBoxGuest+0x1290 (KINTERRUPT ffffd001fee5cb40)
70: fffff801b6d518f0storport!Ra
以上是关于win10系统调用架构分析的主要内容,如果未能解决你的问题,请参考以下文章