Windows via C/C++ 学习(7)进程
时间:2011-01-18 来源:nrj
进程本身不会执行,为了完成某件事,必须有一个运行在它的上下文中的线程,这个线程负责执行进程空间中的代码。系统创建一个进程时会自动创建一个线程,叫做主线程,主线程还可以创建另外的线程,另外的线程还可以继续创建其它线程。每个线程都有一个属于自己的 CPU 寄存器组和栈。如果一个进程中没有线程正在执行,那么进程就没有存在的必要了,它会退出,系统自动销毁这个进程和它的地址空间。
当用户执行一个应用程序时,操作系统的加载器会在可执行映像的头中查找子系统值,如果子系统值表明这是一个 CUI 应用程序,加载器会为应用程序自动地打开文本控制台。
Windows 应用程序必须有一个入口点函数,当应用程序开始运行时被调用。实际上系统一开始不会真正地调用你写的入口点函数,而是先要调用一个 C/C++ 运行时提供的启动函数,这个启动函数负责初始化 C/C++ 库,所以你可以调用 malloc 和 free 函数了,它也确保在你的代码执行之前你声明的全局的和静态的 C++ 对象被初始化,随后才是调用你编写的入口函数。有两类入口点函数,分别对应图形用户接口子系统和控制台用户接口子系统,GUI 应用程序对应的入口点函数是 WinMain 或 wWinMain ,CUI 应用程序对应的入口点函数是 Main 或 wMain。连接器负责选择合适的入口点函数,如果指定了 /SUBSYSTEM:WINDOWS 开关,链接器会期望找到一个 WinMain 或 wWinMain函数,如果指定的 /SUBSYSTEM:CONSOLE 开关,链接器则期望找到一个 Main 或 wMain 函数,如果链接器没有找到合适的入口点函数,返回 “unresolved extenal symbol “错误,否则,它会选择调用一个 WinMainCRTStartup(或 wWinMainCRTStratup)函数或 mainCRTStartup(或 wmainCRTStartup)函数作为启动函数。如果用户没有指定 /SUBSYSTEM 开关,链接器会根据你提供的 main 函数来自动判断子系统类型,并选择合适的启动函数。
启动函数会做以下操作:
1. 检索一个指向新进程的命令行指针
2. 检索一个指向新进程的环境变量的指针
3. 初始化 C/C++ 运行时全局变量
4. 初始化由 C 运行时内存分配函数(malloc 和 calloc)使用的内存堆和其它低级的输入/输出例程
5. 为所有全局和静态 C++ 类对象调用构造函数
当以上操作初始化完成后,启动函数才开始调用你的应用程序的入口点函数,入口点函数返回后,启动函数调用 C 运行时函数 exit,并将入口点函数的返回值传递给它,exit 函数执行以下操作:
1. 调用所有通过调用 _onexit 函数注册的例程
2. 为所有全局和静态 C++ 类对象调用析构函数
3. 在 DEBUG 阶段,如果设置了 _CRTDBG_LEAK_CHECK_DF 标志,C/C++ 运行时内存管理器泄漏的内存通过调用 _CrtDumpMemoryLeaks 函数列出
4. 调用操作系统的 ExitProcess 函数,传递入口点函数返回的值作为参数,这样操作系统会终止进程的运行并设置它的退出代码
进程实例句柄
每一个可执行文件或加载进进程地址空间的 DLL 文件都有一个唯一的实例句柄。你的可执行文件的实例句柄可通过 (w)WinMain 的第一个参数 hInstance 来获得。
实质上,实例句柄就是系统加载到进程地址空间的可执行文件或 DLL 文件的基址,这个基地址可使用 GetModuleHandle 获得,给这个函数传递一个 NULL,表示要获取当前执行文件的基址,传递一个 0 结尾的字符串来取得指定可执行文件或 DLL 文件的基址,如果这个文件没有在当前进程地址空间中找到,返回 NULL。如果你的代码在 DLL 中执行,有两方法可以取得当前 DLL 的基址,一是使用链接器提供的伪变量 __ImageBase 来取得,第二个方法是调用 GetModuleHandleEx 函数,传递 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 给第一个参数,第二参数传递一个当前函数的地址,第三个参数是一个指向结果句柄值的地址。
void DumpModule() { // 获取当前可执行程序的实例句柄,不是当前 DLL 的句柄 HMODULE hModule = GetModuleHandle(NULL); // 使用伪变量 __ImageBase hModule = (HMODULE)&__ImageBase; // 传递一个当前方法的地址作为 GetModuleHandleEx 的参数 GetModuleHandleEx( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)DumpModule, &hModule); }
进程前一个实例句柄
(w)WinMain 的第二个参数是进程的前一个实例句柄。这个参数用于 16 位的 WIndows,现在已不使用。
命令行参数
GUI 应用程序中,C 运行时启动函数会调用 GetCommandLine 来取得命令行参数,这个命令行参数的第一部分是执行文件的全路径名,启动函数会忽略这一部分,将剩余部分传递给 (w)WinMain 函数,所以当你 DEBUG GUI 应用程序时,你会发现 lpCmdLine 参数是一个长度为 0 的字符串。CUI 应用程序的入口函数的 argv 参数包含了执行文件的全路径名。
应用程序可以使用 GetCommandLine 函数来取得指向一个命令行的指针,你会发现,无论何时调用这个函数,返回的是同一个缓冲区的地址,所以,最好不要直接修改这个缓冲区,以免你丢失了原始命令行的内容。使用 CommandLineToArgvW 函数将命令行的各部分分开,就象 CUI 应用程序入口函数的 argv 参数表示的一样,你就可以分析命令选择各个部分。
环境变量
每一个进程都有一个环境变量块,环境变量块是进程的地址空间中分配的一块内存,这块内存中保存了一组由“键=值”组成的字符串。
环境变量块中有一些条目的第一个字符是“=”,这样的字符串不做为环境变量使用。
任何时候可以使用 GetEvironmentStrings 来获得环境变量内存块的首地址,使用 FreeEvironmentStrings 释放分配的内存。CUI 应用程序的入口点函数的 env 参数也包含了所有环境变量,env 参数是一个字符串指针数组,数组中的每一个成员都是指向一个环境变量的指针,这个数组中不包含首字符是“=”的条目。
“=”之前的空格是环境变量名称的一部分,之后的空格也是环境变量值的一部分。
可以使用 GetEnvironmentVeriable 来获得指定环境变量的值,某些环境变量的值的字符串中包含有如“%REPLACEABLE%”之类的可替换字符串,这表示可由名称为“REPLACEABLE”环境变量的值来替换这部分字符串,可以使用 ExpandEnvironmentStrings 函数来进行这个替换。使用 SetEnvironmentVariable 函数来增加、删除或修改一个环境变量的值。
进程的亲缘性
通常进程中的线程可以在宿主机的任何 CPU 中执行,可是一个进程的线程可以被强制运行在可用 CPU 的任意一个子集中,子进程会继承它的父进程的这个亲缘性特征。
进程的错误模式
每一个进程都有一组与之相关的标志告诉系统怎样对严重错误作出响应,这些错误包括磁盘媒介失败,未处理的异常,文件发现失败和数据对齐等,进程可能通过调用 SetErrorMode 来告诉系统怎样处理这些错误,子进程会继承父进程的这个设置,父进程可以在调用 CreateProcess 时使用 CREATE_DEFAULT_ERROR_MODE 来防止这种继承,这样子进程就会有默认的错误模式。
进程的当前驱动器和当前目录
如果用户在操作一个文件时没有使用全路径名,系统会在当前驱动器的当前目录中查找相应的文件,这个当前驱动器的当前目录信息由系统内部负责维护,一个线程修改了进程的当前驱动器或目录,这个改变会影响进程的所有线程。
使用 GetCurrentDirectory 和 SetCurrentDircetory 来获得和设置当前驱动器和当前目录。
如果你提供给 GetCurrentDirectory 的缓冲区不足以容纳当前目录字符串,这个函数会返回一个保存这个目录所需的字符数,包括‘\0’ 字符,当这个调用成功时,返回一个缓冲区内字符的长度,不包括‘\0’ 字符。
注意:MAX_PATH 表示一个目录或文件的最大字符数,它在 Windef.h 中定义,所以一般会声明一个大小为 MAX_PATH 的缓冲区作为文件操作的参数。
系统不会跟踪所有驱动器的当前目录,可是,一些操作系统会利用环境变量字符串来提供对多驱动器当前目录的支持。一个进程的多驱动器的当前目录在环境变量中表示为以下形式:
=C:=C:\Utility\Bin =D:=D:\Program Files
分别表示驱动器 C 的当前目录为 C:\Utility\Bin 和驱动器 D 的当前的目录为 D:\Program Files。
如果你调用一个函数,传递一个 E:Readme.txt 参数,系统会在环境变量块中查找驱动器 E 的环境变量字符串,如果找到,系统会将变量的值做为驱动器 E 的当前目录,Readme.txt 则为这个当前目录下的一个文件,否则,系统会认为驱动器 E 的当前目录是根目录。
如果一个父进程创建了一个子进程,子进程不会继承父进程的当前目录,子进程默认会将每一个驱动器的根目录作为当前目录,如果父进程想要让子进程继承它的当前驱动器,必须在环境变量中加入需要继承的当前目录条目。
可以使用 C 运行时函数 _chdir 来修改一个驱动器的当前目录,_chdir 内部会调用 SetEnvironmentVariable 在环境变量块中增加或修改当前目录,这样指定驱动器的当前目录会被记录下来。一般情况下,驱动器当前目录环境变量位于环境变量块的前部。
系统版本
用户可以使用 GetVersion 来取得当前操作系统的版本,这个函数的返回值是一个 DWORD 类型,这个32位的返回值中高位字表示系统的次要版本,低位字表示系统的主要版本。(有关这个函数的返回值还有一个典故,最初微软为16位的系统设计了这个函数,这个函数结果表达的意思非常简单,MS-DOS 版本值位于高字处,Winodws 版本值位于低字处,这样高字表示主版本,低字表示次要版本。可是编写这段代码的程序员把这个次序弄颠倒了,主版本值放到了低位,次要版本号放到了高位,当发现这个问题时,已有很多人开始使用这个函数,所以微软不得不修改了它的文档。)
还可以使用 GetVersionEx 函数来获取系统版本的详细信息,这个函数需要分配一个 OSVERSIONINFOEX 结构的内存并将这个结构的地址传递给这个函数。
微软还提供了一个 VerifyVersionInfo 函数来便于用户与指定的版本信息进行比较,这个函数的第三参数可以使用 VER_SET_CONDITION 宏来生成一个 DWORDLONG 类型的参数。
CreateProcess 函数
CreateProcess 函数原型:
BOOL CreateProcess( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES psaThread, BOOL bInheritHandles, DWORD fdwCreate, PVOID pvEnvironment, PCTSTR pszCurDir, PSTARTINFO psiStartInfo, PROCESS_INFORMATION ppiProcInfo);
当一个线程调用 CreateProcess 函数时,系统会创建一个进程内核对象,并将这个内核对象的使用计数初始化为 1,系统然后为这个新进程创建一个虚拟地址空间并将可执行文件的代码和数据还有所有需要的 DLL 加载到这个地址空间中,接着,系统为新进程的主线程创建了一个线程内核对象,这个线程开始执行由链接器设置的 C/C++ 运行时启动代码,最后才是你提供的 main 函数开始执行。如果系统成功地创建了新进程和主线程,CreateProcess 返回 TRUE。注意:CreateProcess 这个函数在进程被完成初始化完成之前返回,这意味着,如果一个所需的 DLL 没有找到或没有正确初始化,进程会终止,但父进程不会知道任何有关初始化的问题。
pszApplicationName 和 pszCommandLine 参数
pszCommandLine 是一个指向 TCHAR 类型的指针,CreateProcess 函数期望你传递一个非常量字符串,这个函数内部会对这个参数进行修改,如果将这个参数设置为常量字符串,就是发生访问冲突错误。
pszCommandLine 参数指定了 CreateProcess 函数用来创建新进程的一个完整的命令行,如果 CreateProcess 函数接收到一个 pszCommandLine 参数,函数会假设这个字符串表示一个可执行文件,并假设这个文件有一个 .exe 后缀,CreateProcess 函数会以下面的顺序来搜索这个可执行文件:
1. 包含调用进程可执行文件的目录
2. 调用进程的当前目录
3. Windows\System32 目录,这个目录可通过 GetSystemDircetory 获得
4. Windows 目录
5. 列在 PATH 环境变量中的目录
如果给定的文件名包含了全路径名,系统会只在指定路径中查找可执行文件而不会再搜索以上的目录。
大多数情况下 pszApplicationName 这个参数设置为 NULL,可是如果设置了这个参数,系统会执行这个参数表示的可执行文件,系统不会假设这个参数指定的文件具有 .exe 后缀,所以你必须提供一个完全的名称,系统也不会在以上指定的目录中查找相应的可执行文件,它只会搜索当前目录,如果当前目录中没有可执行文件,CreateProcess 会失败。CreateProcess 有这个参数的原因是为了支持 POSIX 子系统。
psaProcess、psaThread 和 bInheritHandles 参数
psaProcess 和 psaThread 参数表示创建的进程对象和线程对象期望的安全设置,可以将这两个参数都设置为 NULL,系统会给这些对象设置默认的安全描述符。用户还可通过设置这两个参数的 SECURITY_ATTRIBUTES 结构的 bInheritHandle 成员为 TRUE,父进程之后创建的子进程可以继承这些进程和线程的句柄。例如,进程 A 创建了进程 B,将 psaProcess 参数的 SECURITY_ATTRIBUTES 结构的 bInheritHandle 成员设置为 TRUE,将 psaThread 参数的 SECURITY_ATTRIBUTES 结构的 bInheritHandle 成员设置为 FALSE,CreateProcess 返回后,ppiProcInfo 参数结构分别返回了进程 B 的进程和线程句柄,这两个句柄在进程 A 的句柄表中返回的进程句柄标志为可继承,而线程句柄会被打上不可继承标志。之后的某个时刻,进程 A 又创建了进程 C,并向 CreateProcess 函数传递了一个值为 TRUE 的 bInheritHandle 参数,进程 C 创建成功后,进程 A 中的所有可继承句柄都会复制到进程 C 的句柄表中,这样,进程 B 的句柄在进程 C 中可以正确地访问到,而 B 进程的线程句柄不会被进程 C 看到。
bInheritHandle 参数,如上所述,这个参数设置为 TRUE 表示可将父进程的句柄表中可继承句柄继承到新创建的子进程中去。继承的句柄与父进程有相同的句柄值和相同的访问权限。
fdwCreate 参数
这个参数标志指出新进程怎样被创建,还允许你指定一个优先级类型。
pvEnvironment 参数
这个参数是一个向新进程传递的环境变量块字符串的地址,父进程可以使用 GetEnvironmentStrings 函数来取得这个地址并传递给 CreateProcess 函数,大多数情况下传递一个 NULL 给这个函数,这样函数也会在内部调用 GetEnvironmentStrings 函数以便将父进程的环境变量块继承给子进程。