📄 漫谈兼容内核之十:windows的进程创建和映像装入.txt
字号:
);[/code]
这个函数的作用是锁定某个进程的某些存储页面(不让换出),其输入参数之一就是指向该进程的EPROCESS结构的指针。当然,这个函数也是由内核提供的(属于我们所说的设备驱动界面)。所以指针的提供者和使用者都是内核,只要这二者配套即可,.sys模块在这里只不过是传递了一下,所以也不会有问题。
假定proc是指向进程控制块的指针,并且进程控制块中有个成份X,是个整数,那么在Linux的动态安装模块中可以直接用“proc->X”访问这个成分,但是在Windows的.sys模块中则只能通过类似于get_X()、set_X()一类的支撑函数访问这个成分。将数据结构的内容跟对于这些内容的操作(method)相分离,正是“对象”与“数据结构”的区别所在。而将数据结构的内容“封装”起来,也正是微软所需要的,因为它不愿意公开这些数据结构。
对于兼容内核的开发,这意味着我们不必拘泥于采用与Windows完全一致的EPROCESS数据结构(尽管“Secrets”的附录C已经给出了它的定义),一些内部的操作和处理也不必完全与之相同,而只要与DDK所规定的界面相符就可以了。
了解了有关的进程对象,我们可以言归正传了。
所谓创建内核中的进程对象,实际上就是创建以EPROCESS为核心、为基础的相关数据结构,这就是系统调用NtCreateProcess()要做的事情,主要包括:
● 分配并设置EPROCESS数据结构。
● 其他相关的数据结构的设置,例如“打开对象表”。
● 为目标进程创建初始的地址空间。
● 对目标进程的“内核进程块”KPROCESS进行初始化。
● 将系统DLL的映像映射到目标进程的(用户)地址空间。
● 将目标进程的映像映射到其自身的用户空间。
● 设置好目标进程的“进程环境块”PEB。
● 映射其他需要映射到用户空间的数据结构,例如与“当地语言支持”、即NLS有关的数据结构。
● 完成EPROCESS创建,将其挂入进程队列(注意受调度的是线程队列而不是进程队列)。
这里将系统DLL、实际上是ntdll.dll、映射到目标进程的用户空间是很关键的。这是因为,除别的、主流的功能和作用外,ntdll.dll同时也起着相当于Linux中ELF“解释器”的作用,也担负着为目标映像建立动态连接的任务。
值得注意的是,NtCreateProcess()与CreateProcess()不同。CreateProcess()创建一个进程并使其(初始线程)运行,除非在创建时就指定要将其挂起。而NtCreateProcess(),则只是在内核中创建该进程的EPROCESS数据结构并为其建立起地址空间。这只是个空壳架子,因为没有线程就谈不上运行,调度的目标是线程而不是线程。而且,对NtCreateProcess()的调用还有个条件,那就是目标映像已经被映射到一个存储区间(Section)中。
第三阶段:创建初始线程
如上所述,进程只是个空架子,实际的运行实体是里面的线程。所以下一步就是创建目标进程的初始线程,即其第一个线程。
与EPROCESS相对应,线程的数据结构是ETHREAD,并且其第一个成分是数据结构KTHREAD,称为TCB。同样,“Internals”和“Secrets”两本书中所列的ETHREAD内部结构有所不同,后者的附录C中给出了通过逆向工程得到的ETHREAD数据结构定义。
同样,从Windows DDK中申明的一些函数也可以看出,.sys模块只是传递ETHREAD指针或KTHREAD指针(由于KTHREAD是ETHREAD中的第一个成分,二者实际上是一回事),而不会直接访问它的具体成分。
[code]PKTHREAD NTAPI KeGetCurrentThread();
NTKERNELAPI KPRIORITY
KeQueryPriorityThread (IN PKTHREAD Thread);
NTKERNELAPI LONG
KeSetBasePriorityThread (IN PKTHREAD Thread, IN LONG Increment);
NTKERNELAPI PDEVICE_OBJECT
IoGetDeviceToVerify(IN PETHREAD Thread);[/code]
此外,就像进程有“进程环境块”PEB一样,线程也有“线程环境块”TEB,KTHREAD结构中有个指针指向其存在于用户空间的TEB。前面讲过,PEB在用户空间的位置是固定的,PEB下方就是TEB,进程中有几个线程就有几个TEB,每个TEB占一个4KB的页面。
这个阶段的操作是通过系统调用NtCreateThread()完成的,主要包括:
● 创建和设置目标线程的ETHREAD数据结构,并处理好与EPROCESS的关系(例如进程块中的线程计数等等)。
● 在目标进程的用户空间创建并设置目标线程的TEB。
● 将目标线程在用户空间的起始地址设置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用于进程中的第一个线程,后者用于随后的线程。用户程序在调用NtCreateThread()时也要提供一个用户级的起始函数(地址),BaseProcessStart()和BaseThreadStart()在完成初始化时会调用这个起始函数。ETHREAD数据结构中有两个成份,分别用来存放这两个地址。
● 设置目标线程的KTHREAD数据结构并为其分配堆栈。特别地,将其上下文中的断点(返回点)设置成指向内核中的一段程序KiThreadStartup,使得该线程一旦被调度运行时就从这里开始执行。
● 系统中可能登记了一些每当创建线程时就应加以调用的“通知”函数,调用这些函数。
第四阶段:通知Windows子系统
Windows、确切地说是Windows NT、当初的设计目标是支持三种不同系统的应用软件。第一种是Windows本身的应用软件,即所谓“Native”Windows软件,这是微软开发Windows NT的真正目的。第二种是OS/2的应用软件,这是因为当时微软与IBM还有合作关系。第三种是与Unix应用软件相似、符合POSIX标准的软件,那是因为当时美国的军方采购有这样的要求。不过实际上微软对后两种应用的支持从一开始就是半心半意的,后来翅膀长硬了,就更不必勉为其难了。但是,尽管如此,当初在设计的时候还是考虑了对不同“平台”的支持,即在同一个内核的基础上配以不同的外围软件,形成不同的应用软件运行环境,微软称之为“子系统(Subsystem)”。于是,在Windows内核上就有了所谓“Windows子系统”、“OS/2子系统”、和“POSIX子系统”。当然,时至今日,实际上只剩下Windows子系统了。
那么,所谓子系统是怎样构成的呢?“Internals”书中阐明了Windows子系统的构成,说这是由下列几个要素构成的。
一、 子系统进程csrss.exe。包括了对下列成分和功能的支持:
● 控制台(字符型)窗口的操作。面向控制台/终端的应用本身不支持窗口操作(例如窗口的移动、大化/小化、遮盖等等),但是又需要在窗口中运行,所以需要有额外的支持。
● 进程和线程的管理。例如弹出一个对话窗,说某个进程没有响应,让使用者选择是否结束该进程的运行,等等。每个Windows进程/线程再创建/退出时都要向csrss.exe进程发出通知。
● DOS软件和16位Windows软件在(32位)Windows上的运行。
● 其它。包括对当地语言(输入法)的支持。
这个进程之所以叫csrss,是“C/S Run-time SubSystem”的意思,csrss是Windows子系统的服务进程。其实三个子系统都是C/S结构,但是OS/2子系统的服务进程称为os2ss,POSIX子系统的服务进程称为Psxss。之所以如此,据“Internals”说,是因为最初时三个子系统的服务进程是合在一起的,就叫csrss,后来才把那两个子系统移了出来另立门户,但剩下的还继续叫csrss。
二、 内核中的图形设备驱动、即Win32k.sys模块。其功能包括:
● 视窗管理,控制着窗口的显示和各种屏幕输出(例如光标),还担负着从键盘、鼠标等设备接收输入并将它们分发给各个具体应用的任务。
● 为应用软件提供一个图形函数库。
三、 若干“系统DLL”,如Kernel32.dll、Advapi32.dll、User32.dll、以及Gdi32.dll。
上述的第二个要素Win32k.sys原先也是和csrss.exe合在一起的,这部分功能也由服务进程在用户空间提供。应用进程通过进程间通信向csrss发出图形操作请求,由csrss完成有关的图形操作。但是后来发现频繁的进程间通信和调度成了瓶颈,所以就把这一部分功能剥离出来,移进了内核,这就是Win32k.sys。这一来,对于一般的32位Windows应用而言,留给csrss、或者说必须要通过csrss办的事就很少了。但是,尽管如此,在创建WIndows进程时还是要通知csrss,因为它担负着对所有WIndows进程的管理。另一方面,csrss在接到通知以后就会在屏幕上显示那个沙漏状的光标,如果这是个有窗口的进程的话。
注意这里向csrss发出通知的是父进程、即调用CreateProcess()的进程,而不是新创建出来的进程,它还没有开始运行。
至此CreateProcess()的操作已经完成,从CreateProcess()返回就退出了kernel32.dll,回到了应用程序或更高层的DLL中。这四个阶段都是立足于父进程的用户空间,在整个过程中进行了多次系统调用,每次系统调用完成后都回到用户空间中。例如,在第二阶段中就调用了NtCreateProcess(),第三阶段中就调用了NtCreateThread(),而整个创建进程的过程包括了许多次系统调用(有些系统调用属于细节,所以上面并未提及)。
其实Linux的进程创建也不是一次系统调用就可完成的,典型的过程就包括fock()、execve()等系统调用,但是在Windows上就更多了。这跟Windows的整个系统调用界面的设计有关。以用户空间的内存分配为例,Linux的系统调用brk()只有一个参数,那就是区间的长度,但是Windows的系统调用NtAllocateVirtualMemory()却有6个参数,其第一个参数是ProcessHandle,这是标志着一个已打开进程对象的Handle。这说明什么呢?这说明Linux进程只能为自己分配空间,而Windows进程却可以为别的进程分配空间。或者说,在存储空间的分配上Linux进程是“自力更生”的,而Windows进程却可以“包办代替”。
这对于系统设计的影响可能远超读者的想像。就拿为子进程的第一个线程分配用户空间堆栈而言,既然Linux进程(线程)只能为自己分配空间,而用户空间堆栈又必须在进入用户空间运行之前就已存在,那就只好在内核中完成用户空间堆栈的分配。相比之下,Windows进程可以为别的进程分配空间,于是就可以由父进程在用户空间中为子进程完成这些操作。这样,有些事情Linux只能在内核中做,而Windows可以在用户空间做。有些人称Windows为“微内核”,这或许也是个原因。而Windows的CreateProcess()中包含着更多的系统调用,也就很好理解了。
现在,虽然父进程已经从库函数CreateProcess()中返回了,子进程的运行却还未开始,它的运行还要经历下面的第五和第六两个阶段。
第五阶段:启动初始线程
新创建的线程未必是可以被立即调度运行的,因为用户可能在创建时把标志位CREATE_ SUSPENDED设成了1。如果那样的话,就需要等待别的进程通过系统调用恢复其运行资格以后才可以被调度运行。否则现在已经可以被调度运行了。至于什么时候才会被调度运行,则就要看优先级等等条件了。而一旦受调度运行,那就是以新建进程的身份在运行、与CreateProcess()的调用者无关了。
如前所述,当进程的第一个线程首次受调度运行时,由于线程(系统空间)堆栈的设置,首先执行的是KiThreadStartup。这段程序把目标线程的IRQL从DPC级降低到APC级,然后调用内核函数PspUserThreadStartup()。
最后,PspUserThreadStartup()将用户空间ntdll.dll中的函数LdrInitializeThunk()作为APC函数挂入APC队列,再企图“返回到”用户空间。Windows的APC跟Linux的signal机制颇为相似,相当于用户空间的“中断服务”。所以,在返回用户空间的前夕,就会检查APC函数的存在并加以执行(如果存在的话)。
于是,此时的CPU将两次进入用户空间。第一次是因为APC请求的存在而进入用户空间,执行APC函数LdrInitializeThunk(),执行完毕以后仍回到系统空间。然后,第二次进入用户空间才是“返回”用户空间。返回到用户空间的什么地方呢?前面已经讲到,返回到Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),对于进程中的第一个线程是BaseProcessStart()。至于用户程序所提供的(线程)入口,则是作为参数(函数指针)提供给BaseProcessStart()或BaseThreadStart()的,这两个函数都会使用这指针调用由用户提供的入口函数。
第六阶段:用户空间的初始化和DLL的连接
用户空间的初始化和DLL的连接是由LdrInitializeThunk()作为APC函数的执行来完成的。
在应用软件与动态连接库的连接这一点上,我们已经看到,不管是Linux、Windows、还是Wine,都是一致的,那就是在用户空间完成:
● Linux的.so模块连接由“解释器”在用户空间完成。“解释器”相当于一个不需要事先连接的动态库,因为它的入口是固定的。“解释器”的映像是由内核装入用户空间的。
● Windows的DLL连接由ntdll.dll中的LdrInitializeThunk()在用户空间完成。在此之前ntdll.dll与应用软件尚未连接,但是已经被映射到了用户空间。函数LdrInitializeThunk()在映像中的位置是系统初始化时就预先确定并记录在案的,所以在进入这个函数之前也不需要连接。
● Wine的动态库连接分两种情况。一种是ELF格式的.so模块,另一种是PE格式的DLL。二者的连接都是在用户空间完成的,前者仍由ELF解释器ld-linux.so.2完成,后者则由工具软件wine-kthread完成。后者的具体调用路径是:
main() > wine_init() > __wine_process_init() > __wine_kernel_init() >
wine_switch_to_stack() > start_process() > LdrInitializeThunk()
这在“Wine的二进制映像装入和启动”那篇漫谈中已经讲到过了。注意这里最终完成DLL连接的函数也叫LdrInitializeThunk(),显然Wine的作者对于Windows的这一套是清楚的。
通过以上的叙述,我们可以看到Windows的进程创建过程与Linux有较大的不同,但是装入PE映像和实现DLL连接的过程却与Linux的对应过程相似,只是把“解释器”集成到了“系统DLL”里面,并且是作为APC函数执行的,其他就没有太大的区别了。但是,如果跟Wine的PE映像装入过程相比,则显然Wine的过程(见“Wine的二进制映像装入和启动”)是比较复杂、效率也比较低的。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -