第二部分 编程的具体方法


第4章 进程

本章介绍系统如何管理所有正在运行的应用程序。首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程。然后将说明如何使用 相关的内核对象来对进程进行操作。接着,要介绍进程的各种不同的属性,以及查询和修改这些属性所用的若干个函数。还要讲述创建或生成系统中 的辅助进程所用的函数。当然,如果不深入说明如何来结束进程的运行,那么这样的介绍肯定是不完整的。现在就来介绍进程的有关内容。

进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:

• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。

• 另一个是地址空间,它包含所有可执行模块或D L L 模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。

图4-1 操作系统在单个CPU 计算机上用循环方式为各个线程提供时间片

进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上, 单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组CPU 寄存器和它自己的堆 栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统就将自动撤消该进程和它的地址空间。

若要使所有这些线程都能运行,操作系统就要为每个线程安排一定的C P U 时间。它通过以一种循环方式为线程提供时间片(称为量程),造成一种 假象,仿佛所有线程都是同时运行的一样。图4 - 1显示了在单个CPU的计算机上是如何实现这种运行方式的。如果计算机拥有多个CPU ,那么 操作系统就要使用复杂得多的算法来实现CPU 上线程负载的平衡。

当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。

Windows 2000: Micorsoft Windows 2000 能够在拥有多个CPU 的计算机上运行。例如,

我用来撰写本书的计算机就包含两个处理器。Windows 2000可以在每个CPU 上运行不同的线程,这样,多个线程就真的在同时运行了。Windows 2000的内核能够在这种类型的系统上进行所有线程的管理和调度。不必在代码中进行任何特定的设置就能利用多处理器提供的各种优点。

Windows 98: Windows 98 只能在单处理器计算机上运行。即使计算机配有多个处理器,Windows每次只能安排一个线程运行,而其他的处理器则处于空闲状态。


4.1 编写第一个Windows应用程序

Windows支持两种类型的应用程序。一种是基于图形用户界面(GUI)的应用程序,另一种是基于控制台用户界面(CUI)的应用程序。 基于G U I 的应用程序有一个图形前端程序。它能创建窗口,拥有菜单,可以通过对话框与用户打交道,并可使用所有的标准“Wi n d o w s ”组 件。Wi n d o w s 配备的所有应用程序附件(如Notepad 、Calculator 和WordPad ),几乎都是基于G U I 的应用程序。基 于控制台的应用程序属于文本操作的应用程序。它们通常不能用于创建窗口或处理消息,并且它们不需要图形用户界面。虽然基于C U I 的应用程序 包含在屏幕上的窗口中,但是窗口只包含文本。命令外壳程序CMD.EXE(用于Windows 2000 )和COMMAND.COM (用于Windows 98)都是典型的 基于CUI的应用程序。

这两种类型的应用程序之间的界限是非常模糊的。可以创建用于显示对话框的C U I 应用程序。例如,命令外壳程序可能拥有一个特殊的命令,使它 能够显示一个图形对话框,在这个对话框中,可以选定你要执行的命令,而不必记住该外壳程序支持的各个不同的命令。也可以创建一个基于G U I 的应用程序,它能将文本字符串输出到一个控制台窗口。我常常创建用于建立控制台窗口的G U I 应用程序,在这个窗口中,我可以查看应用程序执 行时的调试信息。当然你也可以在应用程序中使用图形用户界面,而不是老式的字符界面,因为字符界面使用起来不太方便。

当使用Microsoft Visual C++来创建应用程序时,这种集成式环境安装了许多不同的链接程序开关,这样,链接程序就可以将相应的子系统嵌入产生的可执行程序。用于C U I 应用程序的链接程序开关是/ S U B S Y S T E M : C O N D O L E ,而用于G U I 应用程序的链接程序开关是SUBSYSTEM : WINDOWS 。当用户运行一个应用程序时,操作系统的加载程序就会查看可执行图形程序的标题,并抓取该子系统的值。如果该值指明一个CUI应用程序,那么加载程序就会自动保证为该应用程序创建文本控制台窗口。

如果该值指明这是个G U I 应用程序,那么加载程序不创建控制台窗口,而只是加载应用程序。一旦应用程序启动运行,操作系统就不再考虑应用程 序拥有什么类型的用户界面。Wi n d o w s 应用程序必须拥有一个在应用程序启动运行时调用的进入点函数。可以使用的进入点函数有4 个:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,
   PSTR pszCmdLine, int nCmdShow);

int WINAPT wWinMain(HINSTANCE hinstExe,HINSTANCE,
   PWSTR pszCmdLine,int nCmdShow);

int __cdecl main(int argc,char *argv[],char *envp[]);

int _cdecl wmain(int argc, wchar_t *argv[],
   wchar_t *envp[]);
操作系统实际上并不调用你编写的进入点函数。它调用的是C / C + +运行期启动函数。该函数负责对C / C + +运行期库进行初始化,这样,就可以 调用m a l l o c 和f r e e 之类的函数。它还能够确保已经声明的任何全局对象和静态C + +对象能够在代码执行以前正确地创建。下面说明源代 码中可以实现哪个进入点以及何时使用该进入点(见表4 - 1 )。

表4-1 应用程序的进入点

应用程序类型 进入点 嵌入可执行文件的启动函数
需要ANSI字符和字符串的GUI应用程序 WinMain WinMainCRTStartup
需要Unicode字符和字符串的GUI应用程序 wWinMainw WinMainCRTStartup
需要ANSI字符和字符串的CUI应用程序 main mainCRTStartup
需要Unicode字符和字符串的CUI应用程序 wmain wmainCRTStartup

链接程序负责在它连接可执行文件时选择相应的C / C + +运行期启动函数。如果设定了/ S U B S Y S T E M : W I N D O W S 链接程序开关,那 么该链接程序期望找到一个Wi n M a i n 或w Wi n m a i n函数。如果这两个函数都不存在,链接程序便返回一个“未转换的外部符号”的错误消 息。否则,它可以分别选择Wi n M a i n C RT S t a r t u p 函数或w Wi n M a i n C RT S t a r t u p 函数。

同样,如果设定了/ S U B S Y S T E M : C O N S O L E 链接程序开关,那么该链接程序便期望找到m a i n 或w m a i n 函数,并且可以分别选 择m a i n C RT S t a r t u p 函数或w m a i n C RT S t a r t u p 函数。同样,如果m a i n 或w m a i n 都不存在,那么链接程序返回一条 “未转换外部符号”的消息。

但是,人们很少知道这样一个情况,即可以从应用程序中全部删除/ S U B S Y S T E M 链接程序开关。当这样做的时候,链接程序能够自动确定应 用程序应该连接到哪个子系统。当进行链接时,链接程序要查看代码中存在4 个函数(Wi n M a i n 、w Wi n M a i n 、m a i n 或w m a i n ) 中的哪一个。然后确定可执行程序应该是哪一个子系统,并且确定可执行程序中应该嵌入哪个C / C + +启动函数。

Wi n d o w s / Visual C++编程新手常犯的错误之一是,当创建新的应用程序时,不小心选择了错误的应用程序类型。例如,编程员可能创建一个 新的Wi n 3 2 应用程序项目,但是创建了一个进入点函数m a i n 。当创建应用程序时,编程员会看到一个链接程序错误消息,因为w i n 3 2 应 用程序项目设置了/ S U B S Y S T E M : W I N D O W S 链接程序开关,但是不存在Wi n M a i n 或w Wi n M a i n 函数。这时,编程员可以有 4 个选择:

• 将m a i n 函数改为Wi n M a i n 。通常这不是最佳的选择,因为编程员可能想要创建一个控制台应用程序。

• 用Visual C++创建一个新的Win32 控制台应用程序,并将现有的源代码添加给新应用程第4 章进程计计47 下载序项目。这个选项冗长而乏味,因为它好像是从头开始创建应用程序,而且必须删除原始的应用程序文件。

• 单击Project Settings 对话框的L i n k 选项卡,将/ S U B S Y S T E M : W I N D O W S 开关改为/ S U B S Y S T E M : C O N S O L E 。这是解决问题的一种比较容易的方法,很少有人知道他们只需要进行这项操作就行了。

• 单击Project Settings 对话框的L i n k 选项卡,然后全部删除/ S U B S Y S T E M : W I N D O W S 开关。这是我喜欢选择的方法,因为它提供了最大的灵活性。现在,连接程序将根据源代码中实现的函数进行正确的操作。当用Visual C++的Developer Studio 创建新Wi n 3 2 应用程序或Wi n 3 2 控制台应用程序项目时,我不知道为什么这没有成为默认设置。

所有的C / C + +运行期启动函数的作用基本上都是相同的。它们的差别在于,它们究竟是处理A N S I 字符串还是U n i c o d e 字符串,以及它 们在对C 运行期库进行初始化后它们调用哪个进入点函数。Visual C++配有C 运行期库的源代码。可以在CR t0.c 文件中找到这4 个启动函数的代码 。

现在将启动函数的功能归纳如下:

• 检索指向新进程的完整命令行的指针。

• 检索指向新进程的环境变量的指针。

• 对C / C + +运行期的全局变量进行初始化。如果包含了S t d L i b . h 文件,代码就能访问这些变量。表4 - 1 列出了这些变量。

• 对C 运行期内存单元分配函数(m a l l o c 和c a l l o c )和其他低层输入/输出例程使用的内存栈进行初始化。

• 为所有全局和静态C + +类对象调用构造函数。

当所有这些初始化操作完成后,C / C + +启动函数就调用应用程序的进入点函数。如果编写了一个w Wi n M a i n 函数,它将以下面的形式被调用 :

GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain(GetMjduleHandle(NULL),
   NULL, pszCommandLineUnicode,
   (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? 
   StartupInfo.wShowWindow:SW_SHOWDEFAULT);
如果编写了一个Wi n M a i n 函数,它将以下面的形式被调用:
GetStartupInfo(&StartupInfo);
 
int nMainReLVal = WinMain(GetModuleHandle(NULL),
   NULL, pszCommandLineANSI,
   (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? 
   Startupinfo.wShowWindow:SW_SHOWDEFAULT);
如果编写了一个w m a i n或m a i n 函数,它将以下面的形式被调用:
int nMainRetVal = wmain(__argc, __wargv, _wenviron); 

int nMainRetVal = main(_argc, __argv, _environ);
当进入点函数返回时,启动函数便调用C 运行期的e x i t 函数,将返回值(n M a i n R e t Va l )传递给它。E x i t 函数负责下面的操作:

• 调用由_onexit函数的调用而注册的任何函数。

• 为所有全局的和静态的C++类对象调用析构函数。

• 调用操作系统的ExitProcess函数,将nMainRetVal传递给它。这使得该操作系统能够撤消进程并设置它的e x i t 代码。

表4 - 2 显示了程序能够使用的C / C + +运行期全局变量。

表4-2 程序能够使用的C / C + +运行期全局变量

变量名 类型 说明
_osver unsigned int 操作系统的测试版本。例如Windows 2000 Beta 3是测试版本2031 。因此_osver的值是2031
_winmajor unsigned int 采用十六进制表示法的Windows主要版本。对于Windows2000来说,它的值是5
_winminor unsigned int 采用十六进制表示法的Windows次要版本。对于Windows2000来说,它的值是0
_winver unsigned int ( _winmajor < < 8 ) + _ winminor在命令行上传递的参数号
__argc unsigned int 带有指向ANSI/Unicode字符串的指针的__argc大小的数组
__argv char * * 带有指向ANSI/Unicode字符串的指针的__argc大小的数组
__wargv wchar_t * * 每个数组项均指向一个命令行参数
_environ char * * 指向ANSI/Unicode字符串的指针的数组。每个数组项指向一个环境字符串
_wenviron wchar_t * * 指向ANSI/Unicode字符串的指针的数组。每个数组项指向一个环境字符串
_pgmptr char * 正在运行的程序的ANSI/Unicode全路径和名字
_wpgmptr wchar_t * 正在运行的程序的ANSI/Unicode全路径和名字

4.1.1 进程的实例句柄

加载到进程地址空间的每个可执行文件或D L L 文件均被赋予一个独一无二的实例句柄。可执行文件的实例作为( w ) Wi n M a i n 的第一个参数h i n s t E x e 来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。例如,若要从可执行文件的映象来加载图标资源,需要调用下面这个函数:

HICON LoadIcon( HINSTANCE hinst, PCTSTR pszIcon); 
L o a d I c o n 的第一个参数用于指明哪个文件(可执行文件或D L L 文件)包含你想加载的资源。许多应用程序在全局变量中保存( w ) Wi n M a i n 的h i n s t E x e 参数,这样,它就很容易被所有可执行文件的代码访问。

Platform SDK 文档中说,有些函数需要HMODULE类型的一个参数。它的例子是下面所示的GetModuleFileName函数:

DWORD GetModuleFileName( HMODULE hinstModule,
   PTSTR pszPath, DWORD cchPath);
注意:实际情况说明,H M O D U L E 与H I N S TA N C E 是完全相同的对象。如果函数的文档指明需要一个H M O D U L E ,那么可以传递一个H I N S TA N C E ,反过来,如果需要一个H I N S TA N C E ,也可以传递一个H M O D U L E 。之所以存在两个数据类型,原因是在1 6 位Wi n d o w s 中,H M O D U L E 和H I N S TA N C E 用于标识不同的东西。

( w ) Wi n M a i n 的h i n s t E x e 参数的实际值是系统将可执行文件的映象加载到进程的地址空间时使用的基本地址空间。例如,如果系统打开了可执行文件并且将它的内容加载到地址0 x 0 0 4 0 0 0 0 0 中,那么( w ) Wi n M a i n 的h i n s t E x e 参数的值就是0 x 0 0 4 0 0 0 0 0 。

可执行文件的映像加载到的基地址是由链接程序决定的。不同的链接程序可以使用不同的默认基地址。Visual C++链接程序使用的默认基地址是0 x 0 0 4 0 0 0 0 0 ,因为这是在运行Wi n d o w s9 8 时可执行文件的映象可以加载到的最低地址。可以改变应用程序加载到的基地址,方法是使用M i c r o s o f t 的链接程序中的/ B A S E : a d d r e s s 链接程序开关。

如果你想在Wi n d o w s 上加载的可执行文件的基地址小于0 x 0 0 4 0 0 0 0 0 ,那么Windows 98 加载程序必须将可执行文件重新加载到另一个地址。这会增加加载应用程序所需的时间,不过,这样一来,至少该应用程序能够运行。如果开发的应用程序将要同时在Windows 98 和Wi n d o w s2 0 0 0 上运行,应该确保应用程序的基地址是0 x 0 0 4 0 0 0 0 0 或者大于这个地址。

下面的G e t M o d u l e H a n d l e 函数返回可执行文件或D L L 文件加载到进程的地址空间时所用的句柄/基地址:

HMODULE GetModuleHandle( PCTSTR pszModule); 
当调用该函数时,你传递一个以0 结尾的字符串,用于设定加载到调用进程的地址空间的可执行文件或D L L 文件的名字。如果系统找到了指定的可 执行文件或D L L 文件名,G e t M o d u l e H a n d l e 便返回该可执行文件或D L L 文件映象加载到的基地址。如果系统没有找到该文件,则 返回N U L L 。也可以调用G e t M o d u l e H a n d l e ,为p s z M o d u l e 参数传递N U L L ,G e t M o d u l e H a n d l e 返回调 用的可执行文件的基地址。这正是C 运行期启动代码调用( w ) Wi n M a i n函数时该代码执行的操作。

请记住G e t M o d u l e H a n d l e 函数的两个重要特性。首先,它只查看调用进程的地址空间。如果调用进程不使用常用的对话框函数,那么 调用G e t M o d u l e H a n d l e 并为它传递“C o m D l g 3 2 ”后,就会返回N U L L ,尽管C o m D l g 3 2 . d l l 可能加载到了其他 进程的地址空间。第二,调用G e t M o d u l e H a n d l e 并传递N U L L 值,就会返回进程的地址空间中可执行文件的基地址。因此,即使通 过包含在D L L 中的代码来调用(N U L L ),返回的值也是可执行文件的基地址,而不是D L L 文件的基地址。

4.1.2 进程的前一个实例句柄

如前所述,C / C + +运行期启动代码总是将N U L L 传递给( w ) Wi n M a i n 的h i n s t E x e P r e v 参数。该参数用在1 6 位Wi n d o w s 中,并且保留了( w ) Wi n M a i n 的一个参数,目的仅仅是为了能够容易地转用1 6 位Wi n d o w s 应用程序。决不应该在代码中引用该参数。由于这个原因,我总是像下面这样编写( w ) Wi n M a i n 函数:

int WINAPI WinMain( 
  HINSTANCE hinstExe, 
  HINSTANCE, PSTR pszCmdLine, int nCmdShow);
由于没有为第二个参数提供参数名,因此编译器不会发出“没有引用参数”的警告。

4.1.3 进程的命令行

当一个新进程创建时,它要传递一个命令行。该命令行几乎永远不会是空的,至少用于创建新进程的可执行文件的名字是命令行上的第一个标记。但是在后面介绍C r e a t e P r o c e s s 函数时我们将会看到,进程能够接收由单个字符组成的命令行,即字符串结尾处的零。当C 运行期的启动代码开始运行的时候,它要检索进程的命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给Wi n M a i n 的p s z C m d L i n e 参数。

值得注意的是,p s z C m d L i n e 参数总是指向一个A N S I 字符串。但是,如果将Wi n M a i n 改为w Wi n M a i n ,就能够访问进程的U n i c o d e 版本命令行。

应用程序可以按照它选择的方法来分析和转换命令行字符串。实际上可以写入p s z C m d L i n e参数指向的内存缓存,但是在任何情况下都不应该写到缓存的外面去。我总是将它视为只读缓存。如果我想修改命令行,首先我要将命令行拷贝到应用程序的本地缓存中,然后再修改本地缓存。也可以获得一个指向进程的完整命令行的指针,方法是调用G e t C o m m a n d L i n e 函数:

PTSTR GetCommandLine();
该函数返回一个指向包含完整命令行的缓存的指针,该命令行包括执行文件的完整路径名。

许多应用程序常常拥有转换成它的各个标记的命令行。使用全局性__argc(或__wargv)变量,应用程序就能访问命令行的各个组成部分。下面这个函数CommandLineToArgvW将Unicode字符串分割成它的各个标记:

PWSTR CommandLineToArgvW(PWSTR pszCmdLine,
   int pNumArgs); 
正如该函数名的结尾处的W 所暗示的那样,该函数只存在于U n i c o d e 版本中(W 是英文单词‘Wi d e ’的缩写)。第一个参数p s z C m d L i n e 指向一个命令行字符串。这通常是较早时调用G e t C o m m a n d L i n e W 而返回的值。P N u m A rg s 参数是个整数地址,该整数被设置为命令行中的参数的数目。C o m m a n d L i n e To A rg v W 将地址返回给一个U n i c o d e 字符串指针的数组。

C o m m a n e L i n e To A rg v W 负责在内部分配内存。大多数应用程序不释放该内存,它们在进程运行终止时依靠操作系统来释放内存。这是完全可行的。但是如果想要自己来释放内存,正确的方法是像下面这样调用H e a p F r e e 函数:

int pNumArgs; 
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(),
   &pNumArgs); 
//Use the arguments...
if (*ppArgv[1] == L'x')
{
   ...
}
//Free the memory block
HeapFree(GetProcessHeap() 0 ppArgv);
4.1.4 进程的环境变量

每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:

VarName1=VarValue1\0 
VarName2=VarValue2\0 
VarName3=VarValue3\0 
... 
VarNameX=VarValueX\0 
\0
每个字符串的第一部分是环境变量的名字,后跟一个等号,等号后面是要赋予变量的值。第4 章进程计计51 下载环境块中的所有字符串都必须按环境变量名的字母顺序进行排序。

由于等号用于将变量名与变量的值分开,因此等号不能是变量名的一部分。另外,变量中的空格是有意义的。例如,如果声明下面两个变量,然后将X Y Z 的值与A B C 的值进行比较,那么系统将报告称,这两个变量是不同的,因为紧靠着等号的前面或后面的任何空格均作为比较时的条件被考虑在内。

XYZ= Windows(Notice the space after the equal sign.) 
   ABC=Windows
例如,如果将下面两个字符串添加给环境块,后面带有空格的环境变量X Y Z 包含H o m e ,而没有空格的环境变量X Y Z 则包含Wo r k 。
XYZ =Home (Notice the space before the equal sign.) 
   XYZ=Work 
最后,必须将一个0 字符置于所有环境变量的结尾处,以表示环境块的结束。

Wi n d o w s 9 8 若要为Windows 98 创建一组初始环境变量,必须修改系统的A u t o E x e c . b a t 文件,将一系列S E T 行放入该文件。每个S E T 行都必须采用下面的形式:

SET VarName=VarValue
当重新引导系统时,A u t o E x e c . b a t 文件的内容被分析,设置的任何环境变量均可供在Windows 98 会话期间启动的任何进程使用。

Windows 2000 当用户登录到Windows 2000 中时,系统创建一个外壳进程并将一组环境字符串与它相关联。通过查看注册表中的两个关键字,系统可以获得一组初始环境字符串。

第一个关键字包含一个适用于系统的所有环境变量的列表:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ SessionManager\Environment

第二个关键字包含适用于当前登录的用户的所有环境变量的列表:

HKEY_CURRENT_USER\Environment

用户可以对这些项目进行增加、删除或修改,方法是选定控制面板的S y s t e m 小应用程序,单击A d v a n c e d 选项卡,再单击Environment Va r i a b l e s 按钮,打开图4 - 2 所示的对话框:

图4-2 使用Environment Variables对话框修改变量

只有拥有管理员权限的用户才能修改系统变量列表中的变量。

应用程序也可以使用各种注册表函数来修改这些注册表项目。但是,若要使这些修改在所有应用程序中生效,用户必须退出系统,然后再次登录。有些应用程序,如E x p l o r e r 、Task Manager 和Control Panel 等,在它们的主窗口收到W M _S E T T I N G C H A N G E 消息时,用新注册表项目来更新它们的环境块。例如,如果要更新注册表项目,并且想让有关的应用程序更新它们的环境块,可以调用下面的代码:

SendMessage(HWND_BROADCAST,WM_SETTINGCHANGE,
   0,(LPARAM)TEXT("Environment"));
通常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么环境变量,后面介绍C r e a t e P r o c e s s 函数时就会看到这个情况。所谓继承,指的是子进程获得它自己的父进程的环境块拷贝,子进程与父进程并不共享相同的环境块。这意味着子进程能够添加、删除或修改它的环境块中的变量,而这个变化在父进程的环境块中却得不到反映。

应用程序通常使用环境变量来使用户能够调整它的行为特性。用户创建一个环境变量并对它进行初始化。然后,当用户启动应用程序运行时,该应用程序要查看环境块,找出该变量。如果找到了变量,它就分析变量的值,调整自己的行为特性。

环境变量存在的问题是,用户难以设置或理解这些变量。用户必须正确地拼写变量的名字,而且必须知道变量值期望的准确句法。另一方面,大多数图形应用程序允许用户使用对话框来调整应用程序的行为特性。这种方法对用户来说更加友好。

如果仍然想要使用环境变量,那么有几个函数可供应用程序调用。使用G e t E n v i r o n m e n t Va r i a b l e 函数,就能够确定某个环境变量是否存在以及它的值:

DWORD GetEnvironmentVariable(
   PCTSTR pszName,
   PTSTR pszValue,
   DWORD cchValue);
当调用G e t E n v i r o n m e n t Va r i a b l e 时,p s z N a m e 指向需要的变量名,p s z Va l u e 指向用于存放变量值的缓存,c c h Va l u e 用于指明缓存的大小(用字符数来表示)。该函数可以返回拷贝到缓存的字符数,如果在环境中找不到该变量名,也可以返回0 。

许多字符串包含了里面可取代的字符串。例如,我在注册表中的某个地方找到了下面的字符串:

%USERPROFILE%\My Documents
百分数符号之间的部分表示一个可取代的字符串。在这个例子中,环境变量的值USERPROFILE 应该被放入该字符串中。在我的计算机中,我的USERPROFILE 环境变量的值是:
C:\Documents and Settings\Administrator
因此,在执行字符串替换后,产生的字符串就成为:
C:\Documents and Settings\Administrator\My Documents
由于这种类型的字符串替换是很常用的,因此Wi n d o w s 提供了E x p a n d E n v i r o n m e n t S t r i n g s 函数:
DWORD ExpandEnvironmentStrings(
   PCSTR pszSrc,
   PSTR pszDst,
   DWORD nSize);
当调用该函数时,p s z S r c 参数是包含可替换的环境变量字符串的这个字符串的地址。p s z D s t参数是接收已展开字符串的缓存的地址,n S i z e 参数是该缓存的最大值(用字符数来表示)。

最后,可以使用S e t E n v i r o n m e n t Va r i a b l e 函数来添加变量、删除变量或者修改变量的值:

BOOL SetEnvironmentVariable(
   PCTSTR pszName,
   PCTSTR pszValue);
该函数用于将p s z N a m e 参数标识的变量设置为p s z Va l u e 参数标识的值。如果带有指定名字的变量已经存在,S e t E n v i r o n m e n t Va r i a b l e 就修改该值。如果指定的变量不存在,便添加该变量,如果p s z Va l u e 是N U L L ,便从环境块中删除该变量。

应该始终使用这些函数来操作进程的环境块。前面讲过,环境块中的字符串必须按变量名的字母顺序来存放,这样,S e t E n v i r o n m e n t Va r i a b l e 就会很容易地找到它们。S e t E n v i r o n m e n t Va r i a b l e 函数具有足够的智能,使环境变量保持有序排列。

4.1.5 进程的亲缘性

一般来说,进程中的线程可以在主计算机中的任何一个C P U 上执行。但是一个进程的线程可能被强制在可用C P U 的子集上运行。这称为进程的亲缘性,将在第7 章详细介绍。子进程继承了父进程的亲缘性。

4.1.6 进程的错误模式

与每个进程相关联的是一组标志,用于告诉系统,进程对严重的错误应该如何作出反映,这包括磁盘介质故障、未处理的异常情况、文件查找失败和数据没有对齐等。进程可以告诉系统如何处理每一种错误。方法是调用S e t E r r o r M o d e 函数:

UINT SetErrorMode(UINT fuErrorMode);
f u E r r o r M o d e 参数是表4 - 3 的任何标志按位用O R 连接在一起的组合。

表4-3 fuError Mode 参数的标志

标志 说明
SEM_FAILCRITICALERRORS 系统不显示关键错误句柄消息框,并将错误返回给调用进程
SEM_NOGOFAULTERRORBOX 系统不显示一般保护故障消息框。本标志只应该由采用异常情况处理程序来处理一般保护(G P)故障的调试应用程序来设定
SEM_NOOPENFILEERRORBOX 当系统找不到文件时,它不显示消息框
SEM_NOALIGNMENTFAULTEXCEPT 系统自动排除内存没有对齐的故障,并使应用程序看不到这些故障。本标志对x 8 6处理器不起作用

默认情况下,子进程继承父进程的错误模式标志。换句话说,如果一个进程的S E M _ N O G P FA U LT E R R O R B O X 标志已经打开,并且生成了一个子进程,该子进程也拥有这个打开的标志。但是,子进程并没有得到这一情况的通知,它可能尚未编写以便处理G P 故障的错误。如果G P 故障发生在子进程的某个线程中,该子进程就会终止运行,而不通知用户。父进程可以防止子进程继承它的错误模式,方法是在调用C r e a t e P r o c e s s 时设定C R E AT E _ D E FA U LT _ E R R O R _ M O D E 标志(本章后面部分的内容将要介绍C r e a t e P r o c e s s 函数)。

4.1.7 进程的当前驱动器和目录

当不提供全路径名时,Wi n d o w s 的各个函数就会在当前驱动器的当前目录中查找文件和目录。例如,如果进程中的一个线程调用C r e a t e F i l e 来打开一个文件(不设定全路径名),那么系统就会在当前驱动器和目录中查找该文件。

系统将在内部保持对进程的当前驱动器和目录的跟踪。由于该信息是按每个进程来维护的,因此改变当前驱动器或目录的进程中的线程,就可以为该进程中的所有线程改变这些信息。

通过调用下面两个函数,线程能够获得和设置它的进程的当前驱动器和目录:

DWORD GetCurrentDirectory(
   DWORD cchCurDir,
   PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);
4.1.8 进程的当前目录

系统将对进程的当前驱动器和目录保持跟踪,但是它不跟踪每个驱动器的当前目录。不过,有些操作系统支持对多个驱动器的当前目录的处理。这种支持是通过进程的环境字符串来提供的。例如,进程能够拥有下面所示的两个环境变量:

=C:=C:\Utility\Bin
=D:=D:\Program files
这些变量表示驱动器C 的进程的当前目录是\ U t i l i t y \ B i n ,并且指明驱动器D 的进程的当前目录是\Program Files。

如果调用一个函数,传递一个驱动器全限定名,以表示一个驱动器不是当前驱动器,那么系统就会查看进程的环境块,找出与指定驱动器名相关的变量。如果该驱动器的变量存在,系统将该变量的值用作当前驱动器。如果该变量不存在,系统将假设指定驱动器的当前目录是它的根目录。

例如,如果进程的当前目录是C : \ U t i l i t y | B i n ,并且你调用C r e a t e F i l e 来打开D : R e a d M e . T x t ,那么系统查看环境变量= D 。因为= D 变量存在,因此系统试图从D:\Program Files 目录打开该R e a d M e . T x t 文件。如果= D 变量不存在,系统将试图从驱动器D 的根目录来打开R e a d M e . T x t 。Wi n d o w s 的文件函数决不会添加或修改驱动器名的环境变量,它们只是读取这些变量。

注意可以使用C 运行期函数_ c h d i r ,而不是使用Wi n d o w s 的S e t C u r r e n t D i r e c t o r y 函数来变更当前目录。_ c h d i r 函数从内部调用S e t C u r r e n t D i r e c t o r y ,但是_chdir 也能够添加或修改该环境变量,这样,不同驱动器的当前目录就可以保留。

如果父进程创建了一个它想传递给子进程的环境块,子进程的环境块不会自动继承父进程的当前目录。相反,子进程的当前目录将默认为每个驱动器的根目录。如果想要让子进程继承父进程的当前目录,该父进程必须创建这些驱动器名的环境变量。并在生成子进程前将它们添加给环境块。通过调用G e t F u l l P a t h N a m e ,父进程可以获得它的当前目录:

DWORD GetFullPathName(
   PCTSTR pszFile,
   DWORD cchPath,
   PTSTR pszPath,
   PTSTR *ppszFilePart);
例如,若要获得驱动器C 的当前目录,可以像下面这样调用G e t F u l l P a t h N a m e :
TCHAR szCurDir[MAX_PATH];
   DWORD GetFullPathName(TEXT("C:"),
   MAX_PATH,szCurDir,NULL);
记住,进程的环境变量必须始终按字母顺序来排序。因此驱动器名的环境变量通常必须置于环境块的开始处。

4.1.9 系统版本

应用程序常常需要确定用户运行的是哪个Wi n d o w s 版本。例如,通过调用安全性函数,应用程序就能利用它的安全特性。但是这些函数只有在Windows 2000 上才能得到全面的实现。

Windows API 拥有下面的G e t Ve r s i o n 函数:

DWORD GetVersion();
该函数已经有相当长的历史了。最初它是为1 6 位Wi n d o w s 设计的。它的作用很简单,在高位字中返回M S - D O S 版本号,在低位字中返回Wi n d o w s 版本号。对于每个字来说,高位字节代表主要版本号,低位字节代表次要版本号。

但是,编写该代码的程序员犯了一个小小的错误,函数的编码结果使得Wi n d o w s 的版本号颠倒了,即主要版本号位于低位字节,而次要版本号位于高位字节。由于许多程序员已经开始使用该函数,M i c r o s o f t 不得不保持函数的原样,并修改了文档,以说明这个错误。

由于围绕着G e t Ve r s i o n 函数存在着各种混乱,因此M i c r o s o f t 增加了一个新函数G e t Ve r s i o n E x :

BOOL GetVersionEx(POSVERSIONINFO pVersionInformation);
该函数要求在应用程序中指定一个O S V E R S I O N I N F O E X 结构,并将该结构的地址传递给G e t Ve r s i o n E x 。O S V E R S I O N I N F O E X 的结构如下所示:
typedef struct
{
   DWORD dwOSVersionInfoSize;
   DWORD dwMajorVersion;
   DWORD dwMinorVersion;
   DWORD dwBuildNumber;
   DWORD dwPlatformId;
   TCHAR szCSDVersion[128];
   WORD wServicePackMajor;
   WORD wServicePackMinor;
   WORD wSuiteMask;
   BYTE wProductType;
   BYTE wReserved;
} OSVERSIONINFOEX, *POSVERSIONINFOEX;
O S V E R S I O N I N F O E X 结构在Windows 2000 中是个新结构。Wi n d o w s 的其他版本使用较老的O S V E R S I O N I N F O 结构,它没有服务程序包、程序组屏蔽、产品类型和保留成员。

注意,对于系统的版本号中的每个成分来说,该结构拥有不同的成员。这样做的目的是,程序员不必提取低位字、高位字、低位字节和高位字节,因此应用程序能够更加容易地对它们期望的版本号与主机系统的版本号进行比较。表4 - 4 描述了O S V E R S I O N I N F O E X 结构的成员。

表4-4 OSVERSIONINFOEX 结构的成员
成员 描述
dwOSVersionInfoSize 在调用GetVersionEx 函数之前,必须置为sizeof(OSVERSIONINFO)或sizeof(OSVERSIONINFOEX)
dwMajorVersion 主系统的主要版本号
dwMinorVersion 主系统的次要版本号
dwBuildNumber 当前系统的构建号
dwPlatformId 用于标识当前系统支持的平台。它可以是VER_PLATFORM_WIN32s (Win32s), VER_PLATFORM_WIN32_WINDOWS (Windows 95/Windows 98), VER_PLATFORM_WIN32_NT (Windows NT/Windows 2000), or VER_PLATFORM_WIN32_CEHH (Windows CE).
szCSDVersion 本域包含了附加文本,用于提供关于已经安装的操作系统的详细信息
wServicePackMajor 最新安装的服务程序包的主要版本号
wServicePackMinor 最新安装的服务程序包的次要版本号
wSuiteMask 用于标识系统上存在哪个程序组
(VER_SUITE_SMALLBUSINESS,
VER_SUITE_ENTERPRISE,
VER_SUITE_BACKOFFICE,
VER_SUITE_COMMUNICATIONS,
VER_SUITE_TERMINAL,
VER_SUITE_SMALLBUSINESS_RESTRICTED,
VER_SUITE_EMBEDDEDNT,and
VER_SUITE_DATACENTER).
wProductType 用于标识安装了下面的哪个操作系统:VER_NT_WORKSTATION, VER_NT_SERVER, or VER_NT_DOMAIN_CONTROLLER.
wReserved 留作将来使用

为了使操作更加容易,Windows 2000 提供了一个新的函数,即Ve r i f y Ve r s i o n I n f o ,用于对主机系统的版本与你的应用程序需要的版本进行比较:

BOOL VerifyVersionInfo(
   POSVERSIONINFOEX pVersionInformation,
   DWORD dwTypeMask,
   DWORDLONG dwlConditionMask);
若要使用该函数,必须指定一个O S V E R S I O N I N F O E X 结构,将它的d w O S Ve r s i o n I n f o S i z e 成员初始化为该结构的大小,然后对该结构中的其他成员(这些成员对你的应用程序来说很重要)进行初始化。当调用Ve r i f y Ve r s i o n I n f o 时,d w Ty p e M a s k 参数用于指明该结构的哪些成员已经进行了初始化。d w Ty p e M a s k 参数是用O R 连接在一起的下列标志中的任何一个标志:
V E R _ M I N O RV E R S I O N ,V E R _ M A J O RV E R S I O N ,V E R _ B U I L D N U M B E R ,V E R _ P L AT F O R M I D ,VER_ SERV I C E PA C K M I N O R ,V E R _ S E RV I C E PA C K M A J O R ,V E R _ S U I T E N A M E ,VER_PRODUCT_ TYPE 。最后一个参数d w l C o n d i t i o n M a s k 是个6 4 位值,用于控制该函数如何将系统的版本信息与需要的信息进行比较。

d w l C o n d i t i o n M a s k 描述了如何使用一组复杂的位组合进行的比较。若要创建需要的位组合,可以使用V E R _ S E T _ C O N D I T I O N 宏:

VER_SET_CONDITION(
   DWORDLONG dwlConditionMask,
   ULONG dwTypeBitMask,
   ULONG dwConditionMask)
第一个参数d w l C o n d i t i o n M a s k 用于标识一个变量,该变量的位是要操作的那些位。请注意,不必传递该变量的地址,因为V E R _ S E T _ C O N D I T I O N 是个宏,不是一个函数。d w Ty p e B i t M a s k 参数用于指明想要比较的O S V E R S I O N I N F O E X 结构中的单个成员。若要比较多个成员,必须多次调用V E R _ S E T _ C O N D I T I O N 宏,每个成员都要调用一次。传递给Ve r i f y Ve r s i o n I n f o 的d w Ty p e M a s k 参数(V E R _ M I N O RV E R S I O N ,V E R _ B U I L D N U M B E R 等)的标志与用于V E R _ S E T _ C O N D I T I O N 的d w Ty p e B i t M a s k 参数的标志是相同的。

V E R _ S E T _ C O N D I T I O N 的最后一个参数d w C o n d i t i o n M a s k 用于指明想如何进行比较。它可以是下列值之一:V E R _ E Q U A L ,V E R _ G R E AT E R ,V E R _ G R E AT E R _ E Q U A L ,V E R _ L E S S 或V E R _ L E S S _ E Q U A L 。请注意,当比较V E R _ P R O D U C T _ T Y P E 信息时,可以使用这些值。例如,V E R _ N T _ W O R K S TAT I O N 小于V E R _ N T _ S E RV E R 。但是对于V E R _ S U I T E N A M E 信息来说,不能使用这些测试值。相反,必须使用V E R _ A N D (所有程序组都必须安装)或V E R _ O R (至少必须安装程序组产品中的一个产品)。

当建立一组条件后,可以调用Ve r i f y Ve r s i o n I n f o 函数,如果调用成功(如果主机系统符合应用程序的所有要求),则返回非零值。如果Ve r i f y Ve r s i o n I n f o 返回0 ,那么主机系统不符合要求,或者表示对该函数的调用不正确。通过调用G e t L a s t E r r o r 函数,就能确定该函数为什么返回0 。如果G e t L a s t E r r o r 返回E R R O R _ O L D _ W I N _ V E R S I O N ,那么对该函数的调用是正确的,但是系统没有满足要求。

下面是如何测试主机系统是否正是Windows 2000 的一个例子:

//Prepare the OSVERSIONINFOEX structure 
//to indicate Windows 2000.
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMajorVersion = 5;

osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;

//Prepare the condition mask.
//You MUST initialize this to 0.
DWORDLONG dwlConditionMask = 0;     

VER_SET_CONDITION(dwlConditionMask,
   VER_MAJORVERSION,VER_EQUAL);
   
VER_SET_CONDITION(dwlConditionMask,
   VER_MINORVERSION,VER_EQUAL);
   
VER_SET_CONDITION(dwlConditionMask,
   VER_PLATFORMID,VER_EQUAL);

//Perform the version test.
if(VerifyVersionInfo(&osver,
   VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID,
   dwlConditionMask))
{
   //The host system is windows 2000 exactly.
}
else
{
   //The host system is NOT windows 2000.
}

4.2 CreateProcess函数

可以用C r e a t e P r o c e s s 函数创建一个进程:

BOOL CreateProcess(
   PCTSTR pszApplicationName,
   PTSTR pszCommandLine,
   PSECURITY_ATTRIBUTES psaProcess,
   PSECURITY_ATTRIBUTES psaThread,
   BOOL bInheritHandles,
   DWORD fdwCreate,
   PVOID pvEnvironment,
   PCTSTR pszCurDir,
   PSTARTUPINFO psiStartInfo,
   PPROCESS_INFORMATION ppiProcInfo);
当一个线程调用CreateProcess时,系统就会创建一个进程内核对象,其初始使用计数是1 。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的D L L 文件的代码和数据加载到该进程的地址空间中。

然后,系统为新进程的主线程创建一个线程内核对象(其使用计数为1 )。与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行C / C + +运行期启动代码,该主线程便开始运行,它最终调用Wi n M a i n 、w Wi n M a i n 、m a i n 或w m a i n 函数。如果系统成功地创建了新进程和主线程,C r e a t e P r o c e s s 便返回T R U E 。

注意在进程被完全初始化之前,C r e a t e P r o c e s s 返回T R U E 。这意味着操作系统加载程序尚未试图找出所有需要的D L L 。如果一个D L L 无法找到,或者未能正确地初始化,那么该进程就终止运行。由于C r e a t e P r o c e s s 返回T R U E ,因此父进程不知道出现的任何初始化问题。

这就是总的概述。下面各节将分别介绍C r e a t e P r o c e s s 的各个参数。

4.2.1 pszApplicationName和pszCommandLine

pszApplicationName和pszCommandLine参数分别用于设定新进程将要使用的可执行文件的名字和传递给新进程的命令行字符串。下面首先让我们谈一谈p s z C o m m a n d L i n e 参数。

注意请注意,p s z C o m m a n d L i n e 参数的原型是P T S T R 。这意味着C r e a t e P r o c e s s 期望你将传递一个非常量字符串的地址。从内部来讲,C r e a t e P r o c e s s 实际上并不修改你传递给它的命令行字符串。不过,在C r e a t e P r o c e s s 返回之前,它将该字符串恢复为它的原始形式。

这个问题很重要,因为如果命令行字符串不包含在文件映象的只读部分中,就会出现违规访问的问题。例如,下面的代码就会导致违规访问的问题,因为Visual C++将“N O T E PA D ”字符串放入了只读内存:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(NULL,
   TEXT("NOTEPAD"),
   NULL,NULL,
   FALSE,0,NULL,NULL,
   &si,π);
当C r e a t e P r o c e s s 试图修改该字符串时,就会发生违规访问(较早的Visual C++版本将该字符串放入读/写内存,因此调用C r e a t e P r o c e s s 不会导致违规访问的问题)。

解决这个问题的最好办法是在调用C r e a t e P r o c e s s 之前像下面这样将常量字符串拷贝到临时缓存中:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szCommandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL,szCommandLine,NULL,NULL,
   FALSE,0,NULL,NULL,&si,π);
也可以考虑使用Visual C++的/ G f 和/ G F 编译器开关,这些开关用于控制重复字符串的删除和确定这些字符串是否被放入只读内存部分(另外请注意,/ Z I 开关允许使用Visual Studio 的Edit &Continue 调试特性,它包含了/ G F 开关的功能)。能做的最好工作是使用/ G F 编译器开关和临时缓存。M i c r o s o f t 能做的最好事情是安装好C r e a t e - P r o c e s s ,使它能够制作一个该字符串的临时拷贝,这样我们就不必进行这项操作。也许将来的Wi n d o w s 版本能够做到这一点。

另外,如果调用Windows 2000 上的C r e a t e P r o c e s s 的A N S I 版本,就不会违规访问,因为系统已经制作了一个命令行字符串的临时拷贝(详细信息请见第2 章)。

可以使用p s z C o m m a n d L i n e 参数设定一个完整的命令行,以便C r e a t e P r o c e s s 用来创建新进程。当C r e a t e P r o c e s s 分析p s z C o m m a n d L i n e 字符串时,它将查看字符串中的第一个标记,并假设该标记是想运行的可执行文件的名字。如果可执行文件的文件名没有扩展名,便假设它的扩展名为. e x e 。C r e a t e P r o c e s s 也按下面的顺序搜索该可执行文件:

1) 包含调用进程的. e x e 文件的目录。

2) 调用进程的当前目录。

3) Wi n d o w s 的系统目录。

4) Wi n d o w s 目录。

5) PAT H 环境变量中列出的目录。

