1.一般将进程定义成一个正在运行的程序的一个实例,由以下两部分构成:
- 一个内核对象,操作系统用它来管理进程,内核对象也是系统保存进程统计信息的地方。
- 一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据。此外它还包含动态内存分配,比如线程堆栈和堆的分配。
2.进程要做任何事情,都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。
3.每个线程都有它自己的一组CPU寄存器和它自己的堆栈。对于所有要运行的线程,操作系统会轮流为每一个线程调度一些CPU时间,它会采取轮询的方式为每个线程都分配时间片,从而营造出所有线程都在并发运行的假象。
4.用户运行应用程序时,操作系统的加载程序会检查可执行文件映像的文件头,并获取这个子系统。如果此值表明是一个CUI程序,加载程序会自动确保有一个可用的文本控制台窗口(从命令提示符启动),或者创建一个新窗口(从windows资源管理器启动)。如果此值表明是一个GUI程序,加载器只是加载这个程序。一旦程序开始运行,操作系统就不再关心应用程序的界面是什么类型。
5.Windows应用程序必须有一个入口点函数,应用程序开始运行时,这个函数会被调用。操作系统实际并不调用我们所写的入口点函数。相反,它会调用由C/C++运行库实现并在链接时使用-entry:命令行选项来设置的一个C/C++运行时启动函数。该函数将初始化C/C++运行库,使我们能调用malloc和free之类的函数。它还确保了在我们的代码开始执行之前,我们声明的任何全局和静态C++对象都被正确地构造。
6.在链接可执行文件时,将根据链接器开关 /SUBSYSTEM:WINDOWS或者 /SUBSYSTEM:CONSOLE以及UNICODE和ANSI来选择正确地C/C++运行库启动函数。
7.一旦链接器开关/SUBSYSTEM被移除,链接器会检查代码中的函数(WinMain,main...)是四个中的哪一个来推算。
8.所有C/C++运行库启动函数所做的事情基本都一样,区别在于它们要处理的是ANSI还是UNICODE字符串。以及在初始化C运行库之后,它们调用的是哪一个入口点函数。这些启动函数的用途简单总结如下。
- 获取指向新进程的完整命令行的一个指针
- 获取指向新进程的环境变量的一个指针
- 初始化C/C++运行库的全局变量。如果包含了StdLib.h,我们的代码就可以访问这些变量。
- 初始化C运行库内存分配函数(malloc和calloc)和其他底层I/O例程使用的堆(heap)。
- 调用所有全局和静态C++类对象的构造函数。
9.完成上述所有的初始化工作之后,C/C++启动函数就会调用应用程序的入口点函数。
// 如果我们写了_tWinMain & 定义了_UNICODE GetStartupInfo(&StartupInfo); int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
// 如果我们写了_tWinMain & 没有定义_UNICODE GetStartupInfo(&StartupInfo); int nMainRetVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineAnsi, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
注意,__ImageBase是一个链接器定义的伪变量,表明可执行文件被映射到应用程序内存中的什么位置。
// 如果我们写了_tmain & 定义了_UNICODE int nMainRetval = wmain(argc, argv, envp);
// 如果我们写了_tmain & 没有定义_UNICODE int nMainRetval = main(argc, argv, envp);
注意,用VS向导生成应用程序时,CUI应用程序的入口中没有定义第三个参数(环境变量块),如果需要访问进程的环境变量,只需将上述调用换成下面这一行:
int _tmain(int argc, TCHAR* argv[], TCHAR* env[])
这个env参数指向一个数组,数组中包含所有环境变量及其值,两者用等号(=)分隔。
10.入口点函数返回后,启动函数将调用C运行库函数exit,向其传递返回值(nMainRetVal)。exit函数执行以下任务。
- 调用_onexit函数调用所注册的任何一个函数
- 调用所有全局和静态C++类对象的析构函数。
- 在DEBUG生成中,如果设置了_CRTDBG_LEAK_CHECK_DF标志,就通过调用_CrtDumpMemoryLeaks函数来生成内存泄漏报告。
- 调用操作系统的ExitProcess函数,向其传入nMainRetVal。这会导致操作系统“杀死”我们的进程,并设置它的退出代码。
11.加载到进程地址空间的每一个可执行文件或DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例被当作WinMain函数的第一个参数hInstanceExe传入。
12.事实上,HMOUDLE和HINSTANCE完全是一回事。之所以有两种数据类型,是由于在16位Windows中,HMOUDLE和HINSTANCE表示不同类型的数据。
13.WinMain的hInstanceExe参数的实际值是一个内存基地址,系统将可执行文件的映像加载到进程地址空间中的这个位置。具体加载到哪一个基地址,是由连接器决定的。不同的链接器使用不同的默认基地址。可以使用GetModuleHandle函数来返回一个句柄/基地址。
14.C/C++运行库启动代码总是向WinMain的hPrevInstance参数传递NULL。该参数用于16位Windows系统。VS在向导生成的C++ GUI项目中利用UNREFERENCED_PARAMETER宏来消除这种警告。
15.C运行库的启动代码开始执行一个GUI应用程序的时候,会调用Windows函数GetCommandLine来获取进程的完整命令行,忽略可执行文件的名称,然后将指向命令行剩余部分的一个指针传给WinMain的pszCmdLine参数。
16.GetCommandLine函数返回一个缓冲区指针,缓冲区中包含完整的命令行(包括已执行的文件的完整路径名),而且这个函数返回的总是同一个缓冲区的地址,所以最好不要向其中写入数据。
17.每个进程都有一个与它关联的环境块,这是在进程地址空间内分配的一块内存,其中包含字符串和下面相似:
注意空格是有意义的。
18.用户登录Windows时,系统会创建外壳(shell)进程,并将一组环境字符串与其关联。系统通过检查注册表中的两个注册表项来获得初始的环境字符串。
- 第一个注册表项包含应用于系统的所有环境变量的列表:
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerEnvironment
- 第二个注册表项包含应用于当前登录用户的所有环境变量的列表:
HKEY_CURRENT_USEREnvironment
19.通常子进程会继承(不共享同一个环境块)一组环境变量,这些环境变量和父进程的环境变量相同,不过,父进程可以控制哪些环境变量允许子进程继承。
20.每个进程都关联了一组标志,作用时让系统知道进程如何响应严重错误,包括磁盘介质错误、未处理的异常、文件查找错误以及数据对齐错误等。进程可以调用SetErrorMode(UINT fuErrorMode)函数来告诉系统如何处理这些错误。
默认情况下,子进程会继承父进程的错误模式标志,例如一个进程已经打开了一个错误标志,并生成了一个子进程,则子进程也会打开这个标志。不过子进程并不知道这一点。父进程可以阻止子进程继承其错误模式。
21.系统跟踪记录着进程的当前驱动器和目录,但没有记录每个驱动器的当前目录,操作系统通过进程的环境字符串来提供多个驱动器的当前目录。例如:
=C:C:UtilityBin
=D:=D:Program Files
如果变量没有找到,系统就假定指定驱动器的当前目录是它的根目录。