进程(Process)
首先让我们看看,Windows中关于进程的定义,以下内容引自《Windows核心编程》:
进程通常被定义为一个正在运行的程序的实例,它由两部分组成:
l 一个是操作系统用来管理进程的内核对象(K32对象)。内核对象也是系统用来存放关于进程统计信息的地方。
l 另一个是地址空间,它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如,线程堆栈和堆分配空间。
进程是不活波的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程地址空间中的代码。实际上,单个进程可能包含多个线程,所有这些线程都“同时”执行进程地址空间中的代码。此外,每个线程都有它自己的一组CPU寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
需要提及的一点时,一个进程中的线程肯定比进程要先结束,不会存在一个不包含任何线程的进程。
进程其实就是一大堆对象的拥有权的集合。也就是说,进程拥有对象。进程可以拥有内存(更确切地说是拥有memory context),可以拥有file handle,可以拥有线程,可以拥有一串的DLL模块(被加载至该进程的地址空间中)
需要注意一点,进程并不代表执行(线程才是)。进程也不是EXE文件。在被加载器载入之前,一个EXE文件只不过是一个程序。只有被载入之后,Windows才为其产生一个进程和一个主线程。
一旦Windows产生一个进程,它也会产生一个memory context,以容纳进程的线程在其中执行。此外,Windows也产生第一个线程,即主线程,用来执行进程本身。如有必要,进程可以再产生线程。系统还会产生一个file handle table,进程可以在其中持有一些打开的文件。最后,也是最重要的。Windows产生一个process database(即进程内核对象,也叫做K32对象),用以表现该进程。
Process database是一种K32对象,内含与进程有关的大量信息。稍后我们将详细讨论Process Database(PDB)的详细结构。Process database所使用的内存来自KERNEL32 Heap,因此所有的Process Database都可以被其他进程看到。
Process database含有一系列的线程、一系列的被载入模块,预设于Process Heap中的handle、指向process handle table的指针、以及指向memory context的指针。此外还有更多的东西。
什么是Process Handle?什么是Process ID?
在继续前进之前,我需要先澄清常感困惑的process handle和process Ids。两个看起来类似的Win32函数:GetCurrentProcess和GetCurrentProcessId迷惑了不少程序设计人员。事实上其差异很容易分辨。
Process handle基本上和file handle一样。它是一个不被明了的数值,你不能够说它是任何东西的指针。系统内部事实上是把K32对象的handle当作process handle table的索引。利用该索引从process handle table中获得的,才是一个K32对象指针。然后,由于应用程序不需要直接处理handle table,所以,process handle是没有用的。
记住,因为每一个应用程序有它自己的handle table,所以不同的进程在各自的地址空间中拥有相同的process handle是绝对可能的。例如,正常而言每个进程都有一个process handle代表自己,其值是1。也就是说,process handle并不是可以赖以判别进程的数据。另一个例子是:如果程序为自己这个进程打开另一个process handle,那么就有两个handle,对应同一个进程。
在GetCurrentProcess函数的伪代码中你可以确定,process handle并不是可以用来判别进程的数据。
GetCurrentProcess函数的伪代码:
// Normally this function does nothing. It appers to be there
// for the benefit of the KERNEL32 developers
x_LogSomeKernelFunction( function number fro GetCurrentProcess );
return 0x7FFFFFFF;
是的,忽略掉“它调用其他函数”的事实,GetCurrentProcess只不过是固定返回0x7FFFFFFF罢了。不管谁调用该函数,他都获得0x7FFFFFFF。0x7FFFFFFF是一个魔术数字,KERNEL32把它解释为“是用当前的进程”。面对0x7FFFFFFF,那些需要process handle的KERNEL32函数会将其替换为当前的process handle。这需要更多证据以证明process handle只在自己的context中才有作用吗?我想不需要了!
现在让我们看看process IDs,早期的Windows 95把process database的基地址当作一个process ID。由于process database位于共享的内存中,所以process database的基地址保证决不会相同。
补充:
在Windows 98/NT/2000/XP 等后续版本中,Process IDs的具体实现是否还如此,不得而知。
在Windows 95的新版中,KERNEL32小组改变了GetCurrentProcessId的实现方式:
GetCurrentProcessId函数的虚拟代码:
x_LogSomeKernelFunction( function number for GetCurrentProcessId );
return PDBToPid( ppCurrentProcess );
再一次,如果我们忽略掉它调用其他函数的话,GetCurrentProcessId只不过是把一个全局变量ppCurrentProcess传给PDBToPid函数。让我们停下来对这里多了解一些,因为它对于后续章节十分重要。ppCurrentProcess是一个指针的指针,指向当前的process database。以C语言来说,就是**ppCurrentProcess指向当前的process database。
之所以需要再次间接指向,基于一个让人困惑的原因,在第六章我会提到。现在,你只要记住ppCurrentProcess是KERNEL32.DLL中的全局变量,允许KERNEL32搜寻当前进程的process database即可。为了尽量简化事情,我将在虚拟代码中使用ppCurrentProcess,我假设它是一个指向process database的指针,而不是一个指针的指针。
好,如果KERNEL32由一个指针,指向当前的process database,为什么GetCurrentProcessId不直接把它传回来呢?我们看看PDBToPid做了些什么:
PDBToPid函数的虚拟代码:
// Parameters:
// PROCESS_DATABASE* ppDB
if ( ObsfucatorDWORD == FALSE )
{
_DebugOut( “PDBToPid() Called too early! Obsfucator not yet initialized!” );
return 0;
}
if ( ppDB & 1 )
{
_DebugOut( “PDBToPid: This PDB looks like a PID (0%1xh) Do a stack trace BEFORE reporting as bug.” );
}
// Here’s the key! XOR the obsfucator DWORD with the process database
// pointer to make the PID value
return ppDB^ObsfucatorDWORD;
哦,这是真的吗?是的!“Obsfucator”一词竟然赤裸裸的出现在微软的二进制代码中。除了检查传进来的是一个合法的process database指针之外,PDBToPid的基本行为就是将当前的process database指针和ObsfucatorDWORD两者做XOR。这个企图非常明显,微软希望阻绝刺探系统内部数据结构的行为。
如果你惊讶ObsfucatorDWORD来自何处,你会狼狈的发现它在每次系统启动时动态产生出来。这又为系统作了一层更好的保护。事实上不只是process database被保护,thread database也被保护了。稍后我会在GetCurrentThreadId函数中展示给你看,GetCurrentThreadId和GetCurrentProcessId有近乎神秘的相似程度。
让我做个总结,一个process handle就像一个file handle。在其所属进程之外别无意义。至于一个process ID,则是在各进程之间独一无二不会冲突的数值。它是一个指针,指向process database结构。
如果你看过TOOLHELP32的Process32First和Process32Next两个函数,可能你会注意PROCESSENTRY32结构中的th32ProcessID成员。这和GetCurrentProcessID传回的东西有关系吗?幸运的是,答案是Yes。
Windows 95 Process Database(PDB)
Windows 95的每一个process dabase 都是一块从KERNEL32 Heap中分配而来的内存。KERNEL32通常以PDB缩写代表Process Database。每一个PDB就是一个第一个字节为5(K32OBJ_PROCESS)的K32对象。让我们仔细来查看一下各个成员:
00h DWORD Type
此值必须为5(K32OBJ_PROCESS)
04h DWODD cReference
引用计数(reference count),也就是此PDB被使用的次数
补充:
使用CloseHandle可以将一个K32对象的引用计数减一
08h DWORD un1
此成员真实意义未知。似乎总是0
0ch DWORD someEvent
这是一个指向K32OBJ_EVENT对象的指针。Event对象用于WaitForSingleObject这样的函数。
补充:
当用WaitForSingleObject等待一个进程结束时,就用到了此Event。
10h DWORD TerminationStatus
当你是用GetExitCodeProcess,返回的就是这个值。所谓退出代码(exit code)就是main或WinMain的返回值。它也可以被ExitProcess或TerminateProcess指定。当一个进程还在执行时,此成员值为0x103(STILL_ACTIVE)。
14h DWORD un2
此成员真实意义未知,似乎总是0。
18h DWORD DefaultHeap
默认进程私有堆的地址。GetProcessHeap返回的就是这个值。
1Ch DWORD MemoryContext
一个指针,指向进程的memory context。所谓memory context,内含page directory mapping,用以提供进程在4GB地址空间中的私人区域。在第5章对memory context有更多地描述。
20h DWORD flags
24h DWORD pPSP
这个值是此进程之DOS PSP的线性地址。Win16和Win32程序都会设定此成员。此线性地址总是在1MB(实模式DOS代码所能访问的最高地址)之下。请参考28h结构成员。
28h WORD PSPSelector
这是一个selector,指向此进程的DOS PSP。Win16和Win32程序都有DOS PSP。请参考24h结构成员。
2Ah WORD MTEIndex
这里内含一个全局模块表(pModuleTableArray)的索引值。可通过该索引值取出此进程对应的IMTE。
2Ch WORD cThreads
此位置记录此进程拥有的线程个数
2Eh WORD cNotTermThreads
此位置记录属于进程所拥有而尚未结束的线程个数。
30h WORD un3
此位置真实意义未知,似乎总是0。
32h WORD cRing0Threads
此位置记录由VMM32.VXD管理的ring0线程个数。对于一般程序而言,其值应该与cThreads相同。然而在KERNEL32.DLL之中,此值比cThreads多1。
34h HANDLE HeapHandle
此位置是一个Heap Handle,此Heap内含属于该进程的表格(或其他什么东西)。这里记录的总是KERNEL32的shared heap handle。
38h HTASK W16TDB
这是一个selector,指向进程相关的Win16 Task Database(TDB)。Win16和Win32程序都有TDB selector,并且维护一个合法的TDB。
3Ch DWORD MemMapFiles
一个指针,指向“该进程所使用的内存映射文件所组成的链表”中的第一个节点。每个内存映射文件都是该链表中的一个节点。该节点的格式是:
DWORD 内存映射文件的基地址
DWORD 指向下一个节点,或者为0
40h PENVIRONMENT_DATABASE pEDB
一个指针,指向environment database。Environment database内含当前的子目录、环境变量、进程命令行参数、标准Handle(例如,stdin)、以及其他项目。我将在[Environment Database]一节中详细描述其格式。
44h PHANDLE_TABLE pHandleTable
一个指针,指向process handle table。所有的handles都在这里面,包括file handles、eventhandles、process handles等等。在DOS/Win16环境中的对等物是DOS的System File Table(SFT)。
48h struct _PROCESS_DATABASE* ParentPDB
一个指针,指向父进程的PROCESS_DATABASE。对一般程序而言父进程是Windows Explorer(资源管理器)。MSGSRV32则又是Explorer和initial service processes的父进程。
4Ch PMODREF MODREFList
此位置指向进程的模块链表的表头。这也就是前面提到的MODREFs链表。
50h DWORD ThreadList
一个指针,指向该进程所拥有的线程的链表。目前,我还不知道该链表的真正格式。
54h DWORD DebuggeeCB
这是一个被调试程序的context。当一个进程处于调试状态时,此位置则指向2GB以上的一个区域,该区域包含一个指针,指向被调式者的process database。
58h DWORD LocalHeapFreeHead
这个指针指向该进程默认Heap的自由区块链表的表头。第5章将详细描述其格式。
5Ch DWORD InitialRing0ID
此位置意义未知,似乎总是0。
60h CRITICAL_SECTION crst
这个位置是一个CRICICAL_SECTION,被各种API用来同步控制同一进程中的各个线程。稍后会在许多虚拟代码中你会看到这个cricital section的作用。
78h DWORD un4[3]
这三个DWORD真实意义未知,似乎总是0。
84h DWORD pConsole
如果这个进程是用console(也就是这是个控制台模式的程序),此位置即指向一个用于输出的console对象(K32OBJ_CONSOLE)。
88h DWORD tIsInUseBits1
这个32位单元表示低位的32个TLS(Thread Local Storage)的索引。如果某位被设立,表示对应的TLS索引被用掉了。每个TLS索引都不断累加其值,例如:
TLS index: 0 = 0x00000001
TLS index: 1 = 0x00000002
TLS index: 2 = 0x00000004
稍后将专有一节讨论TLS
8Ch DWORD tIsInUseBits2
这个DWORD表示TLS之中第32-63个索引的状态。
90h DWORD ProcessDWORD
此位置意义未知。有一个未公开函数(GetProcessDword)可以获取该位的值。
94h struct _PROCESS_DATABASE* ProcessGroup
此位置要么为0,要么就指向一个“进程群”中的首要进程。所谓“进程群”是一群进程,彼此呼应。当一个群组被销毁时,其中的所有进程也一并被销毁。注意,每一个进程都认为自己在自己的“进程群”之中,所以实际上此位置指向进程自己的PDB。如果进程处于调试状态,它就属于调试器“进程群”。
98h DWORD pExeMODREF
此位置指向EXE的MODREF。一般而言,EXE的MODREF是模块链表中的表头,所以这个位置通常和位置4Ch吻合,除非进程又通过LoadLibrary或LoadModule载入了其他DLL。
补充:
LoadModule函数时为了兼容Win16下的代码而保留下来的,在Windows NT/2000/XP及其后续版本中,不应再使用,建议采用CreateProcess替换之。
9Ch DWORD TopExcFilter
这个DWORD内存放进程的“Top Exeception Filter”。如果进程没有安装任何异常处理例程,那么就使用此位所指向的那个。这个例程可以通过SetUnhandledExceptionFilter函数来指定。结构化异常将在稍后讨论。
A0h DWORD BasePriority
这个DWORD存放的是进程的基本优先级。Windows 95支持32个优先级,分为四个等级:
Idle 4
Normal 8
High 13
Realtime 18
A4h DWORD HeapOwnList
这个位置指向“进程所用的heaps形成的链表”。默认情况下,每个进程只有一个Heap,可通过GetProcessHeap获取。然而进程也可以调用HeapCreate产生另一个Heap。这些Heaps都放在该链表中。第5章将对此主题有较多的叙述。
A8h DWORD HeapHandleBlockList
Heap中的可移动块是通过moveable handle table来管理。每个heap对应一个table。许多个这样的table则形成了一个链表。本位置指向该链表的表头。第5章对于moveable handle table有较多的叙述。
ACh DWORD pSomeHeapPtr
本位置的真正意义不十分明确。通常它是0,如果不是0那么就是一个指针,指向该进程的默认堆的moveable handle table。前参看A8h位。
B0h DWORD pConsoleProvider
此位置要么为0,要么就是一个指针,指向KERNEL32的控制台对象(K32OBJ_CONSOLE)。对于Win32 Console程序而言该位似乎总是为0。
B4h WORD EnvironSelector
这是一个selector,指向进程的环境块。这个selector的基地址和Environment Database的pszEnvironment相同。
B6h WORD ErrorMode
这个位置内含由SetErrorMode设定的数值。KERNEL32的SetErrorMode会下移(thunk down)至KRNL386的同名函数,所以这个位置反映了Win16错误模式代码。它们可能是:
0
SEM_FAILCRITICALERRORS
SEM_NOALIGNMENTFAULTEXCEPT
SEM_NOGPFAULTERRORBOX
SEM_NOOPENFILEERRORBOX
B8h DWORD pevtLoadFinished
这个位置指向KERNEL32的Event Object(K32OBJ_EVENT)。当进程创建完成后,此Event即被激活。
BCh WORD UTState
此位置意义不明。通常是0。
GetExitCodeProcess和IgetExitCodeProcess
GetExitCodeProcess取得进程的结束状态。它需要一个hProcess参数。函数的主要功能其实只是确认第二个参数是否为合法的指针。真正的动作则交给IgetExitCodeProcess去做。后者利用hProcess寻找对应的指针(指向PROCESS_DATABASE)。由于hProcess是一个handle,所以上述动作意味着先利用索引进入进程的handle table,再取出进程指针。x_CounvertHandleToK32Object负责“增加process database的引用计数”等。
有了PROCESS_DATABASE指针,函数就可以取出TerminationStatus成员的值,并将其存放在调用者指定的一个缓冲区中。IGetExitCodeProcess会减少process database的引用计数,并留下“must complete”状态。
GetExitCodeProcess虚拟代码:
// Parameters
// HANDLE hProcess;
// LPDWORD lpdwExitCode;
Set up structured exception handling frame
if ( lpdwExitCode ) // If a non-null pointer was passed, verify
EAX = *lpdwExitCode; // that the DWORD it points to can be written.
Remove structured exception handling frame
Goto IgetExitCodeProcess;
IGetExitCodeProcess虚拟代码:
// Parameters
// HANDLE hProcess;
// LPDWORD lpdwExitCode;
// Local:
// PROCESS_DATABASE ppdb;
// BOOL retValue;
retValue = TRUE; // Assume successful return
x_EnterMustComplete(); // Prevent us from being interrupted.
// Increments ptdbx->MustCompleteCount.
x_LogSomeKernelFunction( function number for GetExitCodeProcess );
// Get a pointer to the PROCESS_DATABASE struct
ppdb = x_ConvertHandleToK32Object( hProcess, 0x80000010, 0 );
if ( ppdb )
{
// Save away exit status
*lpdwExitCode = ppdb->TerminationStatus;
x_UnuseObjectWrapper( ppdb ); // Decrement usage count
}
else
{
retValue = FALSE;
}
// Call the API logging function again (???)
x_LogSomeKernelFunction( function number for GetExitCodeProcess );
LeaveMustComplete(); // Decrements ptdbx->MustCompleteCount
return retValue;
}
SetUnhandledExceptionFilter
此函数设定KERNEL32的UnhandledExceptionFilter函数地址—后者将在没有其他异常情况过滤器(exception filter)被用来处理异常情况时使用。这个函数把TopExcFilter的当前值保存在process database中,然后以参数中的值取代,并返回原先的值。
SetUnhandledExceptionFilter虚拟代码:
// Parameters:
// LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
// Locals:
// LPTOP_LEVEL_EXCEPTION_FILTER prevValue
// Save old value
prevValue = ppCurrentProcess->TopExcFilter;
// Stuff in new value
ppCurrentProcess->TopExcFilter = lpTopLevelExceptionFilter;
return prevValue; // Return old value