Windows C++ 程序的入口点

第一个问题,什么是入口点?

  • 对于开发者来说,程序的入口点就是程序执行的时候第一个执行的函数。

对于C++程序,常见的入口点有:

1.main

2.WinMain

3.DllMain

  • 对于操作系统来说,程序的入口点就是把程序装载到内存后,第一条命令开始的地方。

操作系统(Windows)如何确定入口点呢?

首先,Windows下所有可执行程序都是PE格式,PE其中一个组成部分 可选头 ,对应的数据结构:IMAGE_OPTIONAL_HEADER

在可选头中,有一个成员 AddressOfEntryPoint,该成员就表示程序的入口点。

关于PE文件格式,读者可以自行查阅资料。

在这里,我放出两个链接,可以快速了解PE的入口点

深入理解 Win32 PE 文件格式:https://blog.csdn.net/chenlycly/article/details/53378946

_IMAGE_OPTIONAL_HEADER structure:https://docs.microsoft.com/zh-cn/windows/desktop/api/winnt/ns-winnt-_image_optional_header

第二个问题,IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint指向的地址就是main/WinMain/DllMain的函数地址吗?

可能这么问,一些读者可能不好理解。

首先,对于Windows来说,IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint  其实就是程序的实际入口点

知道这一点之后,问题就可以换成这样 main/WinMain/DllMain 是程序的实际入口点吗?

答:当然不是,或者更严谨一点,大部分情况下不是

证明

1.使用vs创建新项目,项目类型选择空项目

创建空项目

2.添加.cpp文件,并添加main函数

main

3.添加端点,启动调试,当程序停在main函数中的断点时,shift+F11 跳出main函数,如图所示

invoke_main

可以看到,调试器进入了一个 exe_common.inl 的文件,该文件中的62行有一个函数 invoke_main(),函数名也很好理解,调用main,函数体也和函数名一样,在64行调用了main函数,也就是我们自己写的main函数。

由此可以证明,main函数并不是程序的实际入口点。

那么,invoke_main() 是实际的入口点吗?当然也不是。

main不是实际入口点,invoke_main也不是实际的入口点,那么哪个函数才是真正的入口点呢?它又是如何一步一步调用到我们写的main的呢?接下来继续分析。

至于一开始说的,大部分情况下入口点不是main/WinMain/DllMain,可能有些细心的读者会问,那剩下的小部分呢?别急,接下来会全部分析到。

查找真正的入口点

首先,先准备好需要的工具

1.dumpbin.exe  这个工具用作查看PE文件,如果读者电脑上没有这个工具,可自行下载

2.vscode  用来打卡c运行时库的代码,当然,读者也可以使用别的文本查看工具

 

开始

1.使用cmd进入我们刚刚新建的项目的输出目录,然后使用dumpbin查看编译后的程序

例如:

cd D:\Test\Debug

dumpbin /headers CPlusPlus.exe

实际输出目录和程序名读者自行修改

dumpbin_headers

大家注意看截图红色部分 OPTIONAL HEADER VALUES 的第6项,这个成员就是上面提到的

IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint ,也就是 程序的 实际入口点

因为我们的输出目录带有.PDB文件,dumpbin把这个 11041 这个地址对应的函数名打印出来了

mainCRTStartup,也就是说,我们刚刚那个程序实际的入口函数是这个,知道了实际入口函数名,接下来就好办了

大家是否还记得 exe_common.inl 这个文件,这个文件是 invoke_main 函数所在的文件,我们找到这个目录

X:\XX\Microsoft Visual Studio 14.0\VC\crt\src\vcruntime

然后通过vscode打开整个目录

全局搜索  mainCRTStartup

搜索结果有很多,我们不用关心 对于这个函数的调用和声明,我们只需要找到它的定义

它的定义位于文件  exe_main.cpp 

找到了实际的入口函数,就可以通过代码查看函数调用关系,一步一步往下,最后就可以看到invoke_main和main了

WinMain和DllMain也是相同的道理,把main函数改为WinMain/DllMain,编译链接后查看真正的入口函数,然后搜索查找入口函数的定义,这里就不一一列举了

main:

