mian函数返回值解析

转载:http://driftcloudy.iteye.com/blog/1063275

本章是该系列最后一篇,打算看一下 exit 函数中究竟做了些什么。

 

main函数的返回值

在第(5)篇里完成了_cinit() 的分析之后,mainCRTStartup中接下来代码是:

C代码   收藏代码
  1. __initenv = _environ;  
  2. mainret = main(__argc, __argv, _environ);  
  3. exit(mainret);  

很显然, 其实main函数是可以接受第三个参数的,_environ是一个环境变量的指针,只不过一般情况下写程序的时候用不到。从代码中可以看出,调用完main函数后,其返回值mainret会被传递给exit 用作参数。

 

这里首先要解决一个问题,如果main函数的返回值类型是void呢?

 

其实准确说写成void main是不对的T T...根据C99的规定,main的返回类型必须是int,并且如果 main 函数的最后没有写 return 语句,编译器要自动加入 return 0 ,表示程序正常退出。例如:

C代码   收藏代码
  1. #include <stdio.h>  
  2. void main()  
  3. {  
  4.     printf("%d",100);  
  5. }  

利用VS2010进行build,OD进入main函数:

 

注意倒数第二行,这里将EAX清0。其实 main 函数也是一个标准的__cdecl 函数,其return的值会存放在EAX中,因此这里等于会返回一个0 。可见VS2010 这点上还是满足C99 标准的,即使程序员写的是 void main,它依然悄悄的在最后添上 return 0。

 

来看看VC 6,如果用VC 6来build同样一段代码,则main函数为:

很显然,这里并没有将EAX的值清0再retn,但是接下来依然会从EAX 中拿值赋给mainret 。换句话说,用VC6 编译的时候,main函数并不会有默认的返回值,真正传进exit函数的还是main调用完后的EAX值,不过鬼知道这个时候EAX 是什么。这里可以看出 VC6并没有遵循C99的规范,貌似VC6是98年出来的,想想也算情有可原了...

 

 

exit   _exit   _cexit   _c_exit

由于有一系列和 exit 类似的函数,这里一起顺便看下~

C代码   收藏代码
  1. void __cdecl exit ( int status )  
  2. {  
  3.         doexit(status, 0, 0); /* full term, kill process */  
  4. }  
  5.   
  6. void __cdecl _exit ( int status)  
  7. {  
  8.         doexit(status, 1, 0); /* quick term, kill process */  
  9. }  
  10.   
  11. void __cdecl _cexit ( void )  
  12. {  
  13.         doexit(0, 0, 1);    /* full term, return to caller */  
  14. }  
  15.   
  16. void __cdecl _c_exit ( void )  
  17. {  
  18.         doexit(0, 1, 1);    /* quick term, return to caller */  
  19. }  

在crt0dat.c中定义了上面四个乍一看名字让人很纠结的函数。根据代码中的注释,它们的大概作用为:

  • exit 函数先进行清理工作(比如析构处理、关闭所有标准IO流),然后利用main 函数返回的status 来终结当前进程
  • _exit 函数用于快速终结进程,它并不进行那些“高层次”的清理
  • _cexit 同exit 函数一样执行清理,它并不终结进程
  • _c_exit 同_exit 一样执行清理,它并不终结进程

用通俗的话说,exit 是 _exit 的安全增强版,_cexit是_c_exit 的安全增强版。不过从它们的实现上看,本质上都是 doexit 函数在起作用。在doexit 的内部负责进行各种清理,然后再终结进程或者返还控制权给程序。

 

来看一下doexit 的大概实现,这里忽略了一些条件编译:

