《C专家编程》读书笔记6

第六章运动的诗章:运行时数据结构

 

        编程语言理论的经典对立之一就是代码和数据的区别。

 

       学习运行时系统,主要有3个理由:

       它有助于优化代码,获得最佳的效率。

       有助于理解更高级的材料

       当陷入麻烦时,可以使分析问题更加容易。

 

6.1       a.out及其传说

6.2       段

 

        段(segments),就目标文件而言,它们是二进制文件中简单的区域,里面保存了和某种特定类型(如符号表条目)相关的所有信息。一个段一般包含几个section。section是ELF(可扩展链接器格式)文件中的最小组织单位。

 

       当在一个可执行文件中运行size命令,它会告诉你这个文件中的三个段(文本段、数据段和bss段)的大小。size命令并不打印标题,所以要用echo命令产生它们。

       检查可执行文件的内容的另一种方法是使用nm或dump实用工具。

       

       BSS段这个名字是“BlockStarted by Symbol(由符号开始的块)”的缩写,有些人喜欢把它记作“Better Save Space(更有效地节省空间)”。由于BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段的大小记录在目标文件中,但BSS段(不像其他段)并不占据目标文件的任何空间。

 

6.3       操作系统在a.out文件里干了些什么

 

       为什么a.out要以段的形式组织。段可以方便地映射到链接器在运行时可以直接载入的对象中!载入器只是取文件中每个段的映像,并直接将它们放入内存中。从本质上说,段在正在执行的程序中是一块内存区域,每个区域有特定的目的。

 

       文本段包含程序的指令。

 

       数据段包含经过初始化的全局和静态变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段之后。当这个内存区进入程序的地址空间后全部清零。包括数据段和BSS段的整个区域此时通常统称为数据区。

       注意,虚拟地址空间的最低部分未被映射。

      

 

       当考虑共享库时,进程的地址空间的样子如下所示:

      

                                图6-3 显示共享库的虚拟地址空间布局

 

6.4       C语言运行时系统在a.out里干了些什么

 

       运行时数据结构有好几种:堆栈、活动记录(activationrecord)、数据、堆等。

 

       堆栈段

       堆栈段包含一种单一的数据结构——堆栈。

       可以修改位于堆栈中部的数据的值。函数可以通过参数或全局指针访问它所调用的函数的局部变量。运行时系统维护一个指针(常位于寄存器中),通常称为sp,用于提示堆栈当前的顶部位置。

 

       堆栈段有三个主要的用途,其中两个跟函数有关,另一个跟表达式计算有关。

       堆栈为函数内部声明的局部变量提供存储空间。按照C语言的术语,这些变量被称为“自动变量”。

       进行函数调用时,堆栈存储与此有关的一些维护信息。这些信息被称为堆栈结构(stackframe),另外一个更常用的名字是过程活动记录(procedure activation record)。其包括函数调用地址(即当所调用的函数结束后跳回的地址)、任何不适合装入寄存器的参数以及一些寄存器值的保存。

       堆栈也可以被用作暂时存储区。有时候程序需要一些临时存储。如果想让内存在函数调用结束之后仍然有效,就不要使用alloca()来分配(它将被下一个函数调用所覆盖)。

 

       除了递归调用之外,堆栈并非必须。因为在编译时可以知道局部变量、参数和返回地址所需空间的固定大小,并可以将它们分配于BSS段。

 

6.5       当函数被调用时发生了什么:过程活动记录

 

       C语言自动提供的服务之一就是跟踪调用链——哪些函数调用了哪些函数,当下一个“return”语句执行后,控制将返回何处等。解决这个问题的经典机制是堆栈中的过程活动记录。当每个函数被调用时,都会产生一个过程活动记录(或类似的结构)。过程活动记录是一种数据结构,用于支持过程调用,并记录调用结束以后返回调用点所需要的全部信息。

       

       结构的具体细节在不同的编译器中各不相同,这些字段的次序可能很不相同,而且可能还存在一个在调用函数前保存寄存器值的区域。

       运行时系统维护一个指针(常常位于寄存器中),通常称为fp,用于提示活动堆栈结构。它的值是罪靠近堆栈顶部的过程活动记录的地址。

 

       编译器设计者通过不存储未使用的信息来提高速度。其他的优化措施包括把信息存于寄存器中而不是堆栈中,对简单的函数调用(自身不调用其他函数)不将整个过程活动记录入栈以及让被调用函数而不是调用者负责寄存器值的保存工作。如果“指向前一个过程活动记录的指针”位于过程活动记录内部,就可以简化当前函数返回时返回到前一个过程活动记录的任务。

 

6.6       auto和static关键字

 

       char * favorite_fruit(){

           char deciduous[]=”apple”;

           return deciduous;

       }

       当进入该函数时,自动变量deciduous在堆栈中分配。当函数结束时,变量不复存在,它所占用的堆栈空间被回收,可能在任何时候被覆盖。如果想返回一个指向在函数内部定义的变量的指针时,要把那个变量声明为static。这样就能保证该数据保存在数据段中而不是堆栈中。该变量的声明期和程序一样长。

 

       存储类型说明符auto关键字在实际中从来用不着。它通常由编译器设计者使用,用于标记符号表的条目——它表示“在进入该块后,自动分配存储”(与编译时静态分配或在堆上动态分配不同)。对于其他程序员来说,auto关键字几乎没有用处,因为它只能用于函数内部。但是在函数内部声明的数据缺省就是这种分配。

 

       尽管谈到了“将过程活动记录压到堆栈中”,但过程活动记录并不一定要存在于堆栈中。事实上,尽可能地把过程活动记录的内存存放到寄存器中会使函数调用的速度更快,效果更好。

 

6.7       控制线程

6.8       setjmp和longjmp

 

       setjmp()和longjmp()部分弥补了C语言有限的转移能力,通过操纵过程活动记录实现。

       longjmp可以跳得很远,甚至到其他文件的函数中,但只能跳回到曾经到过的地方。

       保证局部变量在longjmp过程中一直保持它的值的唯一可以方法是把它声明为volatile(这适用于那些值在setjmp执行和longjmp返回之间会改变的变量)。

       setjmp/longjmp最大的用途是错误恢复。只要还没有从函数中返回,一旦发现一个不可恢复的错误,可以把控制转移到主输入循环,并从那里重新开始。等等。。。

 

6.9       UNIX中的堆栈段

 

       当进程需要更多空间时,堆栈会自动生长。

 

6.10    MS-DOS中堆栈段

 

       在建立可执行文件时,堆栈的大小必须同时确定,而且它不能在运行时增长。

 

6.11    有用的C语言工具

 

6.12    轻松一下——卡耐基-梅隆大学的编程难题

你可能感兴趣的:(C/C++)