[笔记]深入解析Windows操作系统《三》系统机制(五)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
日志服务 SLS,月写入数据量 50GB 1个月
云解析 DNS,旗舰版 1个月
简介: [笔记]深入解析Windows操作系统《三》系统机制(五)

性能

ALPC使用几种策略来改进性能,主要通过支持完成列表(前面已经粗略地介绍过)来做到。在内核层次上,完成列表本质上是一个用户MDL:它已经被探查〈 probe)过,并且被锁定,然后映射到一个地址上。(有关内存描述符列表——Memory Descriptor List的更多信息,参见本书下册第10章。〉因为它与MDL关联(MDL记录了物理页面),所以,当一个客户向服务器发送消息的时候,负荷数据的拷贝可以直接在物理内存层次上进行,而不用像其他IPC机制中常见的那样,请求内核对消息进行双缓冲区处理。

完成列表本身的实现是一个完成项的64位队列,用户模式和内核模式的消费者都可以使用一个互锁的比较-交换操作,从队列中插入和删除项目。更进一步,为了简化内存分配,一旦一个MDL已经被初始化,就利用一个位图来标识出哪些内存还可以使用,以此来留住那些仍然在队列中的新消息。位图算法也使用处理器上原生的锁指令来提供原子的分配和还原操作来操纵完成列表所使用的物理内存的区域。

另一个ALPC性能优化是采用了消息区(message zone)。消息区只是一个预分配的内核缓冲区(也是由MDL来支撑的),消息可以存储在该内核缓冲区中直到服务器或者客户来获取它。消息区将一个系统地址与该消息关联起来,从而使得该消息在任何进程地址空间中都是可见的。更为重要的是,在异步操作的情况下,它并不要求很复杂地建立起延迟的负荷数据,因为无论何时当消息的消费者最终来获取消息数据的时候,该消息区仍将是有效的。完成列表和消息区都可以通过NtAlpcSetInformation建立起来。

最后一个值得提及的优化是,不再是一发送消息内核马上就拷贝数据,而是内核先为将来延迟的拷贝准备好负荷数据,它只抓取必要的信息,此时没有任何拷贝动作。只有当接收者请求该消息的时候才拷贝消息数据。显然,如果一个消息区或者共享内存正在被使用,那么这种方法没有任何优势,但是,在异步的、内核缓冲区消息传递的情况下,这可以用来优化取消的情形和高流量的情形。

调试和跟踪

在检查版本(checked build))的内核中,ALPC消息可以被日志记录下来。所有的ALPC属性、blob、消息区,以及分发事务都可以被单独记录下来;WinDbg中有一个未文档化的!alpc命令,可以将这些日志转储出来。在零售版本的系统上,IT管理员和故障解决人员可以启用ALPCETW(Event Tracing for Windows)记录器来监视ALPC消息。ETW事件并不包含负荷数据,但它们包含了建立连接、断开连接,以及发送/接收、等待/解除阻塞等信息。最后,即使在零售系统上,通过特定的!alpc命令可以获取有关ALPC端口和消息的信息。

实验:转储一个连接端口


3.7 内核事件跟踪

Windows内核和几个核心设备驱动程序的各个组件内置了一些功能来记录下其操作的痕迹数据,以便用于系统诊断。它们依赖于内核中一个公共的基础设施,由它向用户模式的ETW(Event Tracing for Windows,Windows事件跟踪)设施提供痕迹数据。

使用ETW的应用程序必然是以下三类中的某一类或同属于多类:

  • 控制器 控制器启动或者停止记录会话,也管理缓冲区池。控制器的例子有:
    可靠性和性能监视器(参见本节后面的“实验:用内核记录器跟踪TCP/IP的活动”部分)和Windows性能工具箱中的XPerf(参见本章前面的“实验:监视中断和DPC活动”)。
  • 提供者 提供者为它所能产生的事件类定义GUID(全局唯一标识符),并且将它们注册到ETW中。提供者接受来自控制器的命令,以便启动或者停止它所负责的事件类的痕迹跟踪。
  • 消费者 消费者针对它想要读取的痕迹数据,选择一个或者多个跟踪会话。它们可以实时地接收缓冲区中的事件,也可以接收日志文件中的事件。

Windows包含了许多用户模式的提供者,从针对活动目录、服务控制管理器的提供者,到针对资源管理器(Explorer)的提供者,应有尽有。ETW也定义了一个名为“NTKernel Logger”的记录会话(也称为内核记录器),专门用于内核和核心驱动程序。NTKernel Logger的提供者是由Ntoskrnl.exe中的ETW代码和一些核心的驱动程序合起来实现的。

当一个用户模式下的控制器启动内核记录器时,ETW库(在\WindowslSystem32\Ntdll.dll中实现)调用NtTraceControl系统函数,告诉内核中的ETW代码,该控制器想要开始跟踪哪些事件类。如果当前的配置是文件记录(相对于内存记录,即输出到一个缓冲区中),则内核在创建日志文件的系统进程中创建一个系统线程。当内核接收到来自于已启用的痕迹数据源的事件时,它将这些事件记录到一个缓冲区中。如果文件记录线程已被启动的话,则它每隔一秒钟被唤醒一次,以便将缓冲区中的内容转储到日志文件中。