mainCRTStartup(exe_main.cpp)->__scrt_common_main(exe_common.inl)->__scrt_common_main_seh(exe_common.inl)->invoke_main(exe_common.inl)->main

WinMain:

WinMainCRTStartup(exe_main.cpp)->__scrt_common_main(exe_common.inl)->__scrt_common_main_seh(exe_common.inl)->invoke_main(exe_common.inl)->WinMain

DllMain:

DllMainCRTStartup(dll.dllmain.cpp)->dllmain_dispatch(dll.dllmain.cpp)->DllMain

链接器对于入口点的选择

读者朋友们可能会纳闷,为什么我们定义main函数和定义WinMain函数,程序的实际入口不一样,链接器是怎么选择入口点的,以下是我做了一些实验,总结出来的,供大家参考。不一定完全正确,欢迎大神指出问题

链接时,如果使用 /ENTRY 选项,则会使用 /ENTRY 选项指定的入口点,这一点就解释了上面所说的小部分情况,如果我们使用/ENTRY指定入口点为 main/WinMain/DllMain ,那么程序的实际入口点就是main/WinMain/DllMain

如果没有使用 /ENTRY 选项(一般情况下):

对于 EXE, 链接时如果使用 /SUBSYSTEM 选项,链接器则会根据选项参数选择实际的入口点

  1. /SUBSYSTEM:CONSOLE 实际入口: mainCRTStartup (or wmainCRTStartup) 内部调用: main (or wmain)
  2. /SUBSYSTEM:WINDOWS 实际入口: WinMainCRTStartup (or wWinMainCRTStartup) 内部调用 : WinMain (or wWinMain) 调用约定: __stdcall

链接时如果没有使用 /SUBSYSTEM 选项,链接器会根据现有的过程(函数)选择实际的入口点

exe_common.inl 通过宏来控制 invoke_main 函数的版本

例如:

invoke_main不同版本

如果定义了main,则编译器会定义 _SCRT_STARTUP_MAIN 这个宏,就会编译 调用main函数的invoke_main版本

  1. 如果程序定义了 main 过程,则链接器会使用 mainCRTStartup (or wmainCRTStartup) 作为程序的实际的入口点
  2. 如果程序定义了 WinMain 过程,则链接器会使用 WinMainCRTStartup (or wWinMainCRTStartup) 作为程序的实际的入口点
  3. 如果程序同时定义了 main 过程和 WinMain 过程,链接器会优先使用WinMainCRTStartup (or wWinMainCRTStartup) 作为程序的实际的入口点

如果程序既没有定义 main 过程,也没有定义 WinMain 过程,则会链接失败,提示 需要定义入口点

对于 DLL , 链接器选择的实际的入口点是 _DllMainCRTStartup 内部调用 DllMain

如果开发者没有定义 DllMain,系统则会事先编译 Microsoft Visual Studio 14.0\VC\crt\src\vcruntime\dll_dllmain_stub.cpp

并链接

参考:  https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/f9t8842e(v=vs.85)/html

综上,链接器默认(未使用/ENTRY选项)选择的入口点有三个

mainCRTStartup,WinMainCRTStartup,_DllMainCRTStartup

但是开发者的代码中一般不会有这三个函数的实现,这时候就需要借助编译器自带的运行时库 : MSVCRTD.lib

MSVCRTD.lib是Debug版本,其对应的Release版本是MSVCRT.lib,没有最后面的D(ebug)

这个库中实现编译好了这三个函数

要用到这个库,当然就需要链接这个库,但是我们查看刚刚创建的项目属性,并没有添加这个库

依赖项

没有添加这个库,那怎么链接这个库呢?

其实,对于.cpp文件,编译器在编译时,会自动加上这个库

证明:

1.设置汇编文件输出

设置汇编输出

2.重新编译后,在项目的Debug目录下找到汇编文件 xxx.asm,并打开

汇编代码

如图,汇编代码中,引入了这个库 MSVCRTD(忽略了后缀.lib)

附上一张链接器选择入口点的流程图

入口点选择

 

最后,感谢大家看到这里,虽然也不一定有人会看

猜你喜欢

转载自blog.csdn.net/iBliBiliBelieve/article/details/83614820