第三章 模块、进程、线程(Modules、Processes、Threads)
摘要:
Modules(模块)、Process(进程)和Thread(线程)构成ring3 Windows 95的核心。几乎所有的API都与其有关。
这一章,我们将察看模块、进程、线程的核心数据结构。当我们观察这些数据结构时,常会遇到另一些数据结构,这迫使我们继续细究下去。例如,每一个进程内含一个指针指向一个handle table。而一进入handle table,我们将会发现很多内核对象(Kernel32对象)。同样的,观察线程时,我们很难忽略Thread Information Block(TIB)的存在。TIB在结构化异常中扮演非常重要的角色。
在本章中,除了三个关键的数据结构,我还将给出与它们直接发生关系的API的伪代码。这使你有机会看到这些资料结构的运行情况,以及看到内核(Kernel32)如何处理像线程同步控制之类的题目。
在深入挖掘模块、进程和线程的细节之前,我必须先声明,这些资料的透露并未经过微软的核准。微软希望你不要在自己的代码中放进与这些数据结构有关的资料。对于那些需要处理模块、进程和线程的应用程序,微软提供的解决方案是定义在TLHELP32.H中的TOOLHELP32 API。
TOOLHELP32函数提供了对于模块、进程和线程数据结构的有限处理能力,局限在微软认为安全的范围之内。我必须强调,这样的处理只是一种只读处理。但是,微软经常认为足够的资料,对于系统程序员来说如我者是不够的。例如,ToolHelp32没有提供“枚举一个进程的handle table的能力”。如果你需要这样的动作,你就必须直接读取这些资料。
Win32模块(Modules)
一个Win32模块代表的是一个被Win32 Loader加载的EXE或DLL的程序代码、数据和资源。因此,内存中的一个模块都对应到磁盘中的一个程序。EXE和DLL本身并不是模块。是由Win32 Loader将其加载的内存并产生对应的模块。Win32 PE格式的一个优势是:将它们加载到内存是十分简单的。操作系统将一个载入模块的所有高级信息保存在一个结构体中,此结构被我称之为:module database。
应用程序使用HMODULEs来代表被载入的模块。在Win32中,一个HMODULE实际就是程序被加载时在内存的起始地址。例如,大部分EXE程序被加载到0x400000(4MB)处,所以它们的HMODULE就是0x400000。这意味着多个EXE同时执行时,拥有相同的HMODULE。这不是问题,因为Windows 95/NT为每一个进程维护一个独立的地址空间。
补充:
HMODULE和HINSTANCE的关系
在Windows 95/98/Me/NT/2000中,HINSTANCE和HMODULE实际上是相同的东西,如果一个函数需要一个HMODULE作为参数,那么可以传递一个HINSTANCE,反之也成立。之所以存在着两种结构,是因为在Windows 3.x中,HMODULE和HINSTANCE用于标识不同的东西。
Module database非常靠近EXE或DLL被载入后的内存地址的起始处,并且内含一些像是程序中的code/data sections被装入到内存中何处等等的信息。模块中的代码和资料并不仅只是编译器为你的程序生成的二进制代码,还包括import table、export table、resource directory….等等。import table(位于.idata section)告诉加载器这个模块需要动态链接哪一个DLL中拿一个函数;export table则与之相反,告诉操作系统本模块有哪一个函数要开放给别的模块调用。Resource section包含一个类似磁盘目录结构的树形结构,使系统能够快速找到特定的资源。Module database内含如何寻找这些sections的信息,以及需要的操作系统的版本,以及该程序是否为console模式….等等。
Module database的格式是公开的。Win32中的一个module database其实就是EXE或者DLL的PE表头。看看WINNT.H,你会发现IMAGE_NT_HEADERS结构,它由一个DWORD和两个子结构组成。IMAGE_NT_HEADERS结构中的信息就是Windows 95内部用来寻找被载入的EXE或DLL中的代码、数据和资源用的。
Win32要求每个进程有自己的模块数组。如果模块没有隐式链接(implicity link)DLLs,或者说它是通过LoadLibrary载入DLLs,那么该进程就没有办法在内存中看到这些DLL模块(即使其他进程加载了这些DLL)。
补充:
隐式链接(implicitly link):是指程序在链接期间就与DLLs对应的import libraries(.LIBs)作静态链接,于是最后的可执行程序中就会包含所有DLL函数的一份重定位表(relocation table)和相应的修正记录(fixup record)。当可执行程序被Windows加载器载入内存时,加载器会修正所有的fixup records,使其记录DLLs中导出函数在内存中的实际地址,于是动态链接才可以顺利进行。
在此种情况下,系统内核(Kernel32)必须面对一个困难的选择。从应用程序的角度来看,每一个进程有自己的模块数组是不错,但从内核的角度来看,单一模块数组比较容易达到代码和资源的共享。只要有一个新的进程开始执行,或一个新的DLL被加载。内核就可以快速的检查唯一的全局性模块数组,看看那个EXE或DLL是否已经加载,如果是,内核就简单的增加其引用计数。如果不是,内核才需将其加载到内存中以生成一个新的模块。
内核(Kernel32)利用两个结构体来维护一个全局性模块,并且使它看起来好像每个进程都有自己的一个模块链表。第一个结构体是IMTE(Interneal Module Table Entry),第二个结构体是MODREF。
补充:在Windows 2000下的一些不同之处
在Windows NT/2000下,Module(模块)这个概念实际上已经不存在了。HMODULE和HINSTANCE表示的是相同的东西,我们通常所说的Process Handle实际上就是我们上面讨论的HMODULE,而Module Database我认为是指进程/DLL内核对象,例如 PCB(Process Control Block),在上面提到的内核维护的一个全局模块链表,实际上就是进程/DLL内核对象的链表。
在Windows 2000下,HINSTANCE实际上如下:
typedef void* HINSTANCE
而HMODULE实际定义如下:
typedef HINSTANCE HMODULE
对于进程来说,HINSTANCE实际上是一个指向HINSTANCE__结构体的指针。
IMTEs(Internal Module Table Entries)
如上图所示,IMTEs构成了全局模块数组,该数组所使用的内存是从Kernel32 Heap中分配而来的。系统使用HeapAlloc来分配一块内存。当新的模块加入时,Kernel32使用HeapReAlloc动态扩展全局数组。当内核(Kernel32)产生一个新的IMTE,它会搜寻pModuleTableArray中的空白元素,找到一个,就把IMTE指针放进去。这个元素的索引值稍后在我们探索MODREFs时将扮演重要角色。pModuleTableArray的第一个元素(索引为0)用来表示KERNEL32.DLL模块。
pModuleTableArray中的每一个非零元素都代表系统中一个被加载到内存的EXE或者DLL。每一个这样的元素都是一个IMTE指针(在伪代码中我以PIMTE表示)。虽然,module database的格式是公开的(实际上就是IMAGE_NT_HEADERS结构),但IMTE的格式并没有公开。
笔记:
本书中关于IMTE结构的讨论是针对Windows 95的,对于现在的Windows 2000/XP/2003,有什么变化,我还没有进一步研究,所以就不在此详细叙述IMTE结构了。以后再来补充这一节。(2004-11-26)
MODREF结构
一个进程拥有自己的模块链表,但它对其他进程加载的模组却一无所知。把每个进程都有的模块链表和全局模块数组关联起来的就是MODREF结构。每个进程(除了奇怪的Kernel32.dll)都有的模块链表实际上一个MODREF链表,其中一个MODREF时针对进程自身的,其他MODREFs时针对进程使用的每一个Win32 DLLs。MODREFs所需的内存来自Kernel32(内核)的Heap中,这就意味着该内存位于2GB之上。
笔记:
根据对原文的理解,每个进程的MODREF链表是位于系统共享全局堆中的,所以一个进程可以读取其他进程的MODREF链表。但这一点在Windows 2000及其后续版本中是否仍然有效,目前还未知。(2004-11-26)
MODREFs链表的表头位于Process Database中,每个MODREF链表结构都内含一个索引,指向pModuleTableArray数组。图3-2显示了MODREFs和IMTEs之间的关系。
本文地址:http://com.8s8s.com/it/it24069.htm