今天终于有时间来研究一下一个很大很大的工程编译成一个exe和若干dll后,程序是如果执行它的第一条指令的?操作系统以什么规则来找到应该执行的第一条指令(或说如何找到第一个入口函数的)?
我们以前写windows程序时,都是先写个main()函数,然后再写自己的逻辑;然后编译,然后点击exe就能运行我们的程序了;如果我们用VS2005工具生成一个非空工程,工程会为我们提供一个int _tmain(int argc, _TCHAR* argv[])或WinMain()函数的入口,然后我们在里面添加程序等等。我们上学时这是这样做了,但是很多人这时理所当然的,很少人会去问为什么会这样?
我读了MSDN里面的讲解才弄出点眉目了,其实我们以前所写的以main()函数开始的程序都是一个半成品,剩下的也是与系统息息相关的工作由编译器帮我们代劳了。怎么回事呢?编译器是如何帮我们代劳的呢?那么程序被系统加载时,准确的说是被系统中的加载器加载时又是如何知道编译器在我们写的程序上做了手脚呢?难道编译器和加载器之间有什么协定吗?这一些列的问题,做为刚入行的你是否在心里问过自己没有!?
我们以前写的程序在编译器编译成为一个模块(可能是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++ 运行时库代码在静态非局部变量上调用构造函数和析构函数。
先摘录一段msdn的解释如下:
When you link your image, you either explicitly or implicitly specify an entry point that the operating system will call into after loading the image. For a DLL, the default entry point is DllMainCRTStartup. For an EXE, it isWinMainCRTStartup. You can override the default with the /ENTRY linker option. The CRT provides an implementation for DllMainCRTStartup, WinMainCRTStartup, and wWinMainCRTStartup (the Unicode entry point for an EXE). These CRT-provided entry points call constructors on global objects and initialize other da
大家看到了吧,上面我用红色标志了吗,我们可以使用连接器的链接选择来设置我们的函数入口点,但是最好不要这样做,原因就是我用蓝色标志的地方,如果我们重新设置入口点函数,则必须要在入口点函数中自己写上有关的初始化工作,这样岂不麻烦,所有我们最好用默认的入口点函数。
修改入口点方法:proerties->Linker->Advanced->EntryPoint
如果函数与连接器的SubSystem的属性要一致的:
proerties->Linker->System->SubSystem
如果未指定 /DLL 或 /SUBSYSTEM 选项,则链接器将根据是否定义了 main 或 WinMain 来选择子系统和入口点。 函数 main、WinMain 和 DllMain 是三种用户定义的入口点形式。
通过上面的分析就知道,在微软系统中原来操作系统中的加载器与连接器之间是有协议的,要不然在加载运行程序时不可能成功的,比如你将windows程序放到apple系统上运行,就会无法运行,因为apple的加载程序根本不知道加载windows的exe的协议。
【注意】
/***
*mainCRTStartup(void)
*wmainCRTStartup(void)
*WinMainCRTStartup(void)
*wWinMainCRTStartup(void)
*
*Purpose:
* These routines do the C runtime initialization, call the appropriate
* user entry function, and handle termination cleanup. For a managed
* app, they then return the exit co
* is the managed startup co
* never return.
*
* Function: User entry called:
* mainCRTStartup main
* wmainCRTStartup wmain
* WinMainCRTStartup WinMain
* wWinMainCRTStartup wWinMain
* wmainCRTStartup wmain
* WinMainCRTStartup WinMain
* wWinMainCRTStartup wWinMain
*
*
*******************************************************************************/
设有一个Win32下的可执行文件MyApp.exe,这是一个Win32应用程序,符合标准的PE格式。MyApp.exe的主要执行代码都集中在其源文件MyApp.cpp中,该文件第一个被执行的函数是WinMain。初学者会认为程序就是首先从这个WinMain函数开始执行,其实不然。
设有一个Win32下的可执行文件MyApp.exe,这是一个Win32应用程序,符合标准的PE格式。MyApp.exe的主要执行代码都集中在其源文件MyApp.cpp中,该文件第一个被执行的函数是WinMain。初学者会认为程序就是首先从这个WinMain函数开始执行,其实不然。
给出一篇博文,该博文讲的比较好:
Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=455591
在WinMain函数被执行之前,有一系列复杂的加载动作,还要执行一大段启动代码。运行程序MyApp.exe时,操作系统的加载程序首先为进程分配一个4GB的虚拟地址空间,然后把程序MyApp.exe所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0X00400000的位置。加载一个应用程序的时间比一般人所设想的要少,因为加载一个PE文件并不是把这个文件整个一次性的从磁盘读到内存中,而是简单的做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性的全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页也可能被交换到磁盘中。
接着,系统在内核中创建进程对象和主线程对象以及其它内容。
然后操作系统的加载程序搜索PE文件中的引入表,加载所有应用程序所使用的动态链接库。对动态链接库的加载与对应用程序的加载完全类似。
再接着,操作系统执行PE文件首部所指定地址处的代码,开始应用程序主线程的执行。首先被执行的代码并不是MyApp中的WinMain函数,而是被称为C Runtime startup co
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)指定。