您的位置:首页 > 其它

翻译: Windows Internals: 第六章: 进程创建流程

2008-10-10 12:57 211 查看
进程创建流程

到目前为止,你看到了进程的数据结构和可以操作进程的API方法.你也知道了怎样使用工具来查看进程如何与系统交换.但是那些程序是如何出现,为什么一旦完成它们的任务就退出了呢? 在以下的内容里你会发现Windows进程如何开始运行的.

当一个程序调用进程创建函数例如: CreateProcess, CreateProcessAsUser, CreateProcessWithTokenW, 或 CreateProcessWithLongonW时,一个Windows进程就被创建了. 创建一个进程包含了许多步骤, 它们在操作系统的3个模块中执行:Windows客户端库Kernel32.dll,Windows执行者和子系统. 因为Windows的多环境子系统架构创建一个可执行进程对象的过程是和创建进程相关的工作分开的. 虽然以下关于方法CreateProcess的描述有些复杂,但请记住这项工作的一部分是被Windows子系统特别在语意上添加的做为创建进程核心工作的对比.

下面的表总结了用CreateProcess方法创建进程的主要阶段. 每个阶段中进行的操作在后面中有详细描述.

1. 打开要执行的镜象文件
2. 创建执行进程对象
3. 创建初试线程(栈,CPU上下文还有执行线程对象)
4. 通知新进程的Windows子系统为新进程和线程做好准备.
5. 开始执行初始线程(除非CREATE_SUSPENDED标志被指定)
6. 在新进程和新线程上下文中完成地址空间的初始化并开始执行这个程序.

图6-5展示的是Windows创建一个进程的步骤

在打开可执行文件镜象前CreateProcess要运行以下步骤:
* 在调用CreateProcess时, 新进程的优先级别在CreationFlags参数中独立的二进制位中被指定,因此你可以在单次CreateProcess调用中指定多个优先级别.
Windows选择最低优先级别来解决选择这个问题.
* 如果优先级别没有指定,优先级将使用默认值除非进程创建的优先级别是Idel或Below Normal在这种情况下新进程将具有跟创建时一样的优先级别.
* 如果Real-time优先级别被指定,并且父进程没有没有Increase Shceduling Priority权限,将使用High优先级别.换句话说CreateProcess不会因为父进程没有足够的权限创建Real-time优先级别的今晨而失败,只是新进程没有Real-time的优先级别.
* 所有的视窗与桌面这个工作区的图形表示联合在一起,如果调用CreateProcess时桌面没有被指定,新进程将与调用者的当前桌面联合在一起.

第一阶段: 打开要执行的镜象文件
就像图6-6所呈现的那样,CreateProcess的第一步是找到合适的Windows镜象来运行调用者指定的可执行文件并且创建一个段对象稍后映射到新进程的地址空间.
如果没有指定镜象名字命令行的第一个关键字会被用做镜象名.

在Windows XP 和 Windows Server 2003里, CreateProcess检查软件限制政策是否阻止该镜象运行.如果指定的可执行文件是Windows .exe, 它可以直接被使用. 如果不是CreateProcess将会经过一系列步骤来查找Windows支持的镜象去运行.这个过程是必要的, 因为非Windows程序不会直接运行,Windows使用一些特殊的支持镜象中的一个来真正的运行这个非Windows程序.例如:你试着运行一个POSIX程序,CreateProcess识别了并且改变在Posix.exe上来运行该文件.如果你试着运行一个MS-DOS或Win16可执行文件,运行的镜象变成了Ntvdm.exe. 简短地说,你不能创建一个非Windows进程. 假如Windows无法找到激活的镜象做为进程,CreateProcess将会失败.

特别要指出的是, CreateProcess要做的第三个运行镜象的决定如下:
* 如果这个镜象是一个扩展名为.exe, .com或.pif的MS-DOS程序, 一个消息会发送给Windows子系统来检查在当前会话中是否有一个支持MS-DOS的进程被创建了.如果答案是肯定的,这个进程被用来运行这些MS-DOS程序. CreateProcess将会返回.如果支持进程没有被创建,要被运行的镜象变成Ntvdm.exe并且CreateProcess重复第一阶段的工作.

