Windows 95 System Programming SECRENTS学习笔记---第三章(4)

OpenProcess

此函数要求一个process ID作为参数,并返回一个process handleProcess handle随后被交给像ReadProcessMemoryVirtualQueryEx之类的函数。而你知道,TOOLHELP32有能力给你任何进程的process ID。因此,如果你能组合这两股力量,你就可以大有作为。奇怪的是Windows 95允许你打开一个process handle却不允许你打开一个thread handle。或许微软认为线程一旦打开会造成很大破坏,无法承担。

 

OpenProcess首先把process ID转换为一个PROCESS_DATABASE指针。转换process IDprocess database指针的算法和转换thread IDthread 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字符串。请参考前面提到的GetModuleFileNameGetModuleHandle两个函数。

 

本函数的内部实现并不简单,他截获一个指向当前process database的指针,并把设置fFileApisAreOem标志。

 

Environment Database

Process database40h成员中是一个指针,指向一个重要的数据结构,内含与进程相关的数据。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 objectK32OBJ_SEMAPHORE)。

 

30h DWORD BreakEvent

通常为0,但如果程序调用SetConsoleCtrlHandle,此成员就会指向一个KERNEL32 Event ObjectK32OBJ_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 95GetEnvironemntStingA并不真正分配内存,所以其实也没有什么是FreeEnvironmentStirngA必须作的事情。然而,也许纯粹是为了消遣,这个函数检查其字符串参数,看看其是否吻合environment database中的环境区指针。如果不吻合,FreeEnvironmentStringA会将LastError值设定为ERROR_INVALID_PARAMTER

 

GetStdHandle

这个函数和你所能想象的一样直接。给它一个DeviceIDstdinstdoustderr等)这个函数会返回对应的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_DATABASE44h偏移处是一个指针,指向进程的handle table。我将使用handle一词代表可以从handle table中取得的东西。除了file handleWindows 95还会产生其他的系统对象的handle,如进程对象、线程、事件、Mutex等等。

 

Handle的内容理论上来讲是不透明的,也就是说handle本身没有办法告诉你它究竟代表什么东西。如果它的值是5,你判断不出这是一个file handle还是一个mutex handle。然而,一但你了解Windows 95进程的handle table,你就可以轻易的将一个handle值和其引用到的数据产生关系。

 

Windows 95进程的handle table结构十分简单。第一个DWORD放的是这个表格的最大容量(项目个数)。此初始值为0x3048)。然而这并不意味着进程最多只能有48个打开的handle。当进程需要更多的handles时,KERNEL32会重新分配一块内存,使表格有成长空间。每次增加0x1016)个handles。似乎并没有明显的上限。我写了一个小程序,不断打开file handles,在超过255handles之后仍然很好—255DOS的限制。

 

第一个DWORD之后,是由许多结构所组成的数组。每一个结构都由两个DWORD构成:

DWORD flags

DWORD pK32Object

其中第二个DWORD是一个指针,指向17种可能的K32对象。至于第一个DWORD则是此对象的access control flags。这些标志的意义与对象是很种类型有关。对于一个K32OBJ_PROCESS对象,这些标志将是PROCESS_xxx(定义在WINNT.H中),像是PROCESS_TERMNATEPROCESS_VM_READ等等。

 

进行到这里,也许你已经可以感觉到handle是什么东西了。如果你猜测handle是一个索引,指向进程的handle table,你对了!一但这么认为,你就很容易把一个handle值比对其所引用的KERNEL32对象类型。一个没有用的handle,其两个DWORD一定都填满0。当程序分配一个新的handleKERNEL32就使用handle table中的第一个空白项的索引作为handle。但浏览进程的handle table并不是微软建议的程序动作。

补充内容:

以下内容来自《Windows核心编程》第三章

当一个进程被初始化时,系统会为其分配一个句柄表。该句柄表只用于K32对象(即内核对象),不用于用户对象或GDI对象。句柄表的详细结构和管理方法并没有具体的资料说明。但作为一个合格的Windows程序员,必须懂得如何管理进程的句柄表。由于这些信息没有文档资料,因此不能保证所有的详细信息都是正确无误的。下图显示了进程的句柄表的样子,可以看到,它只是个数据结构的数组,每个结构都包含一个指向K32对象(即内核对象)的指针、一个访问屏蔽和一些标志。

当进程被初始化时,它的句柄表是空的。然后,当进程中的线程调用创建K32对象的函数时,比如CreateFileMappingKERNEL32就为该对象分配一块内存,并进行初始化。这些,KERNEL32对该进程的句柄表进行扫描,找出一个空项。将其指针成员初始化为K32对象的内存地址,访问屏蔽设置为全部访问权,同时,各个标志也作了相应设置。

下面列出一些用于创建K32对象(即内核对象)的函数(并不是完整的列表):

l      CreateThread

l      CreateFile

l      CreateFileMapping

l      CreateSemaphore

l      CreateEvent

l      CreateMutex

这些创建K32对象的函数返回与调用进程相关的句柄,这些句柄可以被在相同进程中运行的任何线程使用。该句柄值实际上就是放入进程的句柄表中的索引,它用于标识K32对象的存放位置。因此,当条是一个应用程序并观察K32对象句柄的实际值时,会看到一些较小的值,如12等。请记住,句柄的含义并没有记入文档资料,并且可能随时变更。实际上在Windows 2000种,返回的值用于标识放入进程的句柄表的该对象的字节数,而不是索引号本身。

 

再次强调一下,由于句柄值实际上是K32对象在进程句柄表中的索引,因此这些句柄是与进程相关的,并且不能由其他进程使用。

 

如果创建一个K32对象失败了,那么返回的句柄值通常是0NULL)。发生此种情况是因为系统的内存非常短缺,或者遇到了安全方面的问题。不过有少数函数在运行时失败时返回的句柄值是-1INVALID_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 SectionsEventSemaphoresMutexes四种。

 

关于同步对象的基本功能和运用,参考Jeffrey Richter的《Advanced Windows 3rd》或《Windows核心编程》,还有一本书《Win32多线程程序设计》也非常不错。本书架设你知道同步控制对象的存在,并且知道如何运用它们。

 

最初,每个进程都以一个主线程开始。如果需要,进程可以产生更多线程,使CPU可以在同一时间执行进程中不同区段的代码(在多CPU环境下,可实现真正的并发执行,在单CPU环境下,实际上在同一时刻还是只有一个线程在执行)。标准的例子就是文字处理软件。当文字处理软件需要打印时,它把打印工作交给另一个线程,让主线程依然能够对使用者的动作有所回应。

 

当然,如果你熟悉CPU的基础结构,你就会知道,对于只有一颗CPU的机器来说不可能同时执行两个线程。“许多线程同时执行”的幻觉是靠VMMVirtual Memory Manage虚拟内存管理)对线程的调度实现的。它使用一个硬件计时器和一组复杂的规则,在不同的线程之间快速切换(常见的CPU调度方式有:时间片轮转算法、多级反馈队列调度算法,详细内容参考讲解操作系统原理的教材)。

 

微软宣告Windows 95的时间片(timeslice)是20毫秒(milliseconts)。也就是说,如果不考虑其他因素(例如线程优先级),每个线程执行20毫秒,然后切换到别的线程执行。我将在[Thread Priority线程优先级]一节中说的更详细些。不过我得先声明,本书不打算深入讨论线程调度和VMM线程调度器。就像同步控制对象一样,这些主题应该留待另一本书讨论。

 

和进程一样,线程是一块从KERNEL32共享内存中分配而来的内存块来表现出来的。这块内存保存有所有必要的数据,让KERNEL32用来维护一个线程。虽然我说“所有必要的数据”,实际上这块内存中有一些指针指向其他结构,不过你懂得我的意思就好。这块内存在本书中被称为Thread DatabaseTDB(注意,在不同的时间,微软分别使用TDB代表Task DataBaseThread Database两种意义)。就像Process database一样,Thread Database也是一个K32对象,它的第一个DWORD值为6,表示这是一个K32OBJ_THREAD对象。

 

如果你是一个高级程序员,能够改写DDK或使用Wdeb386SoftIce/W,你可能遭遇过另一个与线程有关的数据结构,名为THCBThread Control Block)。THCB是线程在ring0中的表现形式。在Windows 95中,线程表现为ring0ring3两份数据结构。Ring0级的代码如VMM VXDWDMWindows 2000 or Later)都通过THCB来处理线程。Ring3级的代码如KERNEL32则通过Thread Database来处理线程。本章描述ring3级线程的行为和机制,并不打算涵盖ring0一级。

补充:

如果假设微软的Windows NT/2000是一种微内核结构的操作系统,可否认为,在系统内核(ring0)和用户层面(ring3)分别有两种不同但却相关的控制机制,比如,在OS教材中,提到过PCBProcess Control Block)代表一个进程,而本书则认为Process Database表示一个进程,套用本书中对Thread DatabaseThread 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 StorageTLS)以及一个专用的结构化异常处理链(如果你不知道后两个是什么,别急,稍后我会介绍它们)。此外,线程在执行过程中可能会请求、释放同步控制对象的拥有权。在看过Thread Database之后,我会解释这些东西。

 

什么是Thread Handle?什么是Thread ID

本章稍早我曾说过process handleprocess ID的不同。我的说明可以轻易的套到Thread handleThread 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使用同一个ObsfucatorDWORDprocess database指针和thread database指针转换为IDs。一旦你了解ObsfucatorDWORD的值(并且记住微软拼错了这个字),你就可以把进程或现程的ID转化为有用的指针了。我要再说一次,这并不是被鼓励的程序行为,但是为了多了解系统的底层动作,我们没有太多选择。J

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值