OpenProcess
此函数要求一个process ID作为参数,并返回一个process handle。Process handle随后被交给像ReadProcessMemory或VirtualQueryEx之类的函数。而你知道,TOOLHELP32有能力给你任何进程的process ID。因此,如果你能组合这两股力量,你就可以大有作为。奇怪的是Windows 95允许你打开一个process handle却不允许你打开一个thread handle。或许微软认为线程一旦打开会造成很大破坏,无法承担。
OpenProcess首先把process ID转换为一个PROCESS_DATABASE指针。转换process ID为process database指针的算法和转换thread ID为thread database指针的算法完全相同。接下来参数转换来的指针会被检查。最后,OpenProcess调用一个内部函数,在当前进程的handle table中分配一块空间,并把PROCESS_DATABASE指针存放进去。
OpenProcess虚拟代码:
// Parameters:
// DWORD fdwAccess;
// BOOL fInherit;
// DWORD IDProcess;
// Locals:
// RPOCESS_DATABASE ppdb;
// DWORD flags;
x_LogSomeKernelFunction( function number for OpenProcess );
// Convert the process ID to a PROCESS_DATABASE
ppdb = PidToPDB( IDProcess );
if ( !ppdb )
return 0;
if ( ppdb->Type != K32OBJ_PROCESS ) // Make sure thread ID not passed.
{
InternalSetLastError( ERROR_INVALID_PARAMETER );
return 0;
}
flags = fdAccess & 0x001FFFBF; // Turn off all non-allowed flags.
// flags like PROCESS_QUERY_INFORMATION
// and PROCESS_VM_WRITE are allowed.
if ( fInherit )
flags |= 0x80000000;
flags |= PROCESS_DUP_HANDLE; // Always pass. PROCESS_DUP_HANDLE
// Allocate a new slot in the handle table of the current process.
// The slot contains the ppdb pointer.
return x_OpenHandle( ppCurrentProcess, ppdb, flags );
SetFileApisToOEM
这个函数改变与文件名有关的KERNEL32函数对于文件名的解释方式。默认情况下KERNEL32使用ANSI字符串作为文件名。如果调用了SetFileApisToOEM,就可以改用OEM字符串。请参考前面提到的GetModuleFileName和GetModuleHandle两个函数。
本函数的内部实现并不简单,他截获一个指向当前process database的指针,并把设置fFileApisAreOem标志。
Environment Database
Process database的40h成员中是一个指针,指向一个重要的数据结构,内含与进程相关的数据。KERNEL32内部称此指针为pEDB,我把它解释为“pointer to Environment Database”。就像对待PROCESS_DATABASE一样。我在PROCDB.H中描述了ENVIRONMENT DATABASE的结构布局,如下所示:
typedef struct _ENVIRONMENT_DATABASE
{
PSTR pszEnvironment; // 00h Pointer to Environment
DWORD un1; // 04h
PSTR pszCmdLine; // 08h Pointer to command line
PSTR pszCurrDirectory; // 0Ch Pointer to current directory
LPSTARTUPINFOA pStartupInfo;// 10h Pointer to STARTUPINFOA struct
HANDLE hStdIn; // 14h Standard Input
HANDLE hStdOut; // 18h Standard Output
HANDLE hStdErr; // 1Ch Standard Error
DWORD un2; // 20h
DWORD InheritConsole; // 24h
DWORD
BreakType; // 28h
DWORD
BreakSem; // 2Ch
DWORD
BreakEvent; // 30h
DWORD
BreakThreadID; // 34h
DWORD
BreakHandlers; // 38h
} ENVIRONMENT_DATABASE, *PENVIRONMENT_DATABASE;
现在我们来看看这些成员的具体含义:
00h PSTR pszEnvironment
这个位置指向进程的环境区。所谓环境区是标准的DOS环境(形式如string = value; string =value)。进程环境块是一块内存,位于每个进程私有的地址空间中,通常就是模块被载入的地址之上。
04h DWORD un1
此位置意义未明。通常总是0。
08h PSTR pszCmdLine
此成员内含CreateProcess函数中的命令行参数内容。大部分情况下这个命令行是一个完整的EXE文件名。有时候它会指向空字符串(0)。
0Ch PSTR pszCurrDirectory
此成员指向当前的磁盘目录
10h LPSTARTUPINFOA pStartupInfo
这是一个指针,指向进程的STARUPINFOA结构(定义在WINBASE.H中)。STARTUPINFOA结构是CreateProcess的参数之一,可用来指定窗口的大小、标题、标准的file handles等等。这个成员所指的是该结构的一个副本。
14h HANDLE hStdIn
这是一个file handle,进程用它作为标准的输入设备。如果没有找到(例如一个GUI程序),此值为-1。
18h HANDLE hStdOut
这是一个file handle,进程用它作为标准的输出设备。如果没有找到(例如一个GUI程序),此值为-1。
1Ch HANDLE hStdErr
这是一个file handle,进程用它作为标准的错误输出设备。如果没有找到(例如一个GUI程序),此值为-1。
20h DWORD un2
此成员意义未明。通常是1。
24h DWORD InheritConsole
从名称可以推测,此成员表示进程是否继承自Console程序。请参考CreateProcess函数的CREATE_NEW_CONSOLE标志。在我的观察中,此成员的值总是0。
28h DWORD BreakType
这个成员最可能用来指示console event(例如 Ctrl+C)如何处理。在我所执行过的程序中,它通常为0,偶尔会是0xA。
2Ch DWORD BreakSem
通常是0,但如果程序调用SetConsoleCtrlHandle,此成员就会指向一个KERNEL32 semaphore object(K32OBJ_SEMAPHORE)。
30h DWORD BreakEvent
通常为0,但如果程序调用SetConsoleCtrlHandle,此成员就会指向一个KERNEL32 Event Object(K32OBJ_EVENT)。
34h DWORD BreakThreadID
通常为0,但如果程序调用SetConsoleCtrlHandle,此成员就会指向一个线程对象(K32OBJ_THREAD),而该线程正是安装此处理例程的线程本身。
38h DWORD BreakHandles
通常是0,但如果程序调用SetConsoleCtrlHandle,此成员就会指向一个从KERNEL32 Shared Heap中分配得来的数据结构,存放一系列安装好的主控台控制函数(console control handler)。
现在让我们看看这些函数的虚拟代码。这次是与EVNIRONMENT_DATABAS有关。
GetCommandLineA
其实这个函数没有太多东西可以说。它返回命令行指针,命令行字符串存放在environment database中。
GetCommandLineA的虚拟代码:
return ppCurrentProcess->pEDB.pszCmdLine;
GetEnvironmentStrings
该函数返回与environment database相关的指针。值得注意的事,这个函数的真正代码和SDK说明文件之间有两个差异。
SDK文件上说:
当GetEnvironmentStrings被调用时,它会分配一块内存作为一个环境区。当此环境区不再需要时,它应该调用FreeEnvironmentStrings。
这对于Windows NT是成立的。但对Windows 95却不正确。
FreeEnvironmentStringA
这个函数比较有趣些。由于在Windows 95下GetEnvironemntStingA并不真正分配内存,所以其实也没有什么是FreeEnvironmentStirngA必须作的事情。然而,也许纯粹是为了消遣,这个函数检查其字符串参数,看看其是否吻合environment database中的环境区指针。如果不吻合,FreeEnvironmentStringA会将LastError值设定为ERROR_INVALID_PARAMTER。
GetStdHandle
这个函数和你所能想象的一样直接。给它一个DeviceID(stdin、stdou、stderr等)这个函数会返回对应的file handle。如果你给的是一个冒牌的DeviceID,此函数会失败,并设定LastError代码。
GetStdHandle函数的虚拟代码:
// Parameters:
// DWORD fdwDevice
// Locals:
// PENVIRONMENT_DATABASE pEDB
pEDB = ppCurrentProcess->pEDB;
if ( fdwDevice == STD_INPUT_HANDLE )
return pEDB->hStdIn;
else if ( fdwDevice == STD_OUTPUT_HANDLE )
return pEDB->hStdOut;
else if ( fdwDevice == STD_ERROR_HANDLE )
retrun pEDB->hStdErr;
InternalSetLastError( ERROR_INVALID_FUNCTION );
Return 0xFFFFFFFF;
SetStdHandle
这个函数比GetStdHandle有趣一些。它首先验证handle的确代表一个合法的KERNEL32对象。怎么做呢?请x_ConvertHandleToK32Object代劳。后者会返回一个指针,指向对应的KERNEL32对象—如果handle合法的话。SetStdHandle从不使用K32对象指针,简单的NULL检验是唯一需要做的动作。在检验过hHandle参数的合法性之后,其余函数代码把hHandle塞进environment database结构的适当位置中去。
Process Handle Tables
PROCESS_DATABASE的44h偏移处是一个指针,指向进程的handle table。我将使用handle一词代表可以从handle table中取得的东西。除了file handle,Windows 95还会产生其他的系统对象的handle,如进程对象、线程、事件、Mutex等等。
Handle的内容理论上来讲是不透明的,也就是说handle本身没有办法告诉你它究竟代表什么东西。如果它的值是5,你判断不出这是一个file handle还是一个mutex handle。然而,一但你了解Windows 95进程的handle table,你就可以轻易的将一个handle值和其引用到的数据产生关系。
Windows 95进程的handle table结构十分简单。第一个DWORD放的是这个表格的最大容量(项目个数)。此初始值为0x30(48)。然而这并不意味着进程最多只能有48个打开的handle。当进程需要更多的handles时,KERNEL32会重新分配一块内存,使表格有成长空间。每次增加0x10(16)个handles。似乎并没有明显的上限。我写了一个小程序,不断打开file handles,在超过255个handles之后仍然很好—255是DOS的限制。
第一个DWORD之后,是由许多结构所组成的数组。每一个结构都由两个DWORD构成:
DWORD flags
DWORD pK32Object
其中第二个DWORD是一个指针,指向17种可能的K32对象。至于第一个DWORD则是此对象的access control flags。这些标志的意义与对象是很种类型有关。对于一个K32OBJ_PROCESS对象,这些标志将是PROCESS_xxx(定义在WINNT.H中),像是PROCESS_TERMNATE、PROCESS_VM_READ等等。
进行到这里,也许你已经可以感觉到handle是什么东西了。如果你猜测handle是一个索引,指向进程的handle table,你对了!一但这么认为,你就很容易把一个handle值比对其所引用的KERNEL32对象类型。一个没有用的handle,其两个DWORD一定都填满0。当程序分配一个新的handle,KERNEL32就使用handle table中的第一个空白项的索引作为handle。但浏览进程的handle table并不是微软建议的程序动作。
补充内容:
以下内容来自《Windows核心编程》第三章
当一个进程被初始化时,系统会为其分配一个句柄表。该句柄表只用于K32对象(即内核对象),不用于用户对象或GDI对象。句柄表的详细结构和管理方法并没有具体的资料说明。但作为一个合格的Windows程序员,必须懂得如何管理进程的句柄表。由于这些信息没有文档资料,因此不能保证所有的详细信息都是正确无误的。下图显示了进程的句柄表的样子,可以看到,它只是个数据结构的数组,每个结构都包含一个指向K32对象(即内核对象)的指针、一个访问屏蔽和一些标志。
当进程被初始化时,它的句柄表是空的。然后,当进程中的线程调用创建K32对象的函数时,比如CreateFileMapping,KERNEL32就为该对象分配一块内存,并进行初始化。这些,KERNEL32对该进程的句柄表进行扫描,找出一个空项。将其指针成员初始化为K32对象的内存地址,访问屏蔽设置为全部访问权,同时,各个标志也作了相应设置。
下面列出一些用于创建K32对象(即内核对象)的函数(并不是完整的列表):
l CreateThread
l CreateFile
l CreateFileMapping
l CreateSemaphore
l CreateEvent
l CreateMutex
这些创建K32对象的函数返回与调用进程相关的句柄,这些句柄可以被在相同进程中运行的任何线程使用。该句柄值实际上就是放入进程的句柄表中的索引,它用于标识K32对象的存放位置。因此,当条是一个应用程序并观察K32对象句柄的实际值时,会看到一些较小的值,如1、2等。请记住,句柄的含义并没有记入文档资料,并且可能随时变更。实际上在Windows 2000种,返回的值用于标识放入进程的句柄表的该对象的字节数,而不是索引号本身。
再次强调一下,由于句柄值实际上是K32对象在进程句柄表中的索引,因此这些句柄是与进程相关的,并且不能由其他进程使用。
如果创建一个K32对象失败了,那么返回的句柄值通常是0(NULL)。发生此种情况是因为系统的内存非常短缺,或者遇到了安全方面的问题。不过有少数函数在运行时失败时返回的句柄值是-1(INVALID_HANDLE_VALUE)。例如,如果CreateFile未能打开指定的文件,那么它将返回INVALID_HANDLE_VALUE,而不是NULL。当察看创建K32对象的函数的返回值时,必须格外小心。特别要注意的是,只有当调用CreateFile函数时,才能将其返回值与INVALID_HANDLE_VALUE进行比较。下面的代码是不正确的:
HANDLE hMutex = CreateMutex(….);
if ( hMutex == INVALID_HANDLE_VALUE )
{
// We will never execute this code because CreateMutex returns NULL if it fails。
}
关闭K32对象时,需要调用CloseHandle函数来进行。该函数首先检查调用进程的句柄表,以确保传递给它的索引(句柄)用于标识一个进程有权访问的对象。如果该所引有效,那么系统就可以获取该K32对象的指针,并可确定该对象的引用计数是否为0,如果是,该K32对象就会被KERNEL32销毁,同时收回其占用的内存。
如果将一个无效的句柄值传递给CloseHandle,将会出现两种情况之一:如果进程运行正常,CloseHandle返回FALSE,而GetLastError则返回ERROR_INVALID_HANDLE。如果进程处于调试状态,系统将通知调试程序,以便进行除错。
在CloseHandle返回之前,它会清除进程的句柄表中的项目,该句柄现在对你的进程已经无效,不应该试图再去使用它。无论K32对象是否已经撤销,都会发生清除句柄表项目的操作。当调用CloseHandle汉书之后,将不再拥有对K32对象的访问权。不过,如果该对象的引用计数没有递减为0,那么该K32对象仍将存在
如果忘记调用CloseHandle函数,那么会出现内存泄漏吗?答案是可能,但不是一定。在进程运行时,进程可能会泄漏资源(如K32对象)。但是,当进程结束时,操作系统能确保该进程使用的任何资源都被释放,这是有保证的。对于K32对象来说,系统将执行下列操作:当进程终止运行时,系统会自动扫描该进程的句柄表。如果该表中拥有任何在终止进程运行前没有关闭的对象,系统将关闭这些对象的句柄。如果这些K32对象的引用计数将为0,那么该K32对象将被系统收回。
需要记住的是,K32对象的生命周期至少和其代表的对象一样长,有时会远远长于其代表的对象。比如,进程K32对象,当一个进程结束时,其对应的K32对象的引用计数将递减1,如果此时还有别的进程在使用该K32对象,则其并不会被销毁。
操作句柄的一些函数:
l GetHandleInformation
l SetHandleInformation
l CloseHandle
l DuplicateHandle
Thread(线程)
你也经看过模块和进程,只要再看过线程,就可以完成整个KERNEL32基础结构之旅。进程主要是表达对file handles、地址空间等的拥有权,线程则主要表达对模块中代码执行的事实。你看,有这么多的东西相互关联,我很难把什么东西从另一个东西中完全的抽出来。例如在前面讨论进程时,我必须先提到线程和同步控制对象。
从抽象层面来说,线程是一种方便的表达方式,让你的某一部分代码执行—当其他部分的代码正在等待某些外部事件发生时。将进程的各项工作进一步分配给线程之后,你似乎可以消除像“pooling loop”这样的动作。Pooling loop浪费了许多CPU时间。
任何时候,线程可能处于三种状态之一。第一种是:执行中状态(running state)。这个时候CPU寄存器内容就是该线程的寄存器的值。
第二种是:准备执行(read to run state)。这种状态下的线程没有什么理由不会被执行—只是早晚问题。它终有一刻能够控制CPU。
第三种是:阻塞状态(blocked state)。线程如果被阻塞,表示其正在等待某件事情发生。在那之前CPU调度器不会安排该线程执行起来。引起执行中的线程阻塞的东西称之为同步控制对象(synchronization objects)。Windows的同步控制对象有:Critical Sections、Event、Semaphores、Mutexes四种。
关于同步对象的基本功能和运用,参考Jeffrey Richter的《Advanced Windows 3rd》或《Windows核心编程》,还有一本书《Win32多线程程序设计》也非常不错。本书架设你知道同步控制对象的存在,并且知道如何运用它们。
最初,每个进程都以一个主线程开始。如果需要,进程可以产生更多线程,使CPU可以在同一时间执行进程中不同区段的代码(在多CPU环境下,可实现真正的并发执行,在单CPU环境下,实际上在同一时刻还是只有一个线程在执行)。标准的例子就是文字处理软件。当文字处理软件需要打印时,它把打印工作交给另一个线程,让主线程依然能够对使用者的动作有所回应。
当然,如果你熟悉CPU的基础结构,你就会知道,对于只有一颗CPU的机器来说不可能同时执行两个线程。“许多线程同时执行”的幻觉是靠VMM(Virtual Memory Manage虚拟内存管理)对线程的调度实现的。它使用一个硬件计时器和一组复杂的规则,在不同的线程之间快速切换(常见的CPU调度方式有:时间片轮转算法、多级反馈队列调度算法,详细内容参考讲解操作系统原理的教材)。
微软宣告Windows 95的时间片(timeslice)是20毫秒(milliseconts)。也就是说,如果不考虑其他因素(例如线程优先级),每个线程执行20毫秒,然后切换到别的线程执行。我将在[Thread Priority线程优先级]一节中说的更详细些。不过我得先声明,本书不打算深入讨论线程调度和VMM线程调度器。就像同步控制对象一样,这些主题应该留待另一本书讨论。
和进程一样,线程是一块从KERNEL32共享内存中分配而来的内存块来表现出来的。这块内存保存有所有必要的数据,让KERNEL32用来维护一个线程。虽然我说“所有必要的数据”,实际上这块内存中有一些指针指向其他结构,不过你懂得我的意思就好。这块内存在本书中被称为Thread Database或TDB(注意,在不同的时间,微软分别使用TDB代表Task DataBase和Thread Database两种意义)。就像Process database一样,Thread Database也是一个K32对象,它的第一个DWORD值为6,表示这是一个K32OBJ_THREAD对象。
如果你是一个高级程序员,能够改写DDK或使用Wdeb386或SoftIce/W,你可能遭遇过另一个与线程有关的数据结构,名为THCB(Thread Control Block)。THCB是线程在ring0中的表现形式。在Windows 95中,线程表现为ring0和ring3两份数据结构。Ring0级的代码如VMM VXD、WDM(Windows 2000 or Later)都通过THCB来处理线程。Ring3级的代码如KERNEL32则通过Thread Database来处理线程。本章描述ring3级线程的行为和机制,并不打算涵盖ring0一级。
补充:
如果假设微软的Windows NT/2000是一种微内核结构的操作系统,可否认为,在系统内核(ring0)和用户层面(ring3)分别有两种不同但却相关的控制机制,比如,在OS教材中,提到过PCB(Process Control Block)代表一个进程,而本书则认为Process Database表示一个进程,套用本书中对Thread Database和Thread Control Block的解释,是否可认为PDB是进程在ring3的表示,而PCB是进程在ring0的表示?操作系统如此做的真实意义是什么?这二者之间是否存在某种对应关系?
对于像SoftIce这样的软件,必定会与ring0级的这些结构打交道,仔细研究这些对我们认识Windows将大有帮助。
线程本身拥有一些东西。第一样东西是一组寄存器(register set)。正如我前面说过的,线程要么是在执行,要么就是并为执行(这不是废话吗?呵呵)。当线程正在执行,它的寄存器集合将被放到CPU的寄存器中,也就是说线程的EIP值就是CPU寄存器EIP的值。当线程不在执行状态,它的寄存器必须存放在内存的某处。因此,每个线程有一个指针指向一块内存块,线程的寄存器内容就存放在那里。
与每个线程有关系的另一样东西是进程。进程中的所有线程共享进程的每一样东西。例如,进程拥有memory context和一个私有的地址空间,所以其中的所有线程都在相同的地址空间中运行。进程有一个handle table,用来管理文件、控制台(console)、内存映射文件(memory mapped file)、Events等等,进程中的所有线程也共享这些handles。如果hande 3代表一个内存映射文件,则进程中任何一个线程都可以使用handle 3来使用这个内存映射文件。
线程还拥有许多其它东西。每个线程都有一个专用的堆栈、一个专用的消息队列,一个专用的Thread Local Storage(TLS)以及一个专用的结构化异常处理链(如果你不知道后两个是什么,别急,稍后我会介绍它们)。此外,线程在执行过程中可能会请求、释放同步控制对象的拥有权。在看过Thread Database之后,我会解释这些东西。
什么是Thread Handle?什么是Thread ID?
本章稍早我曾说过process handle和process ID的不同。我的说明可以轻易的套到Thread handle和Thread ID身上—只要把进程改为线程就行了。如果你不确定,请回头去看看什么是Process Handle?什么是Process ID?那一节。
GetThreadHandle返回一个常数(微软总是说那是一个“虚拟handle”),可以适用于任何真正的Thread Handle可以用的地方:
GetThreadHandle函数的虚拟代码:
x_LogSomeKernelFunction( function number for GetCurrentThread );
return 0xFFFFFFFE;
就像GetCurrentProcessId那样,GetCurrentThreadId返回一个指针,指向当前的thread database(但KERNEL32小组会加上一个令人迷惑的数值):
GetCurrentThreadId函数的虚拟代码:
return TDBToTid( ppCurrentThread );
KERNEL32为何如此迷惑世人呢?让我们看看:
TDBToTid函数的虚拟代码:
// Parameters:
// THREAD_DATABSE * ptdb
if ( ObsfucatorDWORD == FALSE )
{
_DebugOut( “TDBToTid() Called too early! ObsFucator not yet initialized!” );
return 0;
}
if ( ptdb & 1 )
{
_DebugOut( “TDBToTid: This TDB looks like a TID ( 0%1xh) Do “
“statck trace BEFORE reporting as bug.” );
}
// Here’s the key! XOR the obsfucator DWORD with the thread database
// pointer to make the TID value.
return ptdb^ObsfucatorDWORD;
如果你认为这一段看起来真像先前提过的PDBToPid函数,那么你是对的。KERNEL32使用同一个ObsfucatorDWORD把process database指针和thread database指针转换为IDs。一旦你了解ObsfucatorDWORD的值(并且记住微软拼错了这个字),你就可以把进程或现程的ID转化为有用的指针了。我要再说一次,这并不是被鼓励的程序行为,但是为了多了解系统的底层动作,我们没有太多选择。J