这篇博文主要内容是程序运行时的数据结构,包括运行时程序中的不同部分如何分配内存、函数调用的内存实现、
还介绍了一个c独有的强大功能,一个被称为“展开堆栈”(unwindingstack)的技术
运行时数据结构,中间的空格是特意留出的,
运行时可以认为是程序执行的一个状态,一般有编译时,运行时等,他们都是表示一个处理状态。
编程语言的的经典对立之一就是代码和数据的区别。代码和数据的区别也可认为是运行时和编译时的界限,编译器的绝大部分工作和翻译代码有关;必要的数据存储管理的绝大部分都在运行时进行。
如果你用过GCC,就会知道用GCC编译程序,都会得到一个默认名为“a.out”的文件。
简单说下“a.out”的由来吧:
他是assembleroutput(汇编程序输出)”的缩写形式。但是,他不是汇编程序输出,而是链接器输出。
这个名字曾被解释为:“新程序就绪,准备执行”它是链接器输出文件。
一般的说,可以认为连接器输出的是二进制文件,这个文件并不是杂乱无章的放在一起的,而是由一定的存放规律。比如说分类存放,这就涉及到了我们接下来要讨论的段的概念。
段
目标文件和可执行文件都可以有多种不同的格式,所有这些不同的格式都有一个概念,就是段(segments)。
他可看作是段的组成部分,一个段通常可以包含几个section
不过这里的段要注意和内存模型中的段区别开来,在内存模型中,段是内存模型设计的结果。
请看段的组成形式:
从上图中可以看出:a.out包含了magicnumber,它可以理解为一个标示符,一般是一些特殊的数字,所谓的特殊数字也就是有特别意义的,比如
#defineFS_MSGIC0x011954它是kirkmckusick的生日。~所以这里不用太注意。
下面的是a.out的其他内容,比如一些标示符等等。。其它的内容在下文有说明就不多说
操作系统对段的操作:
段可以方便的映射到链接器在运行时可以直接载入的对象中,载入器只是提取每个端的印象,直接将他们放入内存中。
从本质上说,端在执行过程的程序中是一块内存区域。
文本段(Thetextsegment)包含程序的指令,链接器把指令直接从文件拷贝到内存中,以后就用管他,因为一般情况下下,文本区域是不会改变的,不论是大小还是内容。
数据段(Thedatasegment)包含经过初始化的全局变量和静态变量以及他们的值。BSS段是未初始化的数据,大小可从可执行文件中得到,然后链接器得到这个大小的内存块。
紧跟在数段之后,包含数据段和BSS段的一般统称为数据区。这是因为,操作系统中,段是一块连续地址,所以相邻的端被结合。一般来讲,数据段在任
何进程中都是最大的段。
堆栈段(Thestacksegment)上图显示了一个即将执行的进程的内存布局,我们仍然需要一些存储空间,用于存放临时变量,临时数据,传递到函数中的参数等等(localvariables,temporaries,parameterpassinginfunctioncalls,)。
注意到虚拟地址空间的最低部分未被映射。它位于地址空间内,但为被赋予物理地址,所以对它的任何引用都是非法的。他用于捕捉使用空指针和小整形值的制造引用内存的情况。
Whenyoutakesharedlibrariesintoaccount,aprocessaddressspaceappears,
当考虑共享库时,进程的地址空间的样子如下图所示:
C运行时对a.out的操作
WhattheCRuntimeDoeswithYoura.out
现在看一下c语言在运行时的数据结构是怎么样的,运行时数据结构一般有好几种,堆栈,活动记录,数据,(thestack,activationrecords,data,heap)堆等
下面将分别讨论,并分析他们所支持的语言特性:
TheStackSegment堆栈段:
堆栈段包含一种单一的数据结构:堆栈。
堆栈也可以作为临时存储区,有时候进程需要一些临时存储空间,比如执行一个复杂的计算时,可以把结果压到堆栈中。
值得一提的是:除了递归调用之外,堆栈并非必须。
函数是怎么被调用的:过程活动记录(precedureactivationrecord)
WhatHappensWhenaFunctionGetsCalled
c运行时系统在他自己的地址空间内如何管理程序的呢?这里做一个简单的讨论。
c语言自动提供一种用于函数调用的功能:称作调用链(keepingtrackofthecallchain)记录了哪些函数调用哪些函数,以及return执行后,控制将返回什么地方。
解决这个问题的经典机制就是堆栈中的过程活动记录,每一个函数调用都会产生一个过程记录。其实它就是一种数据结构,记录调用后返回调用点需要的全部信息。
如下图就是一个过程活动记录的结构,不同的编译器会有所差别,但目的都是记录调用后返回调用点的信息。
AstonishingCFact!
C语言中令人震惊的事实:现在的多数编程语言都允许在函数内部定义函数,但C语言中所有函数都是在此法层次中的的最顶层。
这个限制稍微简化了c编译器。对于前一种允许在内部定义函数的,(也即允许嵌套的过程语言)中,过程活动记录要包括一个指向外层活动记录的指针。这个
指针被称为静态链接(staticlink)它允许内层过程访问外层活动记录,因此也能访问外层的局部数据。这种类型的访问被称为上层引用。
下面的例子显示了程序执行在不同点是堆栈中过程活动记录的情况。
Static和Auto关键字详解
为什么不能从函数中返回一个指向该函数中局部变量的指针
char*favorite_fruit(){chardeciduous[]="apple";returndeciduous;
}
进入该函数的时候,自动为变量deciduous在堆栈中分配空间,当函数结束后变量不存在了,因为它所占的空间被堆栈回收了。可能在任何时候被覆盖,这样
返回的指针就指向一个不确定的堆栈空间,指针失去了有效性,被称为垂悬指针。
依然存在。
setjmpandlongjmp
现在简单讨论一下sejmp和longjmp的用途,他们是通过操作过程活动记录实现的。它是c语言独有的强大机制。部分弥补了c语言有限的转移能力。这两个函数协同工作
setjmp(jmp_bufj)mustbecalledfirst.Itsaysusethevariablejtorememberwhere
youarenow.Return0fromthecall.longjmp(jmp_bufj,inti)canthenbecalled.Itsaysgobacktotheplacethatthejisremembering.Makeitlooklikeyou'rereturningfromtheoriginalsetjmp(),butreturnthevalueofisothecodecantellwhenyouactuallygotbackherevialongjmp().Phew!
Thecontentsofthejaredestroyedwhenitisusedinalongjmp().
setjmp(jmp_bufj)要先调用,它使用变量j记录现在的位置,函数返回0;
longjmp(jmp_bufj,inti)可以接着被调用它表示“回到J所记录的位置”,让程序看上去“好像什么都没发生一样”返回i让代码知道实际上是通过longjmp返回的
当使用longjmp()时,j的内容被销毁。
setjmp保存了一份程序计数器和当前栈顶的指针,还可以保存一些初值。longjmp返回到setjmp设置的地方,有效的转移控制并把状态重置到保存状态的时候。
这被称作“展开堆栈”因为你从堆栈中展开过程活动记录,直到取得保存在其中的值。
它和goto语句的区别:
Agotocan'tjumpoutofthecurrentfunctioninC(that'swhythisisa"longjmp"—youcanjumpalongwayaway,eventoafunctioninadifferentfile).Youcanonlylongjmpbacktosomewhereyouhavealreadybeen,whereyoudidasetjmp,andthatstillhasaliveactivationrecord.Inthisrespect,setjmpismorelikea"comefrom"statementthana"goto".Longjmptakesanadditionalintegerargumentthatispassedback,andletsyoufigureoutwhetheryougotherefromlongjmporfromcarryingonfromthepreviousstatement.
goto语句不能跳出c语言当前的函数
longjmp只能回到曾经到过的地方,(setjmp设置的地方)
下面给一个示例:
#include
%a.outfirsttimethroughinbanana()backinmain
setjmp/longjmp最大的用途在于恢复错误、只要还没从函数中返回,一旦发现一个不可恢复的错误,可以吧控制转移到主输入循环中。