当然,如果文件名包含全路径,系统将使用全路径来查看可执行文件,并且不再搜索这些目录。如果系统找到了可执行文件,那么它就创建一个新进程,并将可执行文件的代码和数据映射到新进程的地址空间中。然后系统将调用C / C + +运行期启动例程。正如前面我们讲过的那样,C / C + +运行期启动例程要查看进程的命令行,并将地址作为( w ) Wi n M a i n 的p s z C m d L i n e 参数传递给可执行文件的名字后面的第一个参数。

这一切都是在p s z A p p l i c a t i o n N a m e 参数是N U L L (9 9 %以上的时候都应该属于这种情况)时发生的。如果不传递N U L L ,可以将地址传递给p s z A p p l i c a t i o n N a m e 参数中包含想运行的可执行文件的名字的字符串。请注意,必须设定文件的扩展名,系统将不会自动假设文件名有一个. e x e 扩展名。C r e a t e P r o c e s s 假设该文件位于当前目录中,除非文件名前面有一个路径。如果在当前目录中找不到该文件,C r e a t e P r o c e s s 将不会在任何其他目录中查找该文件,它运行失败了。

但是,即使在p s z A p p l i c a t i o n N a m e 参数中设定了文件名,C r e a t e P r o c e s s 也会将p s z C o m m a n d L i n e 参数的内容作为它的命令行传递给新进程。例如,可以像下面这样调用C r e a t e P r o c e s s :

//Make sure that the path is in a
//read/write section of memory.
TCHAR szPath[] = TEXT("WORDPAD README.TXT");

//Spawnthe new process.
CreateProcess(
   TEXT("C:\\WINNT\\SYSTEM32\\NOTEPAD.EXE"),
   szPath,...);
系统启动N o t e p a d 应用程序,但是N o t e p a d 的命令行是W O R D PAD README.TXT 。这种变异情况当然有些奇怪,不过这正是C r e a t e P r o c e s s 运行的样子。这个由p s z A p p l i c a t i o n N a m e 参数提供的能力实际上被添加给了C r e a t e P r o c e s s ,以支持Windows 2000 的P O S I X 子系统。

4.2.2 psaProcess 、psaThread和binheritHandles

若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程),由于这些都是内核对象,因此父进程可以得到机会将安全属性与这两个对象关联起来。可以使用p s a P r o c e s s 和p s a T h r e a d 参数分别设定进程对象和线程对象需要的安全性。可以为这些参数传递N U L L ,在这种情况下,系统为这些对象赋予默认安全性描述符。也可以指定两个S E C U R I T Y _ AT T R I B U T E S 结构,并对它们进行初始化,以便创建自己的安全性权限,并将它们赋予进程对象和线程对象。

将S E C U R I T Y _ AT T R I B U T E S 结构用于p s a P r o c e s s 和p s a T h r e a d 参数的另一个原因是,父进程将来生成的任何子进程都可以继承这两个对象句柄中的任何一个(第3 章已经介绍了内核对象句柄的继承性的有关理论)。

清单4 - 1 显示了一个说明内核对象继承性的简单程序。假设Process A 创建了Process B ,方法是调用C r e a t e P r o c e s s ,为p s a P r o c e s s 参数传递一个S E C U R I T Y _ AT T R I B U T E S 结构的地址,在这个结构中,b I n h e r i t H a n d l e s 成员被置为T R U E 。在同样这个函数调用中,p s a T h r e a d 参数指向另一个S E C U R I T Y _ AT T R I B U T E S 结构,在这个结构中,b I n h e r i t H a n d l e s 成员被置为FA L S E 。

当系统创建Process B 时,它同时指定一个进程内核对象和一个线程内核对象,并且将句柄返回给p p i P r o c I n f o 参数(很快将介绍该参数)指向的结构中的Process A 。这时,使用这些句柄,Process A 就能够对新创建的进程对象和线程对象进行操作。

现在,假设Process A 第二次调用C r e a t e P r o c e s s 函数,以便创建Process C 。Process A 可以决定是否为Process C 赋予对Process A 能够访问的某些内核对象进行操作的能力。B I n h e r i t H a n d l e s参数可以用于这个目的。如果b I n h e r i t H a n d l e s 被置为T R U E ,系统就使Process C 继承Process A中的任何可继承句柄。在这种情况下,Process B 的进程对象的句柄是可继承的。无论C r e a t e P r o c e s s 的b I n h e r i t H a n d l e s 参数的值是什么,Process B 的主线程对象的句柄均不能继承。同样,如果Process A 调用C r e a t e P r o c e s s ,为b I n h e r i t H a n d l e s 传递FA L S E ,那么Process C 将不能继承Process A 目前使用的任何句柄。

清单4-1 内核对象句柄继承性的一个示例

/************************************************************
Module name: Inherit.c
Notices:Copyright(c)2000 Jeffrey Richter
************************************************************/
#include <Windows.h>

int WINAPI WinMain (HINSTANCE hinstExe,HINSTANCE,
   PSTR pszCmdLine,int nCmdShow)
{

   //Prepare a STARTUPINFO structure 
   //for spawning processes.
   STARTUPINFO si = { sizeof(si) };
   SECURITY_ATTRIBUTES saProcess,saThread;
   PROCESS_INFORMATION piProcessB,piProcessC;
   TCHAR szPath[MAX_PATH];

   //Prepare to spawn Process B from Process A.
   //The handle identifying the new process
   //object should be inheritable.
   saProcess.nLength = sizeof(saProcess);
   saProcess.lpSecurityDescriptor = NULL;
   saProcess.bInheritHandle = TRUE;

   //The handle identifying the new thread
   //object should NOT be inheritable.
   saThread.nLength = sizeof(saThread);
   saThread.lpSecurityDescriptor = NULL;
   saThread.bInheritHandle = FALSE;

   //Spawn Process B.
   lstrcpy(szPath,TEXT("ProcessB"));
   
   CreateProcess(NULL,szPath,
      &saProcess,&saThread,
      FALSE,0,NULL,NULL,&si,
      &piProcessB);

   //The pi structure contains two handles
   //relative to Process A:
   //hProcess,which identifies Process B's process
   //object and is inheritable; and hThread,
   //which identifies Process B's primary thread
   //object and is NOT inheritable.

   //Prepare to spawn Process C from Process A.
   //Since NULL is passed for the psaProcess and
   //psaThread parameters,the handles to Process C's
   //process and primary thread objects default
   //to "noninheritable."

   //If Process A were to spawn another process,
   //this new process would NOT inherit handles 
   //to Process C's process and thread objects.

   //Because TRUE is passed for the bInheritHandles
   //parameter,Process C will inherit the handle that
   //identifies Process B's process object but will 
   //not inherit a handle to Process B's primary
   //thread object.

   lstrcpy(szPath,TEXT("ProcessC"));
   
   CreateProcess(NULL,szPath,NULL,NULL,
      TRUE,0,NULL,NULL,&si,&piProcessC);

   return(0);
}
4.2.3 fdwCreate

f d w C r e a t e 参数用于标识标志,以便用于规定如何来创建新进程。如果将标志逐位用O R 操作符组合起来的话,就可以设定多个标志。

• E B U G _ P R O C E S S 标志用于告诉系统,父进程想要调试子进程和子进程将来生成的任何进程。本标志还告诉系统,当任何子进程(被调试进程)中发生某些事件时,将情况通知父进程(这时是调试程序)。

• D E B U G _ O N LY _ T H I S _ P R O C E S S 标志与D E B U G _ P R O C E S S 标志相类似,差别在于,调试程序只被告知紧靠父进程的子进程中发生的特定事件。如果子进程生成了别的进程,那么将不通知调试程序在这些别的进程中发生的事件。

• C R E AT E _ S U S P E N D E D 标志可导致新进程被创建,但是,它的主线程则被挂起。这使得父进程能够修改子进程的地址空间中的内存,改变子进程的主线程的优先级,或者在进程有机会执行任何代码之前将进程添加给一个作业。一旦父进程修改了子进程,父进程将允许子进程通过调用R e s u m e T h r e a d 函数来执行代码(第7 章将作详细介绍)。

• D E TA C H E D _ P R O C E S S 标志用于阻止基于C U I 的进程对它的父进程的控制台窗口的访问,并告诉系统将它的输出发送到新的控制台窗口。如果基于C U I 的进程是由另一个基于C U I的进程创建的,那么按照默认设置,新进程将使用父进程的控制台窗口(当通过命令外壳程序来运行C 编译器时,新控制台窗口并不创建,它的输出将被附加在现有控制台窗口的底部)。通过设定本标志,新进程将把它的输出发送到一个新控制台窗口。

• C R E AT E _ N E W _ C O N S O L E 标志负责告诉系统,为新进程创建一个新控制台窗口。如果同时设定C R E AT E _ N E W _ C O N S O L E 和D E TA C H E D _ P R O C E S S 标志,就会产生一个错误。

• C R E AT E _ N O _ W I N D O W 标志用于告诉系统不要为应用程序创建任何控制台窗口。可以使用本标志运行一个没有用户界面的控制台应用程序。

• C R E AT E _ N E W _ P R O C E S S _ G R O U P 标志用于修改用户在按下C t r l + C 或C t r l + B r e a k 键时得到通知的进程列表。如果在用户按下其中的一个组合键时,你拥有若干个正在运行的C U I 进程,那么系统将通知进程组中的所有进程说,用户想要终止当前的操作。当创建一个新的C U I 进程时,如果设定本标志,可以创建一个新进程组。如果该进程组中的一个进程处于活动状态时用户按下C t r l + C 或C t r l _ B r e a k 键,那么系统只通知用户需要这个进程组中的进程。

• C R E AT E _ D E FA U LT _ E R R O R _ M O D E 标志用于告诉系统,新进程不应该继承父进程使用的错误模式(参见本章前面部分中介绍的S e t E r r o r M o d e 函数)。

• C R E AT E _ S E PA R AT E _ W O W _ V D M 标志只能当你在Windows 2000 上运行1 6 位Wi n d o w s 应用程序时使用。它告诉系统创建一个单独的D O S 虚拟机(V D M ),并且在该V D M 中运行1 6 位Wi n d o w s 应用程序。按照默认设置,所有1 6 位Wi n d o w s 应用程序都在单个共享的V D M 中运行。在单独的VDM 中运行应用程序的优点是,如果应用程序崩溃,它只会使单个V D M 停止工作,而在别的V D M 中运行的其他程序仍然可以继续正常运行。另外,在单独的V D M 中运行的1 6 位Wi n d o w s 应用程序有它单独的输入队列。这意味着如果一个应用程序临时挂起,在各个V D M 中的其他应用程序仍然可以继续接收输入信息。运行多个V D M 的缺点是,每个V D M 都要消耗大量的物理存储器。Windows 98 在单个V D M 中运行所有的1 6 位Wi n d o w s 应用程序,不能改变这种情况。

• C R E AT E _ S H A R E D _ W O W _ V D M 标志只能当你在Windows 2000 上运行1 6 位Wi n d o w s 应用程序时使用。按照默认设置,除非设定了C R E AT E _ S E PA R AT E _ W O W _ V D M 标志,否则所有1 6 位Wi n d o w s 应用程序都必须在单个V D M 中运行。但是,通过在注册表中将H K E Y _ L O C A L _ M A C H I N E \ s y s t e m \ C u r r e n t C o n t r o l S e t \ C o n t r o l \ W O W 下的D e f a u l t S e p a r a t e V D M 设置为“y e s ”,就可以改变该默认行为特性。这时,C R E AT E _ S H A R E D _W O W _ V D M 标志就在系统的共享V D M 中运行1 6 位Wi n d o w s 应用程序。

• C R E AT E _ U N I C O D E _ E N V I R O N M E N T 标志用于告诉系统,子进程的环境块应该包含U n i c o d e 字符。按照默认设置,进程的环境块包含的是A N S I 字符串。

• C R E AT E _ F O R C E D O S 标志用于强制系统运行嵌入1 6 位O S / 2 应用程序的M O S - D O S 应用程序。

• C R E AT E _ B R E A K AWAY _ F R O M _ J O B 标志用于使作业中的进程生成一个与作业相关联的新进程(详细信息见第5 章)。

f d w C r e a t e 参数也可以用来设定优先级类。不过用不着这样做,并且对于大多数应用程序来说不应该这样做,因为系统会为新进程赋予一个默认优先级。表4 - 5 显示了各种可能的优先级类别。

表4-5 优先级类别

优先级类别 标志的标识符
空闲 IDLE_PRIORITY_CLASS
低于正常 BELOW_NORMAL_PRIORITY_CLASS
正常 NORMAL_PRIORITY_CLASS
高于正常 ABOVE_NORMAL_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
实时 REALTIME_PRIORITY_CLASS

这些优先级类将会影响进程中包含的线程如何相对于其他进程的线程来进行调度。详细说明请见第7 章。

注意B E L O W _ N O R M A L _ P R I O R I T Y _ C L A S S 和A B O V E _ N O R M A L _ P R I O R I T Y _ C L A S S这两个优先级类在Windows 2000 中是新类,Windows NT 4(或更早的版本)、Windows 95或Windows 98 均不支持这两个类。

4.2.4 pvEnvironment

p v E n v i r o n m e n t 参数用于指向包含新进程将要使用的环境字符串的内存块。在大多数情况下,为该参数传递N U L L ,使子进程能够继承它的父进程正在使用的一组环境字符串。也可以使用G e t E n v i r o n m e n t S t r i n g s 函数:

PVOID GetEnvironmentStrings();
该函数用于获得调用进程正在使用的环境字符串数据块的地址。可以使用该函数返回的地址,作为C r e a t e P r o c e s s 的p v E n v i r o n m e n t 参数。如果为p v E n v i r o n m e n t 参数传递N U L L ,那么这正是C r e a t e P r o c e s s 函数所做的操作。当不再需要该内存块时,应该调用F r e e E n v i r o n m e n t S t r i n g s 函数将内存块释放:
BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock);
4.2.5 pszCurDir

p s z C u r D i r 参数允许父进程设置子进程的当前驱动器和目录。如果本参数是N U L L ,则新进程的工作目录将与生成新进程的应用程序的目录相同。如果本参数不是N U L L ,那么p s z C u r D i r必须指向包含需要的工作驱动器和工作目录的以0 结尾的字符串。注意,必须设定路径中的驱动器名。

4.2.6 psiStartInfo

p s i S t a r t I n f o 参数用于指向一个S TA RT U P I N F O 结构:

typedef struct _STARTUPINFO
{
   DWORD cb;
   PSTR lpReserved;
   PSTR lpDesktop;
   PSTR lpTitle;
   DWORD dwX;
   DWORD dwY;
   DWORD dwXSize;
   DWORD dwYSize;
   DWORD dwXCountChars;
   DWORD dwYCountChars;
   DWORD dwFillAttribute;
   DWORD dwFlags;
   WORD wShowWindow;
   WORD cbReserved2;
   PBYTE lpReserved2;
   HANDLE hStdInput;
   HANDLE hStdOutput;
   HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
当Wi n d o w s 创建新进程时,它将使用该结构的有关成员。大多数应用程序将要求生成的应用程序仅仅使用默认值。至少应该将该结构中的所有成员初始化为零,然后将c b 成员设置为该结构的大小:
STARTUPINFO si = { sizeof(si) };
CreateProcess(...,&si,...);
如果未能将该结构的内容初始化为零,那么该结构的成员将包含调用线程的堆栈上的任何无用信息。将该无用信息传递给C r e a t e P r o c e s s ,将意味着有时会创建新进程,有时则不能创建新进程,完全取决于该无用信息。有一点很重要,那就是将该结构的未用成员设置为零,这样,C r e a t e P r o c e s s 就能连贯一致地运行。不这样做是开发人员最常见的错误。

这时,如果想要对该结构的某些成员进行初始化,只需要在调用C r e a t e P r o c e s s 之前进行这项操作即可。我们将依次介绍每个成员。有些成员只有在子应用程序创建一个重叠窗口时才有意义,而另一些成员则只有在子应用程序执行基于C U I 的输入和输出时才有意义。表4 - 6 描述了每个成员的作用。

表4-6 STARTUPINFO 结构的成员
成员 窗口,控制台还是两者兼有 作用
cb 两者兼有 包含S TA RT U P I N F O 结构中的字节数。如果M i c r o s o f t 将来扩展该结构,它可用作版本控制手段。应用程序必须将c b 初始化为s i z e o f ( S TA RT U P I N F O )
lpReserved 两者兼有 保留。必须初始化为N U L L
lpDesktop 两者兼有 用于标识启动应用程序所在的桌面的名字。如果该桌面存在,新进程便与指定的桌面相关联。如果桌面不存在,便创建一个带有默认属性的桌面,并使用为新进程指定的名字。如果l p D e s k t o p 是N U L L (这是最常见的情况),那么该进程将与当前桌面相关联
lpTitle 控制台 用于设定控制台窗口的名称。如果l p Ti t l e 是N U L L ,则可执行文件的名字将用作窗口名
dwX
dwY
两者兼有 用于设定应用程序窗口在屏幕上应该放置的位置的x 和y 坐标(以像素为单位)。只有当子进程用C W _ U S E D E FA U LT 作为C r e a t e Wi n d o w 的x 参数来创建它的第一个重叠窗口时,才使用这两个坐标。若是创建控制台窗口的应用程序,这些成员用于指明控制台窗口的左上角
dwXSize 两者兼有 用于设定应用程序窗口的宽度和长度(以像素为单位)只有dwYsize 当子进程将C W _ U S E D E FA U LT 用作C r e a t e Wi n d o w 的n Wi d t h参数来创建它的第一个重叠窗口时,才使用这些值。若是创建控制台窗口的应用程序,这些成员将用于指明控制台窗口的宽度
dwXCountChars
dwYCountChars
控制台 用于设定子应用程序的控制台窗口的宽度和高度(以字符为单位)
dwFillAttribute 控制台 用于设定子应用程序的控制台窗口使用的文本和背景颜色
dwFlags 两者兼有 请参见下一段和表4 - 7 的说明
wShowWindow 窗口 用于设定如果子应用程序初次调用的S h o w Wi n d o w 将S W _ S H O W D E FA U LT 作为n C m d S h o w 参数传递时,该应用程序的第一个重叠窗口应该如何出现。本成员可以是通常用于Show Wi n d o w 函数的任何一个S W _ *标识符
cbReserved2 两者兼有 保留。必须被初始化为0
lpReserved2 两者兼有 保留。必须被初始化为N U L L
hStdInput
hStdOutput
hStdError
控制台 用于设定供控制台输入和输出用的缓存的句柄。按照默认设置,h S t d I n p u t 用于标识键盘缓存,h S t d O u t p u t 和h S t d E r r o r用于标识控制台窗口的缓存

现在介绍d w F l a g s 的成员。该成员包含一组标志,用于修改如何来创建子进程。大多数标志只是告诉C r e a t e P r o c e s s ,S TA RT U P I N F O 结构的其他成员是否包含有用的信息,或者某些成员是否应该忽略。表4 - 7 标出可以使用的标志及其含义。

表4-7 使用标志及含义
标志 含义
STARTF_USESIZE 使用d w X S i z e 和d w Y S i z e 成员
STARTF_USESHOWWINDOW 使用w S h o w Wi n d o w 成员
STARTF_USEPOSITION 使用d w X 和d w Y 成员
STARTF_USECOUNTCHARS 使用d w X C o u n t C h a r s 和dwYCount Chars 成员
STARTF_USEFILLATTRIBUTE 使用d w F i l l A t t r i b u t e 成员
STARTF_USESTDHANDLES 使用h S t d I n p u t 、h S t d O u t p u t 和h S t d E r r o r 成员
STARTF_RUN_FULLSCREEN 强制在x 8 6 计算机上运行的控制台应用程序以全屏幕方式启动运行

另外还有两个标志,即STARTF_FORCEONFEEDBACK 和STARTF_+FORCEOFFF -EEDBACK ,当启动一个新进程时,它们可以用来控制鼠标的光标。由于Windows支持真正的多任务抢占式运行方式,因此可以启动一个应用程序,然后在进程初始化时,使用另一个程序。为了向用户提供直观的反馈信息,C r e a t e P r o c e s s 能够临时将系统的箭头光标改为一个新光标,即沙漏箭头光标:

该光标表示可以等待出现某种情况,也可以继续使用系统。当启动另一个进程时,CreateProcess函数使你能够更好地控制光标。当设定STARTF_FORCEONFEEDBACK标志时,C r e a t e P r o c e s s 并不将光标改为沙漏。

STARTF_FORCEONFEEDBACK可使CreateProcess能够监控新进程的初始化,并可根据结果来改变光标。当使用该标志来调用CreateProcess时,光标改为沙漏。过2 s 后,如果新进程没有调用G U I ,CreateProcess 将光标恢复为箭头。

如果该进程在2 s 内调用了GUI ,CreateProcess将等待该应用程序显示一个窗口。这必须在进程调用G U I 后5 s 内发生。如果没有显示窗口,CreateProcess就会恢复原来的光标。如果显示了一个窗口,CreateProcess将使沙漏光标继续保留5 s 。如果某个时候该应用程序调用了G e t M e s s a g e 函数,指明它完成了初始化,那么C r e a t e P r o c e s s 就会立即恢复原来的光标,并且停止监控新进程。

在结束这一节内容的介绍之前,我想讲一讲S TA RT U P I N F O 的w S h o w Wi n d o w 成员。你将该成员初始化为传递给( w ) Wi n M a i n 的最后一个参数n C m d S h o w 的值。该成员显示你想要传递给新进程的( w ) Wi n M a i n 函数的最后一个参数n C m d S h o w 的值。它是可以传递给S h o w Wi n d o w 函数的标识符之一。通常,n C m d S h o w 的值既可以是S W _ S H O W N O R M A L ,也可以是SW_ SHOWMINNOACTIVE 。但是,它有时可以是S W _ S H O W D E FA U LT 。

图4-3 运行N o t e p a d的快捷方式的属性页

当在E x p l o r e r 中启动一个应用程序时,该应用程序的( w ) Wi n M a i n 函数被调用,而S W _ S H O W N O R M A L 则作为n C m d S h o w 参数来传递。如果为该应用程序创建了一个快捷方式,可以使用快捷方式的属性页来告诉系统,应用程序的窗口最初应该如何显示。图4 - 3 显示了运行N o t e p a d 的快捷方式的属性页。注意,使用R u n 选项的组合框,就能够设定如何显示N o t e p a d 的窗口。

当使用E x p l o r e r 来启动该快捷方式时,E x p l o r e r 会正确地准备S TA RT U P I N F O 结构并调用C r e a t e P r o c e s s 。这时N o t e p a d 开始运行,并且为n C m d S h o w 参数将S W _ S H O W M I N N O A C T I V E传递给它的( w ) Wi n M a i n 函数。

运用这样的方法,用户能够很容易地启动一个应用程序,其主窗口可以用正常状态、最小或最大状态进行显示。

最后,应用程序可以调用下面的函数,以便获取由父进程初始化的S TA RT U P I N F O 结构的拷贝。子进程可以查看该结构,并根据该结构的成员的值来改变它的行为特性。

VOID GetStartupInfo(LPSTARTUPINFO pStartupInfo);
注意虽然Wi n d o w s 文档没有明确地说明,但是在调用G e t S t a r t I n f o 函数之前,必须像下面这样对该结构的c b 成员进行初始化:
STARTUPINFO si = { sizeof(si) };
GetStartupInfo(&si);
4.2.7 ppiProcInfo

ppiProcInfo参数用于指向你必须指定的PROCESS_INFORMATION结构。CreateProcess在返回之前要对该结构的成员进行初始化。该结构的形式如下面所示:

typedef struct _PROCESS_INFORMATION
{
   HANDLE hProcess;
   HANDLE hThread;
   DWORD dwProcessId;
   DWORD dwThreadId;
} PROCESS_INFORMATION;
如前所述,创建新进程可使系统建立一个进程内核对象和一个线程内核对象。在创建进程的时候,系统为每个对象赋予一个初始使用计数值1 。然后,在createProcess返回之前,该函数打开进程对象和线程对象,并将每个对象的与进程相关的句柄放入PROCESS_INFORMATION结构的hProcess和hThread 成员中。当CreateProcess 在内部打开这些对象时,每个对象的使用计数就变为2 。

这意味着在系统能够释放进程对象前,该进程必须终止运行(将使用计数递减为1),并且父进程必须调用CloseHandle(再将使用计数递减1,使之变为0)。同样,若要释放线程对象,该线程必须终止运行,父进程必须关闭线程对象的句柄(关于释放线程对象的详细说明,请参见本章后面“子进程”一节的内容)

注意必须关闭子进程和它的主线程的句柄,以避免在应用程序运行时泄漏资源。当然,当进程终止运行时,系统会自动消除这些泄漏现象,但是,当进程不再需要访问子进程和它的线程时,编写得较好的软件能够显式关闭这些句柄(通过调用CloseHandle函数来关闭)。不能关闭这些句柄是开发人员最常犯的错误之一。

由于某些原因,许多开发人员认为,关闭进程或线程的句柄,会促使系统撤消该进程或线程。实际情况并非如此。关闭句柄只是告诉系统,你对进程或线程的统计数据不感兴趣。进程或线程将继续运行,直到它自己终止运行。

当进程内核对象创建后,系统赋予该对象一个独一无二的标识号,系统中的其他任何进程内核对象都不能使用这个相同的ID号。线程内核对象的情况也一样。当一个线程内核对象创建时,该对象被赋予一个独一无二的、系统范围的I D号。进程I D和线程I D共享相同的号码池。这意味着进程和线程不可能拥有相同的I D 。另外,对象决不会被赋予0 作为其I D。在CreateProcess返回之前,它要用这些I D填入PROCESS_INFORMATION结构的dwProcessId和dwThreadId成员中。I D使你能够非常容易地识别系统中的进程和线程。一些实用工具(如Task Manager)对I D使用得最多,而高效率的应用程序则使用得很少。由于这个原因,大多数应用程序完全忽略ID。

如果应用程序使用I D来跟踪进程和线程,必须懂得系统会立即复用进程I D和线程ID。例如,当一个进程被创建时,系统为它指定一个进程对象,并为它赋予ID值1 2 2。如果创建了一个新进程对象,系统不会将相同的I D赋予给它。但是,如果第一个进程对象被释放,系统就可以将1 2 2赋予创建的下一个进程对象。记住这一点后,就能避免编写引用不正确的进程对象或线程对象的代码。获取进程I D是很容易的,保存该I D也不难,但是,接下来你应该知道,该I D标识的进程已被释放,新进程被创建并被赋予相同的I D。当使用已经保存的进程I D时,最终操作的是新进程,而不是原先获得I D的进程。

有时,运行的应用程序想要确定它的父进程。首先应该知道只有在生成子进程时,才存在进程之间的父子关系。在子进程开始执行代码前, Wi n d o w s不再考虑存在什么父子关系。较早的Wi n d o w s 版本没有提供让进程查询其父进程的函数。现在, To o l H e l p 函数通过P R O C E S S E N T RY 3 2结构使得这种查询成为可能。在这个结构中有一个t h 3 2 P a r e n t P r o c e s s I D成员,根据文档的说明,它能返回进程的父进程的I D。

系统无法记住每个进程的父进程的I D,但是,由于I D是被立即重复使用的,因此,等到获得父进程的I D时,该I D可能标识了系统中一个完全不同的进程。父进程可能已经终止运行。如果应用程序想要与它的“创建者”进行通信,最好不要使用I D。应该定义一个持久性更好的机制,比如内核对象和窗口句柄等。

若要确保进程I D或线程I D不被重复使用,唯一的方法是保证进程或线程的内核对象不会被撤消。如果刚刚创建了一个新进程或线程,只要不关闭这些对象的句柄,就能够保证进程对象不被撤消。一旦应用程序结束使用该I D,那么调用C l o s e H a n d l e就可以释放内核对象,要记住,这时使用或依赖进程I D,对来说将不再安全。如果使用的是子进程,将无法保证父进程或父线程的有效性,除非父进程复制了它自己的进程对象或线程对象的句柄,并让子进程继承这些句柄。


4.3 终止进程的运行

若要终止进程的运行,可以使用下面四种方法: 

• 主线程的进入点函数返回(最好使用这个方法)。 

• 进程中的一个线程调用E x i t P r o c e s s函数(应该避免使用这种方法)。 

• 另一个进程中的线程调用Te r m i n a t e P r o c e s s函数(应该避免使用这种方法)。 

• 进程中的所有线程自行终止运行(这种情况几乎从未发生)。 

这一节将介绍所有这四种方法,并且说明进程结束时将会发生什么情况。

4.3.1 主线程的进入点函数返回

始终都应该这样来设计应用程序,即只有当主线程的进入点函数返回时,它的进程才终止运行。这是保证所有线程资源能够得到正确清除的唯一办法。让主线程的进入点函数返回,可以确保下列操作的实现:

• 该线程创建的任何C + +对象将能使用它们的析构函数正确地撤消。 

• 操作系统将能正确地释放该线程的堆栈使用的内存。 

• 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。 

• 系统将进程内核对象的返回值递减1。

4.3.2 ExitProcess函数

当进程中的一个线程调用E x i t P r o c e s s函数时,进程便终止运行:

VOID ExitProcess(UINT fuExitCode);
该函数用于终止进程的运行,并将进程的退出代码设置为f u E x i t C o d e。E x i t P r o c e s s函数并不返回任何值,因为进程已经终止运行。如果在调用E x i t P r o c e s s之后又增加了什么代码,那么该代码将永远不会运行。

当主线程的进入点函数( WinMain、wWinMain、main或wmain)返回时,它将返回给C / C + +运行期启动代码,它能正确地清除该进程使用的所有的C运行期资源。当C运行期资源被释放之后,C运行期启动代码就显式调用E x i t P r o c e s s,并将进入点函数返回的值传递给它。这解释了为什么只需要主线程的进入点函数返回,就能够终止整个进程的运行。请注意,进程中运行的任何其他线程都随着进程而一道终止运行。

Windows Platform SDK文档声明,进程要等到所有线程终止运行之后才终止运行。就操作系统而言,这种说法是对的。但是, C / C + +运行期对应用程序采用了不同的规则,通过调用E x i t P r o c e s s,使得C / C + +运行期启动代码能够确保主线程从它的进入点函数返回时,进程便终止运行,而不管进程中是否还有其他线程在运行。不过,如果在进入点函数中调用E x i t T h r e a d,而不是调用E x t i P r o c e s s或者仅仅是返回,那么应用程序的主线程将停止运行,但是,如果进程中至少有一个线程还在运行,该进程将不会终止运行。

注意,调用E x i t P r o c e s s或E x i t T h r e a d可使进程或线程在函数中就终止运行。就操作系统而言,这很好,进程或线程的所有操作系统资源都将被全部清除。但是, C / C + +应用程序应该避免调用这些函数,因为C / C + +运行期也许无法正确地清除。请看下面的代码:

#include <windows.h>
#include <stdio.h>

class CSomeObj
{
public:
   CSomeObj() 
   { 
      printf("Constructor\r\n");
   }
   
   ~CSomeObj() 
   { 
      printf("Destructor\r\n");
   }
};

CSomeObj g_GlobalObj;

void main()
{
   CSomeObj LocalObj;
   
   //This shouldn't be here
   ExitProcess(0);     

   //At the end of this function,
   //the compiler automatically added
   //the code necessary to call LocalObj's destructor.
   //ExitProces prevents it from executing.
}
当上面的代码运行时,将会看到:
Constructor
Constructor
它创建了两个对象,一个是全局对象,另一个是局部对象。不过决不会看到D e s t r u c t o r这个单词出现, C + +对象没有被正确地撤消,因为E x i t P r o c e s s函数强制进程在现场终止运行,C / C + +运行期没有机会进行清除。

如前所述,决不应该显式调用E x i t P r o c e s s函数。如果在上面的代码中删除了对E x i t P r o c e s s的调用,那么运行该程序产生的结果如下:

Constructor
Constructor
Destructor
Destructor
只要让主线程的进入点函数返回, C / C + +运行期就能够执行它的清除操作,并且正确地撤消任何或所有的C + +对象。顺便讲一下,这个说明不仅仅适用于C + +对象。C + +运行期能够代表进程执行许多操作,最好允许运行期正确地将它清除。

注意显式调用E x i t P r o c e s s和E x i t T h r e a d是导致应用程序不能正确地将自己清除的常见原因。在调用E x i t T h r e a d时,进程将继续运行,但是可能会泄漏内存或其他资源。

4.3.3 TerminateProcess函数

调用Te r m i n a t e P r o c e s s函数也能够终止进程的运行:

BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode);
该函数与E x i t P r o c e s s有一个很大的差别,那就是任何线程都可以调用Te r m i n a t e P r o c e s s来终止另一个进程或它自己的进程的运行。h P r o c e s s参数用于标识要终止运行的进程的句柄。当进程终止运行时,它的退出代码将成为你作为f u E x i t C o d e参数来传递的值。

只有当无法用另一种方法来迫使进程退出时,才应该使用Te r m i n a t e P r o c e s s。终止运行的进程绝对得不到关于它将终止运行的任何通知,因为应用程序无法正确地清除,并且不能避免自己被撤消(除非通过正常的安全机制)。例如,进程无法将内存中它拥有的任何信息迅速送往磁盘。

虽然进程确实没有机会执行自己的清除操作,但是操作系统可以在进程之后进行全面的清除,使得所有操作系统资源都不会保留下来。这意味着进程使用的所有内存均被释放,所有打开的文件全部关闭,所有内核对象的使用计数均被递减,同时所有的用户对象和G D I对象均被撤消。

一旦进程终止运行(无论采用何种方法),系统将确保该进程不会将它的任何部分遗留下来。绝对没有办法知道该进程是否曾经运行过。进程一旦终止运行,它绝对不会留下任何蛛丝马迹。希望这是很清楚的。

注意Te r m i n a t e P r o c e s s函数是个异步运行的函数,也就是说,它会告诉系统,你想要进程终止运行,但是当函数返回时,你无法保证该进程已经终止运行。因此,如果想要确切地了解进程是否已经终止运行,必须调用Wa i t F o r S i n g l e O b j e c t函数(第9章介绍)或者类似的函数,并传递进程的句柄。

进程中的线程何时全部终止运行

如果进程中的所有线程全部终止运行(因为它们调用了E x i t T h r e a d函数,或者因为它们已经用Te r m i n a t e P r o c e s s函数终止运行),操作系统就认为没有理由继续保留进程的地址空间。这很好,因为在地址空间中没有任何线程执行任何代码。当系统发现没有任何线程仍在运行时,它就终止进程的运行。出现这种情况时,进程的退出代码被设置为与终止运行的最后一个线程相同的退出代码。

4.3.4 进程终止运行时出现的情况

当进程终止运行时,下列操作将启动运行: 

1) 进程中剩余的所有线程全部终止运行。 

2) 进程指定的所有用户对象和G D I对象均被释放,所有内核对象均被关闭(如果没有其他 进程打开它们的句柄,那么这些内核对象将被撤消。但是,如果其他进程打开了它们的句柄, 内核对象将不会撤消)。 

3) 进程的退出代码将从S T I L L _ A C T I V E改为传递给E x i t P r o c e s s或Te r m i n a t e P r o c e s s的代码。 

4) 进程内核对象的状态变成收到通知的状态(关于传送通知的详细说明,参见第9章)。系 统中的其他线程可以挂起,直到进程终止运行。 

5) 进程内核对象的使用计数递减1。

注意,进程的内核对象的寿命至少可以达到进程本身那么长,但是进程内核对象的寿命可能大大超过它的进程寿命。当进程终止运行时,系统能够自动确定它的内核对象的使用计数。如果使用计数降为0,那么没有其他进程拥有该对象打开的句柄,当进程被撤消时,对象也被撤消。

不过,如果系统中的另一个进程拥有正在被撤消的进程的内核对象的打开句柄,那么该进程内核对象的使用计数不会降为0。当父进程忘记关闭子进程的句柄时,往往就会发生这样的情况。这是个特性,而不是错误。记住,进程内核对象维护关于进程的统计信息。即使进程已经终止运行,该信息也是有用的。例如,你可能想要知道进程需要多少C P U时间,或者,你想通过调用G e t E x i t C o d e P r o c e s s来获得目前已经撤消的进程的退出代码:

BOOL GetExitCodeProcess(HANDLE hProcess,
   PDWORD pdwExitCode);
该函数查看进程的内核对象(由h P r o c e s s参数来标识),取出内核对象的数据结构中用于标识进程的退出代码的成员。该退出代码的值在p d w E x i t C o d e参数指向的D W O R D中返回。

可以随时调用该函数。如果调用G e t E x i t C o d e P r o c e s s函数时进程尚未终止运行,那么该函数就用S T I L L _ A C T I V E标识符(定义为0 x 1 0 3)填入D W O R D。如果进程已经终止运行,便返回数据的退出代码值。

也许你会认为,你可以编写代码,通过定期调用G e t E x i t C o d e P r o c e s s函数并且检查退出代码来确定进程是否已经终止运行。大多数情况下,这是可行的,但是效率不高。下一段将介绍用什么正确的方法来确定进程何时终止运行。

再一次提醒你,应该通过调用C l o s e H a n d l e函数,告诉系统你对进程的统计数据已经不再感兴趣。如果进程已经终止运行,C l o s e H a n d l e将递减内核对象的使用计数,并将它释放。


4.4 子进程

当你设计应用程序时,可能会遇到这样的情况,即想要另一个代码块来执行操作。通过调用函数或子例程,你可以一直象这样分配工作。当调用一个函数时,在函数返回之前,代码将无法继续进行操作。大多数情况下,需要实施这种单任务同步。让另一个代码块来执行操作的另一种方法是在进程中创建一个新线程,并让它帮助进行操作。这样,当其他线程在执行需要的操作时,代码就能继续进行它的处理。这种方法很有用,不过,当线程需要查看新线程的结果时,它会产生同步问题。

另一个解决办法是生成一个新进程,即子进程,以便帮助你进行操作。比如说,需要进行的操作非常复杂。若要处理该操作,只需要在同一个进程中创建一个新线程。你编写一些代码,对它进行测试,但是得到一些不正确的结果。也许你的算法存在错误,也可能间接引用的对象不正确,并且不小心改写了地址空间中的某些重要内容。进行操作处理时,如果要保护地址空间,方法之一是让一个新进程来执行这项操作。然后,在继续进行工作之前,可以等待新进程终止运行,或者可以在新进程工作时,继续进行工作。

不过,新进程可能需要对地址空间中包含的数据进行操作。这时最好让进程在它自己的地址空间中运行,并且只让它访问父进程地址空间中的相关数据,这样就能保护与手头正在执行的任务无关的全部数据。Wi n d o w s提供了若干种方法,以便在不同的进程中间传送数据,比如动态数据交换( D D E)、O L E、管道和邮箱等。共享数据最方便的方法之一是,使用内存映射文件(关于内存映射文件的详细说明请参见第1 7章)。

如果想创建新进程,让它进行一些操作,并且等待结果,可以使用类似下面的代码:

PROCESS_INFORMATION pi;
DWORD dwExitCode;

//Spawn the child process.
BOOLfSuccess = CreateProcess(..., π);

if(fSuccess)
{
   //Close the thread handle as soon as
   //it is no longer needed!
   CloseHandle(pi.hThread);

   //Suspend our execution until
   //the child has terminated.
   WaitForSingleObject(pi.hProcess,INFINITE);

   //The child process terminated;
   //get its exit code.
   GetExitCodeProcess(pi.hProcess,
      &dwExitCode);

   //Close the process handle as soon as
   //it is no longer needed.
   CloseHandle(pi.hProcess);
}
在上面的代码段中,你创建了一个新进程,如果创建成功,可以调用Wa i t F o r S i n g l e O b j e c t函数:
DWORD WaitForSingleObject(HANDLE hObject,
   DWORD dwTimeout);
第9章将全面介绍Wa i t F o r S i n g l e O b j e c t函数。现在,必须知道的情况是,它会一直等到h O b j e c t参数标识的对象得到通知的时候。当进程对象终止运行时,它们才会得到通知。因此对Wa i t F o r S i n g l e O b j e c t 的调用会将父进程的线程挂起,直到子进程终止运行。当Wa i t F o r S i n g l e O b j e c t返回时,通过调用G e t E x i t C o d e P r o c e s s函数,就可以获得子进程的退出代码。

在上面的代码段中调用C l o s e H a n d l e函数,可使系统为线程和进程对象的使用计数递减为0,从而使对象的内存得以释放。

你会发现,在这个代码段中,在C r e a t e P r o c e s s返回后,立即关闭了子进程的主线程内核对象的句柄。这并不会导致子进程的主线程终止运行,它只是递减子进程的主线程对象的使用计数。这种做法的优点是,假设子进程的主线程生成了另一个线程,然后主线程终止运行,这时,如果父进程不拥有子进程的主线程对象的句柄,那么系统就可以从内存中释放子进程的主线程对象。但是,如果父进程拥有子进程的线程对象的句柄,那么在父进程关闭句柄前,系统将不能释放该对象。

运行独立的子进程

大多数情况下,应用程序将另一个进程作为独立的进程来启动。这意味着进程创建和开始运行后,父进程并不需要与新进程进行通信,也不需要在完成它的工作后父进程才能继续运行。这就是E x p l o r e r的运行方式。当E x p l o r e r为用户创建一个新进程后,它并不关心该进程是否继续运行,也不在乎用户是否终止它的运行。

若要放弃与子进程的所有联系, E x p l o r e r必须通过调用C l o s e H a n d l e来关闭它与新进程及它的主线程之间的句柄。下面的代码示例显示了如何创建新进程以及如何让它以独立方式来运行:

PROCESS_INFORMATION pi;
//Spawn the child process.
BOOL fSuccess = CreateProcess(..., π);
if(fSuccess)
{
   //Allow the system to destroy the process & thread kernel
   //objects as soon as the child process terminates.
   CloseHandle(pi.hThread);
   CloseHandle(pi.hProcess);
}

4.5 枚举系统中运行的进程

许多软件开发人员都试图为Wi n d o w s编写需要枚举正在运行的一组进程的工具或实用程序。Windows API原先没有用于枚举正在运行的进程的函数。不过, Windows NT一直在不断更新称为Performance Data的数据库。该数据库包含大量的信息,并且可以通过注册表函数来访问(比如以H K E Y _ P E R F O R M A N C E _ D ATA为根关键字的R e g Q u e r y Va l u e E x函数)。由于下列原因,很少有Wi n d o w s程序员知道性能数据库的情况:

• 它没有自己特定的函数,它只是使用现有的注册表函数。 

• Windows 95和Windows 98没有配备该数据库。 

• 该数据库中的信息布局比较复杂,许多软件开发人员都不愿使用它。这妨碍了人们通过言传口说来传播它的存在。

为了使该数据库的使用变得更加容易, M i c r o s o f t开发了一组Performance Data Helper函数(包含在P D H . d l l文件中)。若要了解它的详细信息,请查看Platform SDK文档中的P e r f o r m a n c eData Helper的内容。

如前所述,Windows 95和Windows 98没有配备该数据库。不过它们有自己的一组函数,可以用于枚举关于它们的进程和信息。这些函数均在ToolHelp API 中。详细信息请参见Platform SDK文档中的P r o c e s s 3 2 F i r s t和P r o c e s s 3 2 N e x t函数。

更加有趣的是,M i c r o s o f t的Windows NT开发小组因为不喜欢To o l H e l p函数,所以没有将这些函数添加给Windows NT。相反,他们开发了自己的Process Status函数,用于枚举进程(这些函数包含在P S A P I . d l l文件中)。关于这些函数的详细说明,请参见Platform SDK文档中的E n u m P r o c e s s e s函数。

M i c r o s o f t似乎使得工具和实用程序开发人员的日子很不好过,不过我高兴地告诉他们,M i c r o s o f t已经将To o l H e l p函数添加给Windows 2000。最后,开发人员终于有了一种方法,可以为Windows 95、Windows 98和Windows 2000编写具有公用源代码的工具和实用程序。

进程信息示例应用程序

P r o c e s s I n f o应用程序“04 ProcessInfo.exe”(本章结尾处的清单4 - 2列出了该文件)显示了如何使用To o l H e l p函数来开发非常有用的实用程序。用于应用程序的源代码和资源文件均放在本书所附光盘上0 4 - P r o c e s s I n f o目录中。当启动该程序时,便会出现图4 - 4所示的窗口。

P r o c e s s I n f o首先枚举目前正在运行的一组进程,并在顶部的组合框中列出每个进程的名字和I D。然后,第一个进程被选定,并在较大的编辑控件中显示关于该进程的信息。可以看到,与该进程的I D一道显示的还有它的父进程的I D,进程的优先级类,以及该进程环境中当前正在运行的线程数目。这些信息中的大多数不在本章介绍的范围之内,将在本章后面的内容中加以说明。

当查看这个进程列表时,可以使用V M M a p菜单项(当查看模块信息时,该菜单项禁用)。如果选定V M M a p菜单项,可使V M M a p示例应用程序(参见第1 4章)启动运行。该应用程序将在指定进程的地址空间中运行。

模块信息部分显示了映射到进程的地址空间中的模块的列表(可执行文件和D L L文件)。固定模块是指进程初始化时隐含加载的模块。如果是显式加载的D L L模块,则显示D L L的使用计数。第二个域显示映射模块的地址。如果模块不是在它的首选基地址上映射的,那么首选基地址显示在括号中。第三个域显示模块的大小(用字节数表示)。最后显示的是模块的全路径名。线程信息部分显示了该进程中当前运行的一组线程。每个线程I D和优先级均被显示。

除了进程信息外,可以选择M o d u l e s !菜单项。这将使得P r o c e s s I n f o能够枚举当前通过系统加载的模块,并将每个模块的名字放入顶部的组合框。然后P r o c e s s I n f o可以选定第一个模块,并显示关于它的信息,如图4 - 5所示。

图4-4 运行中的P r o c e s s I n f o

图4-5 ProcessInfo显示User32.dll加载到它们的地址空间的所有进程

当以这种方法使用P r o c e s s I n f o实用程序时,能够方便地确定哪些进程正在使用某个模块。如你所见,模块的全路径名显示在顶部。然后,进程信息部分显示包含该模块的进程列表。除了每个进程的I D和名字外,还显示每个进程中模块加载到的地址。

P r o c e s s I n f o应用程序显示的所有信息基本上都是通过调用To o l H e l p的各个函数而产生的。为了使To o l H e l p函数的使用更加容易,我创建了一个C + +类(包含在To o l H e l p . h文件中)。这个C + +类封装了一个To o l H e l p快照,使得调用其他To o l H e l p函数稍稍容易一些。

P r o c e s s I n f o . c p p中的G e t M o d u l e P r e f e r r e d B a s e A d d r函数是个特别有意思的函数:

PVOID GetModulePreferredBaseAddr(DWORD dwProcessId,
   PVOID pvModuleRemote);
该函数接受一个进程I D和该进程中的一个模块的地址。然后它查看该进程的地址空间,找出该模块,并读取模块的标题信息,以确定该模块首选的基地址,一个模块始终应该加载到它的首选基地址中,否则,使用该模块的应用程序将需要更多的内存,并且在初始化时会对性能产生影响。由于这是个非常可怕的情况,因此我增加了这个函数并且显示何时模块没有加载到它的首选基地址中。第2 0章将要进一步介绍首选基地址和这次/内存性能。

清单4-2 ProcessInfo应用程序

/******************************************************************************
Module:  ProcessInfo.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <windowsx.h>
#include <tlhelp32.h>
#include <tchar.h>
#include <stdarg.h>
#include <stdio.h>
#include "Toolhelp.h"
#include "Resource.h"
///////////////////////////////////////////////////////////////////////////////
// Adds a string to an edit control
void AddText(HWND hwnd, PCTSTR pszFormat, ...) 
{
   va_list argList;
   va_start(argList, pszFormat);

   TCHAR sz[20 * 1024];
   Edit_GetText(hwnd, sz, chDIMOF(sz));
   _vstprintf(_tcschr(sz, 0), pszFormat, argList);
   Edit_SetText(hwnd, sz);
   va_end(argList);
}

///////////////////////////////////////////////////////////////////////////////
VOID Dlg_PopulateProcessList(HWND hwnd)
{
   HWND hwndList = GetDlgItem(hwnd, IDC_PROCESSMODULELIST);
   SetWindowRedraw(hwndList, FALSE);
   ComboBox_ResetContent(hwndList);

   CToolhelp thProcesses(TH32CS_SNAPPROCESS);
   PROCESSENTRY32 pe = { sizeof(pe) };
   BOOL fOk = thProcesses.ProcessFirst(&pe);
   for (; fOk; fOk = thProcesses.ProcessNext(&pe))
   {
      TCHAR sz[1024];

      //Place the process name (without its path) & ID in the list
      PCTSTR pszExeFile = _tcsrchr(pe.szExeFile, TEXT('\\'));
      if (pszExeFile == NULL) pszExeFile = pe.szExeFile;
      else pszExeFile++; // Skip over the slash
      wsprintf(sz, TEXT("%s     (0x%08X)"),
         pszExeFile, pe.th32ProcessID);
      int n = ComboBox_AddString(hwndList, sz);

      //Associate the process ID with the added item
      ComboBox_SetItemData(hwndList, n, pe.th32ProcessID);
   }
   ComboBox_SetCurSel(hwndList, 0);  // Select the first entry

   // Simulate the user selecting this first item so that the
   // results pane shows something interesting
   FORWARD_WM_COMMAND(hwnd, IDC_PROCESSMODULELIST, 
      hwndList, CBN_SELCHANGE, SendMessage);

   SetWindowRedraw(hwndList, TRUE);
   InvalidateRect(hwndList, NULL, FALSE);
}

///////////////////////////////////////////////////////////////////////////////
VOID Dlg_PopulateModuleList(HWND hwnd)
{
   HWND hwndModuleHelp = GetDlgItem(hwnd, IDC_MODULEHELP);
   ListBox_ResetContent(hwndModuleHelp);

   CToolhelp thProcesses(TH32CS_SNAPPROCESS);
   PROCESSENTRY32 pe = { sizeof(pe) };
   BOOL fOk = thProcesses.ProcessFirst(&pe);
   for(; fOk; fOk = thProcesses.ProcessNext(&pe)) 
   {
      CToolhelp thModules(TH32CS_SNAPMODULE, pe.th32ProcessID);
      MODULEENTRY32 me = { sizeof(me) };
      BOOL fOk = thModules.ModuleFirst(&me);
      for (; fOk; fOk = thModules.ModuleNext(&me)) 
      {
        int n = ListBox_FindStringExact(hwndModuleHelp, -1, me.szExePath);
         if (n == LB_ERR) 
         {
            //This module hasn't been added before
            ListBox_AddString(hwndModuleHelp, me.szExePath);
         }
      }
   }

   HWND hwndList = GetDlgItem(hwnd, IDC_PROCESSMODULELIST);
   SetWindowRedraw(hwndList, FALSE);
   ComboBox_ResetContent(hwndList);
   int nNumModules = ListBox_GetCount(hwndModuleHelp);
   for (int i = 0; i < nNumModules; i++) 
   {
      TCHAR sz[1024];
      ListBox_GetText(hwndModuleHelp, i, sz);
      //Place module name (without its path) in the list
      int nIndex = ComboBox_AddString(hwndList, 
         _tcsrchr(sz, TEXT('\\')) + 1);
      //Associate the index of the full path with the added item
      ComboBox_SetItemData(hwndList, nIndex, i);
   }

   ComboBox_SetCurSel(hwndList, 0);  //Select the first entry

   //Simulate the user selecting this first item so that the
   //results pane shows something interesting
   FORWARD_WM_COMMAND(hwnd, IDC_PROCESSMODULELIST, 
      hwndList, CBN_SELCHANGE, SendMessage);

   SetWindowRedraw(hwndList, TRUE);
   InvalidateRect(hwndList, NULL, FALSE);
}

///////////////////////////////////////////////////////////////////////////////
PVOID GetModulePreferredBaseAddr(DWORD dwProcessId, PVOID pvModuleRemote)
{
   PVOID pvModulePreferredBaseAddr = NULL;
   IMAGE_DOS_HEADER idh;
   IMAGE_NT_HEADERS inth;

   //Read the remote module's DOS header
   Toolhelp32ReadProcessMemory(dwProcessId, 
      pvModuleRemote, &idh, sizeof(idh), NULL);

   //Verify the DOS image header
   if (idh.e_magic == IMAGE_DOS_SIGNATURE)
   {
      // Read the remote module's NT header
      Toolhelp32ReadProcessMemory(dwProcessId, 
         (PBYTE) pvModuleRemote + idh.e_lfanew,
          &inth, sizeof(inth), NULL);

      // Verify the NT image header
      if (inth.Signature == IMAGE_NT_SIGNATURE)
      {
         //This is valid NT header,
         //get the image's preferred base address
         pvModulePreferredBaseAddr = (PVOID)inth.OptionalHeader.ImageBase;
      }
   }
   return(pvModulePreferredBaseAddr);
}

///////////////////////////////////////////////////////////////////////////////
VOID ShowProcessInfo(HWND hwnd, DWORD dwProcessID) 
{
   SetWindowText(hwnd, TEXT(""));   // Clear the output box

   CToolhelp th(TH32CS_SNAPALL, dwProcessID);

   // Show Process details
   PROCESSENTRY32 pe = { sizeof(pe) };
   BOOL fOk = th.ProcessFirst(&pe);
   for (; fOk; fOk = th.ProcessNext(&pe)) 
   {
      if (pe.th32ProcessID == dwProcessID) 
      {
         AddText(hwnd, TEXT("Filename: %s\r\n"), pe.szExeFile);
         AddText(hwnd, TEXT("   PID=%08X, ParentPID=%08X, ")
            TEXT("PriorityClass=%d, Threads=%d, Heaps=%d\r\n"),
            pe.th32ProcessID, pe.th32ParentProcessID, 
            pe.pcPriClassBase, pe.cntThreads,
            th.HowManyHeaps());
         break;   // No need to continue looping
      }
   }

   // Show Modules in the Process
   // Number of characters to display an address
   const int cchAddress = sizeof(PVOID) * 2;
   AddText(hwnd, TEXT("\r\nModules Information:\r\n")
      TEXT("  Usage  %-*s(%-*s)  %8s  Module\r\n"),
      cchAddress, TEXT("BaseAddr"),
      cchAddress, TEXT("ImagAddr"), TEXT("Size"));

   MODULEENTRY32 me = { sizeof(me) };
   fOk = th.ModuleFirst(&me);
   for (; fOk; fOk = th.ModuleNext(&me)) 
   {
      if (me.ProccntUsage == 65535) 
      {
         // Module was implicitly loaded and cannot be unloaded
         AddText(hwnd, TEXT("  Fixed"));
      } 
      else
      {
         AddText(hwnd, TEXT("  %5d"), me.ProccntUsage);
      }
      
      PVOID pvPreferredBaseAddr = 
         GetModulePreferredBaseAddr(pe.th32ProcessID, me.modBaseAddr);
      if (me.modBaseAddr == pvPreferredBaseAddr)
      {
         AddText(hwnd, TEXT("  %p %*s   %8u  %s\r\n"), 
            me.modBaseAddr, cchAddress, TEXT(""), 
            me.modBaseSize, me.szExePath);
      } 
      else
      {
         AddText(hwnd, TEXT("  %p(%p)  %8u  %s\r\n"), 
            me.modBaseAddr, pvPreferredBaseAddr, 
            me.modBaseSize, me.szExePath);
      }
   }

   // Show threads in the process
   AddText(hwnd, TEXT("\r\nThread Information:\r\n")
      TEXT("      TID     Priority\r\n"));
   THREADENTRY32 te = { sizeof(te) };
   fOk = th.ThreadFirst(&te);
   for (; fOk; fOk = th.ThreadNext(&te))
   {
      if (te.th32OwnerProcessID == dwProcessID)
      {
         int nPriority = te.tpBasePri + te.tpDeltaPri;
         if ((te.tpBasePri < 16) && (nPriority > 15)) 
            nPriority = 15;
         if ((te.tpBasePri > 15) && (nPriority > 31)) 
            nPriority = 31;
         if ((te.tpBasePri < 16) && (nPriority <  1)) 
            nPriority =  1;
         if ((te.tpBasePri > 15) && (nPriority < 16)) 
            nPriority = 16;
         AddText(hwnd, TEXT("   %08X       %2d\r\n"), 
            te.th32ThreadID, nPriority);
      }
   }
}

///////////////////////////////////////////////////////////////////////////////
VOID ShowModuleInfo(HWND hwnd, PCTSTR pszModulePath) 
{
   SetWindowText(hwnd, TEXT(""));   // Clear the output box

   CToolhelp thProcesses(TH32CS_SNAPPROCESS);
   PROCESSENTRY32 pe = { sizeof(pe) };
   BOOL fOk = thProcesses.ProcessFirst(&pe);
   AddText(hwnd, TEXT("Pathname: %s\r\n\r\n"), pszModulePath);
   AddText(hwnd, TEXT("Process Information:\r\n"));
   AddText(hwnd, TEXT("     PID    BaseAddr  Process\r\n"));
   for (; fOk; fOk = thProcesses.ProcessNext(&pe)) 
   {
      CToolhelp thModules(TH32CS_SNAPMODULE, pe.th32ProcessID);
      MODULEENTRY32 me = { sizeof(me) };
      BOOL fOk = thModules.ModuleFirst(&me);
      for (; fOk; fOk = thModules.ModuleNext(&me)) 
      {
         if (_tcscmp(me.szExePath, pszModulePath) == 0) 
         {
            AddText(hwnd, TEXT("  %08X  %p  %s\r\n"), 
               pe.th32ProcessID, me.modBaseAddr, pe.szExeFile);
         }
      }
   }
}

///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) 
{
   chSETDLGICONS(hwnd, IDI_PROCESSINFO);

   // Hide the module-helper listbox.
   ShowWindow(GetDlgItem(hwnd, IDC_MODULEHELP), SW_HIDE);

   // Have the results window use a fixed-pitch font
   SetWindowFont(GetDlgItem(hwnd, IDC_RESULTS), 
      GetStockFont(ANSI_FIXED_FONT), FALSE);

   // By default, show the running processes
   Dlg_PopulateProcessList(hwnd);

   return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnSize(HWND hwnd, UINT state, int cx, int cy) 
{
   RECT rc;
   int n = LOWORD(GetDialogBaseUnits());

   HWND hwndCtl = GetDlgItem(hwnd, IDC_PROCESSMODULELIST);
   GetClientRect(hwndCtl, &rc);
   SetWindowPos(hwndCtl, NULL, n, n, cx - n - n, rc.bottom, SWP_NOZORDER);

   hwndCtl = GetDlgItem(hwnd, IDC_RESULTS);
   SetWindowPos(hwndCtl, NULL, n, n + rc.bottom + n, 
      cx - n - n, cy - (n + rc.bottom + n) - n, SWP_NOZORDER);

   return(0);
}

///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) 
{
   static BOOL s_fProcesses = TRUE;

   switch (id) 
   {
      case IDCANCEL:
         EndDialog(hwnd, id);
         break;

      case ID_PROCESSES:
         s_fProcesses = TRUE;
         EnableMenuItem(GetMenu(hwnd), ID_VMMAP, MF_BYCOMMAND | MF_ENABLED);
         DrawMenuBar(hwnd);
         Dlg_PopulateProcessList(hwnd);
         break;

      case ID_MODULES:
         EnableMenuItem(GetMenu(hwnd), ID_VMMAP, MF_BYCOMMAND | MF_GRAYED);
         DrawMenuBar(hwnd);
         s_fProcesses = FALSE;
         Dlg_PopulateModuleList(hwnd);
         break;

      case IDC_PROCESSMODULELIST:
         if (codeNotify == CBN_SELCHANGE) 
         {
            DWORD dw = ComboBox_GetCurSel(hwndCtl);
            if (s_fProcesses) 
            {
               dw = (DWORD) ComboBox_GetItemData(hwndCtl, dw); // Process ID
               ShowProcessInfo(GetDlgItem(hwnd, IDC_RESULTS), dw);
            } 
            else
            {
               // Index in helper listbox of full path
               dw = (DWORD) ComboBox_GetItemData(hwndCtl, dw); 
               TCHAR szModulePath[1024];
               ListBox_GetText(GetDlgItem(hwnd, IDC_MODULEHELP), 
               dw, szModulePath);
               ShowModuleInfo(GetDlgItem(hwnd, IDC_RESULTS), szModulePath);
            }
         }
         break;

      case ID_VMMAP:
         STARTUPINFO si = { sizeof(si) };
         PROCESS_INFORMATION pi;
         TCHAR szCmdLine[1024];
         HWND hwndCB = GetDlgItem(hwnd, IDC_PROCESSMODULELIST);
         DWORD dwProcessId = (DWORD)
            ComboBox_GetItemData(hwndCB, ComboBox_GetCurSel(hwndCB));
         wsprintf(szCmdLine, TEXT("\"14 VMMap\" %d"), dwProcessId);
         BOOL fOk = CreateProcess(NULL, szCmdLine, NULL, NULL, 
            FALSE, 0, NULL, NULL, &si, π);
         if (fOk)
         {
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
         } 
         else
         {
            chMB("Failed to execute VMMAP.EXE.");
         }
         break;
   }
}


///////////////////////////////////////////////////////////////////////////////


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
   
   switch (uMsg)
   {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_SIZE,       Dlg_OnSize);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
   }
   return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int)
{
   CToolhelp::EnableDebugPrivilege(TRUE);
   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_PROCESSINFO), NULL, Dlg_Proc);
   CToolhelp::EnableDebugPrivilege(FALSE);
   return(0);
}
//////////////////////////////// End of File //////////////////////////////////
/******************************************************************************
Module:  Toolhelp.h
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
#include "..\CmnHdr.h"     /* See Appendix A. */
#include <tlhelp32.h>
#include <tchar.h>
///////////////////////////////////////////////////////////////////////////////
class CToolhelp
{
private:
   HANDLE m_hSnapshot;

public:
   CToolhelp(DWORD dwFlags = 0, DWORD dwProcessID = 0);
   ~CToolhelp();
   BOOL CreateSnapshot(DWORD dwFlags, DWORD dwProcessID = 0);
   
   BOOL ProcessFirst(PPROCESSENTRY32 ppe) const;
   BOOL ProcessNext(PPROCESSENTRY32 ppe) const;
   BOOL ProcessFind(DWORD dwProcessId, PPROCESSENTRY32 ppe) const;

   BOOL ModuleFirst(PMODULEENTRY32 pme) const;
   BOOL ModuleNext(PMODULEENTRY32 pme) const;
   BOOL ModuleFind(PVOID pvBaseAddr, PMODULEENTRY32 pme) const;
   BOOL ModuleFind(PTSTR pszModName, PMODULEENTRY32 pme) const;
   
   BOOL ThreadFirst(PTHREADENTRY32 pte) const;
   BOOL ThreadNext(PTHREADENTRY32 pte) const;
   
   BOOL HeapListFirst(PHEAPLIST32 phl) const;
   BOOL HeapListNext(PHEAPLIST32 phl) const;
   int  HowManyHeaps() const;

   // Note: The heap block functions do not reference a snapshot and
   // just walk the process's heap from the beginning each time. Infinite 
   // loops can occur if the target process changes its heap while the
   // functions below are enumerating the blocks in the heap.
   BOOL HeapFirst(PHEAPENTRY32 phe, DWORD dwProcessID, 
      UINT_PTR dwHeapID) const;
   BOOL HeapNext(PHEAPENTRY32 phe) const;
   int  HowManyBlocksInHeap(DWORD dwProcessID, DWORD dwHeapId) const;
   BOOL IsAHeap(HANDLE hProcess, PVOID pvBlock, PDWORD pdwFlags) const;

public:
   static BOOL EnableDebugPrivilege(BOOL fEnable = TRUE);
   static BOOL ReadProcessMemory(DWORD dwProcessID, LPCVOID pvBaseAddress, 
      PVOID pvBuffer, DWORD cbRead, PDWORD pdwNumberOfBytesRead = NULL);
};

///////////////////////////////////////////////////////////////////////////////
inline CToolhelp::CToolhelp(DWORD dwFlags, DWORD dwProcessID) 
{
   m_hSnapshot = INVALID_HANDLE_VALUE;
   CreateSnapshot(dwFlags, dwProcessID);
}
///////////////////////////////////////////////////////////////////////////////
inline CToolhelp::~CToolhelp()
{

   if (m_hSnapshot != INVALID_HANDLE_VALUE)
      CloseHandle(m_hSnapshot);
}
///////////////////////////////////////////////////////////////////////////////
inline CToolhelp::CreateSnapshot(DWORD dwFlags, DWORD dwProcessID) 
{
   if (m_hSnapshot != INVALID_HANDLE_VALUE)
      CloseHandle(m_hSnapshot);

   if (dwFlags == 0) {
      m_hSnapshot = INVALID_HANDLE_VALUE;
   } else {
      m_hSnapshot = CreateToolhelp32Snapshot(dwFlags, dwProcessID);
   }
   return(m_hSnapshot != INVALID_HANDLE_VALUE);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CToolhelp::EnableDebugPrivilege(BOOL fEnable)
{
   // Enabling the debug privilege allows the application to see
   // information about service applications
   BOOL fOk = FALSE;    // Assume function fails
   HANDLE hToken;

   // Try to open this process's access token
   if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, 
      &hToken)) 
   {

      // Attempt to modify the "Debug" privilege
      TOKEN_PRIVILEGES tp;
      tp.PrivilegeCount = 1;
      LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
      tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0;
      AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
      fOk = (GetLastError() == ERROR_SUCCESS);
      CloseHandle(hToken);
   }
   return(fOk);
}

///////////////////////////////////////////////////////////////////////////////
inline BOOL CToolhelp::ReadProcessMemory(DWORD dwProcessID, 
   LPCVOID pvBaseAddress, PVOID pvBuffer, DWORD cbRead, 
   PDWORD pdwNumberOfBytesRead)
{
   return(Toolhelp32ReadProcessMemory(dwProcessID, pvBaseAddress, pvBuffer, 
      cbRead, pdwNumberOfBytesRead));
}

///////////////////////////////////////////////////////////////////////////////
inline BOOL CToolhelp::ProcessFirst(PPROCESSENTRY32 ppe) const
{
   BOOL fOk = Process32First(m_hSnapshot, ppe);
   if (fOk && (ppe->th32ProcessID == 0))
      fOk = ProcessNext(ppe); 
      //Remove the "[System Process]" (PID = 0)
   return(fOk);
}

inline BOOL CToolhelp::ProcessNext(PPROCESSENTRY32 ppe) const
{
   BOOL fOk = Process32Next(m_hSnapshot, ppe);
   if (fOk && (ppe->th32ProcessID == 0))
      fOk = ProcessNext(ppe); 
      //Remove the "[System Process]" (PID = 0)
   return(fOk);
}

inline BOOL CToolhelp::ProcessFind(DWORD dwProcessId, PPROCESSENTRY32 ppe) 
   const 
{

   BOOL fFound = FALSE;
   for (BOOL fOk = ProcessFirst(ppe); fOk; fOk = ProcessNext(ppe)) 
   {
      fFound = (ppe->th32ProcessID == dwProcessId);
      if (fFound) break;
   }
   return(fFound);
}

///////////////////////////////////////////////////////////////////////////////
inline BOOL CToolhelp::ModuleFirst(PMODULEENTRY32 pme) const
{
   return(Module32First(m_hSnapshot, pme));
}

inline BOOL CToolhelp::ModuleNext(PMODULEENTRY32 pme) const
{
   return(Module32Next(m_hSnapshot, pme));
}

inline BOOL CToolhelp::ModuleFind(PVOID pvBaseAddr, PMODULEENTRY32 pme) const
{
   BOOL fFound = FALSE;
   for (BOOL fOk = ModuleFirst(pme); fOk; fOk = ModuleNext(pme)) 
   {
      fFound = (pme->modBaseAddr == pvBaseAddr);
      if (fFound) break;
   }
   return(fFound);
}

inline BOOL CToolhelp::ModuleFind(PTSTR pszModName, PMODULEENTRY32 pme) const
{
   BOOL fFound = FALSE;
   for (BOOL fOk = ModuleFirst(pme); fOk; fOk = ModuleNext(pme)) 
   {
      fFound = (lstrcmpi(pme->szModule,  pszModName) == 0) || 
               (lstrcmpi(pme->szExePath, pszModName) == 0);
      if (fFound) break;
   }
   return(fFound);
}

///////////////////////////////////////////////////////////////////////////////
inline BOOL CToolhelp::ThreadFirst(PTHREADENTRY32 pte) const
{
   return(Thread32First(m_hSnapshot, pte));
}

inline BOOL CToolhelp::ThreadNext(PTHREADENTRY32 pte) const 
{
   return(Thread32Next(m_hSnapshot, pte));
}

///////////////////////////////////////////////////////////////////////////////
inline int CToolhelp::HowManyHeaps() const 
{
   int nHowManyHeaps = 0;
   HEAPLIST32 hl = { sizeof(hl) };
   for (BOOL fOk = HeapListFirst(&hl); fOk; fOk = HeapListNext(&hl))
      nHowManyHeaps++;
   return(nHowManyHeaps);
}

inline int CToolhelp::HowManyBlocksInHeap(DWORD dwProcessID, 
   DWORD dwHeapID) const 
{

   int nHowManyBlocksInHeap = 0;
   HEAPENTRY32 he = { sizeof(he) };
   BOOL fOk = HeapFirst(&he, dwProcessID, dwHeapID);
   for (; fOk; fOk = HeapNext(&he))
      nHowManyBlocksInHeap++;
   return(nHowManyBlocksInHeap);
}

inline BOOL CToolhelp::HeapListFirst(PHEAPLIST32 phl) const 
{
   return(Heap32ListFirst(m_hSnapshot, phl));
}

inline BOOL CToolhelp::HeapListNext(PHEAPLIST32 phl) const 
{
   return(Heap32ListNext(m_hSnapshot, phl));
}

inline BOOL CToolhelp::HeapFirst(PHEAPENTRY32 phe, DWORD dwProcessID, 
   UINT_PTR dwHeapID) const 
{
   return(Heap32First(phe, dwProcessID, dwHeapID));
}

inline BOOL CToolhelp::HeapNext(PHEAPENTRY32 phe) const 
{
   return(Heap32Next(phe));
}

inline BOOL CToolhelp::IsAHeap(HANDLE hProcess, PVOID pvBlock, 
   PDWORD pdwFlags) const 
{
   HEAPLIST32 hl = { sizeof(hl) };
   for(BOOL fOkHL = HeapListFirst(&hl); fOkHL; fOkHL = HeapListNext(&hl)) 
   {
      HEAPENTRY32 he = { sizeof(he) };
      BOOL fOkHE = HeapFirst(&he, hl.th32ProcessID, hl.th32HeapID);
      for (; fOkHE; fOkHE = HeapNext(&he)) 
      {
         MEMORY_BASIC_INFORMATION mbi;
         VirtualQueryEx(hProcess, (PVOID) he.dwAddress, &mbi, sizeof(mbi));
         if (chINRANGE(mbi.AllocationBase, pvBlock, 
            (PBYTE) mbi.AllocationBase + mbi.RegionSize)) 
         {
            *pdwFlags = hl.dwFlags;
            return(TRUE);
         }
      }
   }
   return(FALSE);
}
//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_PROCESSINFO DIALOGEX 0, 0, 400, 317
STYLE DS_3DLOOK | DS_NOFAILCREATE | DS_CENTER | WS_MINIMIZEBOX | 
    WS_MAXIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_NOPARENTNOTIFY | WS_EX_CLIENTEDGE
CAPTION "Process Information"
MENU IDR_PROCESSINFO
FONT 8, "MS Sans Serif"
BEGIN
    COMBOBOX        IDC_PROCESSMODULELIST,4,4,392,156,CBS_DROPDOWNLIST | 
                    CBS_AUTOHSCROLL | CBS_SORT | WS_VSCROLL | WS_TABSTOP
    LISTBOX         IDC_MODULEHELP,0,0,48,40,NOT LBS_NOTIFY | LBS_SORT | 
                    LBS_NOINTEGRALHEIGHT | NOT WS_VISIBLE | NOT WS_BORDER | 
                    WS_TABSTOP
    EDITTEXT        IDC_RESULTS,4,24,392,284,ES_MULTILINE | ES_AUTOVSCROLL | 
                    ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL | WS_HSCROLL
END
/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//
#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE 
BEGIN
    IDD_PROCESSINFO, DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 393
        TOPMARGIN, 7
        BOTTOMMARGIN, 310
    END
END
#endif    // APSTUDIO_INVOKED
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Menu
//
IDR_PROCESSINFO MENU DISCARDABLE 
BEGIN
    MENUITEM "&Processes!",                 ID_PROCESSES
    MENUITEM "&Modules!",                   ID_MODULES
    MENUITEM "&VMMap!",                     ID_VMMAP
END
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_PROCESSINFO         ICON    DISCARDABLE     "ProcessInfo.ico"
#endif    // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

CZVC编程网出品,一剑[QQ:28077188]整理编译,欢迎联系
MSN:loomman@hotmail.com
  更多精彩VC编程资源尽在CZVC编程网!