您好,登录后才能下订单哦!
今天终于有时间来研究一下一个很大很大的工程编译成一个exe和若干dll后,程序是如果执行它的第一条指令的?操作系统以什么规则来找到应该执行的第一条指令(或说如何找到第一个入口函数的)?
我们以前写windows控制台程序时,都是先写个main()函数,写windows窗口程序时,首先要写winmain()函数,然后再写自己的逻辑;然后编译,然后点击exe就能运行我们的程序了;并且认为main或winmain是程序中第一个运行的程序,也是必须存在的函数,但深入了解window的编程就会发现,main或winmain函数不是第一个运行的函数,在他们之前首先会运行另一个函数,会对全局变量进行初始化和资源的分配,以及程序结束后会对资源的释放。
我们以前写的程序在编译器编译成为一个模块(可能是obj文件或其他形式),然后连接器会将一些所需要的库文件和刚才编译器生成的文件进行连接,最终生成一个exe文件,在所连接的库文件中就包含CRT运行时库,这就是我们今天谈论的主角。在运行时库里面有好一个已经定义如下的函数函数:
(1)mainCRTStartup(或 wmainCRTStartup) //使用 /SUBSYSTEM:CONSOLE 的应用程序
(2)WinMainCRTStartup(或 wWinMainCRTStartup)//使用 /SUBSYSTEM:WINDOWS 的应用程序
(3)_DllMainCRTStartup //调用 DllMain(如果存在),DllMain 必须用 __stdcall 来定义
其中w开头的函数时unicode版本的,分割符‘//’后面的是入口点函数匹配的subsystem属性设置。
如果未指定 /DLL 或 /SUBSYSTEM (也就是subsystem选项)选项,则链接器将根据是否定义了 main 或 WinMain 来选择子系统和入口点。 函数 main、WinMain 和 DllMain 是三种用户定义的入口点形式。
在默认情况下,如果你的程序中使用的是main()或_main()函数,这连接器会将你的使用(1)中的函数连接到你的exe中;如果你的函数是以WinWain()函数开始的则连接器使用(2)中的函数连接进exe中;如果我们写的是DLL程序这连接进DLL的是(3)中的函数。
用我们写的程序最终生成的exe执行时,一开始执行的就是上面的函数之一,而不是我们程序所写的main或WinMain等。那么连接器为什么要这样做呢?这就是因为我们写的程序必须要使用到各种各样的运行时库函数才能正常工作,所有在执行我们自己写程序之前必须要先准备好所需要的一切库,噢,明白了吧,之所以要连接它们是因为他们肩负着很重要的使命,就是初始化好运行时库,准备我们的程序执行时调用。
那么这些函数具体做了什么呢?通过MSDN我们可以知道---它们会去进一步调用其他函数,使得C/C++ 运行时库代码在静态非局部变量上调用构造函数和析构函数。
设有一个Win32下的可执行文件MyApp.exe,这是一个Win32应用程序,符合标准的PE格式。MyApp.exe的主要执行代码都集中在其源文件MyApp.cpp中,该文件第一个被执行的函数是WinMain。初学者会认为程序就是首先从这个WinMain函数开始执行,其实不然。
在WinMain函数被执行之前,有一系列复杂的加载动作,还要执行一大段启动代码。运行程序MyApp.exe时,操作系统的加载程序首先为进程分配一个4GB的虚拟地址空间,然后把程序MyApp.exe所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0X00400000的位置。加载一个应用程序的时间比一般人所设想的要少,因为加载一个PE文件并不是把这个文件整个一次性的从磁盘读到内存中,而是简单的做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性的全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页也可能被交换到磁盘中。
接着,系统在内核中创建进程对象和主线程对象以及其它内容。
然后操作系统的加载程序搜索PE文件中的引入表,加载所有应用程序所使用的动态链接库。对动态链接库的加载与对应用程序的加载完全类似。
再接着,操作系统执行PE文件首部所指定地址处的代码,开始应用程序主线程的执行。首先被执行的代码并不是MyApp中的WinMain函数,而是被称为C Runtime startup code的WinMainCRTStartup函数,该函数是连接时由连接程序附加到文件MyApp.exe中的。该函数得到新进程的全部命令行指针和环境变量的指针,完成一些C运行时全局变量以及C运行时内存分配函数的初始化工作。如果使用C++编程,还要执行全局类对象的构造函数。最后,WinMainCRTStartup函数调用WinMain函数。
WinMainCRTStartup函数传给WinMain函数的4个参数分别为:hInstance、hPrevInstance、lpCmdline、nCmdShow。
hInstance:该进程所对应的应用程序当前实例的句柄。WinMainCRTStartup函数通过调用GetStartupInfo函数获得该参数的值。该参数实际上是应用程序被加载到进程虚拟地址空间的地址,通常情况下,对于大多数进程,该参数总是0X00400000。
hPrevInstance:应用程序前一实例的句柄。由于Win32应用程序的每一个实例总是运行在自己的独立的进程地址空间中,因此,对于Win32应用程序,WinMainCRTStartup函数传给该参数的值总是NULL。如果应用程序希望知道是否有另一个实例在运行,可以通过线程同步技术,创建一个具有唯一名称的互斥量,通过检测这个互斥量是否存在可以知道是否有另一个实例在运行。
lpCmdline:命令行参数的指针。该指针指向一个以0结尾的字符串,该字符串不包括应用程序名。
nCmdShow:指定如何显示应用程序窗口。如果该程序通过在资源管理器中双击图标运行,WinMainCRTStartup函数传给该参数的值为SW_SHOWNORMAL。如果通过在另一个应用程序中调用CreatProcess函数运行,该参数由CreatProcess函数的参数lpStartupInfo(STARTUPINFO.wShowWindow)指定。
操作系统装载应用程序后,做完初始化工作就转到程序的入口点执行。程序的默认入口点由连接程序设置, 不同的连接器选择的入口函数也不尽相同。在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;对图形用户界面(GUI)程序设置的入口函数是 WinMainCRTStartup,WinMainCRTStartup 调用你自己写的 WinMain 函数。具体设置哪个入口点是由连接器的“/subsystem:”选项确定的,它告诉操作系统如何运行编译生成的.EXE文件。可以指定四种方式:CONSOLE|WINDOWS|NATIVE|POSIX。如果这个选项参数的值为 WINDOWS,则表示该应用程序运行时不需要控制台,有关连接器参数选项的详细说明请参考 MSDN 库。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。