内核记录器生成的痕迹记录有一个标准的ETW痕迹事件头,其中记录了时间戳、进程、线程ID,以及关于该记录所对应的那一类事件的信息。事件类可能提供了与它们的事件有关的额外数据。例如,磁盘事件类痕迹记录指明了操作类型(读或者写)、该操作所在的磁盘号,以及该操作的扇区偏移和长度。

可被内核记录器启用的痕迹类,以及产生每一事件类的组件包括:

  • 磁盘I/O磁盘类驱动程序。
  • 文件I/O文件系统驱动程序。
  • 文件I/O完成文件系统驱动程序。
    硬件配置即插即用管理器(有关即插即用管理器的信息,参见本书下册第9章)。
  • 映像加载/卸载内核中的系统映像加载器。
    页面错误内存管理器(有关页面错误的更多信息,参见本书下册第10章)。
  • 硬页面错误内存管理器。
  • 进程创建/删除进程管理器(Process manager,有关进程管理器的更多信息,参见
    第5章)。
  • 线程创建/删除进程管理器(Process manager)。
  • 注册表活动配置管理器(有关配置管理器的更多信息,参见第4章中“注册表”一
    节)。
  • 网络TCP/IP TCP/IP驱动程序。
  • 进程计数器进程管理器(Process manager)。
  • 环境切换( context switch)内核分发器(kernel dispatcher)。口延迟的过程调用(DPC)内核分发器。
    中断内核分发器。
  • 系统调用内核分发器。
    基于采样的性能剖析内核分发器和HAL。驱动程序延迟IO管理器。
  • 分裂的I/O (Split lO)1/O管理器。口电源事件电源管理器。
    ALPC高级本地过程调用。
  • 调度器和同步内核调度器(有关线程调度的更多信息,参见第5章)。

在Windows SDK中可以找到有关ETW和内核记录器的更多信息,其中包括一些控制器和消费者的例子代码。

实验:用内核记录器跟踪TCP/IP的活动


3.8 WOW64

Wow64(64位Windows上的Win32仿真)是指允许在64位Windows上执行32位x86应用程序的软件。它的实现方式是一组用户模式DLL,外加一些来自内核的支持,此内核支持是为了创建32位版本的数据结构,比如进程环境块(PEB)和线程环境块(TEB),这些数据结构正常情况下只有64位版本。

通过Get/SetThreadContext来改变Wow64环境也是由内核实现的。下面是负责Wow64的用户模式DLL:

Wow64.dll:管理进程和线程的创建、钩住异常分发和Ntoskrnl.exe导出的基本系统调用。它也实现了文件系统重定向,以及注册表重定向。

Wow64Cpu.dll:为每个正在Wow64内部运行的线程,管理它们的32位CPU环境;针对从32位到64位或者从64位到32位的CPU模式切换,提供了与处理器体系结构相关的支持。

Wow64Win.dll:截取了Win32k.sys导出的GUI系统调用。

IA64系统上的IA32Exec.bin和Wowia32x.dll:包含IA-32软件仿真器和它的接口库。因为Itanium处理器不能以原生方式高效地执行x86的32位指令(性能差于30%),所以有必要通过这两个额外的组件来实现软件仿真(通过二进制翻译)。

这些DLL之间的关系如图3.31所示。

Wow64进程地址空间布局结构

Wow64进程可以在2GB虚拟空间中运行,也可以在4GB虚拟空间中运行。如果映像文件的头部设置了大地址空间感知标志,则内存管理器将4GB边界之上至用户模式边界末尾之间保留为用户模式地址空间。如果映像文件没有被标记为大地址空间感知的,则内存管理器将保留2GB之上的用户模式地址空间(有关大地址空间支持的更多信息,请参见本书下册第10章中“x86用户地址空间的布局结构”一节)。

系统调用

Wow64钩住了所有从32位代码转变至原生64位系统的代码路径,也钩住了64位原生系统需要调用至32位用户模式代码的所有代码路径。在进程创建的过程中,进程管理器(processmanager)将原生的64位Ntdl.dll和针对Wow64进程的32位Ntdll.dIl映射到进程地址空间中。当加载器的初始化过程被调用时,它调用Wow64.dll内部的Wow64初始化代码。然后Wow64建立起32位Ntdll所要求的启动环境,将CPU模式切换到32位下,并开始执行32位加载器。从这个点开始,执行过程继续进行,就如同该进程运行在原生的32位系统上一样。

Ntdll.dll、User32.dll和Gdi32.dil的特殊32位版本位于\WindowslSyswow64文件夹下(也有一些特定的执行跨进程通信的其他DLL,比如Rpcrt4.dll)。它们调用到Wow64中,而不是发出原生的32位系统调用指令。Wow64转变到原生的64位模式下,捕获到与系统调用有关的参数(将32位指针转换为64位指针),并且发出对应的原生64位系统调用。当原生的系统调用返回时,Wow64把任何输出参数,如果有必要的话,在返回至32位模式之前从64位转换成32位格式。

异常分发

Wow64通过Ntdll的KiUserExceptionDispatcher钩住了异常分发过程。无论何时当64位内核将要给一个Wow64进程分发一个异常时,Wow64会捕获住原生的异常以及用户模式下的环境记录(context record),然后准备一个32位异常和环境记录,并且按照原生32位内核所做的那样将它分发出去。

