《重要》从用户模式切换到内核模式的完整过程分析

Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。

内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权指令。用户模式具有较低的优先级,用户模式只能访问用户空间,且不能执行特权指令。

如果用户代码不慎访问了系统空间的数据或执行了特权指令将会导致保护性异常的发生。但是用户代码可以通过调用系统服务来间接的访问系统空间中的数据或间接执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回用户代码。这就是所谓的模式切换,也被称为上下文切换。

一、使用 INT 2E 切换到内核模式

2e 号向量号专门被用来做系统调用。在 windbg 中可以输入: !idt 2e 来查看该向量号在 IDT 对应的异常处理函数。如:

image

可以看到 2e 号向量对应的异常处理函数为: nt!KiSystemService。该函数是内核中用以分发系统调用的。

下面我们以调用 ReadFile 为例来展示使用 int2e 指令进行系统调用的步骤:

image

因为 ReadFile 是从 Kernel32 导出的,所以我们看到调用首先转到了Kernel32 的 ReadFile 函数。在 ReadFile 中又调用了 ntdll!NtReadFile 函数 ntdll.dll是内核空间和用户控件的桥梁,用户空间的代码通过这个 dll 来调用内核空间的系统服务。它会被加载到所有用户进程的进程空间中,且位于同一位置。

  下图为 ntdll !NtReadFile 函数反汇编代码:

image

通过反汇编代码可以看到 ntdll!NtReadFile 非常的短,首先将请求的读文件的系统调用的系统服务号 0xa1 放入 eax 寄存器,然后便通过 INT 2e 指令引发系统调用异常。 INT 2e 会导致陷阱异常 。异常发生时,发生异常代码处得 CS 和EIP 寄存器会被压入堆栈,用于在处理完异常后的返回。然后系统会在 IDT 表中查询 2e 号向量对应的异常处理函数,得到 KiSystemService 函数地址。

在调用内核态的 KiSystemService 函数值前, cpu 会做一些模式切换工作。包括权限检查和准备内核态使用的栈空间。 N t!KisystemService ,得到传入的系统调用号,并将传入的参数从用户态复制到内核态,调用系统读取文件的系统调用。执行完毕后, nt!KiSystemService 会将操作结果复制到用户态。弹出 CS 和EIP 将执行权交给 NtReadFile 用以执行 INT 2e 后面的指令。

二、快速系统调用

前面介绍了 Windows 使用 INT 2e 来实现系统调用。但是它是使用中断机制实现的,伴随着中断产生的还有权限检查和查询 IDT 表等操作,这会导致额外的开销。因此在最新版本的 Windows 中,微软采用了被称为快速系统调用的机制。这主要是得益于 Intel 从奔腾 2 开始在处理器新加的三个特殊的 MSR 寄存器以及sysenter 和 sysexit 指令。

现在我们通过Windows API OpenProcess的调用过程,来看看Windows 7中快速系统调用过程:

前面的部分还是一样的:因为OpenProcess是从Kernel32导出的,所以调用首先转到了Kernel32的OpenProcess函数。在OpenProcess中又调用了ntdll!NtOpenProcess函数。

我们再来看看ntdll!NtOpenProcess的反汇编代码。

image

eax中保存系统调用号,此处NtOpenProcess的为BEh;edx是SharedUserData!SystemCallStub的地址,里面保存着KiFastSystemCall的地址。SharedUserData总是存放在0x7ffe0000处,其偏移0x300处正是SystemCall。

image

我们来看看0x7ffe0300处到底是不是SystemCall。

image

好了,我们看到了sysenter!!!

Sysenter时eax中保存着系统调用号,edx中保存着用户空间线程栈栈顶地址即保存着NtOpenProcess中call KiFastSystemCall的返回地址。

与指令对INT/IRET不同,快速系统调用指令对SYSENTER/SYSEXIT不具有调用、返回关系,因为指令SYSENTER并不会为指令SYSEXIT保存任何返回信息,不指示SYSEXIT返回到何处继续执行,也就是说指令SYSEXIT并不一定返回到指令SYSENTER后的下一个条指令继续执行。这两条指令所需的相关信息由处理器内部的一组相关的寄存器(MSR)提供,这些寄存器的名称及用途如下。

MSR_IA32_SYSENTER_CS:保存了系统调用处理过程所使用的内核态代码段的段选择子,用于在通过指令SYSENTER进入内核态时,设置代码段段选择子寄存器cs。同时,紧随在该寄存器所指示段描述符后面的三个段描述符被依次认为是内核数据段、用户代码段、用户数据段的段描述符(这些段描述符保存在全局描述符表GDT中,有先后次序)。快速系统调用指令SYSENTER/SYSEXIT依赖这些次序来完成内核态、用户态执行环境的设置工作。

