程序的运行时 数据结构

这篇博文主要内容是程序运行时的数据结构,包括运行时程序中的不同部分如何分配内存、函数调用的内存实现、

还介绍了一个c独有的强大功能,一个被称为“展开堆栈”(unwinding stack)的技术

 

运行时 数据结构,中间的空格是特意留出的,

运行时可以认为是程序执行的一个状态,一般有编译时,运行时等,他们都是表示一个处理状态。

编程语言的的经典对立之一就是代码和数据的区别。代码和数据的区别也可认为是运行时和编译时的界限,编译器的绝大部分工作和翻译代码有关;必要的数据存储管理的绝大部分都在运行时进行。

如果你用过GCC,就会知道用GCC编译程序,都会得到一个默认名为“a.out”的文件。

简单说下“a.out”的由来吧:

他是assembler output(汇编程序输出)”的缩写形式。但是,他不是汇编程序输出,而是链接器输出。

这个名字曾被解释为:“新程序就绪,准备执行”它是链接器输出文件。

一般的说,可以认为连接器输出的是二进制文件,这个文件并不是杂乱无章的放在一起的,而是由一定的存放规律。比如说分类存放,这就涉及到了我们接下来要讨论的段的概念。

目标文件和可执行文件都可以有多种不同的格式,所有这些不同的格式都有一个概念,就是段(segments)。

就目标文件而言,段就是二进制文件的简单区域,里面保存了某种特定类型相关的所有信息。术语section也广泛使用,

他可看作是段的组成部分,一个段通常可以包含几个section

不过这里的段要注意和内存模型中的段区别开来,在内存模型中,段是内存模型设计的结果

请看段的组成形式:

 

 

从上图中可以看出:a.out包含了magic number,它可以理解为一个标示符,一般是一些特殊的数字,所谓的特殊数字也就是有特别意义的,比如

#define FS_MSGIC 0x011954 它是kirk mckusick 的生日。~所以这里不用太注意。

下面的是a.out的其他内容,比如一些标示符等等。。其它的内容在下文有说明就不多说

 操作系统对段的操作:

段可以方便的映射到链接器在运行时可以直接载入的对象中,载入器只是提取每个端的印象,直接将他们放入内存中。

从本质上说,端在执行过程的程序中是一块内存区域。

文本段(The text segment )包含程序的指令,链接器把指令直接从文件拷贝到内存中,以后就用管他,因为一般情况下下,文本区域是不会改变的,不论是大小还是内容。

数据段(The data segment)包含经过初始化的全局变量和静态变量以及他们的值。BSS段是未初始化的数据,大小可从可执行文件中得到,然后链接器得到这个大小的内存块。

紧跟在数段之后,包含数据段和BSS段的一般统称为数据区。这是因为,操作系统中,段是一块连续地址,所以相邻的端被结合。一般来讲,数据段在任

何进程中都是最大的段。

堆栈段(The stack segment)上图显示了一个即将执行的进程的内存布局,我们仍然需要一些存储空间,用于存放临时变量,临时数据,传递到函数中的参数等等(local variables, temporaries, parameter passing in function calls,)。

注意到虚拟地址空间的最低部分未被映射。它位于地址空间内,但为被赋予物理地址,所以对它的任何引用都是非法的。他用于捕捉使用空指针和小整形值的制造引用内存的情况。

When you take shared libraries into account, a process address space appears,

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

 

 

 

C运行时对a.out的操作 

What the C Runtime Does with Your a.out

现在看一下c语言在运行时的数据结构是怎么样的,运行时数据结构一般有好几种,堆栈,活动记录,数据,(the stack, activation records, data, heap)堆等

下面将分别讨论,并分析他们所支持的语言特性:

The Stack Segment 堆栈段

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

堆栈为函数内部声明的局部变量提供存储空间。

函数调用的时候,堆栈存储相关的一些维护信息。这些信息被称为堆栈结构(stack frame)也叫做过程活动记录(precedure activation record)稍后讨论。

堆栈也可以作为临时存储区,有时候进程需要一些临时存储空间,比如执行一个复杂的计算时,可以把结果压到堆栈中。

值得一提的是:除了递归调用之外,堆栈并非必须。

 

函数是怎么被调用的:过程活动记录(precedure activation record)

What Happens When a Function Gets Called

c运行时系统在他自己的地址空间内如何管理程序的呢?这里做一个简单的讨论。

c语言自动提供一种用于函数调用的功能:称作调用链( keeping track of the call chain)记录了哪些函数调用哪些函数,以及return执行后,控制将返回什么地方

解决这个问题的经典机制就是堆栈中的过程活动记录,每一个函数调用都会产生一个过程记录。其实它就是一种数据结构,记录调用后返回调用点需要的全部信息。