用户APC分发

Wow64通过Ntdll的KiUserApcDispatcher也钩住了用户模式APC的递交过程。无论何时当64位内核将要给一个Wow64进程分发一个用户模式APC时,Wow64把32位APC地址映射到一个更高的64位地址空间范围中。然后,64位Ntdll捕获住原生的APC以及用户模式下的环境记录,将它映射回32位地址。然后它准备一个32位用户模式APC和环境记录,并且按照原生32位内核所做的那样将它分发出去。

控制台支持

因为控制台支持是由Csrss.exe在用户模式下实现的,它只是单个原生二进制可执行文件,所以,32位应用程序在64位Windows上不能执行控制台IO。类似于专门有一个特殊的rpcrt4.dil用来将32位RPC适配成64位RPC,Wow64的32位Kernel.dll包含有专门的代码来调用到Wow中,以便在与Csrss和Conhost.exe交互过程中对参数进行适配。

用户回调

Wow64截取了所有从内核到用户模式的回调。Wow64将这样的调用也按照系统调用来对待;然而,数据转换则是按相反的顺序来完成的:输入参数从64位转换为32位,而输出参数则是在该次回调返回时从32位转换至64位。

文件系统重定向

为了维护应用程序的兼容性,以及降低从Win32到64位Windows的应用程序移植代价,系统目录名称仍然保持不变。因此,\WindowslSystem32文件夹包含了原生的64位映像文件。因为Wow64钩住了所有的系统调用,所以,它会解释所有与路径相关的API,将\Windows\System32文件夹的路径名替换为\Windows.Syswow64。Wow64也将\Windows\LastGood重定向到\Windows\LastGoodlsyswow64,将\Windows\Regedit.exe重定向到\Windowsisyswow64\Regedit.exe。

通过使用系统环境变量,%PROGRAMFILES%环境变量对于32位应用程序被设置为\Program Files (x86),而对于64位应用程序被设置为\Program Files文件夹。CommonProgramFiles和CommonProgramFiles(x86)环境变量也存在,它们总是指向32位的位置,而ProgramW6432和CommonProgramWP6432则无条件地指向64位位置。

注因为有些特定的32位应用程序可能真的需要知晓或者能够处理64位映像文件,所以,有一个

虚拟的目录,\Windows\Sysnative,使得任何从32位应用程序发出的针对此目录的I/O,都免于被文件重定向。这个目录实际上并不存在,它只是一个允许访问到真正的System32目录的虚拟路径而已,即使运行在Wow64下的应用程序也不例外。

lWindows\System32中有一些子目录,出于兼容性的原因,这些子目录没有被重定向,所以32位应用程序在访问这些目录时实际上是在访问真正的目录。这些目录包括:

  • %windir%lsystem32\drivers\etc
  • %windir%lsystem32\spool
  • %windir%lsystem32\catroot和%windir%lsystem32lcatroot2%windir%lsystem32logfiles
  • %windir%\system32ldriverstore

最后,Wow64提供了一种机制来控制Wow64中内置的文件系统重定向功能,这种机制是以每个线程为基础的,通过Wow64DisableWow64FsRedirection和 Wow64RevertWow64FsRedirection函数来实施控制。该机制对于延迟加载的DLL、通过公共文件对话框来打开文件,甚至国际化方面,都存在一些问题——因为一旦重定向被关闭,系统要么在内部加载过程中不再使用重定向,要么有些特定的只有64位的文件便无法再找到。使用c:1windowsisysnative路径或者前面介绍的其他的一致路径通常是一种更为安全的方法,可以供开发人员使用。

注册表的重定向

应用程序和组件程序将它们的配置数据保存在注册表中。组件程序在安装过程中,当它们被注册的时候,通常将配置数据写到注册表中。如果同样的组件既安装和注册了一个32位二进制文件,又安装和注册了一个64位二进制文件,那么,最后被注册的那个组件将会覆盖掉以前组件的注册,因为它们写到注册表中同样的位置上。

为了以透明的方式解决这个问题,并且无须对32位组件进行任何代码修改,注册表被分成两个部分:原生的和Wow64的。在默认情况下,32位组件访问32位视图,64位组件访问64位视图。这为32位和64位组件提供了一个安全的执行环境,并且将32位应用程序的状态与64位应用程序(如果存在的话)的状态隔离开来。

为了实现这一点,Wow64截取了所有要打开注册表键的系统调用,并且重新解释这些注册表键的路径,将它们指向注册表的Wow64视图。Wow64在以下这些点上分裂注册表:

  • HKLM\SOFTWARE
  • HKEY_CLASSES_ROOT
    然而,请注意,许多子键实际上在32位和64位应用之间是共享的——也就是说,并非整个储巢被分裂了。

在以上每一个键的下面,Wow64创建了一个称为Wow6432Node的键。在该键下面保存的是32位配置信息。注册表的所有其他部分对于32位应用程序和64位应用程序都是共享的(比如HKLMISYSTEM)。

还有一个额外的帮助,如果一个32位应用程序向注册表中写入一个以数据“%ProgramFiles%”或“%commonprogramfiles%”为开头的REG_SZ或者REG_EXPAND_SZ值,那么Wow64将实际的值修改为“%ProgramFiles(x86)%“或”%commonprogramfiles(x86)%”,以便符合前面介绍的文件系统重定向和布局结构。32位应用程序必须正确地写这些字符串(包括大小写)——任何其他的数据都被忽略,按普通的方式写入。最后,任何包含“system32”的键被替换为“syswow64”(针对所有的大小写),也不管标志和大小写是否敏感,除非使用了KEY_WOW64_64KEY,以及该键位于“反射键”列表中(可在MSDN上查询到)。

如果应用程序需要显式地指定一个注册表键位于某个特定的视图中,那么,在RegOpenKeyEx、RegCreateKeyEx、RegOpenKeyTransacted、RegCreateKeyTransacted和RegDeleteKeyEx函数中使用下述标志可以做到这一点:

KEY_WOW64_64KEY——从一个32位或者64位应用程序中显式地打开一个64位键,

并且禁止前面介绍的REG_SZ或REG_EXPAND_SZ截取转换处理。

KEY_WOW64_32KEY ——从一个32位或者64位应用程序中显式地打开一个32位键。

I/O控制请求

除了普通的读和写操作以外,应用程序可以利用Windows的DeviceloControlAPI,与某些设备驱动程序通过设备IO控制函数进行通信。应用程序可能会在调用时指定一个输入和/或输出缓冲区。如果该缓冲区中包含了与指针相关的数据,并且发送该控制请求的进程是一个Wow64进程,那么,输入和/或输出结构的视图在32位应用程序和64位驱动程序之间是不相同的,因为对于32位应用程序来说,指针是4字节,而对于64位应用程序来说,指针是8字节。在这种情况下,内核驱动程序最好能够转换这些与指针相关的结构。驱动程序可以调用loIs32bitProcess函数来检测一个IO请求是否是从一个Wow64进程发出的。更多的细节可以参考MSDN中的“Supporting 32-Bit IO in Your 64-Bit Driver”。

16位安装器应用程序

Wow64不支持运行16位应用程序。然而,由于许多应用安装器是16位程序,所以,Wow64包含一些特殊的代码,使得对于某些特别知名的16位安装器的引用能够工作。这样的安装器包括:

Microsoft ACME Setup 版本: 1.2、2.6、3.0和3.1。InstallShield版本5.x(这里x是任何一个小版本号)。

无论何时当通过CreateProcess()API创建一个16位进程时,首先Ntvdm64.dll被加载进来,然后控制权被传递给它,以检查该16位可执行文件是否是所支持的安装器中的某一个。如果是的话,则发出另一个CreateProcess调用,以便使用同样的命令行参数来激发该安装器的一个32位版本。

打印

32位打印机驱动程序不能被用在64位Windows中。打印驱动程序必须要被移植为原生的64位版本。然而,由于打印机驱动程序运行在所请求进程的用户模式地址空间中,并且在64位Windows上只支持原生的64位打印机驱动程序,所以,需要一种特殊的机制来支持32位进程中的打印任务。这是这样做到的:将所有的打印函数重定向到Splwow64.exe中,这里Splwow64.exe是Wow64RPC打印服务器。由于Splwow64是一个64位进程,所以它可以加载64位打印机驱动程序。

—些限制

Wow64不支持16位应用程序的执行(而在32位版本的Windows上它们是支持的),也不支持加载32位内核模式的设备驱动程序(它们必须被移植为原生的64位版本)。Wow64进程只能加载32位DLL,不能加载原生的64位DLL。类似地,原生的64位进程不能加载32位DLL。唯一的例外是,在跨越体系结构差异时,能够加载仅包含资源或数据的DLL,这是允许的,因为这些DLL只包含数据,而并非代码。

除了上述限制以外,由于页面大小的差异,在IA64系统上的Wow并不支持ReadFileScatter、WriteFileGather、GetWriteWatch、AVX寄存器、XSAVE,以及AWE函数。而且,通过DirectX得到的硬件加速也是不可用的(针对Wow64进程提供了软件仿真)。

3.9 用户模式调试

对用户模式调试的支持被分在三个不同的模块中。

第一个模块位于内核可执行程序内部,其前缀为Dbgk,代表了调试框架(debugging framework)的意思。

它提供了必要的内部函数.用于注册和监听调试事件、管理调试对象,以及对信息进行打包以供用户模式部分使用。

直接与Dbgk打交道的用户模式组件位于原生的系统库,Ntdl.dll中,在一组以前缀DbgUi打头的API函数中。

这些API负责将底层的调试对象实现(这是不可见的)包装起来,允许所有的子系统应用程序使用调试功能,它们可以在这一DbgUi实现上再包装它们自己的API。

最后,用户模式调试的第三个组件属于子系统DLL。这是指被暴露出来的、文档化的API(对于Windows子系统,位于KernelBase.dll中),每个子系统都会支持这样的API以便可以调试其他的应用程序。

内核支持

内核通过一种前面提到过的对象,调试对象(debug object),来支持用户模式调试。

它提供了一系列系统调用,这些系统调用绝大多数直接映射到Windows调试API上,通常首先通过DbgUi层来进行访问。调试对象本身是一个简单的结构体,由一系列标志(决定了对象的状态)、一个事件(用于通知等待者已经有了调试器事件)、一个调试事件双链表(这些调试事件正在等待被处理),以及一个用于锁住该对象的快速互斥体构成。这是内核为了能够成功地接收和发送调试事件而需要的所有信息,每个被调试的进程在它的结构中有一个调试端口(debug port)成员指向此调试对象。

