本章涉及C/C++程序中main函数是如何被调用的、命令行参数如何传递给main函数、程序的内存空间布局、程序如何使用环境变量、程序如何终止退出。

main函数

C程序或C++程序总是从main函数开始执行的,其中这个总是从main函数开始执行是我们人为约定的,因为main( )函数也是当做一个函数被调用的,所以需要被系统知道被调函数的名字,当然现在从main函数开始执行已经成为语言标准了,在汇编层次,我们可以把程序起始执行地址指向一个自定义的名字。

书本上7.2节这里的翻译很是生硬,字面意思直接翻译过来,让人不太好理解,原文如下:

When a C program is executed by the kernel—by one of the exec functions, which we describe in Section 8.10—a special start-up routine is called before the main function is called. The executable program file specifies this routine as the starting address for the program; this is set up by the link editor when it is invoked by the C compiler. This start-up routine takes values from the kernel—the command-line arguments and the environment — and sets things up so that the main function is called as shown earlier.

中文翻译:

当内核执行C程序时(使用一个exec函数,8.10节将说明exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。

这段话中的“启动例程”是什么意思?启动例程即原版中的start-up routine,拿掉形容词修饰start-up只剩下真正的关键词routine,routine是值什么?routine 或 subroutine是近义词,在不同的编程语言中,称呼不太一样,但意思大致相同,在C/C++语言中,它的意思是function函数,在Java中它的意思是method方法。

因此原文的这段话正确翻译是:当C程序要被内核执行时,是通过一个叫做exec的函数来加载的,exec将在8.10节说明。在C程序的main被调用执行起来之前,会先调用一个特殊的启动函数(它的名字是_start,可在汇编层次看到)。可执行程序文件(也即前面的C程序二进制文件)将该启动函数作为程序启动的入口地址,这个启动地址是C编译器在链接阶段配置的。这个启动函数负责从内核那里接收命令行参数和环境变量,设置好这些之后再调用main函数。

进程终止

进程正常终止:

  • 1.从main函数返回,即retrun 0;
  • 2.调用exit,即在main函数内或者其他会被main调用的函数体内调用exit();
  • 3.调用_exit或_Exit,即在main函数内或者其他会被main调用的函数体内调用_exit或_Exit;
  • 4.最后一个线程从其所在进程返回;
  • 5.最后一个线程在其所在进程调用pthread_exit。

进程异常终止:

  • 6.调用abort;
  • 7.进程接收到信号;
  • 8.进程中最后一个线程最取消做出响应。

可以使用exit、_exit和_Exit函数来正常终止程序,第一个函数和后面两个函数区别是,exit会进行一些资源清理工作,然后返回内核,而_exit和_Exit则不清理立即返回内核。其头文件及函数原型如下:

#include <stdlib.h>

void exit (int status);

void _Exit (int status);

#include <unistd.h>

void _exit (int status);

三个函数的参数都是用于返回给操作系统,其值为程序的退出状态。

在新的C标准和C++标准中,是不允许main函数没有返回值类型说明的,main函数返回类型必须为int类型,如果程序没有通过return语句来返回一个值,则编译器默认强制插入一条return 0;语句。

当我们退出程序时,可以通过ISO C标准提供的一个库函数来实现程序收尾清理工作,该库函数使得进程可以登记至少32个函数来进行收尾处理,该库函数会让exit( )函数在程序退出时自动调用我们登记的清理函数。其头文件及函数原型如下:

#include <stdlib.h>

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

该函数负责登记程序退出时要执行的函数,若注册成功则返回0,否则返回非0。atexit函数的参数是一个函数指针,该指针指向的函数不需要参数,也不返回任何值。atexit函数可以登记多个清理函数,清理函数先后顺序是以的形式压入,即LIFO(last in first out)。如果函数被注册多次,那么也会被调用多次。

需要注意的是,除非通过return语句来返回,或者通过exit( )来退出才会调用清理函数,_exit和_Exit函数是不会调用清理函数的。

C/C++程序的正常启动结束过程使用C语言代码的形式接近于:exit(main(argc,argv)) ,实际上这段过程一般是用汇编来写,流程大致如下图:

命令行参数

命令行参数是例程调用main函数时传递的。除了传递命令行参数给main函数,main程序也可以从环境表中获取环境变量,环境变量是存储在environ指针中的。

环境表

每个程序都在一定的环境下运行的。程序可以从其运行环境中获取一个环境表,环境表中包含很多信息,程序可以从中读取。环境表是个数组,数组的元素是一个个的指针,每个指针指向一个字符串。该数组是使用全局变量environ存储的。

C程序的存储空间布局

典型的C/C++程序内存布局有5部分:

  1. 程序段(Text):程序二进制代码,计算机的工作指令部分,可以被共享,通常是只读的。
  2. 初始化数据段(Data):简称为数据段,用于存储已经初始化的全局变量或全局/局部静态变量,会保存在二进制文件中。
  3. 未初始化过的数据(BSS):用于存储未初始化的全局/局部静态变量,不会保存在二进制文件中,在程序加载时会将变量初始化为0。
  4. 栈 (Stack):保存局部变量、函数调用栈信息,在程序块开始时自动分配内存,结束时自动释放内存,先入后出顺序。
  5. 堆 (Heap):用于动态内存分配,手动分配和手动释放,通常位于BSS和Stack中间。

对于上述布局中的Data和BSS段,并不一定与程序中变量大小的内存总和相等,因为编译器可能会考虑字节对齐而额外占用一些空间。

环境变量

环境表包含环境变量,ISO C提供了一个库函数用于获取环境变量的值。其头文件及函数原型如下:

#include <stdlib.h>

char *getenv (const char *name);

函数成功时返回对应指针,否则返回NULL。

除了获取环境变量值之外,UNIX系统也提供了三个用于增加、更新、删除的库函数。 其头文件及函数原型如下:

#include <stdlib.h>

int putenv (char *string);
int setenv (const char *name, const char *value, int replace);
int unsetenv (const char *name);

上面的函数成功时返回0,出错返回-1。putenv( )比较怪异,慎用。

之前提到过,每个进程都在一个环境下运行,当我们使用上面的设置函数更改了其运行环境之后,该环境会被进程的子进程继承,但不会影响到父进程。

函数setjmp和longjmp

在C/C++中,goto语句只能在函数内部跳转,用于跳出过深的循环。但是想要过多的函数嵌套调用时,goto就无法使用了。ISO C和UNIX标准都做了相同的说明,提供了两个用于跳转多层嵌套函数调用的库函数。 其头文件及函数原型如下:

#include <setjmp.h>

int setjmp (jmp_buf env);
void longjmp (struct jmp_buf_tag env[], int val);

这两个函数是配套使用的。当第一次调用setjmp函数时,其返回值为0,后续调用返回值由传入longjmp的val值决定。

函数getrlimit和setrlimit

每个进程在运行时,都有一个运行环境,除此之外,在书本的开始第一句话就是:所有的操作系统都为它们所运行的程序提供各种服务。提供的服务包括各种资源分配,比如内存分配,对于每个进程都会分配资源,分配资源就会有资源限额方面的规定,这些限额就是资源限制。UNIX系统提供了用于查询和设置资源限制的接口函数。其头文件及函数原型如下:

#include <sys/resource.h>

int getrlimit (rlimit_resource_t resource, struct rlimit *rlimits);
int setrlimit (rlimit_resource_t resource, const struct rlimit *rlimits);

函数成功时返回0,失败则返回非0。

习题

7.1 在Intel x86系统上,使用Linux,如果执行一个输出“hello, world”的程序但不调用exit或return的程序,则程序的返回代码为13(用shell检查),解释其原因。

较早的C/C++标准对于main( )函数的定义过于宽松,允许定义返回值类型为void,所以printf打印输出的返回值作为了程序的返回代码。1999年之后的语言标准已经明确禁止这种行为,所以现在默认都是返回0。

7.2 图7-3中的printf函数的结果何时才会被真正输出?

视具体情况而定,如果重定向输出到文件则是全缓冲,只有在缓冲区满或者程序结束时才会真正输出,如果在交互式终端直接执行则是行缓冲,会在遇到换行时输出。

7.3 是否有方法不使用(a)参数传递(b)全局变量这两种方法,将main中的参数argc和argv传递给它所调用的其他函数?

没有办法,除非UNIX系统提供类似getenv的方法来实现。

7.4 在有些UNIX系统实现中执行程序时访问不到其数据段的0单元,这是一种有意的安排,为什么?

可以将0单元作用空指针的实现地址,该地址不允许访问,那么访问该空指针时一定会时程序内存越界而报错,不会出现可能错误或可能意外写入了内存这种不确定哪种情况的发生。

7.5 用C语言的typedef为终止处理程序定义一个新数据类型Exitfunc,使用该类型修改atexit的原型。

atexit的原型如下:

#include <stdlib.h>

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

使用typedef定义新数据类型Exitfunc:

typedef void Exitfunc(void);

新atexit:

int atexit (Exitfunc *fp);

7.6 如果用calloc分配一个long型的数组,数组的初始值是否为0?如果用calloc分配一个指针数组,数组的初始值是否为空指针?

不一定,因为计算机的编码实现方式可能不一定与实际值匹配。

7.7 在7.6节皆为处size命令的输出结果中,为什么没有给出堆和栈的大小?

因为堆和栈是进程的内存布局,而size命令是对二进制程序的内存布局。进程和二进制不是同一样东西。

7.8 为什么7.7节中两个文件的大小(879443和8378)不等于他们各自的文本和数据大小的和?

因为一个二进制文件除了代码文本部分和数据部分,还有其他组成部分,比如链接库信息、符号表等。

7.9 为什么7.7节中对于一个简单的程序,使用共享库以后其可执行文件的大小变化如此巨大?

使用静态库时,库代码会被整合进二进制代码文件中,使用动态库时只是放入一个符号指示,而不是整合整个库代码。

7.10 在7.10节末尾,我们已经说明为什么不能将一个指针返回给一个自动变量,下面的程序是否正确?

 int f1(int val)
{
int *ptr;
if (val == )
{
int val;
val = ;
ptr = &val
}
return (*ptr + );
}

错误,因为第8行的代码将一个自动变量的地址赋予了指针,在第9行之后该变量已经被销毁,而在第10行ptr指针仍然访问了该变量。

UNIX环境高级编程 第7章 进程环境的更多相关文章

  1. UNIX环境高级编程 第8章 进程控制

    本章是UNIX系统中进程控制原语,包括进程创建.执行新程序.进程终止,另外还会对进程的属性加以说明,包括进程ID.实际/有效用户ID. 进程标识 每个进程某一时刻在系统中都是独一无二的,它们之间是用一 ...

  2. UNIX环境高级编程 第9章 进程关系

    在第8章学习了进程的控制原语,通过各种进程原语可以对进程进行控制,包括新建进程.执行新程序.终止进程等.在使用fork( )产生新进程后,就出现了进程父子进程的概念,这是进程间的关系.本章更加详细地说 ...

  3. Unix编程第7章 进程环境

    准备雄心勃勃的看完APUE,但是总感觉看着看着就像进入一本字典,很多地方都是介绍函数的用法的,但是给出例子远不及函数介绍的多.而且这本书还是个大部头呢.第7章的讲的进程环境,进程是程序设计中一个比较重 ...

  4. UNIX环境高级编程 第13章 守护进程

    守护进程daemon是一种生存周期很长的进程.它们通常在系统引导时启动,在系统关闭时终止.守护进程是没有终端的,它们一直在后台运行. 守护进程的特征 在Linux系统中,可以通过命令 ps -efj ...

  5. UNIX环境高级编程 第6章 系统数据文件和信息

    UNIX系统的正常运作需要用到大量与系统有关的数据文件,例如系统用户账号.用户密码.用户组等文件.出于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库函数来读取. 口令文件 /etc ...

  6. UNIX环境高级编程 第5章 标准I/O库

    本章是关于C语言标准I/O库的,之所以在UNIX类系统的编程中会介绍C语言标准库,主要是因为UNIX和C之间具有密不可分的关系.由于UNIX系统存在很多实现,而每个实现都有自己的标准I/O库,为了统一 ...

  7. UNIX环境高级编程 第1章 UNIX基础知识

    所有操作系统都为运行在它之上的程序提供各种服务,典型的服务包括:执行新程序.打开文件.读写文件.分配存储空间.提供时间等. UNIX体系结构 严格来说,操作系统是一种软件,它控制计算机硬件资源,提供程 ...

  8. UNIX环境高级编程 第16章 网络IPC:套接字

    上一章(15章)中介绍了UNIX系统所提供的多种经典进程间通信机制(IPC):管道PIPE.命名管道FIFO.消息队列Message Queue.信号量Semaphore.共享内存Shared Mem ...

  9. UNIX环境高级编程 第14章 高级I/O

    这一章涉及很多概念和函数,包括:非阻塞I/O.记录锁.I/O复用.异步I/O.readv和writev函数以及内存映射. 非阻塞I/O 在Unix中,可以将系统调用分为两种,一种是“低速”系统调用,另 ...

随机推荐

  1. Discrete Square Roots UVALive - 4270(拓展欧几里得)

    a≡b(mod n)的含义是“a和b除以n的余数相同”,其充要条件是“a-b是n的整数倍”: 求所有满足条件r^2=x(mod m)的r 题目已经给定了一个初始的r,x,m #include < ...

  2. 【转】VS2012简单的使用感受+插件推荐

    转自:http://blog.sina.com.cn/s/blog_58c506600101b34t.html ~PS:后面有博主自己补充的VS2013的哦~ 1.Visual Studio Achi ...

  3. Leetcode模拟题篇

    43. Multiply Strings 高精度非负整数的乘法. string multiply(string num1, string num2) { '); ; <= i; --i) { ; ...

  4. 【2018ICPC沈阳】

    哈密顿距离转切比雪夫距离,多种颜色跟两种颜色没有区别,记录最大最小次大次小即可. 圆求交点.

  5. Genaro Network —— 区块链3.0缔造者

    在2018年1月26日硅谷密探在美国旧金山艺术宫成功举办了“Blockchain Connect Conference”.在大会上,Genaro Network 联合CEO Jason Inch 携同 ...

  6. Android Room使用详解

    使用Room将数据保存在本地数据库 Room提供了SQLite之上的一层抽象, 既允许流畅地访问数据库, 也充分利用了SQLite. 处理大量结构化数据的应用, 能从在本地持久化数据中极大受益. 最常 ...

  7. linux内核分析 第七周读书笔记

    第七章 链接 1.链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载到存储器并执行. 2.链接可以执行于编译时,加载时,运行时. 7.1编译器驱动程序 1.大多数编译系统 ...

  8. bzoj3839【Pa2013】Działka

    题目描述 平面上有n个不重复的点.每次询问一个边平行坐标轴的矩形内(包含边界)的点组成的凸包的面积.. 输入格式 第一行两个整数k,n(1<=k<=1000000,3<=n<= ...

  9. [CQOI2016] 手机号码 (数位dp)

    link $solution:$ $10^{10} \leq L \leq R < 10^{11}$这个数据范围很容易想到数位$dp$. 依照题意模拟即可. #include<iostre ...

  10. 简例s - Variables

    1. Set Variable 定义:Returns the given values which can then be assigned to a variables. 示例1: ${a}    ...