C代码   收藏代码
  1. // 是否需要终结进程,0表示终结当前进程,1表示返回控制权给程序  
  2. char _exitflag = 0;  
  3.   
  4. /* 
  5.  * 两个标志 
  6.  * 一旦进入了doexit ,_C_Termination_Done会被设置为true 
  7.  * 在doexit 完成了所有清理工作后(进入内核之前),_C_Exit_Done 会被设置为true 
  8.  */  
  9. int _C_Termination_Done = FALSE;  
  10. int _C_Exit_Done = FALSE;  
  11.   
  12. static void __cdecl doexit ( int code, int quick, int retcaller )  
  13. {  
  14.         if (_C_Exit_Done == TRUE)                          /*如果doexit()被递归的调用*/  
  15.                 TerminateProcess(GetCurrentProcess(),code);/*直接TerminateProcess终结当前进程*/  
  16.         _C_Termination_Done = TRUE;  
  17.   
  18.         /* 在执行其他清理的时候可能会用到retcaller,因此先将它赋值给全局变量_exitflag */  
  19.         _exitflag = (char) retcaller;  /* 0 = term, !0 = callable exit */  
  20.   
  21.         if (!quick) {  
  22.             /* 
  23.              * 如果该程序曾经利用_onexit 或者 atexit  注册过函数,那么在退出前需要执行这些函数。 
  24.              * 执行的顺序与被注册的顺序相反,即采用LIFO的模式。 
  25.              * 利用atexit 来注册函数的时候,内存中会生成一张函数指针列表, 
  26.              * __onexitbegin 和__onexitend 分别指向列表的头部和尾部。 
  27.              * 
  28.              * 注意: 
  29.              * 是先从__onexitend指针开始,逐渐向前遍历,直到__onexitbegin, 
  30.              * 这样就能确保LIFO的调用顺序。 
  31.              */  
  32.   
  33.             if (__onexitbegin) {  
  34.                 _PVFV * pfend = __onexitend;  
  35.   
  36.                 while ( --pfend >= __onexitbegin )  
  37.                 /* 
  38.                  * if current table entry is non-NULL, 
  39.                  * call thru it. 
  40.                  */  
  41.                 if ( *pfend != NULL )  
  42.                     (**pfend)();  
  43.             }  
  44.   
  45.             /* 
  46.              * 会进行endstdio之类的操作,进行清理 
  47.              */  
  48.             _initterm(__xp_a, __xp_z);  
  49.         }  
  50.   
  51.         /* 
  52.          * 调用C terminators,貌似实际上没调用什么函数 
  53.          */  
  54.         _initterm(__xt_a, __xt_z);  
  55.   
  56.         /* 如果定义了retcaller,那么需要将控制权返回 */  
  57.         if (retcaller) {  
  58.             return;  
  59.         }  
  60.   
  61.         _C_Exit_Done = TRUE;  
  62.   
  63.         /* 结束进程 */  
  64.         ExitProcess(code);  
  65. }  
 

从上述实现可以看出,如果是对于正常的退出,doexit 进行4个步骤操作:

1. 执行 _onexit 或者 atexit 中已经注册了的函数

2. _initterm(__xp_a, __xp_z)

3. _initterm(__xt_a, __xt_z)

4. ExitProcess(code)

 

析构

如果对象是定义在一个函数的内部,相当于局部变量,那么在函数调用结束之前,会自动析构该对象。

如果是一个全局对象,那么析构其实运行在上面4个步骤中的第1步,即调用_onexit、atexit 注册过的函数时发生。

可以用一段简单的示例代码来说明这些问题:

Cpp代码   收藏代码
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. typedef struct foo1 {  
  5.     foo1() { printf("1"); }  
  6.     ~foo1() { printf("2"); }  
  7.     static void bar() { printf("3"); }  
  8. } Foo1;  
  9.   
  10. typedef struct foo2 {  
  11.     foo2() { printf("4"); }  
  12.     ~foo2() { printf("5"); }  
  13. } Foo2;  
  14.   
  15. Foo1 f1;  
  16.   
  17. void main()  
  18. {  
  19.     Foo2 f2;  
  20.     atexit(&Foo1::bar);  
  21. }  

这是一段C++代码,因为C中的struct是不被允许定义方法的。最终的输出结果是:

运行结果
14532
 

这段示例代码中定义了两个变量,全局变量f1和局部变量f2,并且利用atexit注册了一个函数bar 。

 

根据第(5)篇中的描述,f1 的初始化工作在_cinit 函数中调用_initterm( __xc_a, __xc_z )时完成,至于f2 的初始化,肯定是在运行至main函数中Foo2 f2 一句时才开始进行。当main函数中的语句都执行完毕(此时尚未退出main函数),开始对f2 执行析构。析构完毕随后就退出main 调用,进入exit----> doexit,开始上述的4个步骤。在第1步中会运行注册的bar函数,然后调用f1 的析构函数,在第2步中调用endstdio 关闭IO,第3步没做啥,第4步ExitProcess。

 

因此从 cinit ----> main ----> exit 大概发生的事情顺序如下所示:

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值