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++启动函数将会调用相应的应用程序的入口点函数 (WinMain,wWinMain,main,wmain) 。
入口点函数返回后,启动函数将调用C运行库函数exit, 向其传递返回值(nMainRetVal)。
exit 函数执行以下任务:
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 创建进程的主要阶段:
1.4终止进程
进程可以通过以下4种方式终止:
1.4.1主线程的入口点函数返回
设计一个应用程序时,应该保证只有在主线程的入口点函数返回之后,这个应用程序的进程才终止。只有这样才能保证主线程的所有资源都被正确清理:
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当进程终止运行时
一个进程终止时,系统会一次执行以下操作: