上节课我们分析函数栈帧时,有同学问了个特别好的问题:“老师,main 函数是被谁调用的呀?” 今天咱们就用 VS2022 的调试工具,揭开这个底层逻辑 —— 其实main函数并不是程序的起点,在它执行之前,已经有一系列 “幕后工作者” 完成了准备工作。
一、先看现象:调用堆栈揭示的真相
我们以main函数调用mul函数的代码为例,在 Visual Studio 2022(以下简称 VS2022)中启动调试。当程序执行流程进入mul函数内部后,通过「调试→窗口→调用堆栈」路径打开「调用堆栈」窗口,可清晰看到如下调用关系:
mul()
main()
invoke_main()
__scrt_common_main_seh()
__scrt_common_main()
mainCRTStartup()

该堆栈的显示逻辑为「从上到下:当前函数→直接调用者」,结合上述信息可梳理出完整调用链:
main由invoke_main直接调用,invoke_main的调用者是__scrt_common_main_seh,__scrt_common_main_seh又被__scrt_common_main触发,最终由mainCRTStartup启动整个程序流程。
这一现象明确揭示了一个关键概念:main只是用户编写代码的入口,而非程序运行的真正起点。程序的「真正起点」是mainCRTStartup—— 它是操作系统直接调用的「程序入口点」。
在 VS2022 及整个 Visual Studio 系列版本中,C/C++ 程序的main函数均不直接由操作系统调用,仅作为用户代码的入口存在。上述「mainCRTStartup→...→main→mul」的调用堆栈关系,完全符合 Visual Studio 环境下的标准程序启动流程,而这一流程的底层支撑,正是微软 C 运行时库(CRT, C Runtime Library)的初始化逻辑。
二、为什么需要这些 “启动函数”?
有同学可能会问:“为什么不能让操作系统直接调用main呢?” 这就像开课前需要做准备工作一样 ——main函数能顺利执行,全靠这些 “启动函数” 做了三件关键的 “课前准备”:
1. 初始化 CRT 环境(搭舞台)
- 给全局变量、静态变量分配内存并初始化(比如
int g_val = 10;); - 准备标准输入输出设备(
stdin/stdout),否则printf一开始就用不了; - 初始化堆空间,为
malloc、new这些内存分配函数铺路。
2. 处理命令行参数(备道具)
- 解析我们运行程序时输入的命令行参数(比如
myprog.exe 10 20),把它们转换成main函数的argc和argv参数; - 准备环境变量列表,让
getenv等函数能正常工作。
3. 初始化异常处理(设安全网)
- 注册 SEH(结构化异常处理)机制,就像给程序装了 “安全气囊”—— 如果程序崩溃(比如除 0 错误),能捕获错误信息并友好提示。
三、启动函数的 “分工明细”
这些启动函数看似复杂,其实各司其职,我们按调用顺序一个个看:
1. mainCRTStartup:总导演
这是操作系统加载程序后第一个调用的函数,相当于

:C 程序的启动流程 ——main 函数前发生了什么?&spm=1001.2101.3001.5002&articleId=151790653&d=1&t=3&u=0386042258c14ae5aa339277c848677e)
1万+

被折叠的 条评论
为什么被折叠?



