对于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
可能这么问,一些读者可能不好理解。
首先,对于Windows来说,IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint 其实就是程序的实际入口点
知道这一点之后,问题就可以换成这样 main/WinMain/DllMain 是程序的实际入口点吗?
答:当然不是,或者更严谨一点,大部分情况下不是
1.使用vs创建新项目,项目类型选择空项目
2.添加.cpp文件,并添加main函数
3.添加端点,启动调试,当程序停在main函数中的断点时,shift+F11 跳出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
实际输出目录和程序名读者自行修改
大家注意看截图红色部分 OPTIONAL HEADER VALUES 的第6项,这个成员就是上面提到的
IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint ,也就是 程序的 实际入口点
因为我们的输出目录带有.PDB文件,dumpbin把 00411041 这个地址对应的函数名打印出来了
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 选项,链接器则会根据选项参数选择实际的入口点
链接时如果没有使用 /SUBSYSTEM 选项,链接器会根据现有的过程(函数)选择实际的入口点
exe_common.inl 通过宏来控制 invoke_main 函数的版本
例如:
如果定义了main,则编译器会定义 _SCRT_STARTUP_MAIN 这个宏,就会编译 调用main函数的invoke_main版本
如果程序既没有定义 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)
附上一张链接器选择入口点的流程图