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

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

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

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

32位x86系统,每个进程的空间是4GB,即地址0x00000000到0xFFFFFFFF。
为了高效调用,Windows会把操作系统的内核数据和代码映射的系统中所有进程的进程空间中。因此4GB空间被划分为两个区域:用户空间和系统空间,默认大小为各2GB。(32位linux是 3g用户空间, 1g系统空间)
为了保护映射到进程空间的系统代码和数据,Windows提供了权限控制机制。也就是两种访问模式:用户模式和内核模式。
处理器在硬件一级保证高优先级别的数据和代码不会被低优先级破坏。
对x86处理器来说,没有任何寄存器表明处理器当前处于何种模式下,优先级知识代码或数据所在的内存段或页的一个属性。

模式切换的两种方式:软中断、快速系统调用指令。

1.INT 2E切换到内核模式
CPU在把执行权交给KiSystemService函数前,需要做一些准备工作:
(1)权限检查,即检查源和目标位置所在的代码段权限;
(2)准备内核态使用的栈。所有线程在内核态执行时必须使用内核栈;

KiSystemService的流程
(1)根据服务ID从SSDT中查找要调用的服务函数地址和参数;
(2)将参数从用户态栈复制到该线程的内核栈中;
(3)KiSystemService调用内核中正在的NtReadFile();
(4)KiSystemService将操作结果复制回线程用户态栈;
(5)通过IRET指令将执行权交回给NtDll.dll中的NtReadFile();

开销
(1)CPU必须从内存中加载门描述符和段描述符,以便得到KiSystemService()的地址;
(2)进行权限检查;
系统调用使用的很频繁,如果能减少这些开销,是很有意义的。
可以从两个方面来降低开销:
(1)把系统服务的例程KiSystemService()的地址放到寄存器中;
(2)避免权限检查,也就是使用特殊的指令让CPU省去那些对系统服务调用来说不需要的权限检查。奔腾II引入的SYSENTER/SYSEXIT就是为此设计的,AMD K7引入的是SYSCALL/SYSRETURN;

可以看到上述 的一个函数若是涉及到了, 用户模式到内核模式的切换,其实是有非常多的事情要做的, 有更多的函数调用和功能判断等, 因此一次用户模式到核心模式的切换是 非常昂贵的!!

2.快速系统调用
从WindoXp和Windows Service 2003开始,系统在启动过程中会通过CPUID指令检测CPU是否支持快速系统调用指令。如果支持这些指令,Windows会决定用新的方式进行系统调用,并做好如下准备工作:
(1)在GDT中建立4个段描述符,分别用来描述供SYSENTER/SYSEXIT使用的CS和SS;
(2)设置MSR寄存器,填充SYSENTER/SYSEXIT要跳转的地址等;
(3)将一小段名为SystemCallStub的代码复制到ShareUserData内存区。该代码调用SYSENTER或SYSCALL进入内核态;

由于 SharedUserData 中包含了可执行指令,那么 SharedUserData 所在的页就会被映射成可执行代码段。当微软试图对 XP SP2 以及 2003 SP1 做一些安全性增强工作时(例如:数据执行保护 DEP),他们大概意识到让 SharedUserData (所在页面)变得可执行非常地多余,这留下了潜在的滥用可能性。为了解决这个问题,那个在 KUSER_SHARED_DATA 里的域由一组指令变成了指向 ntdll.dll 里指令的函数指针.

其实这个过程 同上面过程一样也是 非常昂贵的!!

可见通常情况下尽量避免 用户模式到 核心模式的切换工作

下面是从网络上获取的更多的 关于用户模式切换到核心模式的工作过程, 作为上述一个初步的结论的粗略说明:

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

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

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

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

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

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

通过反汇编代码可以看到 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的反汇编代码。

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

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

好了,我们看到了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()负责完成这组寄存器的初始化:

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寄存器相应位置保存的什么:

sysenter切换堆栈时是从用户栈切换到DPC栈,这是因为MSR寄存器中的值是操作系统安排好的固定的值,它与具体

的线程上下文无关,所以需要在DPC栈中再切换到线程的内核栈。
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, 0x7ffe0000与0xffdf000映射到同一块物理内存, 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 + 0x00或0x10
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

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

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

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

windows下有哪些常见的 操作涉及 用户模式到核心模式的切换?

看下面的图

从图中可以知道, 通常的io操作一定涉及用户模式到核心模式的切换操作, 包括网络操作也是io操作的一部分!

 

--------------------

上面是 windows下用户模式到 内核模式的切换的一个情况, 那么linux是否这样, 下面是获取的其他资料

Linux用户模式和内核模式

在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。如果所有的程序都能使用这些指令,那么你的系统一天死机n回就不足为奇了。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。

linux的内核是一个有机的整体。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, 如上所提到的intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。Ring3状态不能访问Ring0的地址空间,包括代码和数据。Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运 行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必 须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。

处理器总处于以下状态中的一种:

1、内核态,运行于进程上下文,内核代表进程运行于内核空间;

2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;

3、用户态,运行于用户空间。

从用户空间到内核空间有两种触发手段:

1.用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

2.硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

一个程序我们可以从两种角度去分析。其一就是它的静态结构,其二就是动态过程。下图表示了用户态和内核态直接的关系(静态的角度来观察程序)

转自:http://www.cnblogs.com/cobbliu/archive/2012/11/07/2758184.html

下面是从另外角度说明的

MS-DOS等操作系统在单一的CPU模式下运行,但是一些类Unix的操作系统则使用了双模式,可以有效地实现时间共享。在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之中。

内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和I/O空间。如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码发出请求。另外,用户模式的代码允许发生缺页,而内核模式的代码则不允许。

在2.4和更早的内核中,仅仅用户模式的进程可以被上下文切换出局,由其他进程抢占。除非发生以下两种情况,否则内核模式代码可以一直独占CPU:

(1) 它自愿放弃CPU;

(2) 发生中断或异常。

2.6内核引入了内核抢占,大多数内核模式的代码也可以被抢占。

下面是从网上贴过来的,可能说的更明白一些。

一、内核空间和用户空间
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~ 4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间“)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。二、内核态和用户态
当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

三、进程上下文和中断上下文
处理器总处于以下状态中的一种:

1、内核态,运行于进程上下文,内核代表进程运行于内核空间;

2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;

3、用户态,运行于用户空间。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

可以看到 linux同windows一样都是  用户模式到核心模式切换时, 有很多昂贵的操作, 因此一定要了解!

 

结论:

1. 无论是 windows还是 linux 都存在着 用户模式到 核心模式的切换, 并且成本是非常高昂的!

2. 通常的io, 包括磁盘, 网络等io都是涉及 用户模式到 核心模式的切换的

3. 网络方面的性能就更差, 任何的网络调用都至少涉及本地用户模式同核心模式切换 以及 远程 用户模式同核心模式的切换两个过程, 因此网络调用 比本地io调用更慢, 何况可能多个交换机, 多个路由器都有类似切换, 以及处理时间等, 因此慢的至少一个数量级。  加上网络传输等, 抢占等过程, 就更慢了!!

5. 最快的是  同一个进程内的  用户模式下的函数调用

6. 然后应该是  用户进程下 用户模式到 核心模式的切换, 例如本地io等

7. 然后是 本地的进程间的  通讯, 原因是 你要有两个  用户模式的转换通常情况下(应答时通常也会有io的)