在 C 和 C++ 的教科书中会告诉程序员,main 函数是程序的入口函数。这个在初学 C 或 C++ 的时候并没有被怀疑过,因为每个 C 或 C++ 程序都会有一个 main 函数。
其实在进入 main 函数前,操作系统、编译器等已经做了很多工作了。只要在 VC 中,通过调用栈就可以看到相关一些内容。这里使用 VC 2015 来进行简单的演示。
用 VC 2015 创建一个控制台的程序,代码如下:
intmain() { printf("Hello World\r\n"); return0; }
其实代码是什么无所谓,只要这里有一行代码即可。
有了上面的代码后,按下 F10 键,进入调试状态。通过CTRL + ALT + C 打开调用窗口,调用窗口如下所示。
可以看到,此时调用栈的栈顶是 main 函数,也就是我们的代码当中。通过调用栈可以看到,在 main 函数上面还有 “外部代码”,还有一个没有 kernel32.dll 符号的提示。这样已经可以看出,在 main 函数之前肯定是有相关的代码已经被执行了。那么,如果能看到更详细的信息呢?
在调用栈窗口上单击右键,在弹出的菜单上选择 “显示外部代码”,在调用栈窗口中就会把 “外部代码” 显示出来,如下图所示。
从上图可以看到,刚刚显示 “外部代码” 的部分已经被具体的方法替换了,分别是 invoke_main()、__scrt_common_main_seh()、__scrt_common_main() 和 mainCRTStartup() 四个函数。它们的调用关系是从下往上的。
mainCRTStartup() 函数是由 kernel32.dll 的 76bffa29() 的函数调用的,而且在这个函数之前还有 ntdll.dll 的函数被调用了。那么这里是否可以显示呢?也是可以显示的。
这里在 kernel32.dll!76bffa29() 上单击右键,在弹出的菜单上选择 “加载符号”,如下图所示。
然后会出现一个加载符号文件的提示,耐心等待一下,然后再观察调用栈的信息,如下图。
可以看到,kernel32.dll!@BaseThreadInitThunk@12() 已经被显示出来了,继续在 ntdll.dll 上进行加载,都加载完后的调用栈显示如下:
可以看到,调用栈中的调用关系的显示也都完整了。
到此,我们可以看出,在进入 main 函数之前,经历了 ntdll.dll 中的 __RtlUserThreadStart@8() 和 __RtlUserThreadStart() 函数,到了 kernel32.dll 中的 BaseThreadInitThunk@12() 函数,然后到了当前 exe 文件的启动函数 mainCRTStartup() 函数,在启动函数中调用了 __scrt_common_main()、__scrt_common_main_seh()、invoke_main() 后调用到了程序员编写的 main() 函数处,也就是程序员的入口函数处。
最后,我们可以在菜单中选择 调试 -> 选项,在弹出的设置框中选择 调试 -> 符号 来进行设置,设置如下图所示。
所有的 pdb 文件,也就是符号文件都下载到了 SymbolCache 目录中了,这个目录是 VS 为我们提供的一个默认的目录。