程序的启动和退出过程详解
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应用程序WinMainWinMainCRTStartup需要Unicode字符和字符串的GUI应用程序wWinMainwWinMainCRTStartup需要ANSI字符和字符串的CUI应用程序mainmainCRTStartup需要Unicode字符和字符串的CUI应用程序wmainwmainCRTStartup 链接程序负责在它连接可执行文件时选择相应的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 + +运行期全局变量变量名类型说明_osverunsigned int操作系统的测试版本。例如Windows 2000 Beta 3是测试版本2031 。因此_osver的值是2031_winmajorunsigned int采用十六进制表示法的Windows主要版本。对于Windows2000来说,它的值是5_winminorunsigned int采用十六进制表示法的Windows次要版本。对于Windows2000来说,它的值是0_winverunsigned int( _winmajor < < 8 ) + _ winminor在命令行上传递的参数号__argcunsigned int带有指向ANSI/Unicode字符串的指针的__argc大小的数组__argvchar * *带有指向ANSI/Unicode字符串的指针的__argc大小的数组__wargvwchar_t * *每个数组项均指向一个命令行参数_environchar * *指向ANSI/Unicode字符串的指针的数组。每个数组项指向一个环境字符串_wenvironwchar_t * *指向ANSI/Unicode字符串的指针的数组。每个数组项指向一个环境字符串_pgmptrchar *正在运行的程序的ANSI/Unicode全路径和名字_wpgmptrwchar_t *正在运行的程序的ANSI/Unicode全路径和名字