• 欢迎访问C语言网www.dotcpp.com 比赛栏每月有奖月赛!举办比赛联系QQ:2045302297
  • 问题反馈、粉丝交流 QQ群327452739 蓝桥杯训练群:113766799 申请群时请备注排名里的昵称
  • C语言研究中心 为您提供有图、有料、解渴的C语言专题! 欢迎讨论!

C语言程序真正的启动函数

C语言研究中心 CTO 2495次浏览 4个评论

 

为什么要用”真正”这个词?因为我们从学C语言开始,都会先明白这个道理,即C语言有且仅有一个main函数,main函数是C语言的入口点和出口点!(可以参考<<一个C语言程序的基本机构>>)不光C语言如此,C++也如此,甚至无论黑窗口的控制台程序和Windows应用程序,都是从main函数或者WinMain函数开始执行,这当然没错,但事实上main函数仅仅是一个C语言语法规定的入口点,而不是真正的程序入口,因为它也有函数返回值!它也需要被调用!所以,今天我们将带大家去揭秘main函数之前的代码,去看看真正的启动函数是什么!来让大家深入理解C语言程序,方便大家日后的逆向学习!

 

由于大多数情况下,我们在VC环境下,常常C/C++混编,或控制台程序和windows应用程序都有接触,同时会因为编码方式的区分如ANSI或者Unicode编码启动函数还各不相同,为保持简单、纯粹。我们今天仅仅讨论ANSI编码控制台程序下纯C语言的程序入口分析。

 

事实上,在VC6编译器下,ANSI编码环境下C语言的真正启动函数名叫做mainCRTStarup,英语好的同学应该可以明白一些,Starup就是初始化、启动的意思,其实也可以根据这点明白这个函数作用就是在C语言启动之前做一些必要的工作,如堆栈初始化、获得主函数的参数等等。

还是本着我们“实践教学”的原则,我们还是以实践、做实验来验证和理解我们的知识,由于关系到函数间调用的关系,我们应该联想到VC6编译器带给我们的栈回溯功能。有兴趣的同学可以参考   VC6断点调试之窗口监视(内存监视、寄存器和栈回溯)<第五篇>

 

依次View – Debug Windows-> Call Stack

通过编译器提供的栈回溯功能可以看到程序启动后的调用过程,如下如:

C语言程序真正的启动函数

通过断点提示,我们看到目前程序位于main函数第四行。可以看到上一次是被mainCRTStartup函数调用,在第206行的25个字节偏移处开始调用,再之前就是KERNEL32了,它是windows系统三大主要文件之一。软件系统层面的调用就到此为止了。

因为大多数逆向分析工具基本都会从这里开始,所以我们也重点研究mainCRTStartup函数的原理。幸运的是,VC6编译器为我们提供了mainCRTStartup函数的源码,但需要大家安装完整版才可以看到,不然只能看到反汇编代码。

 

这里我们摘录一部分主要的mainCRTStartup代码,供大家参考学习:

 