一旦一个进程有一个关联的调试端口,那么,表3.23中描述的事件可以导致在事件列表中插入一个调试事件。

除了上表中提到的原因以外,还有一些超越于这些常规条件下当一个调试器对象被第一次与一个进程关联起来时的特殊触发情形。当调试器被附载(attach)到一个进程时,就会手工发送第一个创建进程( create process)和创建线程(create thread)的消息,这是针对进程本身,以及它的主线程;接着,为进程中的所有其他线程发送创建线程消息。最后,针对被调试的可执行程序(Ntdll.dll)发送加载dll事件,再为被调试进程中的所有当前DLL发送加载dll事件。

一旦一个调试器对象已经关联上一个进程,则该进程中的所有线程都被挂起。在这时候,调试器有责任开始请求发送这些调试事件。调试器通过在调试对象上执行一个wait动作,请求这些调试事件被送回到用户模式。此调用对调试事件链表进行循环。当每个请求被从链表中移除时,该请求的内容将从dbgk内部结构转换为上一层可以理解的原生结构。我们将会看到,这一结构与Win32的结构并不相同,因此,还需要另外一层转换。即使当调试器处理完了所有待处理的调试消息以后,内核不会自动地重新启动这一进程。调试器有责任调用ContinueDebugEvent函数来恢复该进程的执行。

除了一些跟多线程有关的复杂处理事项以外,这一框架的基本模型是非常简单的,只不过是生产者(provider),即内核中产生上面表格中所列的调试事件的代码,加上消费者(consumer),即,在这些事件上等待并且做出响应的调试器。

原生支持

虽然用户模式调试的基本协议非常简单,但是,Windows应用程序并非直接使用用户模式调试。相反地,用户模式调试被包装为Ntdl.dll中的DbgUi函数族。这一抽象是必要的,这使得原生应用程序以及不同的子系统可以使用这些例程(因为Ntdll.dll中的代码没有依赖性)。这一组件提供的函数绝大多数与Windows API函数和有关的系统调用非常类似。

在其代码内部也提供了请求“创建一个与当前线程相关联的调试对象”的功能。被创建出来的调试对象的句柄永远不会被暴露出去。相反地,它被保存在正在执行此关联操作的调试器线程的TEB(线程环境块,thread environment block)中。(有关TEB的更多信息,请参考第5章。)此值被保存在

DbgSsReserved[1]中。

当一个调试器附载到一个进程时,它希望该进程可以被侵入(break into),也就是说,一个int 3(断点)操作应该已经发生了,这是由注入在该进程中的一个线程产生的。如若不然,调试器应该永远不会真正控制该进程,它只不过可以看到调试事件发生而已。Ntdl.dl负责创建此线程并将其注入到目标进程中。

最后,Ntdll.dll也提供了API来把调试事件的原生数据结构转换为Windows API可以理解的数据结构。


实验:查看调试器对象


Windows子系统的支持

使得诸如Microsoft Visual Studio或WinDbg之类的调试器可以调试用户模式应用程序的最后一个组件是在Kernel32.dll中。它提供了文档化的Windows API。在这里列举出这些函数名称并非重要,这部分调试设施的一个重要的管理任务是:管理复制的文件和线程句柄。

回忆一下,每次当一个加载dll事件被送出的时候,内核就会复制一个指向该映像文件的句柄,并放在事件结构中,这就如同在创建进程的事件过程中处理指向进程可执行映像文件的句柄一样。在每一个等待调用过程中,Kernel32.dl检查是否有事件导致在内核中新的进程和/或线程句柄被复制(两个创建事件)。如果是的话,则分配一个数据结构,其中存放进程ID、线程ID,以及与该事件相关联的线程和/或进程句柄。此数据结构被链接到TEB的第一个DbgSsReserved数组索引中,上一小节曾经提到过,调试对象的句柄也被存放在这里。同样地,Kernel32.dll也会检查退出事件。当它检测到这样的事件时,它会在数据结构中“标记”相应的句柄。

一旦调试器用完了这些句柄,并且执行了继续调用,Kernel32.dIl将解析这些数据结构,检查那些已经退出的线程的句柄,并且为调试器关闭这些句柄。否则的话,这些线程和进程将永远不会退出,因为只要调试器在运行,就总会有打开的句柄指向这些线程或进程。

3.10 映像加载器

当系统中一个进程被启动时,内核创建一个进程对象来代表该进程(有关进程的更多信息,请参考第5章),并执行各种与内核有关的初始化任务。然而,这些任务并不会导致应用程序被执行起来,而仅仅做了一些准备上下文和执行环境的工作。事实上,不像驱动程序是内核模式的代码,应用程序是在用户模式下执行的,所以,实际的初始化工作绝大部分是在内核之外完成的。这些工作是由映像加载器(image loader)来完成的,在内部用Ldr来表示。

映像加载器驻留在用户模式系统DLLNtdll.dll中,不在内核库中。因此,它的行为表现就像标准的、位于一个DLL中的代码一样,而且,在内存访问和安全权限方面也受同样的限制。使这部分代码变得特殊的是,它可以确保总是出现在任何正在运行的进程中(Ntdll.dll总是被加载到进程中),而且它是新的应用程序中最先在用户模式下运行的代码。(当系统建立起初始的上下文环境后,程序计数器或者指令指针被设置为Ntdll.dll中的一个初始化函数。更多的信息请参考第5章。)

因为加载器总是在实际的应用程序代码之前运行,所以,它对于用户和开发人员通常是不可见的。而且,尽管加载器的初始化任务被隐藏起来了,但是,一个程序在运行过程中,往往确实需要跟加载器的接口打交道,例如,当加载或卸载一个DLL,或者查询一个DLL的基地址的时候。加载器负责的一些主要任务如下所列:

  • 为应用程序初始化其用户模式状态,比如创建初始的堆、建立起线程局部存储(TLS,thread local storage)和纤程局部存储(FLS,fiber local storage)槽。
  • 解析应用程序的导入表(IAT),查找所有它要求的DLL(然后递归地为每个DLL解析IAT),接着,解析DLL的导出表,确保导入的函数确实存在(特殊的前转项(forwarderentry)也可以将一个导出表项重定向到另一个DLL中)。
  • 在运行时候或者根据需要加载或卸载DLL,并且维护一个包含所有已被加载的模块
    的列表(模块数据库)。
  • 使得可以支持运行时刻打补丁(称为热补丁,hotpatching),本章后面会进一步解释。口处理清单文件(manifest file)。
  • 读取任何铺垫形式的应用程序兼容性数据库,如果有必要的话,加载此铺垫(Shim)
    引擎DLL。

启用对API集和API重定向的支持,这是MinWin重构工程的一个核心部分。口启用基于SwitchBranch机制的运行时刻动态兼容性缓解方案(mitigation)。

正如你所看到的,绝大多数这些任务对于一个应用程序真正运行其代码都是至关重要的;否则,从调用外部函数,到使用堆内存,一切都马上宕掉。在进程已被创建起来以后,加载器将调用一个特殊的原生API,因而可以基于栈中的一个环境帧(context frame)继续执行。此环境帧是由内核建立起来的,包含了应用程序的实际入口点。因此,由于加载器并不使用标准的调用或跳转指令进入到正在运行的应用程序中,所以,你在一个线程的栈痕迹中,永远不会看到加载器的初始化函数出现在调用树中。

实验:观察映像加载器


进程初始化早期工作

因为加载器是在Ntdll.dll中,这是一个不附属于任何子系统的原生DLL,所以,所有进程都遵从同样的加载器行为(有一些细微的差别)。在第5章,我们将会详细地看一下在内核模式下一个进程创建过程中的步骤,以及Windows函数CreateProcess完成的一些工作。然而,现在我们来讨论发生在用户模式下的工作,这些工作独立于任何一个子系统,并且从第一条用户模式指令执行就开始了。当一个进程启动时,加载器执行以下步骤:

  1. 构建起应用程序的映像路径名称,并且确定该应用程序的Image File Execution
    Options键,以及DEP和SHE有效性链接器设置。
  2. 检查映像可执行文件的头,看它是否为一个.NET应用(取决于是否出现了与.NET相
    关的映像目录)。
  3. 初始化进程的国家语言支持(NLS,National Language Support)表(国际化支持)。
  4. 如果该映射是32位的,但是在64位Windows上运行,则初始化Wow64引擎。
  5. 把映像可执行文件头中指定的配置选项都加载上。这些选项能够控制此可执行文件
    的行为,开发人员在编译该应用程序时可以定义这些选项。
  6. 如果在可执行文件头中指定了亲和性掩码( affinity mask),则设置此亲和性掩码。
  7. 初始化FLS和TLS。
  8. 为当前进程初始化堆管理器,并创建第一个进程堆。
  9. 为进程分配一个SxS(Side-by-Side Assembly,并行程序集)/Fusion激活环境,这使得系
    统可以使用正确的DLL版本文件,而不再默认指向那个与操作系统一起发行的DLL(更多的信息请参考第5章)。
  10. 打开\KnownDlls对象目录,构建起已知DLL的路径。对于Wow64进程,使用
    lKnownDlls32。
  11. 确定进程的当前目录和默认加载路径(当加载映像文件和打开文件的时候会用到)。
  12. 为应用程序可执行文件和Ntdll.dlI建立起相应的加载器数据表项,然后将表项插入到
    模块数据库中。

到这时候,映像加载器已经做好了准备,可以开始解析属于该应用程序的可执行文件的导入表,以及加载任何在应用程序编译过程中动态链接的DLL了。因为每一个被导入的DLL也可以有它自己的导入表,所以,这个过程会递归地进行,直到所有的DLL都被满足,所有被导入的函数都已经找到。随着每一个DLL被加载进来,加载器会记录下它的状态信息,并构建起模块数据库。

dll名称解析

名称解析是指这样一个过程:当调用者没有指定或者不能指定一个唯一文件标识的情况下,系统把一个PE格式二进制文件的名称转换成一个物理文件。因为各个目录(应用目录、系统目录等等)的位置无法在链接的时候以硬编码的方式确定,所以,这也包括所有二进制依赖性的解析,以及当调用者没有指定一个完整路径的情况下LoadLibrary操作的解析过程。