如下图就是一个过程活动记录的结构,不同的编译器会有所差别,但目的都是记录调用后返回调用点的信息。

Astonishing C Fact! 

C语言中令人震惊的事实:
现在的多数编程语言都允许在函数内部定义函数,但C语言中所有函数都是在此法层次中的的最顶层 。

这个限制稍微简化了c编译器。对于前一种允许在内部定义函数的,(也即允许嵌套的过程语言)中,过程活动记录要包括一个指向外层活动记录的指针。这个

指针被称为静态链接(static link)它允许内层过程访问外层活动记录,因此也能访问外层的局部数据。这种类型的访问被称为上层引用。

 

下面的例子显示了程序执行在不同点是堆栈中过程活动记录的情况。

 

 

Static 和 Auto关键字详解

为什么不能从函数中返回一个指向该函数中局部变量的指针

 

char * favorite_fruit () {
  char deciduous [] = "apple";
  return deciduous;

}

进入 该函数的时候,自动为变量deciduous在堆栈中分配空间,当函数结束后变量不存在了,因为它所占的空间被堆栈回收了。可能在任何时候被覆盖,这样

返回的指针就指向一个不确定的堆栈空间,指针失去了有效性,被称为垂悬指针。

如果想反悔一个在函数内部定义的指针,声明为static就行。static的声明在数据段中而不是堆栈段中分配空间,当定义的变量退出函数是依然有效,下次进入函数

依然存在。

存储类型auto在实际中基本用不上,因为默认的声明就是auto。他表示“进入该块后自动分配内存”在函数内部什么的数据缺省就是这种分配

 

setjmp and longjmp 

现在简单讨论一下sejmp和longjmp的用途,他们是通过操作过程活动记录实现的。它是c语言独有的强大机制。部分弥补了c语言有限的转移能力。这两个函数协同工作

• setjmp(jmp_buf j) must be called first. It says use the variable j to remember where 

you are now. Return 0 from the call.
• longjmp(jmp_buf j,int i) can then be called. It says go back to the place that
the j is remembering. Make it look like you're returning from the original setjmp(), but
return the value of i so the code can tell when you actually got back here via longjmp().
Phew!

•  The contents of the j are destroyed when it is used in a longjmp(). 

setjmp(jmp_buf j)要先调用,它使用变量j记录现在 的位置,函数返回0;

longjmp(jmp_buf j ,int i)可以接着被调用它表示“回到J所记录的位置”,让程序看上去“好像什么都没发生一样”返回i让代码知道实际上是通过longjmp返回的

当使用longjmp()时,j的内容被销毁。

setjmp保存了一份程序计数器和当前栈顶的指针,还可以保存一些初值。longjmp返回到setjmp设置的地方,有效的转移控制并把状态重置到保存状态的时候。

这被称作“展开堆栈”因为你从堆栈中展开过程活动记录,直到取得保存在其中的值。

它和goto语句的区别:

A goto can't jump out of the current function in C (that's why this is a "longjmp"— you can
jump a long way away, even to a function in a different file).
You can only longjmp back to somewhere you have already been, where you did a setjmp,
and that still has a live activation record. In this respect, setjmp is more like a "come from"
statement than a "go to". Longjmp takes an additional integer argument that is passed back,
and lets you figure out whether you got here from longjmp or from carrying on from the
previous statement.

goto语句不能跳出c语言当前的函数

longjmp只能回到曾经到过的地方,(setjmp设置的地方)

下面给一个示例:

#include <setjmp.h>  

jmp_buf buf;
#include <setjmp.h>
banana() {
printf("in banana()\n");
longjmp(buf, 1);
/*NOTREACHED*/
printf("you'll never see this, because I longjmp'd");
}


main()
{
if (setjmp(buf))
printf("back in main\n");
else {
printf("first time through\n");
banana();
}
}

输出结果:


% a.out
first time through
in banana()
back in main

 

setjmp/longjmp最大的用途在于恢复错误、只要还没从函数中返回,一旦发现一个不可恢复的错误,可以吧控制转移到主输入循环中。

 

希望能和更多的朋友交流、学习 

先写这么多吧、有什么不正确的地方还望大家指出~

 

参考资料《expert c programming》

http://en.wikipedia.org/wiki/Setjmp.h

http://en.wikipedia.org/wiki/A.out

 

 


转载请注明出处:http://www.cnblogs.com/yanlingyin/  

 一条鱼~@博客园   2011-11-28

 

 

posted @ 2011-11-28 11:19  Geek_Ling  阅读(9023)  评论(1编辑  收藏  举报