0

    Windows C++ 程序的入口点

    2023.11.18 | admin | 90次围观

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

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

    1.main

    2.WinMain

    3.DllMain

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

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

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

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

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

    深入理解 Win32 PE 文件格式:

    _IMAGE_OPTIONAL_HEADER structure:

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

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

    首先,对于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函数。

    由此可以证明dll找不到入口点,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

    找不到入口点dll_dll找不到入口点_在dll中找不到入口点

    然后通过vscode打开整个目录

    全局搜索mainCRTStartup

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

    它的定义位于文件 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:CONSOLE 实际入口: mainCRTStartup (or wmainCRTStartup) 内部调用: main (or wmain)/SUBSYSTEM:WINDOWS 实际入口: WinMainCRTStartup (or wWinMainCRTStartup) 内部调用 : WinMain (or wWinMain) 调用约定: __stdcall

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

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

    例如:

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

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

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

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

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

    并链接

    参考: (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)

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

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论