精华内容
下载资源
问答
  • 在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之中。  内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和...
  • 使用 C++ 编写内核模式驱动程序的优点与缺点.mht
  • 用户模式和内核模式(线程级)

    千次阅读 2021-03-31 11:44:49
    用户模式和内核模式区分主要是因为,为了计算机系统的正常安全运行,有些硬件资源(如:中断装置)和特权指令是不可以对用户进程开放的,所以区分了两种模式。 内核模式和用户模式 内核空间存放的是操作系统内核代码...

    用户模式和内核模式区分主要是因为,为了计算机系统的正常安全运行,有些硬件资源(如:中断装置)和特权指令是不可以对用户进程开放的,所以区分了两种模式。

    内核模式和用户模式

    内核空间存放的是操作系统内核代码和数据,是被所有程序共享的,在程序中修改内核空间中的数据不仅会影响操作系统本身的稳定性,还会影响其他程序,这是非常危险的行为,所以操作系统禁止用户程序直接访问内核空间。

    要想访问内核空间,必须借助操作系统提供的 API 函数,执行内核提供的代码,让内核自己来访问,这样才能保证内核空间的数据不会被随意修改,才能保证操作系统本身和其他程序的稳定性。

    内核模式(Kernel Mode):用户程序调用系统 API 函数称为系统调用(System Call);发生系统调用时会暂停用户程序,转而执行内核代码(内核也是程序),访问内核空间,这称为内核模式(Kernel Mode)。

    任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件。

    用户模式(User Mode):用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。当执行应用程序自己的代码时,称为用户模式(User Mode)。

    硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查,可以通过操作系统中的某种门机制进入内核模式访问。

    内核模式和用户模式之间切换

    当运行在用户模式的应用程序需要输入输出、申请内存等比较底层的操作时,就必须调用操作系统提供的 API 函数,从而进入内核模式;操作完成后,继续执行应用程序的代码,就又回到了用户模式。

    用户模式就是执行应用程度代码,访问用户空间;内核模式就是执行内核代码,访问内核空间(当然也有权限访问用户空间)。

    下列表格展示了两种模式切换过程:

    用户模式到内核模式内核模式到用户模式
    由中断/异常/系统调用中断用户进程执行而触发 。
    1.处理器模式转为内核模式。
    2.保存当前进程的PC/PSW值到核心栈。
    3.转向中断/异常/系统调用处理程序。
    OS执行中断返回指令将控制权交还用户进程而触发。
    1.从待运行进程核心栈中弹出PC/PSW值。
    2.处理器模式转为用户模式。

    内核级线程(KLT)和用户级线程(ULT)

    进程是资源拥有的基本单位,进程切换需要保存进程状态,会造成资源的消耗。同一进程中的线程,共享进程获取的部分资源。在同一进程中,线程的切换不会引起进程切换,线程的切换需要的资源少于进程切换,可以提高效率。

    内核级线程(Kernel-Level Threads), KLT 也有叫做内核支持的线程。
    • 线程管理的所有工作(创建和撤销)由操作系统内核完成
    • 操作系统内核提供一个应用程序设计接口API,供开发者使用KLT
    用户级线程(User-Level Threads ULT)
    • 用户空间运行线程库,任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它提供多线程应用程序的开发和运行支撑环境,包含:用于创建和销毁线程的代码、在线程间传递数据和消息的代码、调度线程执行的代码以及保存和恢复线程上下文的代码。
    • 所以线程的创建,消息传递,调度,保存/恢复上下文都有线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)。
    内核级线程特点用户级线程的特点
    1.进程中的一个线程被阻塞,内核能调度同一进程的其他线程(就绪态)占有处理器运行。
    2.多处理器环境中,内核能同时调度同一进程的多线程,将这些线程映射到不同的处理器核心上,提高进程的执行效率。
    3.应用程序线程在用户态运行,线程调度和管理在内核实现。线程调度时,控制权从一个线程改变到另一线程,需要模式切换,系统开销较大。
    1.线程切换不需要内核模式,能节省模式切换开销和内核资源。
    2.允许进程按照特定的需要选择不同的调度算法来调度线程。调度算法需要自己实现。
    3.由于其不需要内核进行支持,所以可以跨OS运行。
    4.不能利用多核处理器有点,OS调度进程,每个进程仅有一个ULT能执行
    5.一个ULT阻塞,将导致整个进程的阻塞。

    jacketing技术可以解决用户级线程ULT一个线程阻塞导致整个进程阻塞。

    jacketing的目标是把一个产生阻塞的系统调用转化成一个非阻塞的系统调用。例如,当进程中的一个线程调用IO中断钱,先调用一个应用级的I/O jacket例程,而不是直接调用一个系统I/O。让这个jacket例程检查并确定I/O设备是否忙。如果忙,则jacketing将控制权交给该进程的线程调度程序,决定该线程进入阻塞状态并将控制权传送给另一个线程(若无就绪态线程咋可能执行进程切换)。

    线程实现的组合策略

    可以看出,用户级线程和内核级线程都有各自的优点和缺点,在应用上主要表现为:

    • 用户级多线程对于处理逻辑并行性问题有很好的效果。不擅长于解决物理并发问题。
    • 内核级多线程适用于解决物理并行性问题。

    组合策略:
    由操作系统内核支持内核级多线程,由操作系统的程序库来支持用户级多线程,线程创建完全在用户空间创建,现成的调度也在应用程序内部进行,然后把用户级多线程映射到(或者说是绑定到)一些内核级多线程。

    编程人员可以针对不同的应用特点调节内核级线程的数目来达到物理并行性和逻辑并行性的最佳方案。
    在这里插入图片描述

    参考文章

    • https://blog.csdn.net/winterfeng123/article/details/79788714
    • https://blog.csdn.net/sinat_38104725/article/details/98474760
    • https://blog.csdn.net/winterfeng123/article/details/79784430
    • https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode
    • https://www.kanzhun.com/msh/post/1691.html

    关注公众号 ,专注于java大数据领域离线、实时技术干货定期分享!个人网站 www.lllpan.top

    在这里插入图片描述

    展开全文
  • 根据前面的介绍,NT内核会把操作系统的代码和数据映射到系统中所有进程的内核空间中。这样,每个进程内的应用程序代码便可以很方便地调用内核空间中的系统服务。这里的“很方便”有多层含义,一方面是内核代码和用户...

    根据前面的介绍,NT内核会把操作系统的代码和数据映射到系统中所有进程的内核空间中。这样,每个进程内的应用程序代码便可以很方便地调用内核空间中的系统服务。这里的“很方便”有多层含义,一方面是内核代码和用户代码在一个地址空间中,应用程序调用系统服务时不需要切换地址空间,另一方面是整个系统中内核空间的地址是统一的,编写内核空间的代码时会简单很多。但是,如此设计也带来一个很大的问题,那就是用户空间中的程序指针可以指向内核空间中的数据和代码,因此必须防止用户代码破坏内核空间中的操作系统。怎么做呢?答案是利用权限控制来实现对内核空间的保护。

    2.6.1 访问模式

    Windows定义了两种访问模式(access mode)——用户模式(user mode,也称为用户态)和内核模式(kernel mode,也称为内核态)。应用程序(代码)运行在用户模式下,操作系统代码运行在内核模式下。内核模式对应于处理器的最高权限级别(不考虑虚拟机情况),在内核模式下执行的代码可以访问所有系统资源并具有使用所有特权指令的权利。相对而言,用户模式对应于较低的处理器优先级,在用户模式下执行的代码只可以访问系统允许其访问的内存空间,并且没有使用特权指令的权利。

    本书卷1介绍过,IA-32处理器定义了4种特权级别(privilege level),或者称为环(ring),分别为0、1、2、3,优先级0(环0)的特权级别最高。处理器在硬件一级保证高优先级的数据和代码不会被低优先级的代码破坏。Windows系统使用了IA-32处理器所定义的4种优先级中的两种,优先级3(环3)用于用户模式,优先级0用于内核模式。之所以只使用了其中的两种,主要是因为有些处理器只支持两种优先级,比如Compaq Alpha处理器。值得说明的是,对于x86处理器来说,并没有任何寄存器表明处理器当前处于何种模式(或优先级)下,优先级只是代码或数据所在的内存段或页的一个属性,参见卷1的2.6节和2.7节。

    因为内核模式下的数据和代码具有较高的优先级,所以用户模式下的代码不可以直接访问内核空间中的数据,也不可以直接调用内核空间中的任何函数或例程。任何这样的尝试都会导致保护性错误。也就是说,即使用户空间中的代码指针正确指向了要访问的数据或代码,但一旦访问发生,那么处理器会检测到该访问是违法的,会停止该访问并产生保护性异常(#GP)。

    虽然不可以直接访问,但是用户程序可以通过调用系统服务来间接访问内核空间中的数据或间接调用、执行内核空间中的代码。当调用系统服务时,主调线程会从用户模式切换到内核模式,调用结束后再返回到用户模式,也就是所谓的模式切换。在线程的KTHREAD结构中,定义了UserTime和KernelTime两个字段,分别用来记录这个线程在用户模式和内核模式的运行时间(以时钟中断次数为单位)。模式切换是通过软中断或专门的快速系统调用(fast system call)指令来实现的。下面通过一个例子来分别介绍这两种切换机制。

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

    图2-4展示了在Windows 2000中通过INT 2E从应用程序调用ReadFile() API的过程。因为ReadFile() API是从Kernel32.dll导出的,所以我们看到该调用首先转到Kernel32.dll中的ReadFile()函数,ReadFile()函数在对参数进行简单检查后便调用NtDll.dll中的NtReadFile()函数。

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    图2-4 通过INT 2E从应用程序调用ReadFile() API的过程

    通过反汇编可以看到,NtDll.dll中的NtReadFile ()函数非常简短,首先将ReadFile()对应的系统服务号(0xa1,与版本有关)放入EAX寄存器中,将参数指针放入EDX寄存器中,然后便通过INT n指令发出调用。这里要说明的一点是,虽然每个系统服务都具有唯一的号码,但微软公司没有公开这些服务号,也不保证这些号码在不同的Windows版本中会保持一致。

    ntdll!NtReadFile: // Windows 2000
    77f8fb5d b8a1000000    mov      eax,0xa1
    77f8fb62 8d542404      lea      edx,[esp+0x4]
    77f8fb66 cd2e          int      2e
    77f8fb68 c22400        ret      0x24

    在WinDBG下通过!idt 2e命令可以看到2e号向量对应的服务例程是KiSystemService ()。KiSystemService ()是内核态中专门用来分发系统调用的例程。

    lkd> !idt 2e
    Dumping IDT:
    2e:   804db1ed nt!KiSystemService

    Windows将2e号向量专门用于系统调用,在启动早期初始化中断描述符表(Interrupt Descriptor Table,IDT)时(见第11章)便注册好了合适的服务例程。因此当NTDll.DLL中的NtReadFile()发出INT 2E指令后,CPU便会通过IDT找到KiSystemService ()函数。因为KiSystemService ()函数是位于内核空间的,所以CPU在把执行权交给KiSystemService ()函数前,会做好从用户模式切换到内核模式的各种工作,包括:

    (1)权限检查,即检查源位置和目标位置所在的代码段权限,核实是否可以转移;

    (2)准备内核模式使用的栈,为了保证内核安全,所有线程在内核态执行时都必须使用位于内核空间的内核栈(kernel stack),内核栈的大小一般为8KB或12KB。

    KiSystemService ()会根据服务ID从系统服务分发表(System Service Dispatch Table)中查找到要调用的服务函数地址和参数描述,然后将参数从用户态栈复制到该线程的内核栈中,最后KiSystemService ()调用内核中真正的NtReadFile()函数,执行读文件的操作,操作结束后会返回到KiSystemService (),KiSystemService ()会将操作结果复制回该线程用户态栈,最后通过IRET指令将执行权交回给NtDll.dll中的NtReadFile()函数(继续执行INT 2E后面的那条指令)。

    通过INT 2E进行系统调用时,CPU必须从内存中分别加载门描述符和段描述符才能得到KiSystemService ()的地址,即使门描述符和段描述符已经在高速缓存中,CPU也需要通过“内存读(memory read)”操作从高速缓存中读出这些数据,然后进行权限检查。

    2.6.3 快速系统调用

    因为系统调用是非常频繁的操作,所以如果能减少这些开销还是非常有意义的。可以从两个方面来降低开销:一是把系统调用服务例程的地址放到寄存器中以避免读IDT这样的内存操作,因为读寄存器的速度比读内存的速度要快很多;二是避免权限检查,也就是使用特殊的指令让CPU省去那些对系统服务调用来说根本不需要的权限检查。奔腾II处理器引入的SYSENTER/SYSEXIT指令正是按这一思路设计的。AMD K7引入的SYSCALL/SYSRETURN指令也是为这一目的而设计的。相对于INT 2E,使用这些指令可以加快系统调用的速度,因此利用这些指令进行的系统调用称为快速系统调用。

    下面我们介绍Windows系统是如何利用IA-32处理器的SYSENTER/SYSEXIT指令(从奔腾II开始)实现快速系统调用的[2]。首先,Windows 2000或之前的Windows系统不支持快速系统调用,它们只能使用前面介绍的INT 2E方式进行系统调用。Windows XP和Windows Server 2003或更新的版本在启动过程中会通过CPUID指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位)。如果CPU不支持这些指令,那么仍使用INT 2E方式。如果CPU支持这些指令,那么Windows系统便会决定使用新的方式进行系统调用,并做好如下准备工作。

    (1)在全局描述符表(GDT)中建立4个段描述符,分别用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段。这4个段描述符在GDT中的排列应该严格按照以上顺序,只要指定一个段描述符的位置便能计算出其他的。

    (2)设置表2-1中专门用于系统调用的MSR(关于MSR的详细介绍见卷1的2.4.3节),SYSENTER_EIP_MSR用于指定新的程序指针,也就是SYSENTER指令要跳转到的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为KiFastCallEntry例程是Windows内核中专门用来受理快速系统调用的。SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段。SYSENTER_ESP_MSR用于指定新的栈指针(ESP)。新的栈段是由SYSENTER_CS_MSR的值加8得来的。

    (3)将一小段名为SystemCallStub的代码复制到SharedUserData内存区,该内存区会被映射到每个Win32进程的进程空间中。这样当应用程序每次进行系统调用时,NTDll.DLL中的残根(stub)函数便调用这段SystemCallStub代码。SystemCallStub的内容因系统硬件的不同而不同,对于IA-32处理器,该代码使用SYSENTER指令,对于AMD处理器,该代码使用SYSCALL指令。

    表2-1 供SYSENTER指令使用的MSR(略)

    例如在配有Pentium M CPU的Windows XP系统上,以上3个寄存器的值分别为:

    lkd> rdmsr 174
    msr[174] = 00000000`00000008
    lkd> rdmsr 175
    msr[175] = 00000000`bacd8000
    lkd> rdmsr 176
    msr[176] = 00000000`8053cad0

    其中SYSENTER_CS_MSR的值为8,这是Windows系统的内核代码段的选择子,即常量KGDT_R0_CODE的值。WinDBG帮助文件中关于dg命令的说明中列出了这个常量。SYSENTER_EIP_MSR的值是8053cad0,检查nt内核中KiFastCallEntry函数的地址。

    lkd> x nt!KiFastCallEntry
    8053cad0 nt!KiFastCallEntry = <no type information>

    可见,Windows把快速系统调用的目标指向内核代码段中的KiFastCallEntry函数。

    通过反汇编Windows XP下NTDll.DLL中的NtReadFile ()函数,可以看到SystemCallStub被映射到进程的0x7ffe0300位置。与前面Windows 2000下的版本相比,容易看到该服务的系统服务号码在这两个版本间是不同的。

    kd> u ntdll...
    ntdll!NtReadFile: // Windows XP
    77f5bfa8 b8b7000000       mov     eax,0xb7
    77f5bfad ba0003fe7f       mov     edx,0x7ffe0300
    77f5bfb2 ffd2             call edx {SharedUserData!SystemCallStub (7ffe0300)}
    77f5bfb4 c22400           ret     0x24
    77f5bfb7 90               nop

    观察本段下面反汇编SystemCallStub的结果,它只包含3条指令,分别用于将栈指针(ESP寄存器)放入EDX寄存器中、执行sysenter指令和返回。第一条指令有两个用途:一是向内核空间传递参数;二是指定从内核模式返回时的栈地址。因为笔者使用的是英特尔奔腾M处理器,所以此处是sysenter指令,对于AMD处理器,此处应该是syscall指令。

    kd> u...
    SharedUserData!SystemCallStub:
    7ffe0300 8bd4             mov     edx,esp
    7ffe0302 0f34             sysenter
    7ffe0304 c3               ret

    下面让我们看一下KiFastCallEntry例程,其清单如下所示。

    kd> u nt!KiFastCallEntry L20
    nt!KiFastCallEntry:
    804db1bb 368b0d40f0dfff   mov      ecx,ss:[ffdff040]
    804db1c2 368b6104         mov      esp,ss:[ecx+0x4]
    804db1c6 b90403fe7f       mov      ecx,0x7ffe0304
    804db1cb 3b2504f0dfff     cmp      esp,[ffdff004]
    804db1d1 0f84cc030000     je       nt!KiServiceExit2+0x13f (804db5a3)
    804db1d7 6a23             push     0x23
    804db1d9 52               push     edx
    804db1da 83c208           add      edx,0x8
    804db1dd 6802020000       push     0x202
    804db1e2 6a02             push     0x2
    804db1e4 9d               popfd
    804db1e5 6a1b             push     0x1b
    804db1e7 51               push     ecx // Fall Through,自然进入KiSystemService函数
    nt!KiSystemService:
    804db1e8 90               nop
    804db1e9 90               nop
    804db1ea 90               nop
    804db1eb 90               nop
    804db1ec 90                nop
    nt!KiSystemService:
    804db1ed 6a00             push      0x0
    804db1ef 55               push      ebp

    显而易见,KiFastCallEntry在做了些简单操作后,便下落(fall through)到KiSystemService函数了,也就是说,快速系统调用和使用INT 2E进行的系统调用在内核中的处理绝大部分是一样的。另外,请注意ecx寄存器,mov ecx,0x7ffe0304将其值设为0x7ffe0304,也就是SharedUserData内存区里SystemCallStub例程中ret指令的地址(参见上文的SystemCallStub代码)。在进入nt!KiSystemService之前,ecx连同其他一些参数被压入栈中。事实上,ecx用来指定SYSEXIT返回用户模式时的目标地址。当使用INT 2E进行系统调用时,由于INT n指令会自动将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会使用栈中保存的CS和EIP值返回合适的位置。因为sysenter指令不会向栈中压入要返回的位置,所以sysexit指令必须通过其他机制知道要返回的位置。这便是压入ECX寄存器的原因。通过反汇编KiSystemCallExit2例程,我们可以看到在执行sysexit指令之前,ecx寄存器的值又从栈中恢复出来了。

    kd> u nt!KiSystemCallExit l20
    nt!KiSystemCallExit:
    804db3b4 cf            iretd
    nt!KiSystemCallExit2:
    804db3b5 5a            pop      edx
    804db3b6 83c408        add      esp,0x8
    804db3b9 59            pop      ecx
    804db3ba fb            sti
    804db3bb 0f35          sysexit
    nt!KiSystemCallExit3:
    804db3bd 59            pop      ecx
    804db3be 83c408        add      esp,0x8
    804db3c1 5c            pop      esp
    804db3c2 0f07          sysret

    以上代码中包含了3个从系统调用返回的例程,即KiSystemCallExit、KiSystemCallExit2和KiSystemCallExit3,它们分别对应于使用INT 2E、sysenter和syscall发起的系统调用,如表2-2所示。

    表2-2 系统调用(略)

    图2-5展示了使用sysenter/sysexit指令对进行系统调用的完整过程(以调用ReadFile服务为例)。

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    图2-5 快速系统调用(针对IA-32处理器)


     

     

    格物

    下面通过一个小的实验来加深大家对系统调用的理解。首先启动WinDBG程序,选择File → Open Crash Dump,然后选择本书实验文件中的dumps\w732cf4.dmp文件。在调试会话建立后,先执行.symfix c:\symbols和.reload加载模块与符号,再执行k命令,便得到清单2-4所示的完美栈回溯。

    第22章将详细讲解栈回溯的原理,现在大家只要知道栈上记录着函数相互调用时的参数和返回地址等信息。栈回溯是从栈上找到这些信息,然后显示出来的过程,是追溯线程执行轨迹的一种便捷方法。

    清单2-4还显示了任务管理器程序(taskmgr)调用NtTerminateProcess系统服务时的执行过程。栈回溯的结果包含4列,第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是使用“函数名+字节偏移量”形式表达的执行位置。以00栈帧为例,它对应的函数是著名的蓝屏函数KeBugCheckEx,它的栈帧基地址是9796fb9c,它的返回地址是82b1ab51,翻译成符号便是PspCatchCriticalBreak+0x71。

    清单2-4 完美栈回溯

    # ChildEBP RetAddr  
    00 9796fb9c 82b1ab51 nt!KeBugCheckEx+0x1e
    01 9796fbc0 82a6daa8 nt!PspCatchCriticalBreak+0x71
    02 9796fbf0 82a605b6 nt!PspTerminateAllThreads+0x2d
    03 9796fc24 8287c87a nt!NtTerminateProcess+0x1a2
    04 9796fc24 77da7094 nt!KiFastCallEntry+0x12a
    05 001df4dc 77da68d4 ntdll!KiFastSystemCallRet
    06 001df4e0 76193c82 ntdll!NtTerminateProcess+0xc
    07 001df4f0 00bf57b9 KERNELBASE!TerminateProcess+0x2c
    08 001df524 00bf67ec taskmgr!CProcPage::KillProcess+0x116
    09 001df564 00bebc96 taskmgr!CProcPage::HandleWMCOMMAND+0x10f
    0a 001df5d8 76abc4e7 taskmgr!ProcPageProc+0x275
    0b 001df604 76ad5b7c USER32!InternalCallWinProc+0x23
    0c 001df680 76ad59f3 USER32!UserCallDlgProcCheckWow+0x132
    0d 001df6c8 76ad5be3 USER32!DefDlgProcWorker+0xa8
    0e 001df6e4 76abc4e7 USER32!DefDlgProcW+0x22
    0f 001df710 76abc5e7 USER32!InternalCallWinProc+0x23
    10 001df788 76ab5294 USER32!UserCallWinProcCheckWow+0x14b
    11 001df7c8 76ab5582 USER32!SendMessageWorker+0x4d0
    12 001df7e8 74e94601 USER32!SendMessageW+0x7c
    13 001df808 74e94663 COMCTL32!Button_NotifyParent+0x3d
    14 001df824 74e944ed COMCTL32!Button_ReleaseCapture+0x113
    15 001df884 76abc4e7 COMCTL32!Button_WndProc+0xa18
    16 001df8b0 76abc5e7 USER32!InternalCallWinProc+0x23
    17 001df928 76abcc19 USER32!UserCallWinProcCheckWow+0x14b
    18 001df988 76abcc70 USER32!DispatchMessageWorker+0x35e
    19 001df998 76ab41eb USER32!DispatchMessageW+0xf
    1a 001df9bc 00be16fc USER32!IsDialogMessageW+0x588
    1b 001dfdac 00be5384 taskmgr!wWinMain+0x5d1
    1c 001dfe40 76bbed6c taskmgr!_initterm_e+0x1b1
    1d 001dfe4c 77dc377b kernel32!BaseThreadInitThunk+0xe
    1e 001dfe8c 77dc374e ntdll!__RtlUserThreadStart+0x70
    1f 001dfea4 00000000 ntdll!_RtlUserThreadStart+0x1b

    仔细观察清单2-4中的地址部分,很容易看出用户空间和内核空间的分界,也就是在栈帧04和栈帧05之间。栈帧05中的KiFastSystemCallRet函数属于ntdll模块,位于用户空间。栈帧04中的KiFastCallEntry函数属于nt模块,位于内核空间。栈帧04的基地址是9796fc24,属于内核空间;栈帧05的基地址是001df4dc,属于用户空间。它们分别来自这个线程的内核态栈和用户态栈。WinDBG的k命令穿越两个空间,遍历两个栈,显示出线程在用户空间和内核空间执行的完整过程,能产生如此完美的栈回溯显示了WinDBG的强大。


    2.6.4 逆向调用

    前文介绍了从用户模式进入内核模式的两种方法,通过这两种方法,用户模式的代码可以“调用”位于内核模式的系统服务。那么内核模式的代码是否可以主动调用用户模式的代码呢?答案是肯定的,这种调用通常称为逆向调用(reverse call)。

    简单来说,逆向调用的过程是这样的。首先内核代码使用内核函数KiCallUserMode发起调用。接下来的执行过程与从系统调用返回(KiServiceExit)类似,不过进入用户模式时执行的是NTDll.DLL中的KiUserCallbackDispatcher。而后KiUserCallbackDispatcher会调用内核希望调用的用户态函数。当用户模式的工作完成后,执行返回动作的函数会执行INT 2B指令,也就是触发一个0x2B异常。这个异常的处理函数是内核模式的KiCallbackReturn函数。于是,通过INT 2B异常,CPU又跳回内核模式继续执行了。

    lkd> !idt 2b
    Dumping IDT:
    2b:   8053d070 nt!KiCallbackReturn

    以上是使用WinDBG的!idt命令观察到的0x2B异常的处理函数。

    2.6.5 实例分析

    下面通过一个实际例子来进一步展示系统调用和逆向调用的执行过程。清单2-5显示了使用WinDBG的内核调试会话捕捉到的记事本进程发起系统调用进入内核和内核函数执行逆向调用的全过程(栈回溯)。

    清单2-5 记事本进程从发起系统调用进入内核和内核函数逆向调用的全过程

    kd> kn
     # ChildEBP RetAddr  
    00 0006fe94 77fb4da6 USER32!XyCallbackReturn
    01 0006fe94 8050f8ae ntdll!KiUserCallbackDispatcher+0x13
    02 f4fc19b4 80595d2c nt!KiCallUserMode+0x4
    03 f4fc1a10 bf871e98 nt!KeUserModeCallback+0x87
    04 f4fc1a90 bf8748d4 win32k!SfnDWORD+0xa0
    05 f4fc1ad8 bf87148d win32k!xxxSendMessageToClient+0x174
    06 f4fc1b24 bf8714d3 win32k!xxxSendMessageTimeout+0x1a6
    07 f4fc1b44 bf8635f6 win32k!xxxSendMessage+0x1a
    08 f4fc1b74 bf84a620 win32k!xxxMouseActivate+0x22d
    09 f4fc1c98 bf87a0c1 win32k!xxxScanSysQueue+0x828
    0a f4fc1cec bf87a8ad win32k!xxxRealInternalGetMessage+0x32c
    0b f4fc1d4c 804da140 win32k!NtUserGetMessage+0x27
    0c f4fc1d4c 7ffe0304 nt!KiSystemService+0xc4
    0d 0006feb8 77d43a21 SharedUserData!SystemCallStub+0x2
    0e 0006febc 77d43c95 USER32!NtUserGetMessage+0xc
    0f 0006fed8 010028e4 USER32!GetMessageW+0x31
    10 0006ff1c 01006c54 notepad!WinMain+0xe3
    11 0006ffc0 77e814c7 notepad!WinMainCRTStartup+0x174
    12 0006fff0 00000000 kernel32!BaseProcessStart+0x23

    根据执行的先后顺序,最下面一行(帧#12)对应的是进程的启动函数BaseProcessStart,而后是编译器生成的进程启动函数WinMainCRTStartup,以及记事本程序自己的入口函数WinMain。帧#0f表示记事本程序在调用GetMessage API进入消息循环。接下来GetMessage API调用Windows子系统服务的残根函数NtUserGetMessage。从第2列的栈帧基地址都小于0x800000000可以看出,帧#12~#0d都是在用户模式执行的。帧#0d执行我们前面分析过的SystemCallStub,而后(帧#0c)便进入了内核模式的KiSystemService。KiSystemService根据系统服务号码,将调用分发给Windows子系统内核模块win32k中的NtUserGetMessage函数。

    帧#0a~#05表示内核模式的窗口消息函数在工作。帧#07~#05表示要把一个窗口消息发送到用户态。帧#04的SfnDWORD表示在将消息组织好后调用KeUserModeCallback函数,发起逆向调用。帧#02表明在执行KiCallUserMode函数,帧#01表明已经在用户模式下执行,这两行之间的部分过程没有显示出来。同样,帧#01 和帧#00 之间执行用户模式函数的过程没有完全体现出来。XyCallbackReturn函数是用于返回内核模式的,它的代码很简单,只有如下几条指令。

    USER32!XyCallbackReturn:
    001b:77d44168 8b442404     mov   eax,dword ptr [esp+4] ss:0023:0006fe84=00000000
    001b:77d4416c cd2b          int   2Bh
    001b:77d4416e c20400        ret   4

    第1行把用户模式函数的执行结果赋给EAX寄存器,第2行执行INT 2B指令。执行过INT 2B后,CPU便转去执行异常处理程序KiCallbackReturn,回到了内核模式。

    本文摘自《软件调试(第2版)卷2:Windows平台调试(上、下册)》

    Windows操作系统管理进程和线程:内核模式和用户模式

     

    本书是国内当前集中介绍软件调试主题的权威著作。本书第2卷分为5篇,共30章,主要围绕Windows系统展开介绍。第一篇(第1~4章)介绍Windows系统简史、进程和线程、架构和系统部件,以及Windows系统的启动过程,既从空间角度讲述Windows的软件世界,也从时间角度描述Windows世界的搭建过程。第二篇(第5~8章)描述特殊的过程调用、垫片、托管世界和Linux子系统。第三篇(第9~19章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和JIT调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第20~25章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第26~30章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在Visual Studio和Visual Studio(VS)Code中的调试器,最后深度解析WinDBG调试器的历史、结构和用法。

    本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。

    展开全文
  • 架构模式 - 微内核模式

    千次阅读 2019-08-12 12:14:21
    1. 微内核模式简介 微内核(Microkernel)架构模式结构如下图所示,有时也被称为插件架构模式(Plug-in Architecture Pattern),通过插件向核心应用添加额外的功能,可以实现功能的独立和分离。 微内核架构包含两...

    1. 微内核模式简介

    微内核(Microkernel)架构模式结构如下图所示,有时也被称为插件架构模式(Plug-in Architecture Pattern),通过插件向核心应用添加额外的功能,可以实现功能的独立和分离。

    微内核架构包含两部分组件,即内核系统(Core system)和插件(Plug-in Component)。微内核架构的内核系统通常提供系统运行所需的最小功能集,插件是独立的组件,包含特定的处理、额外的功能和自定义代码,用来向内核系统增强或扩展额外的业务能力。

    微内核是内核的一种精简形式。将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选件  这样就可提供更好的可扩展性和更加有效的应用环境。使用微内核设计,对系统进行升级,显然只要用新模块替换旧模块,不需要改变整个系统架构。

    那么插件是什么?插件一般由以下几部分组成:插件暴露的接口(一般称为叫API),插件内部实现,插件扩展点以及插件配置。其中插件扩展点我们一般设计为SPI(Service Provider Interface,服务提供接口)。

    微内核模式的本质是管理插件以及协调插件之间的调用。插件插件本身是一个很大粒度的扩展点,可以整个被替换。同时插件可以提供自己的小粒度扩展点。这样整个系统就是由一个微内核加很多插件组成一个具备很强扩展性的系统。

    2. 微内核模式在Dubbo中的应用

    微内核架构风格在Dubbo中应用广泛,通信框架Mina、Netty和Grizzly,序列化方式Hession、JSON,传输协议Dubbo、RMI等都是这一架构风格的体现。我们可以通过简单的配置就能对这些具体实现进行排列组合构成丰富的运行时环境。       微内核架构风格提供的是一种解决扩展性问题的思路,Dubbo中实现这一思路的是SPI(Service Provider Interface)机制。

    JDK提供了服务实现查找的一个工具类java.util.ServiceLoader来实现SPI机制。当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录同时创建一个以服务接口命名的文件,该文件里配置着实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里硬编码指定。Dubbo基于JDK中的SPI机制并做了优化,不会一次性实例化扩展点的所有实现并做了异常处理。

    Dubbo提供专门的@SPI注解,只有添加@SPI注解的接口类才会去查找扩展点实现,查找位置包括META-INF/dubbo/和META-INF/services/,而META-INF/dubbo/internal/中则定义了各项用于供Dubbo本身使用的内部扩展。举例来说,前面提到Dubbo对传输协议提供了Hessian、Dubbo等多种实现,Dubbo内部通过扩展点的配置确定使用何种机制。在dubbo-rpc-default工程和dubbo-rpc-hessian工程中,我们在META-INF/dubbo/internal/目录下都发现了com.alibaba.dubbo.rpc. Protocol配置文件,但里面的内容分别指向了com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol和com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol类,意味着当我们引用某个具体工程时,通过该工程中的配置项就可以找到相应的扩展点实现。dubbo-rpc-default工程的配置见下图。

    ExtensionLoader是实现扩展点加载的核心类,我们在上面例子的配置中确定了protocol的实现类,然后ExtensionLoader通过以下方式获取该类的实例:

    我们注意到ExtensionLoader有个getAdaptiveExtension方法,该方法的命名源于@Adaptive注解。ExtensionLoader注入的扩展点是一个Adaptive实例,直到扩展点方法执行时才决定调用哪一个扩展点实现。Dubbo使用URL对象传递配置信息,扩展点方法调用会根据URL参数中包含的Key-Value实现自适应,而URL的Key通过@Adaptive注解在接口方法上提供。如在下面的例子中,对于bind方法定义,Adaptive实现先查找"server" Key,如果该Key没有值则找“transport” Key值,从而决定加载哪个具体扩展点:

    而在Dubbo配置模块中,扩展点均有对应配置属性或标签,通过配置指定使用哪个扩展实现,如<dubbo:protocol name= "dubbo" />即代表应该获取dubbo协议扩展点,在调用过程中,Dubbo会在URL中自动加入相应的Key-Value对。

    对于Dubbo的整体架构,Microkernel作为一种架构风格只负责组装功能而所有功能通过扩展点实现。Dubbo自身功能也是基于这一机制实现,所有功能点都可被用户自定义扩展和替换。所有扩展点定义都通过传递携带配置信息的方式在运行时传入Dubbo,确保整个框架采用一致性数据模型。

     

    如果对文章感兴趣,可以关注我的微信公众号:程序员向架构师转型,或扫描下面的二维码。

    我出版了《系统架构设计:程序员向架构师转型之路》、《向技术管理者转型:软件开发人员跨越行业、技术、管理的转型思维与实践》、《微服务设计原理与架构》、《微服务架构实战》等书籍,并翻译有《深入RabbitMQ》和《Spring5响应式编程实战》,欢迎交流。

    展开全文
  • 用户模式和内核模式

    千次阅读 多人点赞 2019-08-04 21:39:24
    ·当运行在用户模式的应用程序需要输入输出、申请内存等比较底层的操作时,就必须调用操作系统提供的 API 函数,从而进入内核模式; ·操作完成后,继续执行应用程序的代码,就又回到了用户模式。 总结: 用户...

    首先我们要解释一个概念——进程(Process)。简单来说,一个可执行程序就是一个进程,我们使用C语言编译生成的程序,运行后就是一个进程。进程最显著的特点就是拥有独立的地址空间。

    严格来说,程序是存储在磁盘上的一个文件,是指令和数据的集合,是一个静态的概念;进程是程序加载到内存运行后一些列的活动,是一个动态的概念。

    “程序的地址空间”的说法,这其实是不严谨的,应该说“进程的地址空间”。一个进程对应一个地址空间,而一个程序可能会创建多个进程

    内核模式和用户模式

    内核空间存放的是操作系统内核代码和数据,是被所有程序共享的,在程序中修改内核空间中的数据不仅会影响操作系统本身的稳定性,还会影响其他程序,这是非常危险的行为,所以操作系统禁止用户程序直接访问内核空间。

    要想访问内核空间,必须借助操作系统提供的 API 函数,执行内核提供的代码,让内核自己来访问,这样才能保证内核空间的数据不会被随意修改,才能保证操作系统本身和其他程序的稳定性。

    用户程序调用系统 API 函数称为系统调用(System Call);发生系统调用时会暂停用户程序,转而执行内核代码(内核也是程序),访问内核空间,这称为内核模式(Kernel Mode)。

    用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。当执行应用程序自己的代码时,称为用户模式(User Mode)。

    计算机会经常在内核模式和用户模式之间切换:
    ·当运行在用户模式的应用程序需要输入输出、申请内存等比较底层的操作时,就必须调用操作系统提供的 API 函数,从而进入内核模式;

    ·操作完成后,继续执行应用程序的代码,就又回到了用户模式。

    总结:用户模式就是执行应用程度代码,访问用户空间;内核模式就是执行内核代码,访问内核空间(当然也有权限访问用户空间)。

    为什么要区分两种模式

    内核最主要的任务是管理硬件,包括显示器、键盘、鼠标、内存、硬盘等,并且内核也提供了接口(也就是函数),供上层程序使用。当程序要进行输入输出、分配内存、响应鼠标等与硬件有关的操作时,必须要使用内核提供的接口。但是用户程序是非常不安全的,内核对用户程序也是充分不信任的,当程序调用内核接口时,内核要做各种校验,以防止出错。

    从 Intel 80386 开始,出于安全性和稳定性的考虑,CPU 可以运行在 ring0 ~ ring3 四个不同的权限级别,也对数据提供相应的四个保护级别。不过 Linux 和 Windows 只利用了其中的两个运行级别:

    ·一个是内核模式,对应 ring0 级,操作系统的核心部分和设备驱动都运行在该模式下。
    ·另一个是用户模式,对应 ring3 级,操作系统的用户接口部分(例如 Windows API)以及所有的用户程序都运行在该级别。

    为什么内核和用户程序要共用地址空间

    既然内核也是一个应用程序,为何不让它拥有独立的4GB地址空间,而是要和用户程序共享、占用有限的内存呢?

    让内核拥有完全独立的地址空间,就是让内核处于一个独立的进程中,这样每次进行系统调用都需要切换进程。切换进程的消耗是巨大的,不仅需要寄存器进栈出栈,还会使CPU中的数据缓存失效、MMU中的页表缓存失效,这将导致内存的访问在一段时间内相当低效。

    而让内核和用户程序共享地址空间,发生系统调用时进行的是模式切换,模式切换仅仅需要寄存器进栈出栈,不会导致缓存失效;现代CPU也都提供了快速进出内核模式的指令,与进程切换比起来,效率大大提高了。

    展开全文
  • 内核模式和用户模式

    万次阅读 2018-04-02 09:15:59
    内核模式和用户模式 tags: 内核模式 用户模式 总是发现在要讲解一个问题的时候不得不去先讲解另一个问题。比如要想彻底弄明白ULT和KLT,则需要先明白他们工作的模式。ULT是不需要在内核模式和用户模式之间切换的...
  • Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。 内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权...
  • 理解用户模式和内核模式(译)

    千次阅读 2017-07-20 17:01:45
    原文来自:... 28 Aug 2015 ...###理解用户模式和内核模式 本文出处: http://blog.codinghorror.com/understanding-user-and-kernel-mode/ 绝大
  • linux内核面试题[2]-Linux用户模式和内核模式 内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和I/O空间; 如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码...
  • 本文主要描述了PPPoE内核模式拨号的设计与实现,该模式将封包动作从用户空间转移到内核空间,从而大大降低了内核空间与用户空间切换的次数,目前这种内核模式下的拨号已经大量地应用于各种网关产品中。
  • 驱动内核模式下的apihook例子,编写驱动程序.zip
  • linux的用户模式和内核模式

    千次阅读 2015-03-13 11:24:17
    在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之中。 内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和I/...
  • 理解用户模式(UserMode)和内核模式(KernelMode)
  • 微软的内核模式如何访问和读写文件,这个是基本的原理介绍和应用举例。
  • 理解Windows内核模式与用户模式

    千次阅读 2016-04-07 20:50:50
    运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。多个驱动...
  • 背景知识大约一年前,我写了Cervus,这是另一个在Linux内核中运行的WebAssembly“用户模式”子系统。 到那时,内核就变得僵化了,在Linux内核中安全地运行WebAssembly,其性能比本地内核快。 背景知识大约一年前,我...
  • Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。 内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权...
  • 无法添加内核模式驱动的打印机

    千次阅读 2017-11-12 21:52:00
    因为内核模式可以访问系统底层的内存,因此写得不好的内核模式驱动可以造成停止错误,所以从系统稳定性和安全性考虑,Windows默认禁止添加内核模式的打印机驱动程序,只能使用用户模式的驱动,而添加的打印机的驱动...
  • Windows内核模式下套接字结构

    千次阅读 2017-03-27 17:02:59
    Windows内核模式下套接字结构。
  • 内核模式和用户模式的切换

    千次阅读 2013-01-22 11:29:06
    32位x86系统,每个进程的空间是4GB,即地址0x00000000到0xFFFFFFFF。 为了高效调用,Windows会把操作系统的内核数据和代码映射的系统中所有进程的...也就是两种访问模式:用户模式和内核模式。 处理器在硬件一级保证
  • IIS内核模式缓存导致串号问题

    千次阅读 2014-03-29 15:40:30
    经过不断的调试追踪,终于找出了问题症结所在,就是配置了IIS内核模式缓存。为了说明问题,写了两个测试页面: 第一个页面:Default.aspx模拟用户登录 前端部分: 登录 用户名: <asp:
  • linux 系统调用如何进入内核模式

    千次阅读 2016-05-04 08:46:46
    linux 系统调用如何进入内核模式 分类: LINUX 原文地址:linux 系统调用如何进入内核模式 作者:szbryan linux下的系统调用如何从用户态进入内核态?这个问题一直以来都是模模糊糊,最近阅读了《程序员...
  • 驱动开发之用户模式和内核模式

    千次阅读 2014-06-09 12:50:56
    运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。多个驱动...
  • cpu既可以运行在用户态下,也可以运行在内核态。当一个程序在用户态执行时,它不能直接访问内核数据结构或内核的程序。每种CPU模型都提供了从用户态到内核态的转换的...进程/内核模式假定:请求内核服务的进程使用所谓
  • 内核模式 vs 用户模式

    千次阅读 2013-05-12 16:25:43
    有关os内核模式正确的是: 内核模式中所有进程共享同一个地址空间 系统调用一定会切换到内核模式中执行 内核模式中未处理的异常会导致系统崩溃 驱动程序主要是在内核模式中执行 32位x86系统,每个进程...
  • Windows内核模式和用户模式

    千次阅读 2015-03-11 16:34:13
    运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。多个驱动...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 428,851
精华内容 171,540
关键字:

内核模式