MSR_IA32_SYSENTER_EIP:保存了系统调用处理过程的入口地址,用于在通过指令SYSENTER进入内核态时,设置指令指针寄存器eip。

MSR_IA32_SYSENTER_ESP:保存了系统调用处理过程所使用内核态的栈指针信息,用于在通过指令SYSENTER进入内核态时,设置内核态栈的栈指针寄存器esp。

1.请求快速系统调用处理过程

在用户态的代码执行了SYSENTER指令之后,处理器中的控制单元将会完成以下操作,然后进入内核态进行系统调用的处理。

(1)将寄存器MSR_IA32_SYSENTER_CS所指示的段描述符装载到代码段段选择子寄存器cs中。

(2)将寄存器MSR_IA32_SYSENTER_EIP的值装载到指令指针寄存器eip中。

(3)将寄存器MSR_IA32_SYSENTER_CS的值加8作为一个段选择子,然后将该段选择子对应的段描述符装载到栈基址寄存器ss中。

(4)将寄存器MSR_IA32_SYSENTER_ESP的值装载到栈指针寄存器esp中。

2.快速系统调用返回处理过程

在内核态对系统调用服务完毕之后,执行指令SYSEXIT完成系统调用的返回过程。在该过程中处理器的控制单元完成以下处理,设置用户态的执行环境。

(1)将寄存器MSR_IA32_SYSENTER_CS的值加16作为一个段选择子,然后将该段选择子对应的段描述符装载到代码段段选择子寄存器cs中。

(2)将寄存器edx的值装载到指令指针寄存器eip中。

(3)将寄存器MSR_IA32_SYSENTER_CS的值加24作为一个段选择子,然后将该段选择子对应的段描述符装载到栈段段选择子寄存器ss中。

(4)将寄存器ecx的值装载到栈指针寄存器esp中。

由上面的分析可知,在使用指令SYSENTER请求快速系统调用之前,需要初始化好相关的型号相关寄存器;在使用指令SYSEXIT进行快速系统调用返回之前,还要保证寄存器ecx、 edx的正确性,以便能正确地返回到用户态继续运行。

在内核的初始化过程中,函数enable_sep_cpu()负责完成这组寄存器的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void enable_sep_cpu(void)
{
       //调用宏定义get_cpu()首先禁用内核态抢占,然后返回当前处理器在系统中的编号.
       int cpu = get_cpu();
       //获得当前处理器对应的任务状态段的地址,并将该地址保存在局部变量tss中。
       //宏定义per_cpu()用于获得每处理器变量的本地拷贝。
       struct tss_struct *tss = &per_cpu(init_tss, cpu);
 
       //判断负责系统引导的处理(BootSstrap Processor)是否支持快速系统调用指令SYSENTER/SYSEXIT,如果不支持,
       //则调用宏定义put()使能内核态抢占,然后返回。也就是说,只有在系统引导处理器支持快速系统调用的情况下,
       //才为系统中的每个处理器设置快速系统调用所需要的参数;否则,系统中不支持快速系统调用。
       if (!boot_cpu_has(X86_FEATURE_SEP)) {
            put_cpu();
            return;
       }
 
       //设置当前任务状态段中的成员变量ss1、esp1分别为快速系统调用所使用的代码段段选择子(内核代码段__KERNEL_CS)
       //和快速系统调用使用的临时(紧急)内核态栈的栈底。
       tss->ss1 = __KERNEL_CS;
       tss->esp1 = sizeof(struct tss_struct) + (unsigned long) tss;
       //调用宏定义wrmsr()设置快速系统调用所需的3个MSR寄存器。
       //分别设置MSR_IA32_SYSENTER_CS为内核代码段段择子__KERNEL_CS;
       wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
        //设置MSR_IA32_SYSENTER_ESP为任务状态段中预留的临时(紧急)内核态栈的栈底地址;
       wrmsr(MSR_IA32_SYSENTER_ESP, tss->esp1, 0);
       //设置MSR_IA32_SYSENTER_EIP为快速系统调用处理函数的入口地址sysenter_entry。
       wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0);
       //宏定义put_cpu()与get_cpu()相对,用于使能内核态抢占。
       put_cpu();    
}

WRMSR可以设置上面说的MSR寄存器,对应关系如下:
SYSENTER_CS_MSR 174H

