• 深入解析Windows操作系统笔记——CH3系统机制


    3.系统机制

    微软提供了一些基本组件让内核模式的组件使用:

    1.陷阱分发,包括终端,延迟的过程调用(DPC),异步过程调用(APC),异常分发以及系统服务分发

    2.执行体对象管理器

    3.同步,包括自旋锁,内核分发器对象,以及等待是如何实现的。

    4.系统辅助线程

    5.其他的机制,比如Windows全局标记

    6.本地过程调用

    7.内核事件跟踪

    8.Wow64

    3.系统机制... 1

    3.1陷阱分发... 3

    3.1.1 中断分发... 4

    3.1.1.1 硬件中断... 4

    3.1.1.2 软中断请求级别(IRQL)5

    3.1.1.3 软中断... 8

    3.1.2 异常分发... 11

    3.1.3 系统服务分发... 14

    3.1.3.1 32位系统服务分发... 14

    3.1.3.2 64位系统服务分发... 14

    3.1.3.3 内核模式的系统服务分发... 14

    3.1.3.4 服务描述符表... 16

    3.2 对象管理器... 17

    3.2.1 执行体对象... 18

    3.2.2 对象结构... 19

    3.2.2.1 对象头和对象体... 20

    3.2.2.2 对象类型... 21

    3.2.2.3 对象方法... 22

    3.2.2.4 对象句柄和进程句柄表... 23

    3.2.2.5 对象安全性... 25

    3.2.2.6 对象保持力... 25

    3.2.2.7 资源记账... 26

    3.2.2.8 对象名称... 27

    3.2.2.9 会话名称空间... 28

    3.3 同步... 28

    3.3.1 IRQL的同步... 28

    3.3.1.1 互锁操作... 29

    3.3.1.2 自旋锁... 29

    3.3.1.3 排队自旋锁... 29

    3.3.1.4 栈内排队自旋锁... 30

    3.3.1.5 执行体的互锁操作... 30

    3.3.2 IRQL的同步... 30

    3.3.2.1 内核分发对象... 31

    3.3.2.2 快速互斥体和受限互斥体... 33

    3.3.2.3 执行体资源... 33

    3.3.2.4 压栈锁... 34

    3.4 系统辅助线程... 34

    3.5 windows全局标志... 35

    3.6 本地过程调用(LPC)35

    3.7 内核事件跟踪... 36

    3.8 Wow64. 36

    3.8.1 Wow64进程地址空间布局结构... 37

    3.8.2 系统调用... 37

    3.8.3 异常分发... 38

    3.8.4 用户回调... 38

    3.8.5 文件系统重定向... 38

    3.8.6 注册表重定向和反射... 38

    3.8.7 I/O请求... 38

    3.9总结... 38

     

    3.1陷阱分发

    中断和异常是导致处理器转向正常控制流之外代码的两种系统条件。陷阱(trap)是指当异常或者中断发生时,处理器捕捉到一个执行线程,并将控制权转移到操作系统中某处固定地址处的机制。

    Windows中处理器将控制权转给一个陷阱处理器 (trap handle)所谓陷阱处理器是指与某个特殊的中断或者一场相关的一个函数

    内核对待中断和异常是有区别的,中断是异步事件,并且与当前正在运行的任务毫无关系。中断主要由I/O设备,处理器时钟,定时器产生。中断可以允许和禁止。异常是一个同步过程,它是一个特殊指令执行的结果。

    异常可以在同样数据在一个程序里重现。异常的例子:内存访问违例,特定的调试器指令,以及除0错误。内核把系统服务调用异常(从技术上讲,他们是系统陷阱(trap))

    当一个硬件异常或者中断产生的时候,处理器在被中断的线程的内核栈中记录机器状态信息,当它可以回到控制流中该点处继续执行。如果该线程在用户模式下执行,那么windows就切换到该线程的内核模式栈。然后windows在被中断的线程的内核栈上创建一个陷阱帧(trap frame),并把线程的执行状态保存在陷阱帧里。在内核调试器中输入dtnt!_ktrap_frame就可以看到陷阱定义。

     

    多数情况下内核安装了前端陷阱处理函数,在内核将控制权交给与改陷阱香瓜的其他函数之后或者之前,由这些前段陷阱来执行一些常规的陷阱任务。

    如陷阱条件是一个设备中断,则内核硬件中断陷阱处理器将控制权转交给一个由设备驱动程序提供给改中断设备的中断服务例程(ISR)

    若陷阱条件是因为调用了一个系统服务引发,那么通用的系统服务陷阱处理器将控制前交给执行体中指定的系统服务。内核不会为不处理的陷阱安装陷阱处理器。陷阱处理器一般使用KeBugCheckEx,当内核检测到可能导致数据被破坏的行为时,改函数会停止计算机。

    3.1.1 中断分发

    硬件产生的中断往往是有I/O设置激发的。当设备需要服务就会以中断的方式通知处理器。中断驱动的设置可以一步的进行I/O处理。

    系统可以产生软中断,如内核可能触发一个软中断,触发线程分发过程,同时也以异步的方式打断一个线程的执行。

    内核安装了中断陷阱处理器来响应设备中断,中断陷阱处理器将控制权递给一个负责该中断的外部例程(ISR)或者传递给一个响应中断的内部内核例程。

    下面介绍硬件如何向处理器通知中断,内核支持中断类型,设备驱动如何与内核交互,以及内核如何识别软中断。

    3.1.1.1 硬件中断

    windows锁支持的平台上,外部I/O中断进入中断控制器的一个引脚,该控制器在cpu的引脚上中断cpu。中断控制器将IRQ(中断请求)翻译成中断号,利用该中断号作为中断分发表的索引。并将控制权传递给恰当的中断分发例程。

    在引导时,windows填充IDT(中断分发表),其中包含了指向内核中负责处理每个中断和异常的指针。

    windows将硬件IRQ映射到IDT上,同时它利用IDT来为异常配置陷阱处理器。虽然windows支持最多256IDT项,但是支持的IRQ数据量由中断控制机设计决定。

     

    3.1.1.2 软中断请求级别(IRQL)

    虽然中断控制器已经实现一层中断优先级,但是windows仍然强迫使用它自己的中断优先级方案,称为中断请求级别(IRQL)

    X86,X64,IA64中断请求级别:

    中断是按优先级别来处理的高优先会抢占低优先级中断的执行权,当一个高优先级中断发送,处理器会把中断线程上下文保存起来,并调用与中断相关的陷阱分发器,陷阱分发器提升IRQL,并调用中断服务例程,调用完成后降低IRQL,回到中断发送前,被中断线程运行。但是当有其他,低优先级中断时,当IRQL降低,低优先级中断出现。这样,内核会恢复到上述过程来处理中断。

     

    线程优先是线程的属性,IRQL是中断源的属性。每个处理器的IRQL设置可以随系统代码的执行变化。

    每个处理器的IRQL设置决定了该处理器可以接收哪些中断。当一个内核模式线程运行时,可以通过KeRaiseIrqlKeLowerIrql来提升和降低处理器IRQL或通过调用内核同步对象的函数间接提高或者降低IRQL。当处理器的IRQL高于中断源则被屏蔽,否则被中断打断。

    访问。

    因为访问PIC(中断控制器)比较慢所以引入了优化技术延迟IRQL以避免访问PIC。当IRQL被提升,HAL记下新的IRQL而不是去修改中断屏蔽值。当一个较低中断发生则HAL将中断屏蔽值设置为对于第一个中断正常的值。这样当IRQL被提升的时候没有更低优先级中断,则HAL需要修改PIC

    一个内核模式线程根据请求,来降低和升高处理器IRQL。当中断发生时,陷阱处理器(或处理器本身)将改处理器的IRQL提升到中断源的IRQL.这样会把等于或者低于它的所有中断都屏蔽。保证了不被低级中断截掉。被屏蔽的中断由其他处理器处理或者被保存下来知道IRQL下降。

    因此系统组件包括内核和设备驱动,都试图让IRQL保持在被动级别。这样可以提高设备启动可以更加及时的响应硬件中断。

    每个中断级别都有特定的目的,如内核发出一个处理器间的中断,以请求另外一个处理器执行一个动作。

    将中断映射到IRQL:IRQL级别和中断控制器定义的中断请求并不相同,在hal中决定一个中断分配给那个IRQL。然后调用HAL函数HalGetSystemInterruptVector把中断映射到对应的IRQL

    预定义的IRQL:以下介绍一下预定义的IRQL

    1.只有当内核在KeBugCheckEx中停止了系统并屏蔽所有中断的时候,内核才会使用高级别的IRQL

    2.电源失败,出现在NT文档中,但是从来没有使用过。

    3.处理器间的中断,用于向另外一个处理器请求执行一个动作。

    4.时钟,主要用于系统时钟,内核利用该黄总段级别来跟踪具体时刻,以及现场测量或者分配cpu时间。

    5.性能剖析(Profile),当内核的性能剖析功能被打开的时候,内核性能剖析陷阱处理器会记录下中断发生时被执行的代码的地址。(性能剖析器Kernrate)

    6.设备IRQL,用来对设别中断优先级分区

    7.DPC/Dispath级别和APC级别是由内核和设备驱动程序产生的软中断。

    8.被动级别,最低的IRQL优先级别,它不是一个中断级别,它是普通线程运行时设置的,允许所有中断发生。

    对于DPC/Dispatch级别或者更高级别上的代码,一个重要的限制是它不能等待一个对象。另外一个限制DPC/Dispatch或者更高级别的IRQL只能访问非换出页内存。若2个限制都违反了系统会崩溃,代码为IRQL_NOT_LESS_OR_EQUAL。在驱动上违反限制是一种常见的错误。在驱动上违反限制上一种创建的错误。

    中断对象,内核提供了一种可移植的机制使得设备驱动程序可以为它们的设备注册ISR。这是一个称为中断对象的内核控制对象。

    中断对象包含了所有供内核将一个设别的ISR与一个特定级别的中断关联起来的所有信息,包含该ISR的地址,该设备中断时所在的IRQL级别,以及内核中该ISR关联的IDT项。

    驻留在中断对象中的代码调用了实际的中断分发器,通用是内核的KiInterruptDispatch或者KiChainedDispatch例程,并将指向中断对象的指针传递给它。

    下图显示了中断控制流:

    ISR与特定中断级别关联起来的称为连接一个中断对象,将ISRIDT项断开关联称为断开一个中断对象。这些操作通过内核函数IoconnectInterruptIoDisconnectInterrupt完成。

    3.1.1.3 软中断

    虽然大多数中断都是硬件产生,但是windows内核也为各种各样的任务产生软中断。包括:

    1.激发线程分发

    2.非时间紧急中断处理

    3.处理器定时到期

    4.特定线程的环境中异步执行一个过程

    5.支持异步I/O操作

    分发或者延迟过程调用(DPC)中断,当一个线程不能继续执行的时候,比如因为线程已经终止了或者主动进入等待状态,内核就会直接调用分发器,从而立即导致一个环境切换。但是有时检测到线程已深入到许多层代码中,这时应该进行重新调度,这种情况下内核请求分发,但是将它推迟到完成了当前的行为之后再进行。

    当啮合对共享的内核数据访问,会把IRQL拉到DPC/Dispatch级别当内核检查到需要分发的时候,请求一个DPC/Dispatch中断。所以只有当内核完成了当前的活动,把IRQL拉低,分发中断才能处理。

    延迟事务也在这个IRQL上运行,DPC是完成一项系统任务,但是不是那么紧迫,这些函数被称为延迟的,是因为不会了立即执行。

    DPC赋予操作系统一种能力,产生一个中断并且在内核模式下执行系统函数。内核利用dpc来处理定时到期,以及一个线程的时限到期以后重新调度处理器。为了给硬件中断提供及时的服务,windows视图把IRQL保持在低于设备IRQL之下。为了达到这个目的,让设备驱动程序ISR执行最少必要的工作来响应他们的设备,将异变的中断状态保存起来,并将数据传输非时间紧迫的中断处理推迟到 DPC/Dispatch IRQL级别上的DPC中在执行。

    DPC是通过DPC对象来表示的,DPC对象是内核控制对象,对于用户模式不可见,对于设备驱动和内核代码是可见的。DPC对象包含最重要的信息是DPC中断将要调用哪个系统函数地址。正在等待的DPC被存放在队列中,每个处理器都有一个队列称为DPC队列。要想请求一个DPC,系统会初始化DPC对象,然后放入DPC队列中。

    默认情况下内核把DPC对象放在发生该DPC请求的处理器的DPC队列末尾。在设备驱动程序只需指定一个DPC优先级别和指定特定CPU,就可以改变这种默认方式。指定在某个CPU上叫定向DPC。如果一个DPC的优先级为低级或者中级则放入队列尾,否则放入队列头部。

    当处理器的IRQLDPC/Dispatch或更高降到某个更低的级别时,内核处理DPC。在处理DPCIRQLDPC/Dispatch级别上,并且将DPC来出来运行直到队列为空。当队列为空内核才让IRQL降低到DPC/Dispatch以下。让正常的线程执行过程继续执行。

    DPC优先级可以以另一种方式影响到系统行为。内核通常通过一个DPC/Dispatch级别的中断来激发队列抽干的动作。只有当一个DPC被定为在ISR所在的处理器上,且改DPC的优先级是高级或者中级时,内核才产生一个中断,若为低级只有当DPC请求到一个阀值或一段时间后,内核才会请求中断。

    DPC被定为在一个不同于其ISR运行的CPU上,并DPC为高级。内核立即用一个信号通知CPU,以便抽干它的DPC队列。若优先级为中级或者低级则DPC数据超过阀值,内核才会激发一个DPC/Dispatch中断。

    DPC中断产生的规则:

    DPC优先级别

    DPC被定为在ISR的处理器上

    DPC被定为在另一个处理器上

    低级

    DPC队列长度超过最大的DPC队列长度值,或者DPC请求率小于最小的DPC请求率。

    DPC队列长度超过最大的DPC队列长度或者系统空闲

    中级

    总是激发

    DPC队列长度超过最大的DPC队列长度或者系统空闲

    高级

    总是激发

    总是激发

    DPC主要为设备驱动提供的,但是内核也使用DPC,内核使用DPC来处理限时到期事件。在系统时钟每个嘀嗒点上,就发生一个时钟IRQL级别的中断。时钟中断处理器对系统时间进行更新,将一个记录了当前线程运行多长时间的计数器递减。当计数器减到0,线程到期,内核可能需要重新调度该处理器,这个任务在DPC/Dispatch IRQL上完成。

    时钟中断处理器将一个DPC插入到队列中以便激发分发过程。然后结束他的工作并且降低IRQL,因为DPC中断级别较低,所以在时钟中断完成前出现尚未处理的设备中断,都在DPC中断之前被处理。

    APC异步调用,异步过程调用提供了一种在特定用户线程环境中执行用户程序和系统代码的途径。APC经过排队以便在特定线程的环境中执行。

    APC是由一个内核控制对象(APC对象)来描述的,正在等待执行的APC驻留在一个由内核管理的APC队列中。APC队列是特定线程相关的,即每个线程有它自己的APC队列,当内核请求要将APC排队时,它将一个APC排队,她将APC插入到将来执行此APC例程的那个线程的队列中。当内核请求APC级别中断,当该线程最终开始执行的时候,会执行此APC

    2APC:内核和用户模式。内核模式的APC并不要求从目标获取许可就可以运行在改线程的环境中,而用户模式必须先获取许可。内核模式的APC有普通和特别2种,将IRQL提升到APC级别或调用KeEnterGuardRegion,就可以静止这两种类型的内核模式APC

    执行体使用内核模式的APC来完成那些必须要在特定线程的地址空间(执行环境)中才能完成的操作系统任务。它可以利用特殊的内核模式APC来指示某个线程停止执行一个可中断的系统服务。

    用户模式APC(ReadFileEX,WriteFileExQueueUserApc),如ReadfileEx,WritefileEx允许调用者指定一个完成例程,当I/O完成是例程就会被调用。I/O完成机制是通过I/O的线程插入一个APC来实现的。内核APC运行在APC级别上,用户模式APC运行在被动级别上。

    APC交付会导致等跌队列重新排序,如APC用来把等待资源的线程挂起,那么该线程就会进入等待访问这个资源队列的末尾。

    3.1.2 异常分发

    中断可以在任何时候发生,异常则是直接由当前正在运行的程序产生。windows引入了一种称为结构化异常处理的设施,应用程序可以在异常发生时获得控制,然后应用程序可以修正条件,并返回到异常发生处,将栈展开(使引发异常的子例程执行过程中止),或想系统报告,改异常不可识别,因为系统应该继续搜索一个有可能处理此异常的异常处理器。

    x86上所有异常都在预定义的中断号,这些中断号对应IDT项。每个项指向了某个特定异常的陷阱处理器。

    所有异常,除了简单的通过陷阱处理器,可以解决的之外,其他都由异常分发器的内核模块服务。异常分发器就是找到一个异常处理器,处理要处理的异常。

    异常处理对用户来说都是透明的,有些异常也允许原封不动的回到用户模式。如内存访问违例,算法溢出,操作系统不对他们处理。环境子系统可以建立起基于帧的异常处理器来处理异常。

    基于帧是将一个异常处理与一个特定的过程激活动作关联起来。当一个过程被调用,代表该过程的帧被压到栈中。一个栈帧可以关联多个异常处理器,每个异常处理器保护源程序中一块特定代码。当发生一个异常时,内核查找与当前帧关联在一起的某个异常处理器。如果没有找到内核继续查找与上一个栈帧关联在一起的某个处理。如果最终还是没有找到异常处理器,内核会调用自己默认的异常处理器。

    异常发生,CPU硬件将控制权递交给内核陷阱处理器,内核陷阱处理器创建一个陷阱帧。正由于陷阱帧处理完异常后,系统可以从停止的地方恢复。

    如果在内核模式下的异常,异常分发器调用一个例程来找到一个基于帧的异常处理器。由它来处理异常。

    在用户模式下,windows子系统有一个调试器端口和异常端口,通过它们来接收windows进程中用户模式异常的通知。内核在它默认的异常处理器中用了这些端口。

    调试器端口是最常见的异常来源,因此异常分发器采取动作,

    1.查看引发该异常的进程是否有一个相关的调试器进程。若存在异常分发器发送一个调试器对象信息到调试对象相关的进程。

    2.若该进程没有附载的调试器进程或调试器并没有处理该异常,那么异常分发器切换到用户模式下。将陷阱帧按照Context数据结构的格式拷贝到用户栈中,并调用一个例程来找到一个基于帧的异常处理器。

    3.如果没有找到或者虽然找到了但是它不处理该异常,则异常分发器切换到内核模式下,并且再次调用调试器,以便让用户做更多的调试。

    4.若调试器不在运行,并没有找到基于帧的处理器,那么内核向与该线程的进程关联在一起的异常端口发送一个信息。该异常端口如果存在的话,则一定是由控制该线程的环境子系统注册的。环境子系统监听该端口,在恰当的时机把一个异常转化为一个与环境相关的信号或异常。客户/服务器运行时子系统(CSRSS)简单的弹出一个消息框来通知用户发生了错误,并且终止进程。

    5.POSIX从内核收到一个消息,指定的一个线程产生了异常,当内核在处理异常过程走得比较深了,而子系统并没有处理该异常,那么内核执行一个默认的异常处理器,它只是简单的将引发该异常的线程所在的进程终止掉。

    未处理的异常

    所有windows线程都有一个异常处理器来处理未被处理的异常。该异常处理器是在windows内部的进程启动函数或线程启动函数中声明。如:

    如果一个线程的异常没有被处理,则windows的未处理异常过滤器将会被调用。这个函数目的是,当一个异常未被处理时,可以提供一种系统统一的行为和方法。

    3.1.3 系统服务分发

    内核陷阱处理器分发中断,异常和系统服务调用

    3.1.3.1 32位系统服务分发

    x86 Pentium II处理器以上,使用windows使用sysenter执行触发一个陷阱,这个是intel特别为快速系统服务定义的。为了支持这一指令,windows在引导时刻把内核的服务分发器的地址保存与该指令相关的寄存器中。

    执行该指令会导致变化到内核模式下,并且执行系统服务分发器,为了返回到用户模式,系统服务分发器通常执行sysexit执行(当处理器单步标记被打开,系统分发器改而使用iretd指令。)

    K6和更高的32AMD处理器上,windows使用syscall类似于sysenter,系统嗲用号也在EAX上,而调用者参数则保存在栈中。在完成了分发之后,内核执行sysret指令。

    3.1.3.2 64位系统服务分发

    64位体系结构上,windows使用syscall指令进行系统分发(AMD处理器上syscall类似),系统调用号存在EAX寄存器,前4个参数存放在寄存器汇总其他参数存放在栈中。

    IA64上使用EPC指令,前8个系统调用参数通过寄存器来传递,其他参数通过栈传递。

    3.1.3.3 内核模式的系统服务分发

    内核利用传递进来的参数找到系统分发表中的服务信息(类似IDT)

    系统服务分发器KiSystemService将调用的参数从用户模式栈中复制到内核模式,然后执行服务。如果换地给一个系统服务的参数指向了用户空间中的缓冲区,那么在内核模式代码复制到缓冲区或从缓冲区读前先要查明缓冲区是否可以访问。

    每个线程都有一个指针指向它的系统服务表,windows2个系统服务表,最多可以支持4个。系统服务分发器确定哪个表包含了所有请求的服务,它将32位系统服务号中的其中2个位解释成一个索引表。系统服务号低12位被用在该表索引所指定的表中进行的索引。

    3.1.3.4 服务描述符表

    一个主要的默认数组表(KeDescriptorTable)定义了Ntosrknl.exe中实现的核心执行体系统服务。另一个默认数组表(KeserviceDeseriptorTableShadow)包含了在windows子系统的内核模式部分win32.sys中实现的windows userGDI服务。

    windows线程第一次调用一个windows userGDI服务,该线程的系统服务表的地址被指向一个包含windows userGDI服务的表格。KeAddSystemSericeTable可以让win32.sys加入到系统服务表中。

    针对windows执行体服务的系统服务分发指令位于NTdll.dll中。子系统调用Ntdll.dll来实现。windows userGDI函数,在这些函数中,系统分发指令是直接在user32.dllGDI.dll中实现,没有涉及ntdll.dll

    3.2 对象管理器

    windows实现了一个对象模型,以便为执行体的实现各种内部服务提供了一致的,安全的访问路径。执行体内部复制创建,删除,保护和跟踪对象的组件(windows对象管理器)

    对象管理器把原本可能散落在整个系统各处的资源控制操作集中在一起。对象管理器的设计意图是为了实现本章稍后列出的一些功能。

    1.提供一种公共,同一个的机制来使用系统资源

    2.将对象保护隔离到操作系统统一的区域中,从而可以做到c2安全等级

    3.提供一种来记录进程使用对象数量的机制,从而可以对系统资源的使用上加限制。

    4.简历一套对象命名方案,可以很方便的融合现有对象。

    5.支持各种操作系统环境的需要

    6.简历统一的规则来维护对象的保持力。

    windows内部有两种类型的对象:执行体对象和内核对象。

    执行体对象是指执行体的各种组件所实现的对象(如,进程管理器,内存管理器,I/O子系统)内核对象是指windows内核实现的一组更为基本的对象。这些对象在执行体内部被创建和使用。

    3.2.1 执行体对象

    每个windows环境子系统总是把操作系统的不同面貌呈现给它的应用程序。执行体对象和对象服务是环境子系统用于构建其自己版本的对象和其他资源基础。

    执行体对象往往由在用户应用程序中一般的环境子系统或由操作系统的组件作为他们常规操作的一部分而创建。如为了创建一个文件,windows应用程序调用windowscreatefile函数,该函数在windows子系统DLL kernel32.dll中现实中实现的,在经过了一些验证和初始化工作以后,createfile会调用原生的windows服务ntcreatefile来创建一个执行体文件对象。

    windows子系统使用执行体对象来导出它自己对象集合,其中许多对象直接对应于执行体对象。

    暴露给windows api的执行体对象:

    对象类型

    所代表的含义

    符号链接(Symbolic link)

    间接的引用一个对象名字的机制

    进程(Process)

    虚拟地址空间,以及为了执行一组线程对象而必需的控制信息

    线程(Thread)

    进程内部的一个可执行实体

    作业(Job)

    指一组进程,通过作业机制,可以像单个实体那样来管理他们

    内存区(section)

    共享内存的一个区域(windows中也称为文件映射对象)

    文件(File)

    一个已打开的文件或者I/O设备的实例

    访问令牌(Access token)

    一个进程或者线程的安全轮廓(安全ID,用户权限等)

    事件(Event)

    一个具有持久状态(有信号,或者无信号的)的对象,可被用于同步或者通知。

    信号量(Semaphore)

    信号量是一个计数器,它提供了资源门控能力,对该信号量所保护的资源只允许某个最大数目的线程来访问它。

    互斥体(Mutex)

    用于顺序访问一个资源的一种同步机制

    定时器(Timer)

    这是一种当固定长时间过去时,通知一个线程的机制

    IO完成(IoCompletion)

    使线程能够将”I/O操作完成通知进出队列的一种方法,windows中称为IO完成端口。

    (Key)

    这是一种引用注册表中数据的机制。

    窗口站(WindowStation)

    该对象包含了一个剪贴板,一组全局原子和一组桌面对象

    桌面(Desktop)

    这是一个被包含在窗口站内部的对象。桌面对象有一个逻辑显示器表面,其中包含了窗口,菜单和钩子。

    3.2.2 对象结构

    每个对象都有一个对象头和对象体,对象管理器控制了对象头,而执行体组件则控制了由它们创建的对象类型的对象体。每个对象头指向一个进程列表,类表中每个进程都打开了此对象。对象类型指向了称为类型对象的特殊对象。

    3.2.2.1 对象头和对象体

    对象管理器使用对象头中保存的数据来管理这些对象,而无须关系它们的类型。

    标准对象头的属性:

    属性

    用途

    对象名称

    使一个对象对于其他的进程也是可见的,以便于共享

    对象目录

    提供了一个层次结构来存储对象名称

    安全描述符

    决定谁可以使用该对象,以及允许它们如何使用它(注:对于没有名称的对象来说,安全描述符是空[null])

    配额花费

    列出了当一个进程打开一个指向该对象的句柄时,针对该进程收取的资源花费额

    已打开句柄的计数

    记录了打开一个句柄来指向该对象的次数

    已打开句柄的列表

    指向一个进程列表,其中每个进程都打开了指向该对象的句柄。

    对象类型

    指向一个类型对象,该对象包含了正对这个类型的对象都是公共属性

    引用计数

    记录了一个内核模式组件引用该对象地址的次数

    除了对象头以外,每个对象也有一个对象体,并且其格式和内容只有这种对象类型才有,同一类型的所有对象共享同样的对象体格式。一个执行体组件通过创建一个对象类型,并且为它提供一些服务,就可以控制和维护所有这个类型的对象体中的数据。

    对象管理器提供了少量通过服务,通过这些服务可以对一个对象头中保存的属性进行操作,通过服务可以用再任何类型的对象上。

    虽然通用的对象服务都支持所有对象类型,但是每个对象都有它自己的创建,打开和查询服务。

    3.2.2.2 对象类型

    对象头中包含的数据对于所有对象都是公共的,但是每个对象实例可以取不同的值。

    为了节省内存,对象管理器只在创建一个新的对象类型时才会存储静态的,特定于对象类型的属性。

    进程对象和进程类型对象:

    类型对象的属性

    属性

    用途

    类型名称

    此种类型的对象名称(“process”,”event”,”port”)

    池类型

    指明了这种类型的对象是从换页的还是非换页的内存中分配

    默认的配额花费

    默认从进程配额中扣除的换页内存池值和非换页内存池值

    访问类型

    当一个线程打开某个指向该类型的对象时可以请求的访问类型(“”,””,”终止”,”挂起”)

    通用访问权限的映射关系

    4种通用的访问权限和属于该类型的访问权限之间的映射关系。

    同步

    指明了一个线程是否可以等待这种的对象

    方法

    在一个对象的生命周期的特定点上,对象管理器自动调用的一个或者多个例程。

    一个对象能否支持同步,取决于该对象是否包含了一个内嵌的分发器对象,在IRQL的同步中有介绍。

    3.2.2.3 对象方法

    上面的表中,最后一个属性就是方法。方法是由一组内部例程构成的,这些例程类似构造和解析函数(在创建和销毁时被使用)

    当一个执行体组件创建了一个新的对象类型时,它可以像对象管理器注册一个或多个方法,对象管理器在此种类型的对象生命周期中,某些明确定义的点上调用这些方法。

    对象方法:

    方法

    何时调用

    Open

    当一个对象句柄被打开

    Close

    当一个对象句柄被关闭

    Delete

    在对象管理器中删除一个对象之前

    Query name

    当一个线程在一个从属名字空间中查询一个对象的名称时

    Parse

    当对象管理器在一个从属名字空间中搜索一个对象名称时

    Security

    当一个进程读写(如文件)在其从属名字空间中的保护属性时。

    3.2.2.3.1 Open函数

    对象管理器创建一个指向对象的句柄时会调用open方法,在对象被创建或者打开时候运行。只有一个对象类型(windowstation)定义了open方法。这样win32.sys能够与服务于桌面相关内存池的进程共享内存。

    3.2.2.3.2 Close函数

    close方法的例子是IO中,对象管理器关闭一个句柄使用close方法。close方法先检查看正在关闭该文件句柄进程是否有任务用于该文件并且未完成的锁,如果有则除去锁。

    3.2.2.3.3 Delete函数

    对象管理器在内存中删除临时对象以前,调用delete方法。内存管理器为内存区对象类型注册了delete方法,它会释放该内存区使用的物理页面。并在删除内存区对象前验证一下内存管理器为该内存区所分配的任何内部数据结构已被删除了。

    3.2.2.3.4 Parse函数(类似于Query name)

    若发现对象存在于对象管理器名字空间外,允许对象管理器把查找一个对象的控制权交给一个从属的对象管理器。若在搜索路径上碰到一个关联了parse的对象,会暂停搜索。对象管理器调用parse方法。将正在搜索的对象名称的剩余部分传给parse方法。除了对象管理器方法外在windows中还有注册表名字空间和文件系统名字空间。例如打开一个名为DeviceFloppy0docs esume.doc的文件句柄,对象管理器遍历它的名称树,直到到达Floppy0。调用parse,把docs esume.doc传入。I/O管理器的parse例程接受名称,并且传给文件系统,文件系统找到文件并打开。

    3.2.2.3.5 Security方法

    Security也是I/O系统使用方法,类似parse。一旦一个线程视图查询或改变那些用于保护一个文件的安全信息时,该方法就会被调用。安全信息是存储在文件对象中,而不是内存,因此必须调用I/O系统才能找到安全信息,并将它们读出来或进行修改。

    3.2.2.4 对象句柄和进程句柄表

    当进程根据名称来创建或者打开一个对象时,它会接受到一个句柄,通过句柄来访问一个对象,要比使用名称访问快得多。因为对象管理器可以跳过名称查找过程,直接找到目标对象。进程也可以在其创建时刻通过继承句柄的方式获得句柄或从另一个进程接收一个复制的句柄。

    所有的用户模式进程在其线程使用一个对象以前,必须先拥有一个指向该对象的句柄。句柄被用做指向系统资源的间接指针,这样可以让应用程序不与系统数据结构直接交互。

    对象句柄还是提供了额外的一些好处:第一,不同句柄没有什么区别可以使用统一的接口来引用。第二,对象管理器有独立的权利来创建句柄,查找句柄。也就是对象管理器可以仔细地审查每个可能会影响对象的用户模式动作。

    对象句柄是索引与进程相关的句柄表中的项相关。执行体进程(EPROCESS)块中一个域指向句柄表。句柄表实现方式是3层和虚拟地址到物理地址映射类似。

    当进程被创建对象管理器分配了句柄表的最高层结构,其中包含了指向中间层表的指针;同时也创建了中间层,其中包含了第一个指向子句柄表的指针数组,还分配了最底层,其中包含了第一个子句柄表。

    把低24位看成38位,分别索引到3层结构中的一层。在xp2003在进程创建时,最底层句柄表被分配,其他的都会被按需分配。在windows 2000中一个子句柄表是255个可用表项。在xp2003表项=(页大小/表项大小)-1。在windows xp2003上句柄表项:

    P:说明了调用者是否允许关闭句柄。I:该进程创建的子进程是否在它们句柄表中有一份该句柄的拷贝。A:关闭该对象时是否应该产生一个升级信息(对象管理器内部使用该标记)

    系统组件和设备驱动程序通常需要打开一些不应该让用户访问的对象。可以通过内核句柄表来表示。内核句柄表只有在内核模式下可以飞昂文,可以在任何进程环境下。

    对象管理器看到一个句柄的高位被设置时,就会将它识别为内核句柄表中的句柄。也就是说内核句柄表中的句柄的引用值大于0x80000000。在windwos 2000中内核句柄表是一张独立的句柄表,但是在xp2003中,内核句柄表也被用做system进程的句柄表。

    3.2.2.5 对象安全性

    当一个进程打开一个句柄,对象管理器调用安全引用监视器,监视该对象描述符是否允许该进程所请求的访问类型,若允许,引用监视器返回一组准许的访问权限,同时对象管理器放入它创建的对象句柄中。

    当下次进程要使用句柄时可以快速的检查这一组句柄中的准许访问权限。

    3.2.2.6 对象保持力

    对象保持力,分为暂时和永久的。暂时是当需要的时候使用,不需要的时候释放。永久是一直保持知道被显式释放。

    对象管理器通过两个阶段来实现对象保持力。第一阶段称为名称保持力,第二个阶段,不再有用时,停止保留对象本身(也就是删除)

    名称保持力:当一个进程打开一个对象的句柄。会在该对象头信息中的已打开句柄计数器+1。当用完关闭句柄,对象管理器已打开句柄-1,。当计数器为0,对象管理器从全局名字空间中删除该对象名称。

    不再有用时删除:对象专门提供一个引用计数来记录。

    已打开句柄计数器:当进程打开一个对象句柄+1,关闭句柄-1

    引用计数:提供一个对象指针+1,用完了-1

    当已打开计数器为0,引用计数大于0.表示对象还在使用。当引用计数为0,对象管理器会从内存中将它删除。

    进程A,进程B和内核结构引用了一个对象,因此handlecount=2referencecount=3

    3.2.2.7 资源记账

    windows对象管理器提供了一个中心设施来实现资源记账。每个对象头都包含了配额花费。

    windows每个进程都指向一个配额的数据结构,配额为0表示不限制。

    3.2.2.8 对象名称

    对象名称可以满足1.区分对象之间的方法。2.找到并获得特定对象的方法。3.允许程序间共享。

    只有2种情况会使用名称进行查找,1.创建一个命名对象时,会通过名称查找,验证全局名称空间中不存在。2.当打开一个句柄,句柄指向一个命名对象时,对象管理器查找该名称并返回一个对象句柄。

    对象的名称存储位置取决于对象类型

    目录

    所存储对象名称的类型

    GLOBAL??

    Ms-dos设备名(DosDevices是指向此目录的符号链接)

    BaseNameObjects

    互斥体,时间,信号量,可等待的定时器和内存区对象

    Callback

    回调对象

    Device

    设备对象

    Driver

    驱动程序对象

    FileSystem

    文件系统驱动程序对象和文件系统识别器对象

    KnowDlls

    已知DLL(在启动时候由系统映射的DLL)的内存区名称和路径

    Nls

    已映射的国家语言支持表的内存区名称

    ObjectTyoes

    对象类型名称

    RPC Control

    远程过程调用(RPC)所使用的端口对象

    Security

    与安全子系统相关的对象的名称

    Windows

    Windows子系统的端口和窗口站

    对象的名称相对于一台计算机而言是全局的,但是他们的跨越网络是不可见的。但是对象管理器解析名称的方法使得有可能访问其他机器上的命名对象。

    对象目录是对象管理器支持这种层次型命名结构手段。对象目录解析对象名称为指向对象的指针。对象管理器利用指针来构建对应的句柄,将这些对象句柄返回给用户模式的调用者。

    符号链接,在某些文件系统中,通过符号链接,用户可以创建一个文件名或一个目录名,当被使用的时候,实际上被操作系统转译成另外一个不同的文件或文件名。使用符号链接,是一种让用户间接的共享一个文件或目录的内容。

    符号链接对象,完成的功能类似于对象名称的功能一样。当对象名称中有符号链接,对象管理器遍历它的对象名称空间,找到该符号链接对象,并找到一个取代该符号链接名的字符串。

    3.2.2.9 会话名称空间

    一个登陆到控制台会话上,用户可以访问全局名称空间,另外会话可以获得该名称空间的私有名称空间实例。DosDevices,Windows,BaseNamedObjects属于会话局部的名称空间,都会被放在私有名称空间中。将名称空间中相同部分复制,来初始化名称空间。

    对象管理器在sessionX下创建私有版本,对象管理器以透明的方法,将对象的名称从BaseNameObjects重定向到session2BaseNamedObjects

    windows子系统DLLwindows应用程序传过来的位于DosDevice中对象引用加上??前缀(c:windows变成??c:windows)。依赖于EPROCESSDeviceMapDeviceMap结构中DosDevicesDirectory域所指的对象目录管理器代表了进程的局部DosDevices

    windows 2003xp上,系统没有将全局对象拷贝到局部DosDevices目录中,当看到??会通过DeviceMapDosDevicesDirectory找到该进程局部DosDevices目录。若在局部中没有,并且DeviceMap有效,则会在GlobalDosDevicesDirectory查找对象。

    当会话中的应用程序要与其他会话的实例同步在任何对象名称前加入GlobalApplicationInitialized被重定向到BasedNameObjectsApplicationInitized而不是Sessions2BaseNamedObjectsApplicationInitialized

    2003xp中应用程序只要在DosDevices中没有这样的对象就会访问全局,不需要使用Global

    3.3 同步

    当一个资源不允许共享访问,或共享访问导致不可预测的后果则需要互斥。若一段代码访问了一个不可共享的资源,则这样的代码区成为临界区。

    3.3.1 IRQL的同步

    在内核执行的各个阶段,内核必须保证,在临界区内部同一时刻只有一个处理器在执行。

    在中断发生,内核可能在更新一个全局数据结构,而中断的处理例程可能也要修改此数据结构。在单处理器,可以用以下方式来避免。如,当线程修改全局数据结构时,禁止所有中断。windows的作法是执行临界区时,把IRQL拉高会用到该数据结构的最高IRQL

    3.3.1.1 互锁操作

    这个最简单的同步方式,依赖于支持多处理器硬件,操作一个整型值来进行比较。

    3.3.1.2 自旋锁

    自旋锁是内核用来实现多处理器互斥的机制

    windows中,所有内核模式自旋锁都有一个与之关联的IRQL。当运行自旋锁就会拉高IRQL

    3.3.1.3 排队自旋锁

    工作方式:当一个处理器要获得已被其他处理器持有的队列自旋锁锁时,把自己的标示符放入一个与该自旋锁关联的一个队列中。

    当自旋锁被释放,将锁交给队列中第一个标示符对于的cpu

    在同时处理器等待一个较忙的自旋锁不是检查自旋锁本身而是每个处理器的标志。在队列中位于它之前的处理器会对它设置,表明轮到这个等待的处理器了。

    排队的自旋锁的自然结果是,他们在每个处理器标识符上旋转,而不是全局自旋锁上旋转有2个效果:

    1.多处理器总线不会因为处理器之间的同步招致繁重的流量。

    2.排队加强了先进先出的顺序,处理器之间性能更加一致。

    windows中定义了很多全局队列自旋锁,并且在每个处理器的处理器控制区域(PCR)”包含了一组数组(保存了指向这些全局队列的自旋锁指针)。当调用KeAcquireQueueSpinLock的时候将一个PCR的索引传进去,可以获得对应的全局自旋锁。

    3.3.1.4 栈内排队自旋锁

    除了使用全局定义的静态排队自旋锁,xp2003还提供了KeAcquireInstackQueuedSpinlockKeReleaseInstackQueuedSpinlock。来支持动态分配的排队自旋锁。

    3.3.1.5 执行体的互锁操作

    内核提供了很多简单的建立在自旋锁基础上的同步函数。

    3.3.2 IRQL的同步

    自旋锁使用有严格的限制:

    1.对于受保护的资源,必须快速访问,不要与其他代码有复杂的交互关系

    2.临界区代码的内存页不能换出去,不能引用可被换页的数据,不能调用外部过程,不能中断或异常。

    在自旋锁不适合是可用:内核分发器对象,快速互斥体和受限互斥体,压栈锁,执行体资源。

     

    是否暴露给设备驱动程式使用

    禁止常规的内核模式APC

    禁止特殊的内核模式APC

    支持递归获取操作

    支持共享的和独占的获取操作

    内核分发器互斥体

    内核分发器信号量

    快速互斥体

    受限互斥体

    压栈锁

    执行体资源

    3.3.2.1 内核分发对象

    内核以内核对象的形式,向执行体提供了额外的同步机制,这些内核对象合起来统称为分发器对象。每个支持同步的用户可见对象都封装了至少一个内核分发器对象。

    3.3.2.1.1 等待分发器对象

    一个用户模式的线程等待一个事件对象的句柄。

    内核将该线程的调度状态从就绪状态改变成等待状态,然后将该线程加入到正在等待该事件的线程列表中。

    另外一个线程设置了该事件,内核沿着该事件的等待线程队列向前搜索。若有一个线程等待条件满足将线程状态从等待改为就绪。若是一个可变优先级的线程,则内核可能也要提升它的执行优先级。

    因为一个新线程已经变成就绪执行状态,所以进行重新调度。如果它找到一个正在运行的线程。其优先级低于就绪线程的优先级。那么会抢占此低优先级的线程,并且发出一个软中断,以便激发一个环境切换,切换到高优先级的线程中。

    如果没有处理器可以抢占的话,则分发器将该就绪线程放到分发器就绪队列中,以后再被调度。

    3.3.2.1.2 如果Signals一个对象

    3.3.2.1.3 数据结构

    typedef struct _DISPATCHER_HEADER {

    UCHAR Type;

    UCHAR Absolute;

    UCHAR Size;

    UCHAR Inserted;

    LONG SignalState;

    LIST_ENTRY WaitListHead;

    } DISPATCHER_HEADER;

    typedef struct _KWAIT_BLOCK {

    LIST_ENTRY WaitListEntry;

    struct _KTHREAD *RESTRICTED_POINTER Thread;

    PVOID Object;

    struct _KWAIT_BLOCK *RESTRICTED_POINTER NextWaitBlock;

    USHORT WaitKey;

    USHORT WaitType;

    } KWAIT_BLOCK, *PKWAIT_BLOCK, *RESTRICTED_POINTER PRKWAIT_BLOCK;

    每个分发器对象都有一个等待列表,列表中一项代表一个等待该对象的线程。所以当线程向一个分发器对象发出信号,内核可以很快的确定谁在等待对象。

    等待快结构WaitListEntry指向被等待对象,Thread指向等待线程,NextWaitBlock指向下一个等待块。

    3.3.2.2 快速互斥体和受限互斥体

    快速互斥体也成为执行体互斥体,比互斥体对象提供了更好的性能。尽管他们也是建立在分发器事件对象基础上的,但是如果对于快速互斥体没有竞争的话,他们无须等待事件对象。

    受限互斥体,本质上它与快速互斥体是相同的。使用了KGATE同步对象,通过调用KeEnterGuardedRegion来进制所有内核模式APC的事务,主要用户内存管理器。

    3.3.2.3 执行体资源

    执行体资源是一种支持共享和独占访问的同步机制,要求APC事务禁止,如果一个线程正在等待获得一个共享访问权,则它应该等待一个与该资源相关联的信号量,如果一个线程正在等待获得一个资源的独占访问权,则应该等待一个事件。

    当一个独占持有者通过给信号量发型号来释放一个资源唤醒共享访问者。

    当一个线程在等待独占访问一个资源,而该资源正在被其他线程拥有,该线程等待一个同步事件对象。

    3.3.2.4 压栈锁

    压栈锁是建立在KGATE同步对象基础之上,相比快速互斥体好处是可以按照共享的方式独占的模式来获得。

    有两种类型压栈锁:普通压栈锁和能感知缓存的压栈锁。

    普通压栈锁:当一个线程想要获得一个普通的压栈锁,若尚未被使用则压栈锁代码标记为已被占用。若已被占有(共享,独占),线程在自己栈上分配一个等待块,将初始化等待块中的事件对象,等待块加入到与压栈锁相关联的等待列表中。向该等待着的等待块中的事件发出信号。

    能感知缓存的压栈锁简历在基本压栈锁之上。为每个处理器分配一个压栈锁,然后将这些压栈锁和处理器关联起来。当一个线程希望以共享方式获得压栈锁时,简单的获得对应于当前处理器的那个压栈锁以独占方式获得独占锁时,以独占模式获得每个处理器的压栈锁。

    压栈锁的使用范围包括对象管理器和内存管理器,对象管理器中,可以保护全局对象管理器结构和对象安全描述符,在内存管理器,他们可以保护awe数据结构。

    3.4 系统辅助线程

    windowssystem进程中创建了几个线程,这些线程称为系统辅助线程它们代表其他线程来完成一些工作。如DPC级别的IRQL不能运行更低IRQL级别才能执行的函数,必须将这样的处理过程传递给一个低于DPC级别的IRQL的执行线程上。

    设备驱动程序或执行体组件通过ExQueueWorkItemIoQueueWorkItem把工作放到一个队列分发器对象上,系统复制线程在该对象上寻找工作。工作包含一个例程以及一个参数,当辅助线程处理该工作,会把参数传递给例程。

    系统辅助线程有以下三类:

    延迟型辅助线程:优先级12上,处理器非紧急工作项目,当它在等待工作项目时允许栈页面被换出到页面文件中。

    紧急型辅助线程:优先级13,处理一些紧急工作项目,始终在内存上

    超紧急型辅助线程:优先级15,总在内存中。

    执行体函数ExpWorkerThreadBalanceManager确定是否创建新的紧急型辅助线程,新的线程被称为动态的辅助线程。创建时必须满足下列条件:

    1.在紧急工作队列下有工作项目

    2.不活动的紧急型辅助线程的数目必须少于系统处理器个数

    3.动态辅助线程数据少于16个。

    3.5 windows全局标志

    3.6 本地过程调用(LPC)

    LPC用于进程间通信,LPC为了以下3中方式通信而设计:

    1.短于256字节的信息可以通过LPC发送。从发送进程拷贝到系统地址空间中,再从系统地址空间中拷贝到接收进程地址空间中。

    2.若多于256字节则复制到共享内存区。

    3.若超过了内存共享区,可以直接在地址空间中读取或写入。

    LPC有多种端口:服务器连接端口,服务器通信端口,客户通信端口,未命名的通信端口

    3.7 内核事件跟踪

    一个公共的基础设施,向内核和ETW提供痕迹数据应用程序要使用到ETW,要属于以下3类:

    1.控制器,启动或停止,也管理缓冲区

    2.提供者,为它所能产生的事件类定义GUID,并注册到ETW以上,并接受控制器命令,启动,停止它所负责的事件类跟踪。

    3.消费者,选择一个或多个会话,读取数据。

    控制器启动内核记录器(ETW)WMI发送一个IO请求说明要开始跟踪哪些事件类。当WMI接受到已启动跟踪源接收到数据就会写入buffer,每隔一秒触发一次写入日志文件。

    3.8 Wow64

    64windowswin32仿真,也就是可以在64位上执行32位,x86应用程序。以DLL形式来实现。

    wow64.dll实现了文件系统重定向,以及注册表重定向和反射

    wow64cpu.dll实现了cpu3264,从6432之间的切换

    wow64win.dll截取了win32k.sys导出GUI系统调用

    3.8.1 Wow64进程地址空间布局结构

    wow64进程可以是2G,也可以是4G虚拟空间。若没有设置大地址空间感知标志则最多保留2G,若开启大地之空间感知标志最多保留4G

    3.8.2 系统调用

    Wow64.dll钩住从32位代码变值原生64为系统代码路径,也钩住64位原生系统需要调用至32位用户模式代码的所有代码路径。

    启动应用程序,64ntdll.dll映射到地址空间,初始化判断映像头为32x86则加载wow64.dll映射32ntdll.dll,建立ntdll内部启动环境切换到32位,执行32位加载器。

    64位和32位间通过wow64在之间转换。

    3.8.3 异常分发

    通过wow64来转化异常分发。

    3.8.4 用户回调

    通过wow64来转化

    3.8.5 文件系统重定向

    为了降低应用程序移植代价,所有相关APIwindowssystem32替换为windowssyswow64。使用文件系统重定向来实现。

    线程使用wow64enablewow64FsRedirection函数进制文件系统重定向。

    3.8.6 注册表重定向和反射

    注册表也使用重定向在注册表上创建原生和wow642中,视图为了允许3264com组件互操作,在注册表中某些特定部分被更新,wow64也会将这些更新映射到迎来一个视图。

    3.8.7 I/O请求

    IO除了读写硬盘还可以用于程序通信,驱动程序通IoIs32bitProcess来检测是否从一个wow64进程发出的。

    3.9总结

    主要介绍执行体建立起来的基本系统机制。

  • 相关阅读:
    html2jspdf文档
    监听localstorage
    vue插件 webpack打包 style中的element样式没有打包进去
    css-loader导致vue中样式失效
    webpack vue-clii-service vite create-react-app umi对比
    react使用antd数据改变视图没更新原因
    npx
    Webpack原理—编写Loader和Plugin
    VUE Element el-input只能输入数字,限制数字长度,重写label宽度
    php--laravel --debug--mac
  • 原文地址:https://www.cnblogs.com/Amaranthus/p/4083451.html
Copyright © 2020-2023  润新知