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把 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 选项,链接器则会根据选项参数选择实际的入口点

  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)

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

入口点选择

 

  • 17
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
C++中设计Windows窗口程序的基本步骤如下: 1. 引入Windows.h头文件,该头文件包含了Windows窗口程序所需的函数和常量。\[1\] 2. 定义WinMain函数作为窗口程序入口。\[1\] 3. 创建一个空的Windows窗口项目。\[2\] 4. 在WinMain函数中,注册窗口类,这是创建窗口的必要步骤。\[2\] 5. 创建窗口,指定窗口的样式、标题、位置和大小等属性。\[2\] 6. 进入消息循环,等待用户的输入和系统的消息。\[2\] 7. 在消息循环中,使用回调函数处理窗口的消息,例如鼠标击、键盘输入等。\[2\] 8. 如果需要显示窗口,使用ShowWindow函数将窗口显示出来。\[3\] 9. 使用UpdateWindow函数更新窗口,确保窗口的显示内容正确。\[3\] 10. 编写完整的代码,包括窗口类的注册、窗口的创建和消息循环等。\[2\] 请注意,以上是一个简单的窗口程序设计的基本步骤,具体的实现可能会有一些细微的差异。 #### 引用[.reference_title] - *1* [C++ Windows 窗体程序入门 - 1.你的第亿个窗体程序](https://blog.csdn.net/wind_2067/article/details/124568379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [C/C++ 从零实现一个windows窗口(非常详细)](https://blog.csdn.net/weixin_50964512/article/details/124140735)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值