SYSENTER_ESP_MSR 175H

SYSENTER_EIP_MSR 176H

我们来看看MSR寄存器相应位置保存的什么:

image

sysenter切换堆栈时是从用户栈切换到DPC栈,这是因为MSR寄存器中的值是操作系统安排好的固定的值,它与具体的线程上下文无关,所以需要在DPC栈中再切换到线程的内核栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
kd> u KiFastCallEntry l 80
nt!KiFastCallEntry:
8288a300 b923000000      mov     ecx,23h   ; KGDT_R3_DATA 0x23 = 0x20 + 011b(CPL Ring3)
8288a305 6a30            push    30h    ; KGDT_R0_PCR  0x30 = 0x30 + 000b(CPL Ring0), DPC Stack
8288a307 0fa1            pop     fs     ; DPC Stack
8288a309 8ed9            mov     ds,cx
8288a30b 8ec1            mov     es,cx
8288a30d 648b0d40000000  mov     ecx,dword ptr fs:[40h]  ;fs Base:ffdff000 Limit:1fff Data RW, KPCR 0x40处为TSS Ptr32 _KTSS
8288a314 8b6104          mov     esp,dword ptr [ecx+4]   ; KTSS 0x4处为Esp0 Uint4B, Ring0下的esp
8288a317 6a23            push    23h     ; 此时已在内核堆栈, Ring3下的ss, KTRAP_FRAME.HardwareSegSs
8288a319 52              push    edx     ; Ring3下的esp, KTRAP_FRAME.HardwareEsp
8288a31a 9c              pushfd          ; eflags  ; KTRAP_FRAME.EFlags
8288a31b 6a02            push    2
8288a31d 83c208          add     edx,8   ; Ring3堆栈的参数
8288a320 9d              popfd           ; 初始eflags为2, 即各位清零
8288a321 804c240102      or      byte ptr [esp+1],2     ; 4字节eflags第二个字节的IF中断允许位置1
8288a326 6a1b            push    1Bh                    ; KGDT_R3_CODE 0x1B = 0x18 + 011b(CPL Ring3), KTRAP_FRAME.SegCs
8288a328 ff350403dfff    push    dword ptr ds:[0FFDF0304h]   ; KTRAP_FRAME.EIP, 0x7ffe00000xffdf000映射到同一块物理内存, 0xffdf0304处存放的是KiFastSystemCallRet
8288a32e 6a00            push    0    ; KTRAP_FRAME.ErrCode
; 以下四个寄存器从Ring3到Ring0没修改过, 直接保存
8288a330 55              push    ebp
8288a331 53              push    ebx
8288a332 56              push    esi
8288a333 57              push    edi
8288a334 648b1d1c000000  mov     ebx,dword ptr fs:[1Ch]     ; 指向自己的指针, 头部即是TIB
8288a33b 6a3b            push    3Bh        ; KTRAP_FRAME.SegFs, fs Base:00000000 Limit:fff Data RW, Ring3
8288a33d 8bb324010000    mov     esi,dword ptr [ebx+124h]     ; KPCR.SelfPcr->PrcbData.CurrentThead Ptr32 _KTHREAD
8288a343 ff33            push    dword ptr [ebx]         ; KTRAP_FRAME.ExceptionList, TIB的头部即是ExceptionList
8288a345 c703ffffffff    mov     dword ptr [ebx],0FFFFFFFFh
8288a34b 8b6e28          mov     ebp,dword ptr [esi+28h]     ; KTHREAD.InitialStack Ptr32 Void
8288a34e 6a01            push    1       ; KTRAP_FRAME.PreviousPreviousMode
8288a350 83ec48          sub     esp,48h     ; 预留KTRAP_FRAME中Eax到DbgEbp的空间
8288a353 81ed9c020000    sub     ebp,29Ch
8288a359 c6863a01000001  mov     byte ptr [esi+13Ah],1    ; KTHREAD.PreviousMode
8288a360 3bec            cmp     ebp,esp
8288a362 7597            jne     nt!KiFastCallEntry2+0x49 (8288a2fb)
8288a364 83652c00        and     dword ptr [ebp+2Ch],0   ; KTRAP_FRAME第12(0x2C/4+1)参数Dr7
8288a368 f64603df        test    byte ptr [esi+3],0DFh    ; KTHREAD.DebugActive
8288a36c 89ae28010000    mov     dword ptr [esi+128h],ebp    ; KTHREAD.TrapFrame Ptr32 _KTRAP_FRAME
8288a372 0f8538feffff    jne     nt!Dr_FastCallDrSave (8288a1b0)   ; if(KTHREAD.DebugActive != 0) ...
8288a378 8b5d60          mov     ebx,dword ptr [ebp+60h]
8288a37b 8b7d68          mov     edi,dword ptr [ebp+68h]
8288a37e 89550c          mov     dword ptr [ebp+0Ch],edx     ; 将Ring3参数地址赋给KTRAP_FRAME.DbgArgPointer
8288a381 c74508000ddbba  mov     dword ptr [ebp+8],0BADB0D00h   ; KTRAP_FRAME.DbgArgMark = 0xBADB0D00
8288a388 895d00          mov     dword ptr [ebp],ebx      ; KTRAP_FRAME.DbgEbp = KTRAP_FRAME.Ebp
8288a38b 897d04          mov     dword ptr [ebp+4],edi    ; KTRAP_FRAME.DbgEip = KTRAP_FRAME.Eip
8288a38e fb              sti
; 当调用KeServiceDescriptorTableShadow中的系统服务时, eax是系统调用号再加0x1000
8288a38f 8bf8            mov     edi,eax
8288a391 c1ef08          shr     edi,8
8288a394 83e710          and     edi,10h
8288a397 8bcf            mov     ecx,edi
; 经过以上4条指令后, 若ecx为0则为调用KeServiceDescriptorTable中的系统服务, 否则ecx为0x10调用KeServiceDescriptorTableShadow中的系统服务
8288a399 03bebc000000    add     edi,dword ptr [esi+0BCh]     ; edi = KTHREAD.ServiceTable + 0x000x10
8288a39f 8bd8            mov     ebx,eax
8288a3a1 25ff0f0000      and     eax,0FFFh   ; 系统调用号
8288a3a6 3b4708          cmp     eax,dword ptr [edi+8]    ; 系统调用号必须小于系统服务数
8288a3a9 0f8333fdffff    jae     nt!KiBBTUnexpectedRange (8288a0e2)
8288a3af 83f910          cmp     ecx,10h
8288a3b2 751a            jne     nt!KiFastCallEntry+0xce (8288a3ce)   ; KeServiceDescriptorTable
8288a3b4 8b8e88000000    mov     ecx,dword ptr [esi+88h]
8288a3ba 33f6            xor     esi,esi
8288a3bc 0bb1700f0000    or      esi,dword ptr [ecx+0F70h]    ; TEB.GdiBatchCount
8288a3c2 740a            je      nt!KiFastCallEntry+0xce (8288a3ce)    ; KeServiceDescriptorTable
8288a3c4 52              push    edx    ; Ring3参数地址
8288a3c5 50              push    eax    ; 系统调用号
8288a3c6 ff154cf99a82    call    dword ptr [nt!KeGdiFlushUserBatch (829af94c)]
8288a3cc 58              pop     eax
8288a3cd 5a              pop     edx
8288a3ce 64ff05b0060000  inc     dword ptr fs:[6B0h]    ; KeServiceDescriptorTable直接跳转到这来, ++KPCR.KPRCB.KeSystemCalls
8288a3d5 8bf2            mov     esi,edx
8288a3d7 33c9            xor     ecx,ecx
8288a3d9 8b570c          mov     edx,dword ptr [edi+0Ch]   ; SSDT参数表的地址
8288a3dc 8b3f            mov     edi,dword ptr [edi]   ; 系统服务表地址
8288a3de 8a0c10          mov     cl,byte ptr [eax+edx]    ; 该系统调用的参数个数
8288a3e1 8b1487          mov     edx,dword ptr [edi+eax*4]   ; 该系统服务地址
8288a3e4 2be1            sub     esp,ecx   ; 在内核栈中分配参数空间
8288a3e6 c1e902          shr     ecx,2   ; 参数以DWORD大小拷贝
8288a3e9 8bfc            mov     edi,esp
8288a3eb 3b351cf79a82    cmp     esi,dword ptr [nt!MmUserProbeAddress (829af71c)]
8288a3f1 0f832e020000    jae     nt!KiSystemCallExit2+0xa5 (8288a625)    ; 若参数地址超过用户空间地址(0x7fff0000)
8288a3f7 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
8288a3f9 f6456c01        test    byte ptr [ebp+6Ch],1
8288a3fd 7416            je      nt!KiFastCallEntry+0x115 (8288a415)
8288a3ff 648b0d24010000  mov     ecx,dword ptr fs:[124h]
8288a406 8b3c24          mov     edi,dword ptr [esp]
8288a409 89993c010000    mov     dword ptr [ecx+13Ch],ebx
8288a40f 89b92c010000    mov     dword ptr [ecx+12Ch],edi
8288a415 8bda            mov     ebx,edx
8288a417 f60588c8978240  test    byte ptr [nt!PerfGlobalGroupMask+0x8 (8297c888)],40h
8288a41e 0f954512        setne   byte ptr [ebp+12h]
8288a422 0f858c030000    jne     nt!KiServiceExit2+0x17b (8288a7b4)
8288a428 ffd3            call    ebx       ; 调用!
8288a42a f6456c01        test    byte ptr [ebp+6Ch],1
8288a42e 7434            je      nt!KiFastCallEntry+0x164 (8288a464)
8288a430 8bf0            mov     esi,eax
8288a432 ff1568818482    call    dword ptr [nt!_imp__KeGetCurrentIrql (82848168)]
8288a438 0ac0            or      al,al
8288a43a 0f853b030000    jne     nt!KiServiceExit2+0x142 (8288a77b)
8288a440 8bc6            mov     eax,esi
8288a442 648b0d24010000  mov     ecx,dword ptr fs:[124h]
8288a449 f68134010000ff  test    byte ptr [ecx+134h],0FFh
8288a450 0f8543030000    jne     nt!KiServiceExit2+0x160 (8288a799)
8288a456 8b9184000000    mov     edx,dword ptr [ecx+84h]
8288a45c 0bd2            or      edx,edx
8288a45e 0f8535030000    jne     nt!KiServiceExit2+0x160 (8288a799)
8288a464 8be5            mov     esp,ebp
8288a466 807d1200        cmp     byte ptr [ebp+12h],0
8288a46a 0f8550030000    jne     nt!KiServiceExit2+0x187 (8288a7c0)
8288a470 648b0d24010000  mov     ecx,dword ptr fs:[124h]
8288a477 8b553c          mov     edx,dword ptr [ebp+3Ch]
8288a47a 899128010000    mov     dword ptr [ecx+128h],edx
nt!KiServiceExit:
8288a480 fa              cli
8288a481 f6457202        test    byte ptr [ebp+72h],2       ; eflags是否是Virtual-8086 Mode
8288a485 7506            jne     nt!KiServiceExit+0xd (8288a48d)     ; 非Virtual-8086 Mode则跳
8288a487 f6456c01        test    byte ptr [ebp+6Ch],1
8288a48b 7467            je      nt!KiServiceExit+0x74 (8288a4f4)      ; KTRAP_FRAME.SegCs, CPL为Ring0则跳
8288a48d 648b1d24010000  mov     ebx,dword ptr fs:[124h]   ; 非Virtual-8086 Mode, 以及下面的交付APC, 跳到这来
8288a494 f6430202        test    byte ptr [ebx+2],2
8288a498 7408            je      nt!KiServiceExit+0x22 (8288a4a2)
8288a49a 50              push    eax
8288a49b 53              push    ebx
8288a49c e8a4dc0900      call    nt!KiCopyCounters (82928145)
8288a4a1 58              pop     eax
8288a4a2 c6433a00        mov     byte ptr [ebx+3Ah],0     ; KPCR.KPRCB.CurrentThread->Alerted = 0
8288a4a6 807b5600        cmp     byte ptr [ebx+56h],0
8288a4aa 7448            je      nt!KiServiceExit+0x74 (8288a4f4)     ; if(KPCR.KPRCB.CurrentThread->ApcState.UserApcPending == 0)
8288a4ac 8bdd            mov     ebx,ebp
8288a4ae 894344          mov     dword ptr [ebx+44h],eax    ; KTRAP_FRAME.Eax, 系统调用号
8288a4b1 c743503b000000  mov     dword ptr [ebx+50h],3Bh    ; KTRAP_FRAME.SegFs = 0x3B
8288a4b8 c7433823000000  mov     dword ptr [ebx+38h],23h    ; KTRAP_FRAME.SegDs = KGDT_R3_DATA
8288a4bf c7433423000000  mov     dword ptr [ebx+34h],23h    ; KTRAP_FRAME.SegEs = KGDT_R3_DATA
8288a4c6 c7433000000000  mov     dword ptr [ebx+30h],0      ; KTRAP_FRAME.SegGs = 0

image

下图为快速系统调用的完整过程:

image

现在我相信大家对用户模式到内核模式的切换有了一个比较清晰的认识。

本文链接:http://www.alonemonkey.com/sysenter-sysexit.html