C语言程序运行时的一些细节

本章可以看作是 《Unix 环境高级编程》Ch7 的笔记。

C 程序运行的开始和结束

一个可以运行的 C 语言总要有一个 main 函数,main 函数现在的完整定义是 int main(int argc, char *argv[] ) ,现在的 C 语言已经不允许写成 void main() 的形式了,main 函数必须有返回值,如果 main 函数没有返回值,那么编译器会在最后默认加上 return 0 的语句。

C 语言从 main 函数开始运行是一个约定俗成的东西,甚至影响了后面的好多语言也是从类似名为 main 函数的入口开始运行程序。但是当内核执行 C 语言程序的时候,并不是直接调用了 main 函数,而是先通过一个特殊的启动程序 (a special start-up routine),这个特殊的启动程序将作为程序的起始地址开始运行,启动程序会从内核中获取命令行参数和环境变量值,然后去调用 main 函数。

一个进程的正常终止是有五种方式:

  1. 从 main 函数返回
  2. 调用 exit 函数。
  3. 调用 _exit 函数或者 _Exit 函数。
  4. 最后一个线程从其启动例程返回。
  5. 从最后一个线程调用 pthread_exit 。

所以,这个启动的例程实际上调用的是 exit(main(argc, argv[])) 这个函数。

退出程序有三种分别是

  1. exit()
  2. _exit()
  3. _Exit()

C 语言结束的时候,也是调用的 exit 函数,exit 函数在执行的时候,会先执行一些清理处理,并且关闭所有可能的 IO 流,对所有在缓冲的数据进行冲洗。

如果直接调用 _exit 或者 _Exit 函数,则会直接退出,进入内核。

这三个函数都有一个调用参数,称为终止状态,如果调用这些函数不带这个终止状态,或者 main 函数执行了一个没有返回值的 return 语句(这个时候相当于调用的 exit 函数没有参数),或者 main 函数没有声明返回值为整数类型,那么这个进程终止的状态就是未定义的。否则,进程的终止状态就是 0 。

一个完整的 C 语言的启动过程如图。

C语言程序运行时的一些细节_第1张图片

我们之前说过,exit 函数退出的时候,还需要进行收尾工作,那么如果我们想自定义这个收尾工作的话,可以调用 atexit () 函数,它会登记我们想要调用的收尾工作调用的函数,然后让 exit 自动进行调用。这些收尾函数可以称为 handler 。

atexit 函数原型如下,它的参数是一个函数地址,exit 调用这些函数的顺序与登记这些函数的顺序相反,可以理解为存在一个handler 函数栈,越后登记的函数越先被调用。

int atexit( void (*func) (void));

C 程序的存储空间布局

一个典型的C语言程序结构如图。
C语言程序运行时的一些细节_第2张图片
C 程序一般由几个部分组成,

  • 正文段/程序段 (text) ,就是记录了程序的二进制代码。通常是只读的,防止出现意外指令被改写。
  • 初始化数据段 (Data) 存储已经初始化的全局变量或者静态变量。
  • 未初始化数据段(BSS)保存了未初始化的静态变量,在程序家在的时候会将此段中的数据初始化为 0 。
  • 栈 保存局部变量,函数调用栈信息,程序开始时自动分配内存,结束后自动释放内存。递归函数调用的时候,会形成一个新的栈帧,函数调用不会互相影响。
  • 堆 在堆中进行动态的内存分配,需要手动分配和释放内存。

你可能感兴趣的:(C语言程序运行时的一些细节)