本章是该系列最后一篇,打算看一下 exit 函数中究竟做了些什么。
main函数的返回值
在第(5)篇里完成了_cinit() 的分析之后,mainCRTStartup中接下来代码是:
__initenv = _environ;
mainret = main(__argc, __argv, _environ);
exit(mainret);
很显然, 其实main函数是可以接受第三个参数的,_environ是一个环境变量的指针,只不过一般情况下写程序的时候用不到。从代码中可以看出,调用完main函数后,其返回值mainret会被传递给exit 用作参数。
这里首先要解决一个问题,如果main函数的返回值类型是void呢?
其实准确说写成void main是不对的T T...根据C99的规定,main的返回类型必须是int,并且如果 main 函数的最后没有写 return 语句,编译器要自动加入 return 0 ,表示程序正常退出。例如:
#include <stdio.h>
void main()
{
printf("%d",100);
}
利用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 类似的函数,这里一起顺便看下~
void __cdecl exit ( int status )
{
doexit(status, 0, 0); /* full term, kill process */
}
void __cdecl _exit ( int status)
{
doexit(status, 1, 0); /* quick term, kill process */
}
void __cdecl _cexit ( void )
{
doexit(0, 0, 1); /* full term, return to caller */
}
void __cdecl _c_exit ( void )
{
doexit(0, 1, 1); /* quick term, return to caller */
}
在crt0dat.c中定义了上面四个乍一看名字让人很纠结的函数。根据代码中的注释,它们的大概作用为:
- exit 函数先进行清理工作(比如析构处理、关闭所有标准IO流),然后利用main 函数返回的status 来终结当前进程
- _exit 函数用于快速终结进程,它并不进行那些“高层次”的清理
- _cexit 同exit 函数一样执行清理,它并不终结进程
- _c_exit 同_exit 一样执行清理,它并不终结进程
用通俗的话说,exit 是 _exit 的安全增强版,_cexit是_c_exit 的安全增强版。不过从它们的实现上看,本质上都是 doexit 函数在起作用。在doexit 的内部负责进行各种清理,然后再终结进程或者返还控制权给程序。
来看一下doexit 的大概实现,这里忽略了一些条件编译:
// 是否需要终结进程,0表示终结当前进程,1表示返回控制权给程序
char _exitflag = 0;
/*
* 两个标志
* 一旦进入了doexit ,_C_Termination_Done会被设置为true
* 在doexit 完成了所有清理工作后(进入内核之前),_C_Exit_Done 会被设置为true
*/
int _C_Termination_Done = FALSE;
int _C_Exit_Done = FALSE;
static void __cdecl doexit ( int code, int quick, int retcaller )
{
if (_C_Exit_Done == TRUE) /*如果doexit()被递归的调用*/
TerminateProcess(GetCurrentProcess(),code);/*直接TerminateProcess终结当前进程*/
_C_Termination_Done = TRUE;
/* 在执行其他清理的时候可能会用到retcaller,因此先将它赋值给全局变量_exitflag */
_exitflag = (char) retcaller; /* 0 = term, !0 = callable exit */
if (!quick) {
/*
* 如果该程序曾经利用_onexit 或者 atexit 注册过函数,那么在退出前需要执行这些函数。
* 执行的顺序与被注册的顺序相反,即采用LIFO的模式。
* 利用atexit 来注册函数的时候,内存中会生成一张函数指针列表,
* __onexitbegin 和__onexitend 分别指向列表的头部和尾部。
*
* 注意:
* 是先从__onexitend指针开始,逐渐向前遍历,直到__onexitbegin,
* 这样就能确保LIFO的调用顺序。
*/
if (__onexitbegin) {
_PVFV * pfend = __onexitend;
while ( --pfend >= __onexitbegin )
/*
* if current table entry is non-NULL,
* call thru it.
*/
if ( *pfend != NULL )
(**pfend)();
}
/*
* 会进行endstdio之类的操作,进行清理
*/
_initterm(__xp_a, __xp_z);
}
/*
* 调用C terminators,貌似实际上没调用什么函数
*/
_initterm(__xt_a, __xt_z);
/* 如果定义了retcaller,那么需要将控制权返回 */
if (retcaller) {
return;
}
_C_Exit_Done = TRUE;
/* 结束进程 */
ExitProcess(code);
}
从上述实现可以看出,如果是对于正常的退出,doexit 进行4个步骤操作:
1. 执行 _onexit 或者 atexit 中已经注册了的函数
2. _initterm(__xp_a, __xp_z)
3. _initterm(__xt_a, __xt_z)
4. ExitProcess(code)
析构
如果对象是定义在一个函数的内部,相当于局部变量,那么在函数调用结束之前,会自动析构该对象。
如果是一个全局对象,那么析构其实运行在上面4个步骤中的第1步,即调用_onexit、atexit 注册过的函数时发生。
可以用一段简单的示例代码来说明这些问题:
#include <stdio.h>
#include <stdlib.h>
typedef struct foo1 {
foo1() { printf("1"); }
~foo1() { printf("2"); }
static void bar() { printf("3"); }
} Foo1;
typedef struct foo2 {
foo2() { printf("4"); }
~foo2() { printf("5"); }
} Foo2;
Foo1 f1;
void main()
{
Foo2 f2;
atexit(&Foo1::bar);
}
这是一段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 大概发生的事情顺序如下所示:
分享到:
相关推荐
最终,`__rt_entry`会调用用户定义的`main`函数,并在完成执行后退出。 #### 2. __main实现 **2.1 工程配置** 为了使`__main`函数能够正确地执行,需要对KEIL工程进行适当的配置。 - 当选择使用MicroLIB时,需要...
6. 在右侧的属性列表中,找到“入口点”(Entry Point),并在其后的文本框中输入你想要的自定义入口函数名。 例如,如果你希望自定义的入口函数名为`myCustomEntryPoint`,则在这里输入`myCustomEntryPoint`。 在...
系统调用与系统函数调用是操作系统中至关重要的概念,它们是用户程序与操作系统交互的主要方式。在计算机科学中,当一个应用程序需要执行只有操作系统才能提供的服务时,比如磁盘I/O、进程管理或者网络通信,它就...
6. **浏览器信息函数**: - `get_browser()`: 返回用户浏览器的性能。 7. **退出脚本函数**: - `exit()`: 输出一条消息,并退出当前脚本。 - `die()`: 输出一条消息,并退出当前脚本。 8. **常量操作函数**: ...
本程序提供了一个可直接调用的函数,用于遍历指定目录下的所有文件,并且在`ProcessFile()`函数中对每个文件进行自定义处理,如加密或访问。这一功能在文件管理系统、数据备份、文件处理应用等方面非常实用。 首先...
`BL`指令的特点是调用后会返回,这意味着如果`main`函数没有无限循环,执行完毕后会返回到`rt_entry_postli_1`,然后调用`exit`函数。这就解释了为什么`main`函数通常只执行一次。 全局变量的存储区域在堆栈之外,...
每个系统调用在`syscall.tbl`中都有一个条目,由多个部分组成:系统调用号(`num`)、应用程序二进制接口(`abi`)、系统调用名称(`name`)、内核接口函数(`entry point`)以及可能的OABI兼容入口点(`oabi compat...
3. 加载完成和执行main函数:加载完成后,将PC(EIP)设定指向Entry point(即符号_start处),最终执行main函数,以启动程序执行。 ELF头信息 ELF头信息是可执行文件的元数据,用于描述可执行文件的结构和组织...
6. **用户空间调用**:在用户程序中,可以使用`syscall()`函数或`__NR_my_new_syscall`(系统调用号)来调用新的系统调用。 理解系统调用的运行原理,我们还需关注以下几个关键点: - **系统调用中断**:在x86架构...
- 从接收缓冲区复制数据到`pbuf`。 - **5.4.9 EMACSendPacket()函数**: - 发送数据帧。 - **5.4.10 编译**: - 确保`ethernetif.c`和`lib_emac.c`文件正确编译链接。 #### 6. ping——结束LwIP的移植 - **背景*...
3. **修改系统调用表**:在`/usr/src/linux/arch/i386/kernel/entry.S`中,根据系统调用号添加新条目,指定系统调用函数的符号名称。 4. **构建新内核**:在`/usr/src/linux-2.4`目录下运行`make xconfig`配置内核...
6. **XML 处理函数**: - `xml_` 系列函数:提供 XML 解析和处理能力,如设置事件处理器、创建和配置解析器、解析 XML 数据等。 - `xml_set_element_handler()`:定义开始和结束元素的处理函数。 - `xml_parse_...
然后,需要在 `entry.S` 文件中添加新的系统调用入口。这个文件位于 `/usr/src/linux-2.4.20-8/arch/i386/kernel` 目录下。在这个文件中,添加一个新的系统调用入口,例如 `.long SYMBOL_NAME(sys_mycall)`。 重新...
首先,当我们从父函数调用子函数时,几个关键步骤发生: 1. **保存寄存器状态**:对于那些不被子函数占用但在返回后仍需使用的非SOE(Save On Entry)类型的寄存器,它们的值会被保存到栈中。 2. **结构体参数...
在VB(Visual Basic)编程中,有时我们需要访问操作系统底层的功能,这时就需要调用系统API(Application Programming Interface)函数。在给定的文件中,重点介绍了如何使用VB来调用`CreateToolhelp32Snapshot`函数...
1. 如果寄存器不是SOE类型的(入口保存,save on entry),即它的值没有被被调用函数占用,但是在被调用函数返回值之后又会用到该寄存器的值的话,则该寄存器的值被保存在栈中。 2. 如果被调函数返回一个结构体,则...
【OpenHarmony】ArkTS 语法基础 ③ ( @Component 自定义组件生命周期回调函数 | @Entry 页面生命周期回调函数 ) https://hanshuliang.blog.csdn.net/article/details/139424435 博客源码快照 一、ArkTS @Component ...
此外,递归的效率通常较低,因为它涉及到多次函数调用和重复计算。为了优化,可以考虑使用循环或者记忆化搜索等方法。 在实际编程中,我们可能会遇到需要处理文件或目录结构的场景,如遍历目录下的所有文件。这时,...