//预编译宏
#else /*_WINMAIN_*/
#ifdef WPRFLAG
//宽字符版控制台启动函数
void wmainCRTStartup(
#else/*WPRFLAG*/
//多字节版控制台启动函数
void mainCRTStartup(
#endif /*WPRFLAG*/
#endif /*_WINMAIN_*/
void )
{
//获取系统版本信息
_osver=GetVersion();
_winminor=(_oserver>>8)&0x00FF;
_winmajor=_oserver & 0xFF; //主版本
_winver=(_winmajor<<8)+_winminor; _osver=(_osver>>16) & 0x00FFFF;
//堆空间初始化过程,在这个函数里,指定了程序中堆空间的起始地址
//_MT是多线程标记
#ifdef _MT
if(!_heap_init(1))
#else /*_MT*/
if(!_heap_intit(0))
#endif /*_MT*/
fast_error_exit(_RT_HEAPINIT);
//初始化多线程环境
#ifdef _MT
if(!_mtinit())
fast_error_exit(_RT_THREAD);
#endif /*_MT*/
_try{
//宽字符处理代码略
//多字节版获取命令行
_acmdln=(char*)GetCommandLineA();
//多字节版获取环境变量信息
_aenvptr=(char*)_crtGetEnvironmentStringsA();
//多字节版获取命令行信息
_setargv();
//多字节版获取环境变量信息
_setenvp();
#endif /*WPRFLAG*/
//初始化全局数据和浮点寄存器
_cinit();
//窗口程序处理代码略
//宽字符串里代码略
//获取环境变量信息
_initenv=_environ;
//调用main函数,传递命令行参数信息
mainret=main(_argc,_argv,_environ);
#endif /*WPRFLAG*/
#endif/*_WINMAIN_*/
//检查main函数返回值执行析构函数或atexit注册函数指针,并结束程序
exit(mainret)
}
//退出结束代码略
}

以上语法依旧是C语言,大家可以自行对照注释进行理解,熟悉main函数在调用前的一些准备工作,可以总结如下:

1.GetVersion函数:获取当前运行平台的版本号。控制台下则为MS-DOS的版本信息。

2._heap_init函数:用于初始化堆空间。在函数实现中使用HeapCreate申请堆空间

3.GetCommandLineA函数:获取命令行参数信息的首地址

4._crtGetEnvironmentStringA函数:获取环境变量信息的首地址

5._setargv函数:此函数根据GetCommandLineA获取命令行参数信息的首地址并进行参数分析 注意主函数的参数就在这里获得!

6._setenvp函数:此函数根据_crtGetEnvironmentStringA函数获取环境变量信息的首地址进行分析。

7._cinit函数:用于全局变量数据和浮点数寄存器的初始化。

 

大家可以对比代码加注释深入理解main函数启动前的准备工作,来加深程序启动的机制理解。

通过观察,在_cinit()函数之后,我们可以看到有主函数的调用语句mainret = main(_argc,_argv,_environ),现在知道主函数的返回值给谁了吧?

 

至此,我们最熟悉的main函数就出现了,怎么样,大家连起来了吗?

 

如果还能理解,我们接下来做一个更改入口函数的实验,来加深大家的学习。如下:

编译器工具栏 Project – Setting – Link – Output 如下图:

C语言程序真正的启动函数

在入口点出输入你想自定义的函数名,比如起名MyDotcpp,将替换掉mainCRTStartup函数,重新被KERNEL32调用,main函数作为C语言语法入口点,被MyDotcpp调用,如图:

C语言程序真正的启动函数

重新打开栈回溯查看调用情况,可以看到入口函数已经被更改掉了

C语言程序真正的启动函数

 

当然,这里我们定义的MyDotcpp函数仅仅用来测试更改入口函数,正如mainCRTStartup之前描述的代码一般,入口函数拥有更多的比如初始化堆空间、浮点数等功能,如果我们这里在多加一些如开辟内存等语句,运行将会报错,大家可以亲自上机尝试。

 

以上就是关于程序真正的入口函数的介绍,请大家好好消化!

 

在阅读本文和学习过程中,大家本机实际情况可能和截图不一致,欢迎大家留言交流、讨论!

如有问题,欢迎纠正!

 


C语言网, 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明C语言程序真正的启动函数
喜欢 (17)
[jinyangH@aliyun.com]
分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
(4)个小伙伴在吐槽
  1. 看到这么好的文章没有人评论,好可惜啊。点赞
    yuliner2017-03-21 21:42 回复
    • 谢谢支持!! 看到的人比较少吧,多多分享吧!
      CTO2017-03-29 10:36 回复
  2. “如果我们这里在多加一些如开辟内存等语句,运行将会报错” 博主说的是在main()函数里面开辟内存会报错么?(因为自定义的启动函数MyDotCpp()没有初始化?)
    wtw2162017-04-21 00:34 回复
    • 对,因为在进入main之前没有进行内存相关的初始化工作
      CTO2017-04-21 14:25 回复