当解析二进制依赖性的时候,基本的Windows应用程序模型是按照搜索路径来查找文件,这里的搜索路径是一个位置列表,其中每个位置被顺序搜索以发现一个匹配的基本名称;不过各种系统组件为了扩展默认的应用程序模型,会覆盖掉这一路径搜索机制。搜索路径的概念是从命令行时代遗留下来的产物,那时候一个应用程序的当前目录是一个有意义的概念;对于现代的GUI应用程序,这多少有点不合时宜了。

然而,由于当前目录在这一路径顺序中的特殊位置,通过在应用程序的当前目录下放置一些相同基本文件名的恶意二进制文件,使得系统二进制文件的加载操作可以被改变。为了防止与这一行为相关联的安全风险,在路径搜索计算上新加入一个称为安全DLL搜索模式的特性,并且从Windows XPSP2开始,这一特性对所有的进程都默认启用。在安全搜索模式下,当前路径被移到三个系统目录的后面,从而导致下面的路径顺序:

  1. 应用程序被激发时的目录;
  2. 原生的Windows系统目录(例如,C:\Windows\System32);
  3. 16位Windows系统目录(例如,C:\Windows\System);
  4. Windows目录(例如,C:\Windows)
  5. 应用程序激发时刻的当前目录;
  6. 任何在%PATH%环境变量中指定的目录。

对于每个后续的DLL加载操作,DLL搜索路径都要重新计算。用于计算搜索路径的算法与计算默认搜索路径所使用的算法相同,但是应用程序可以通过SetEnvironmentVariable API来编辑%PATH%变量,从而改变特定的路径元素;也可以使用SetCurrentDirectory API来改变当前目录,或者使用SetDIIDirectory API来为当前进程指定一个DLL目录。当指定了DLL目录的时候,该目录代替了搜索路径中的当前目录,并且对于该进程,加载器将会忽略安全DLL搜索模式的设置。

调用者也可以在调用LoadLibraryEx API时提供LOAD_WITH_ALTERED_SEARCH_PATH标志,以便针对特定的加载操作修改DLL搜索路径。当提供了这一标志,并且提供给此API的DLL名称是一个全路径字符串时,在计算该操作的搜索路径的时候将使用包含该DLL文件的路径来代替应用程序目录。

DLL名称重定向

在将一个DLL名称字符串解析成一个文件以前,加载器试图使用DLL名称重定向规则。这些重定向规则被用于扩展或者改变DLL名字空间的某些部分,以进一步扩展Windows应用程序模型。这里的DLL名字空间通常对应于Win32文件系统名字空间。对于应用程序,这些规则是:

  • MinWin API集重定向API集机制的设计目标是,允许Windows开发组以一种对应用
    程序透明的方式来改变一个要导出特定系统API的二进制文件。
  • .LOCAL重定向,.LOCAL重定向机制允许应用程序将某个特定DLL基本名称的所有加载操作(不管是否指定了一个全路径),都重定向到该应用程序目录中DLL文件的一份本地副本上,做法有两种:用相同基本名称再加上.local,为该DLL创建一个副本文件(例如,MyLibrary.dll.local);或者,在应用程序目录下创建一个名为.local的

文件夹,再把本地DLL的一份副本放在该文件夹中(例如,C:\ProgramFilesMyApp.LOCALMyLibrary.dll)。通过.LOCAL机制来重定向的文件,其处理过程与通过SxS来重定向的DLL一样(参见下一条)。只有当可执行文件没有一个关联的清单文件(无论是内嵌的还是外部的)的时候,加载器才为DLL使用.LOCAL重定向。