* 假如要运行的文件有.bat或.cmd扩展名, 要运行的镜象变成了Cmd.exe,也就是Windows命令行窗口, CreateProcess重复第一阶段的工作.
* 如果这个镜象是一个Win16可执行文件, CreateProcess必须决定是否一个新的VDM进程要被创建并运行或者是否使用默认的会话间共享VDM进程(这个进程可能还没有被创建).CreateProcess旗标CREATE_SEPARATE_WOW_VDM和CREATE_SHARED_WOW_VDM左右了这个决定. 如果这些旗标没有被指定, 注册表键值HKLM/SYSTEM/CurrentControlSet/Control/WOW/ DefaultSeparateVDM决定了默认的行为. 如果这个程序在一个单独的VDM里运行, 要被运行的镜象变成HKLM/SYSTEM/CurrentControlSet/Control/WOW/wowcmdline的值并且CreateProcess会重复第一阶段的工作.否则Windows子系统发送一个消息来检查共享的VDM进程是否存在并且可以被使用.如果共享程序确实存在并且可用,Windows子系统发送一个消息给它让它运行这个镜象CreateProcess退出.如果这个VDM进程还没有被创建(或存在但不可用)要运行的镜象变成支持VDM的镜象CreateProcess重复第一阶段的工作.

到这个时候,CreateProcess成功地打开了一个有效的Windows可执行文件并且为它创建了一个段对象. 这个对象还没有被映射到内存里,但是它已经在打开状态了.仅仅是因为一个段对象被成功的创建并不意味着那个文件是一个有效的Windows镜象,然而它可能是一个DLL或POSIX可执行文件. 如果它是后者要运行的镜象变成了Posix.exe, CreateProcess从第一阶段开始重复工作.如果这个文件是一个DLL,CreateProcess会以失败告终.

现在,CreateProcess找到了一个有效的Windows可执行镜象,它在注册表HKLM/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Image File Execution Options下查找是否有个一子键包括可执行镜象的文件名和扩展名称存在(但没有目录和路径,例如Image.exe). 如果确实如此,CreateProcess在那个键下寻找一个叫Debugger的值,如果这个值存在要被执行的镜象变成这个值中字符串指定的镜象名,CreateProcess重复第一阶段的工作.

第二阶段: 创建Windows可执行进程对象

到这个时候,CreateProcess打开了一个有效的Windows可执行文件并且为它创建了一个段对象映射到新进程的地址空间.下一步它会创建一个Windows可执行进程对象通过调用内部系统方法NtCreateProcess来运行这个镜象. 创建Windows可执行进程对象(在创建线程的时候会完成这一步)牵涉到以下子阶段:
* 建立EPROCESS块
* 创建初始进程地址空间
* 创建内核进程块(KPROCESS)
* 结束地址空间的创建(包括初始化工作集列表和虚拟地址空间描述子以及映射镜象到地址空间)
* 安装PEB
* 完成Windows可执行进程对象的创建

第二阶段A子阶段: 建立EPROCESS块

这一子阶段涉及以下9个步骤:
1. 分配并初始化EPROCESS块
2. 从父进程那继承进程相关性掩码
3. 进程的最小和最大工作集被分别设置为PsMinimunWorkingSet和PsMaximumWorkingSet的值.
4. 设置新进程的配额块到父进程的配额块地址,并增加父进程配额块的引用记数.
5. 继承Windows的设备命名空间(包括驱动器字母,COM端口,等等).
6. 把父进程的进程ID存到新进程对象的InheritedfromUniqueProcessId字段.
7. 创建进程的主要访问表征(父进程主要访问表征的复用). 新进程继承他们父进程的安全概要. 如果为新进程使用方法CreateProcessAsUser并指定一个不同的访问表征, 访问表征也将被相应地改变.
8. 初始化进程句柄表. 如果父进程的继承句柄旗标被设置了, 任何可继承的句柄从父进程的对象句柄表拷贝新进程.
9. 设置新进程的退出状态为STATUS_PENDING.

第二阶段B子阶段: 创建初始进程地址空间

原始进程地址空间包括以下页:
* 页目录(对于页表大于二的系统很可能有多个页目录, 比如PAE模式下的x86系统或64位系统)
* 超空间页
* 工作集列表
要创建这3个页, 需要执行以下步骤:
1. 在合适的也表中创建页表入口来映射原始页
页的数目从内核变量MmTotalCommittedPages中扣除并被加到MmProcessCommit里.
2. 系统范围内的默认进程最小工作集(PsMinimumWorkingSet)从MmResidentAvailablePages扣除.
3. 系统空间非分页页表部分和系统缓存被映射到进程.

第二阶段C子阶段: 创建内核进程块(KPROCESS)

CreateProcess的下一步就是初始化KPROCESSK块,这个块包含一个指向内核线程的列表(内核对象不知道这些句柄,所以它绕过了对象表). 内核进程块也指向进程页表目录(这被用来追踪进程的虚拟地址空间),进程的线程执行的时间和, 进程的默认基本调度优先级, 进程中线程的默认处理器密切性, 和进程默认的配额初始值,这个值来自PspForegroundQuantum[0], 也是系统范围配额队列的第一个入口.

第二阶段D子阶段: 结束地址空间的创建

安装新进程的地址空间有些复杂, 所以让我们看看在同一时间每步涉及到哪些操作了. 要更多得了解这些, 你最好成绩对Windows内存管理器有一定的了解,这在第七章有描述.
* 虚拟内存管理器把进程最近一次休整时间设为当前时间. 工作集管理器(它在平衡集管理器的系统线程中运行)使用这个值来决定什么时候初始化工作集休整.
* 内存管理器初始化进程的工作集列表-页错误现在可以被除掉.
* 段(当打开镜象文件时被创建)现在被映射到进程的地址空间了, 进程短基址被设置为镜象的基址.
* Ntdll.dll被映射到进程.
* 系统范围的国家语言支持(NLS)表被映射到进程地址空间.

第二阶段E子阶段: 安装PEB

CreateProcess为PEB分配一个页, 并初始化一些字段, 这在表6-6里有描述.
如果镜象文件显示指定了Windows版本号, 这些信息替代表6-6的原始值. 表6-7描述了镜象版本信息到PEB字段的映射过程.

第二阶段F子阶段: 完成Windows可执行进程对象的创建

在返回进程句柄之前, 必须完成最后一些步骤:
1. 如果系统范围内进程审计被起启用了(或因为本地或来自域控制器的组策略的设置),进程的创建会被写到Security日志.
2. 如果父进程包含了一个作业, 新进程被加到这个作业中.
3. 如果设置了镜象头特性的IMAGE_FILE_UP_SYSTEM_ ONLY旗标(这表明该镜象只能在单处理器系统中运行), 一个单独处理器被选中来运行进程中所有的线程. 这个选择的过程是通过在每次运行此类镜象时简单地循环可用的处理器,并使用下一个可用的处理器来完成的. 在这种方式下, 多个处理器拥有同等的机率运行这类镜象?.
4. 如果镜象指定了处理器相关性掩码(例如, 配置头文件中的一个字段), 这个值被拷贝到PEB然后被设置为默认进程相关性掩码.
5. CreateProcess把新进程块插入到Windows活动进程列表的末尾(PaActiveProcessHead).
6. 设定进程的创建时间, 返回进程的句柄给调用函数(Kernel32.dll中的CreateProcess).

第三阶段: 创建初始线程,栈,上下文

到了这个阶段, 已经完成创建可执行进程对象. 但它还没有线程, 所以它还不能做任何事. 在创建线程之前需要创建线程的栈和CPU上下文, 这个阶段就是要作这些事. 栈的初始大小值由镜象, 没有其他方式设置为另一个值.

现在可以通过调用NtCreateThread创建初始线程了. 线程参数(它不能在调用CreateProcess时指定,但可以在调用CreateThread时指定)是PEB的地址. 在初始化线程中要运行的代码时会用到这个参数. 然后这个线程将不会作任何事--它在挂起状态下被创建直到进程完全完成初始化时它才会继续运行. NtCreateThread调用PspCreateThread并执行以下步骤:
1. 增加进程对象的线程计数
2. 创建一个执行线程块(ETHREAD)并初始化.
3. 为该线程生成一个线程ID
4. 在进程用户模式地址空间下下建立TEB.
5. 用户模式线程启始函数保存在ETHREAD中. 对Windows线程来说这是一个系统提供的在Kernel32.dll里的线程启始函数(第一个线程使用BaseProcessStart,额外的线程使用BaseThreadStart). 用户指定的启始函数被保存在ETHREAD块中不同的地方以便系统提供的启始函数可以调用用户指定的启始函数.
6. 调用KeInitThread建立KTHREAD块. 该线程的原始以及当前基本优先级被设定为进程的基本优先级, 相关性和配额也被设置为进程的相关值. 这个方法也设置初始线程空闲处理器. 下一步KeInitThread为该线程分配一个内核栈以及初始化依赖机器的硬件上下文, 包括上下文, 陷阱, 异常帧. 设置线程的上下文以便该线程在KiThreadStartup里在内核模式下开始运行.最后, KeInitThread把线程的状态设置为Initialzied并返回给PspCreateThread.
7. 任何注册过的系统范围的创建线程通知过程(函数)将会被调用.
8. 线程的访问表征被设置指向进程的访问表征, 一访问被检查是否调用者有权限创建该线程. 如果你在本地进程里创建一个线程总会通过以上检查. 但如果你使用CreateRemoteThread在另一个进程中创建线程,并且创建该线程的进程没有调试权限的话你就无法通过该检查.
9. 最终, 这个线程准备好执行了.

