作者:shenzi
链接:http://blog.youkuaiyun.com/shenzi
Windows核心编程:进程
1.1进程概念
- 专用的虚拟地址空间(包括程序代码,数据,堆,栈等);
- 可执行程序,它定义初始代码和数据,并映射到进程的虚拟地址空间;
- 各种内核对象的开放句柄列表,它们对所有线程都是可访问的;
- 标识用户并成为“访问令牌”的安全环境、安全组和同进程相关的特权;
- 称为“进程ID”的唯一标示符;
- 至少一个执行线程;
图3:进程相关数据结构
1.2Microsoft C/C++创建应用程序
Microsoft Visual Studio集成开发环境会设置各种链接器开关,是链接器将子系统的正确类型嵌入最终生成的可执行文件中。对于CUI (Console User Interface,控制台用户界面)程序,这个链接器开关是/SUBSYSTEM:CONSOLE ,对于GUI (Graphical User Interface,图形用户界面)程序,则是/SUBSYSTEM:WINDOWS 。
Windows应用程序必须有一个入口点函数,应用程序开始运行时,这个函数会被调用。C/C++开发人员可以使用一下两种入口点函数:
int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
具体的符号拒绝与我们是否要使用Unicode字符串。操作系统实际并不调用我们所写的入口点函数。相反,它会调用由C/C++运行库实现并在链接时使用 -entry:命令行选项来设置一个C/C++运行时启动函数。该函数初始化C/C++运行库,确保在我们的代码开始执行之前,声明的任何全局和静态 C++对象都被正确地构造。
应用程序类型 | 入口点函数 | 嵌入可执行文件的启动函数 |
---|---|---|
处理ANSI字符和字符串的GUI应用程序 | _tWinMain (WinMain) | WinMainCRTStartup |
处理Unicode字符和字符串的GUI应用程序 | _tWinMain (wWinMain) | wWinMainCRTStartup |
处理ANSI字符和字符串的CUI应用程序 | _tmain (Main) | mainCRTStartup |
处理Unicode字符和字符串的CUI应用程序 | _tmain (Wmain) | wmainCRTStart |
链接器根据/SUBSYSTEM链接器开关,选择相应的C/C++运行库启动函数。如果指定 /SUBSYSTEM:CONSOLE ,链接器会寻找main或wmain,并选择相应 C/C++运行时启动函数 。 如果没有找到这两个函数,链接器将返回“unresolved external symbol”(无法解析的外部符号)错误。 /SUBSYSTEM:WINDOWS 的情况类似。
如果我们移除 /SUBSYSTEM 链接器开关 ,链接器将自动判断应该将应用程序设备哪一个子系统,链接器会检查代码中包括4个函数中的哪一个(WinMain,wWinMain,main,wmain),并据此推算可执行文件应该是哪个子系统,以及应该在可执行文件中嵌入哪个C/C++启动函数。
所以我们在创建一个新项目时如果错误的选择了项目的类型,我们可以更改 /SUBSYSTEM: 开关,或则直接删除 /SUBSYSTEM: 开关,让链接器自动判断应该将应用程序设为哪个子系统。
C/C++运行库启动函数所做的事情基本都是一样的,区别在于它们要处理的是ANSI字符串,还是Unicode字符串;以及在初始化C运行库之后它们调用的是哪一个入口点函数。启动函数将做了以下一些工作:
- 获取指向新进程的完整命令行的一个指针;
- 获取指向新进程的环境变量的一个指针;
- 初始化C/C++运行库的全局变量。如果定义了StdLib.h,我们的代码将可以访问这些变量;
- 初始化C/C++运行库内存分配函数(malloc和calloc)和其它底层I/O例程使用的堆;
- 调用所有全局和静态C++类对象的构造函数;
完成所有这些初始化工作之后,C/C++启动函数将会调用相应的应用程序的入口点函数 (WinMain,wWinMain,main,wmain) 。
入口点函数返回后,启动函数将调用C运行库函数exit, 向其传递返回值(nMainRetVal)。
exit 函数执行以下任务:
- 调用_onexit 函数调用所注册的任何一个函数;
- 调用所有全局和静态C++对象的析构函数;
- 在DEBUG 生成中,如果设置了_CRTDBG_LEAK_CHECK_DF 标志,就通过调用_CrtDumpMemoryLeaks 来生成内存泄露报告;
- 调用操作系统的ExitProcess 函数,向其传入nMainRetVal。这回导致操作系统“杀死”我们的进程,并设置它的推出代码。
1.2.1进程实例句柄
加载到进程地址空间的每一个可执行文件或者DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例被当做(w)WinMain函数的第一个参数hInstanceExe传入。在需要加载资源的函数调用中,一般都要提供此句柄的值。许多应用程序都会将 hInstanceExe参数保存在一个全局变量中。
有的函数需要一个HMODULE类型的参数,HMODULE 和HINSTANCE 完全是一回事。(16位Windows遗留问题。)
实例句柄实际值是一个内存基地址。 hInstanceExe实际是可执行文件的映像加载到进程地址空间中的位置。使用Microsoft链接器的/BASE:address链接器开关,可以更改要将应用程序加载到哪个基地址。
为了知道一个可执行文件或DLL文件被加载到进程地址空间的什么位置,可以使用 GetModuleFileName 函数返回一个句柄/基地址:
HMODULE GetModuleHandle(PCTSTR pszModule);
GetModuleFileName传入NULL,返回主调进程的可执行文件的基地址。
1.2.2进程前一个实例的句柄
C/C++运行库启动代码总是向(w)WinMain的hPrevInstance参数传递NULL,又是 16位Windows遗留问题 ,绝对不要在自己的代码中引用这个参数:
int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PSTR pszCmdLine,
int nCmdShow);
1.2.3进程的命令行
系统在创建一个新进程时,会传一个命令行给它。这个命令行几乎总是非空的;至少,用于创建新进程的可执行文件的名称是命令上的第一个标记(token)。
C运行库的启动代码开始执行一个GUI应用程序时,会调用Windows函数 GetCommandLine() 来获取进程的完整命令行,忽略可执行文件的名称,然后将指向命令行剩余部分的一个指针传给WinMain的pszCmdLine参数。
将命令行分解成单独的标记:
PWSTR* CommandLineToArgvW(
PWSTR pszCmdLine,
int* pNumArgs);
1.2.4进程的环境变量
每个进程都有一个与它关联的环境块,这是在进程地址空间内分配的一块内存。
1.3CreateProcess函数
BOOL CreateProcess(
PCTSTR pszApplicationName, //可执行文件的名称
PTSTR pszCommandLine, //传给新进程的命令行字符串
PSECURITY_ATTRIBUTES psaProcess, //进程对象安全属性
PSECURITY_ATTRIBUTES psaThread, //线程对象安全属性
BOOL bInheritHandles, //指定继承性
DWORD fdwCreate, //标识影响新进程创建方式的标志
PVOID pvEnvironment, //指向新进程要使用的环境字符串
PCTSTR pszCurDir, //允许父进程指定子进程的当前驱动器和目录
PSTARTUPINFO psiStartInfo,//指向一个STARTUPINFO结构或STARTUPINFOEX结构
PPROCESS_INFORMATION ppiProcInfo); //指向一个 PROCESS_INFORMATION 结构
CreateProcess 创建进程的主要阶段:
- 打开将在进程中被执行的映像文件(.EXE);
- 创建Windows内核进程对象;
- 创建初始线程(堆栈,环境和内核线程对象);
- 向Win32子系统通知新进程以便它设置新的进程和线程;
- 启动初始线程的执行(除非CREATE-SUSPENDED标记被指定);
- 在新进程和线程的环境中,完成地址空间的初始化(例如加载所需DLL)并开始程序的执行;
图4:进程创建主要阶段
1.4终止进程
进程可以通过以下4种方式终止:
- 主线程的入口点函数返回(强烈推荐的方式);
- 进程中的一个线程调用ExitProcess函数(要避免这种方式);
- 另一个进程中的县城调用TerminateProcess函数(要避免这种方式);
- 进程中的所有线程都“自然死亡”(这种情况几乎从来不会发生);
1.4.1主线程的入口点函数返回
设计一个应用程序时,应该保证只有在主线程的入口点函数返回之后,这个应用程序的进程才终止。只有这样才能保证主线程的所有资源都被正确清理:
- 该线程创建的任何C++对象都将由这些对象的析构函数正确销毁;
- 操作系统将正确释放线程栈使用的内存;
- 系统将进程的退出代码(在进程内核对象中维护)设为入口点函数的返回值;
- 系统递减进程内核对象的使用计数;
1.4.2ExitProcess函数
进程中的某个线程调用 ExitProcess 函数来终止进程:
VOID ExitProcess(UINT fuExitCode);
该函数将终止进程,并将进程的退出代码设为 fuExitCode。 ExitProcess 不会返回值,因为进程已经终止了。 详见
1.4.3 TerminateProcess函数
调用 TerminateProcess 也可以终止一个进程:
BOOL TerminateProcess(
HANDLE hProcess,
UINT fuExitCode);
此函数与 ExitProcess 的一个明显区别是:任何线程都可以调用 TerminateProcess 来终止另一个进程或者它自己的进程。 详见
1.4.4当进程中的所有线程终止时
当一个进程中的所有县城都终止了,操作系统就认为没有任何理由再保持进程的地址空间,就会终止这个进程。
1.4.5当进程终止运行时
一个进程终止时,系统会一次执行以下操作:
- 终止进程中遗留的任何线程;
- 释放进程分配的所有USER对象和GDI对象,关闭所有内核对象(如果使用计数减为0,销毁内核对象);
- 进程的退出代码从STILE_ACTIVE变为传给ExitProcess或TerminateProcess函数的代码;
- 进程内核对象的状态变为已触发状态;
- 进程内核对象的使用计数递减1;