Fusion (SxS)重定向 Fusion(也称为并行〔程序集),side-by-side,或SxS)是对Windows应用程序模型的扩展,允许二进制组件嵌入二进制资源(称为清单, manifest>来表达更为详细的二进制依赖性信息(通常是版本信息)。当Windows公共控件包( comctl32.dll)被分裂成多个可以相互并存的不同版本以后,Fusion机制率先被使用,因而应用程序可以加载正确版本的二进制文件。此后,其他的二进制文件也采用同样的方式进行版本管理。到了Visual Studio 2005,用Microsoft链接器编出来的应用程序将使用Fusion来定位到正确版本的C运行时库。

Fusion运行时工具利用Windows资源加载器,从一个二进制文件的资源区,读入内嵌的依赖性信息,然后把依赖性信息封装成称为激活环境( activation context)的查找结构。系统分别在引导时刻和进程启动时刻创建起系统级的默认激活环境,和进程级的默认激活环境;而且,每个线程有一个关联的激活环境栈,在栈顶的激活环境结构被认为是活动的(active)的。每个线程的激活环境栈既可以被显式地管理(通过ActivatcActCtx和DeactivateActCtx API),也可以在特定的点上由系统隐式地管理(比如当一个内嵌有依赖性信息的二进制文件的DLL主例程被调用的时候)。当一个Fusion DLL名称重定向查找发生的时候,系统在该线程的激活环境栈的头部处的激活环境中搜索重定向信息,接着再搜索进程激活环境和系统激活环境;如果找到了重定向信息,则当前的加载操作使用激活环境中指定的文件标识。

已知DLL重定向已知DLL是指这样一种系统机制:将特定的DLL基本名称映射到系统目录中的文件上,从而阻止这些DLL被不同位置处其他版本的文件替换掉。

在DLL路径搜索算法中,一个边界情形是,在64位和Wow64应用程序上执行的DLL版本检查。如果找到了一个基本名称匹配的DLL,但是随后发现该DLL编译的机器体系结构不正确—一例如,在32位应用程序中的64位映像文件,那么,加载器会忽略此错误,并恢复路径搜索操作,从找到此不正确文件所使用的那个元素之后的下一个元素继续开始搜索。这么设计的目的是,让应用程序可以在全局的%PATH%环境变量中同时指定64位和32位路径项。

相关文章
|
9天前
|
设计模式 算法 安全
实时操作系统(RTOS)深度解析及Java实现初探
【10月更文挑战第22天】实时操作系统(RTOS,Real-Time Operating System)是一种能够在严格的时间限制内响应外部事件并处理任务的操作系统。它以其高效、高速、可靠的特点,广泛应用于工业自动化、航空航天、医疗设备、交通控制等领域。本文将深入浅出地介绍RTOS的相关概念、底层原理、作用与功能,并探讨在Java中实现实时系统的方法。
36 1
|
12天前
|
移动开发 Android开发 开发者
移动应用与系统:探索移动开发与操作系统的协同进化###
本文深入探讨了移动应用开发与移动操作系统之间错综复杂的关系,揭示了技术进步如何推动用户体验的飞跃。通过案例分析和技术解析,本文阐述了开发者在适应不断变化的操作系统环境中面临的挑战与机遇,以及这种互动如何塑造了我们的数字生活。 ###
|
15天前
|
存储 弹性计算 运维
阿里云国际Windows操作系统迁移教程
阿里云国际Windows操作系统迁移教程
|
21天前
|
人工智能 Linux Android开发
移动应用与系统:探索移动应用开发和操作系统的奥秘
【10月更文挑战第5天】 这篇文章将深入探讨移动应用与系统的关键技术,包括移动应用开发的基本流程、工具和技术,以及移动操作系统的核心原理和架构。我们将从浅入深,逐步揭开移动应用与系统的神秘面纱,帮助读者更好地理解和掌握这一领域的知识。
30 2
|
3天前
|
前端开发 测试技术 调度
移动应用与系统:探索开发与操作系统的奥秘####
【10月更文挑战第22天】 本文深入剖析了移动应用的开发流程与移动操作系统的核心原理,揭示了两者如何相互依存、共同推动移动互联网的发展。从应用架构设计到操作系统性能优化,全方位解读移动生态的技术细节,为开发者和用户提供有价值的参考。 ####
12 5
|
2天前
|
人工智能 前端开发 Android开发
移动应用与系统:探索移动应用开发与操作系统的协同进化
本文深入探讨了移动应用开发与移动操作系统之间的紧密关系,以及它们如何相互影响、共同推动移动技术的发展。文章从移动应用开发的基础知识出发,逐步深入到移动操作系统的核心特性,分析了两者在技术层面的交互作用,并展望了未来的发展趋势。通过具体案例和数据分析,本文揭示了移动应用开发与移动操作系统协同进化的重要性,为开发者提供了宝贵的参考和启示。
|
1天前
|
搜索推荐 前端开发 测试技术
移动应用与系统:探索开发之道与操作系统的演进#### 一、
【10月更文挑战第24天】 本文将带你深入探索移动应用开发的全过程,从构思到上架的每一个细节。同时,我们还将回顾移动操作系统的发展历程,分析当前主流系统的技术特点和未来趋势。无论你是开发者还是普通用户,都能在这里找到感兴趣的内容。 #### 二、
9 1
|
6天前
|
安全 Android开发 数据安全/隐私保护
移动应用与系统:探索开发趋势与操作系统革新#### 一、
【10月更文挑战第20天】 本文旨在剖析当前移动应用开发的热门趋势,并探讨移动操作系统的最新进展与未来展望。通过梳理从原生应用到跨平台开发的转变,以及主流操作系统如iOS和Android的技术创新,本文为开发者提供了一份详尽的行业指南,助力他们在快速迭代的移动科技领域保持领先。 #### 二、
19 2
|
17天前
|
消息中间件 中间件 数据库
NServiceBus:打造企业级服务总线的利器——深度解析这一面向消息中间件如何革新分布式应用开发与提升系统可靠性
【10月更文挑战第9天】NServiceBus 是一个面向消息的中间件,专为构建分布式应用程序设计,特别适用于企业级服务总线(ESB)。它通过消息队列实现服务间的解耦,提高系统的可扩展性和容错性。在 .NET 生态中,NServiceBus 提供了强大的功能,支持多种传输方式如 RabbitMQ 和 Azure Service Bus。通过异步消息传递模式,各组件可以独立运作,即使某部分出现故障也不会影响整体系统。 示例代码展示了如何使用 NServiceBus 发送和接收消息,简化了系统的设计和维护。
32 3
|
18天前
|
安全 Android开发 UED
移动应用与系统:探索移动应用开发和操作系统的融合
【10月更文挑战第8天】 本文深入探讨了移动应用开发和操作系统之间的紧密联系,分析了它们如何共同塑造用户体验。我们将从技术角度出发,揭示移动应用开发的最佳实践,并讨论移动操作系统的关键特性。通过案例研究,我们展示了如何利用这些技术来创建高效、用户友好的移动应用。
31 2