第四阶段: 通知Windows子系统关于新进程的信息

如果软件限制政策有限制, 一个受限制的访问表征将会为进程创建. 到这个时候所有的必须的执行进程和线程序对象已经创建成功了. Kernel32.dll下一步会发送一个消息给Windows子系统, 以便它为新进程和线程序做好准备. 这条小心包含以下内容:
* 进程和线程序句柄
* 创建旗标的入口
* 创建该进程的进程的ID
* 用来表明这个进程是否属于一个Windows应用程序的旗标(这样Csrss可以决定是否显示光标)

当Windows子系统收到消息时执行以下动作:
1. CreateProcess复制进程和线程的句柄. 在这里进程和线程的使用计数从1增加到2.
2. 如果进程的优先级别没有被设置, CreateProcess根据这段前面描述的算法来设置优先级.
3. 分配Csrss进程块.
4. 进程的异常端口被设置为Windows子系统的通用功能端口以便当进程中发生异常时Windwos子系统可以收到相关消息.
5. 如果进程正在被调试(也就是说被附加到一个调试进程中), 进程的调试端口被被设置为Windows子系统的通用功能端口. 这样来保证Windows将会把新进程中的调试事件以消息的实习发送给Windows子系统以便它分发事件给调试进程.
6. 分配很初始化Csrss线程块.
7. CreateProcess把这个线程插入到进程的线程列表.
8. 增加当前会话的进程计数.
9. 把进程的关闭等级设置为0x280.
10. 新进程块被插入到Windows子系统范围的进程列表.
11. 分配和初始化Windows子系统内核部分使用的每个进程的数据结构(W32PROCESS).
12. 显示应用程序光标. 这个光标类似沙漏-Windows通过这种方式告诉用户"程序正在做什么事情, 但同时你可以使用光标". 如果进程2秒种后没有作GUI调用, 光标还原为标准光标. 如果程序在规定的时间内确实作了GUI调用, CreateProcess等待5秒以便程序显示一个视窗. 过了那段时间CreateProcess将会再次还原光标.

第五阶段: 开始执行初始线程

到这时, 进程环境已经决定好了. 线程需要的资源也分配好了, 进程拥有一个线程, Windows子系统知道了进程的存在. 初始线程现在被还原来开始执行并完成下一阶段上下文的初始化工作, 它会这样作除非CREATE_SUSPENT被指定.

第六阶段: 在进程的上下文中初始化进程

线程开始生命周期运行内核线程起始函数KiThreadStartup. KiThreadStartup把线程的IRQL从DPC/dispath降低到本世纪末APC并调用系统初始线程函数, PspUserThreadStartup. 用户模式线程起始地址做为参数传递给这个函数.

在Windows 2000里, PspUserThreadStartup开始启用工作集扩展. 如果创建的是被调试进程, 进程中所有的线程被挂起. 一个创建进程的消息被发送给进程的调试端口,以便子系统可以发送进程开始的调试事件(CREATE_PROCESSS_DEBUG_INFO)给适当的调试进程. PspUserThreadStartup然后等待Windows子系统从调试器获得响应. 当Windows子系统回复时所有线程继续执行.

在Windows XP和Windows Server 2003里, PspUserTreadStartup检查系统应用程序预取机制(applciation prefetching)是否启用, 如果被启用了调用逻辑预取器来处理预取指令文件(如果存在)并预取上次进程开始10秒内最先引用的页. 最后, PspUserThreadStartup把用户模式的APC压进队列来运行镜象装载初始化方法(Ntdll.dll中的LdrInitializeThunk).当线程试着返回到用户模式时,这个APC(巳步程序调用)将会被发送.

当PspUserThreadStartup返回到KiThreadStartup, 它从内核模式返回了, APC被发送了, 并且LdrInitializeThunk也被调用了. LdrInitializeThunk方法初始化装载器, 堆管理器,NLS表,线程局部存储队列, 临界段结构. 然后它装载任何需要的DLLL并使用DLL_PROCESS_ATTACH功能代码调用DLL入口点.

最终, 当装载器初始化返回给用户模式APC分发器时镜象通过调用线程起始函数开始在用户模式下执行, 这个函数被做为参数在用户APC被发送时压入用户栈.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: