首页 文章详情

C++入口不是main?知乎上打起来了!

w3cschool | 56 2023-01-23 06:18 0 0 0
UniSMS (合一短信)

知乎上居然有人为了C++的入口函数到底是什么打了起来!

e80111a608406f338a06544a8a17742c.webp

至于打的有多激烈我就不知道了,我们来关注这个问题本身。

你说main函数是入口,那main是被谁调用的呢?

他说mainCRTStartup是入口,那mainCRTStartup又是被谁调用的呢?

从进程创建说起

一切的一切,让我们从创建进程开始说起。

进程创建完成后,接着会创建主线程,这是进程中第一个开始执行代码的线程。

主线程创建后,就得到了时间片,开始参与系统的线程调度,那么程序从哪里开始执行呢?

在Windows平台,C++代码编译后的可执行文件叫PE文件。

PE文件中有一个叫OEP的术语便是指的程序入口点。所谓入口点顾名思义就是主线程最开始执行的地方,许多病毒加壳技术其中一点就是对这个OEP进行处理。

现在,我们来使用工具PEID来看一个程序(VC8.0编译)的OEP如图:

0471aa76fc34f2ff57238fbeeb0d4198.webp

0x00011078乃是RVA(相对虚拟地址),要看在进程地址空间中真正的起始地址,还得加上PE文件的映射基址,默认为0x00400000,不过,可以通过编译器选项进行调整。不知道也没关系,将程序放入OllyDbg,在内存映射中可以看到程序的映射基址:

76e973df8c16731842c40bf8fec6223b.webp

由图看到映射基址是0x00400000。

那么由前面所述,程序执行的第一条指令应该位于0x00400000 + 0x00011078 = 0x00411078。

没错,就是这样。

切换到OllyDbg的主窗口,我们发现了,程序确实初始停在了这里,并且这里是一条jmp指令。

03d8893dd891ef9c687e5b765efe2e23.webp

我们到Jmp的目的地0x00411800去看看那里是什么东东?

ddaf6934bd968ff8783bd089c730aa17.webp

这是什么东西?先卖个关子,总之,这里是程序进来之后真正做的第一件事。

main函数被谁调用?

换个思路,我们打开VS2008写一个简单的程序,程序做什么并不重要,我们要看它的启动原理。

c0386f51f4f52aa9d6cccab248cba53a.webp

注意看调用堆栈窗口,因为我是使用UNICODE编码环境,故_tmain()就是wmain(),如果是ANSI编码就是最开始学程序时的main()函数了。

以前写程序就想过一个问题,我们写的所有函数都会被我们自己直接或间接调用,但有一个函数例外,那就是main()函数。我们写了它但从不会去调用它,事实上也不可能去调用它。

从调用堆栈看到,我们的wmain函数是被_tmainCRTStartup函数调用的,这是个什么东西?再往前推是wmainCRTStartup调用的_tmainCRTStartup。这两个函数是做什么的,他们之间有什么关系?

双击调用堆栈里的项即可转到对应的源代码,我们可以发现,这两个函数是在crtexe.c文件中实现的。阅读源码可以发现,有四个启动函数分别是:

  • mainCRTStartup() ANSI + 控制台程序
  • wmainCRTStartup() UNICODE + 控制台程序
  • WinMainCRTStartup() ANSI + GUI程序
  • wWinMainCRTStartup() UNICODE + GUI程序

这一点在《windows核心编程》中也有提到。不过我们可以更进一步一窥它们的实现代码:

ccaa05abd892d3c8e41912018e4e4f8b.webp

就这么简单,先调用了__security_init_cookie(),然后是我们前面看到的_tmainCRTStartup()。

第一个函数是做什么的呢?这个是微软在VS2003后引入的防止缓冲区溢出攻击的技术。简单的说就是在调用函数的时候在栈里安装一个随机的cookie值,这一cookie值在内存的一个地方有备份,函数调用完成后需要检测这个cookie和备份的一不一致,以此来判断有没有栈溢出发生。那么,这个函数就是来初始化这个备份区域的数据的。

然后第二个函数调用_initterm()进行全局变量、对象初始化。之后,我们可以看到才是真正调用了我们的main()/wmain()/WinMain()/wWinMain()的地方。饶了一大圈,回答了开始的疑问了。

4b084785da1483bb63e6d33038f047eb.webp

这两个函数是编译器在生成可执行文件的时候给我们链接进来的。

至此,我们来看看第一个函数wmainCRTStartup的汇编代码。如图:

af787c154e0a4e16c691e0fc5881ec91.webp

请注意和我们前面使用OllyDbg调试时的图对比:

ddaf6934bd968ff8783bd089c730aa17.webp

发现没有?一样的!我们之前留的那个问题的答案想必已经出来了:

程序一进来从OEP处执行了jmp指令,这条指令转向了wmainCRTStartup开始了程序真正的起点!

结论

编译生成的exe文件,双击运行后,建立新进程的地址空间,然后主线程开始运行。

程序一进来通过jmp指令来到前面列出的四个启动函数,它们再调用最终的启动器_tmainCRTStartup。

这个启动器干了几件大事,分别是,使用GetStartupInfo获取进程启动信息,然后使用_inititem初始化全局变量和对象,最后调用我们main、wmain、WinMain、wWinMain进入我们的程序。。。

所以,从编程语言的角度来说,main函数就是入口函数,这一点毋庸置疑。至于mainCRTStartup,则是VC++这个编译器额外增加的包含C/C++运行时库初始化操作在内的封装函数,可以算可执行文件的入口函数。

说明:这里谈到的是使用VC2008编译器生成的exe文件形态(不同的VC版本可能情况有所不同),至于Linux上的ELF文件,情况则更不一样。

最后给大家留一个思考题:

进程创建后,又是从哪里进入到OEP的呢?

前面我们说了,OEP是程序运行的入口,是一切的起点。那在进入入口之前,进程又在干什么?

这个问题有点类似于:在宇宙大爆炸之前,世界是怎么样的?

欢迎大家留言交流,告诉我你的想法!

be8cf924d7507ec4ee17f8c0cdbeb2ba.webp

近来我们已经陆续上线了价值¥3000+的课程,进一步丰富了VIP的权益。随着课程研发成本的提高,以及为了给每位小伙伴提供更优质、更丰富的学习权益和全方位的服务,接下来VIP将随时涨价。

在涨价之前,给每位小伙伴们再送一波优惠福利!涨价前的最后一次优惠福利活动!

b43b0b60e69c6f30c38058b76570bfdf.webp

 🔶【活动内容】 

1、原价1998元的终身VIP,现在 仅需开通2年VIP就能直接送终身VIP ,新春特惠价仅需 ¥798 ,趁着机会赶紧囤,终身畅学站内好课【推荐!】

2、VIP买1年送1年,比平时多学12个月,仅需 365元优惠畅学2年VIP好课

 🔶【抢购方式】扫码↓立即参与

edfdad3cb4446be4ab2365dc554752c5.webp

优惠限时,错过本次活动,就没有机会了!

551f11375b1031b0be29aed466129e47.webp点击阅读原文 即可参与活动
good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter