C语言运行时数据结构
段(Segment):
对象文件/可执行文件:
SVr4 UNIX上被称为ELF(起初"Extensible Linker Format", 现在"Executable and Linking Format")文件。BSD UNIX上被称为a.out。这些格式都具有段的概念
section是存放特定类型二进制文件区域,section是ELF文件的最小组织单元,段通常由多个section组成
段主要有:
- BSS段:Block Started by Symbol,放置全局的但是没有初始化的变量。由于BSS段的变量没有任何值,所以不会真的在a.out文件中存储,所以BSS在a.out文件中不占空间(除了需要指明大小需要部分外),而是在运行时申请
- text段:代码段,放置指令
- data段:数据段,放置全局的,而且已经初始化的变量。局部变量不会放到a.out中,而是在运行时创建
示例:
只有main函数:
text data bss dec hex filename
14292 1532 112 15936 3e40 Hello.exe
定义全局未初始化int:
text data bss dec hex filename
14292 1532 116 15940 3e44 Hello.exe
定义全局初始化int:
text data bss dec hex filename
14292 1536 112 15940 3e44 Hello.exe
定义局部未初始化int:
text data bss dec hex filename
14292 1532 112 15936 3e40 Hello.exe
定义局部初始化int:
text data bss dec hex filename
14308 1532 112 15952 3e50 Hello.exe
结论:
- 全局未初始化的变量放到了BSS段
- 全局初始化的放到了data段
- 局部未初始化变量只意味着在运行时为其分配空间,而不会在生成可执行文件的时候分配空间或者生成相关的语句,所以不涉及BSS\DATA\TEXT段
- 局部初始化变量不涉及BSS和DATA段,而是生成语句执行初始化
操作系统如何处理可执行文件:
段会生成运行时链接器可以直接加载的对象,加载器直接把每个段对应到内存中的一部分
这些段就成为执行中程序的一块实际的内存区域
highest memory address
+------------------------+
| stack segment |
| . |
| . |
| . |
+------------------------+
| BSS segment |--未初始化的全局变量
+------------------------+
| data segment |--初始化的全局变量
+------------------------+
| text segment |
+------------------------+
| |
+------------------------+
lowest memory address
text段:
放置程序指令,是加载器直接从文件中复制过来的。一般情况下,text段不会改变,某些操作系统和链接器可以给段的不同section设置不同的权限。比如:text是只读和只执行的,某些data是只读的。某些data是读写但是不可执行的
data段:
包含了初始化的全局变量和静态变量,并进行了初始化赋值
BSS段:
在data段生成之后,加载器就从可执行文件中获取BSS段的大小并获取相应的空间放到data段后面。通常BSS段和data段会合并在一起,由于段在OS内存管理中只是一段连续的虚拟地址空间,所以相连的会被合并。所以数据段通常是最大的段
局部变量、临时存储单元、函数调用的参数传递等就需要用到分配的栈空间
对于动态分配的空间还需要堆空间,堆空间是在需要的时候创建的,在第一次malloc()函数调用的时候
需要注意的是虚拟地址空间的最低的一部分没有映射到物理地址空间,所以任何对这部分地址的引用都是非法的。这部分一般是从0开始的几个字节空间,null指针或含有很小整数值的指针将指向这里
如果考虑到共享库文件,那么实际的映射将是这样的:
highest memory address
+--------------------------+
| stack segment |
| . |
| . |
| . |
+--------------------------+
| linker |
+---------------------------+
| unmapped segment|
| . |
+---------------------------+
| data segment |
+---------------------------+库文件
| text segment |
+----------------------------+
| |
+----------------------------+
| data segment |
+----------------------------+库文件
| text segment |
+----------------------------+
| |
+----------------------------+
| data segment |
+-----------------------------+执行代码
| text segment |
+-----------------------------+
| |
+-----------------------------+
lowest memory address
C运行时如何处理可执行文件:
C在运行时会维护很多数据结构,比如栈、活动记录、数据、堆等等
栈段:
栈段只包含一个数据结构——栈
经典的栈定义是先进后出的队列,只有push和pop操作。但是这里的栈不仅可以push和pop还可以改变栈中某位置的值
运行时维护一个指针,通常位于寄存器中被称为sp,指向栈顶
栈段的作用主要有三个,两个关于函数,一个关于表达式计算:
- 栈为函数中的局部变量提供存储空间,这些变量被成为"自动变量"
- 栈保存函数调用时需要的维护信息,被称为"程序活动记录"。包含调用结束时的返回地址、不能放到寄存器中的参数和保存调用前的寄存器状态
- 栈也可以作为高速暂存寄存器,当程序需要临时存储的时候使用。如长表达式的计算,中间结果会被放到栈中并在使用的时候取出
alloca()函数分配的空间也在栈中,但是这部分空间会被下一次函数调用重写
如果不是有函数递归调用,栈是不被需要的。如果没有递归调用,局部变量、参数需要的空间和返回地址都可以在编译器知道并且在BSS段分配
程序活动记录
活动记录的目的是追踪调用链,每次调用函数都会在栈中生成一个活动记录,活动记录支持函数的调用以及记录调用结束后需要恢复的状态。具体活动记录的设计和实现相关,活动记录内部各区域的顺序可能各不相同,也可能有一个区域存储函数调用之前的寄存器值
大多数现代程序语言都支持函数内部定义函数(和数据一起)。但是C不允许函数嵌套声明,所有的函数都必须在词法顶层。这种限制能够一定程度的简化C编译器实现
在允许嵌套函数的语言中,活动记录会包含一个指向其外部函数的指针,这个指针被称为静态链接(static link)(和编译文件时的静态链接区分一下),这个指针允许内部函数获取外部函数的栈帧
虽然可能一个外部函数同时被多次调用,但是静态链接总能指向正确的栈帧,访问到正确的局部数据
对外部函数数据的获取被称为上层引用(uplevel reference)
之所以被称为静态链接,是因为对其外部函数的指向是在编译期确定的,而动态链接是运行时被调用时指向其调用者的栈帧
典型的活动记录如下:
+-------------------------+
| local vars | -- 存储如调用结束时需要恢复得寄存器值等
+-------------------------+
| arguments | -- 参数
+-------------------------+
| prev frame | -- 调用者的栈帧
+-------------------------+
| return addr | -- 返回地址
+-------------------------+
每次调用函数都会生成一个这样的活动记录。但是编译器作者会尽量的减少存储的信息以提高程序的性能,比如:
- 用寄存器存储某些信息而不是在栈中
- 对于叶函数(不会调用其他函数的函数)不生成完整的栈帧,调用者不再存储寄存器的值而是让被调用者存储
- 使用指向调用者栈帧的指针可以简化函数返回时弹栈到先前记录的工作
auto和static
auto的变量是函数调用时在栈中分配空间存储的,函数调用结束的时候对应的栈空间就会被释放并且可以被重写。所以如果函数返回一个指针指向局部变量就会返回一个"悬空指针",指向的值不是有效的。
如果想要返回一个函数中定义的变量,可以将其定义为static。static变量不是存储在栈中,而是在数据段分配空间存储。这样变量就会在程序的生命周期中存在,即便函数调用结束数据也依然存在,下一次函数调用还可以访问
栈帧不一定在栈中
如果把活动记录放到寄存器中会有更好的性能。SPARC架构以"寄存器窗口"来提高栈帧的性能。芯片中有一组专门用来存放活动记录中的参数的寄存器。空的栈帧还会被压入栈中,如果调用链过长导致寄存器窗口被用光,那么就会通过把寄存器中的值填充到对应的空栈帧中来释放寄存器
线程控制:
每个线程都会有自己的栈并且用red zone和其他线程的栈结构区分
setjmp和longjmp
它们通过操纵活动记录实现,这个特性弥补了C在跳转能力上的不足
工作方式:
- setjmp(jmp_buf j)首先被调用,用变量j记录当前语句所在的位置,调用后返回0
- longjmp(jmp_buf j,int i)在setjmp之后调用,返回j记录的地址,使之看起来像是从函数setjmp()返回,而且返回值是整数i,用以区分这是从longjmp()返回的
- j的内容在使用longjmp()之后被销毁
setjmp()保存了当前程序计数器和当前栈顶指针的内容。然后longjmp()恢复这些内容,高效的将控制流转移到原来的位置恢复保存的状态。并且会回退所有保存的栈顶之前的栈空间
和goto的不同:
- goto语句无法跳出当前函数,但是longjmp甚至可以调到另一个文件的函数
- longjmp只能回到之前控制流到过的某个地方,即设置了setjmp()并且被执行过的地方
setjmp/longjmp最有用的地方是错误恢复。只要你没有从函数返回,如果发现了一个不可恢复的错误,你就可以移动控制流到之前的某个节点,然后从那里重新开始。可以用来从多重函数调用中立即返回,也可以用来预防危险的代码
如:
switch(setjmp(jbuf)) {
case 0:
apple = *suspicious;
break;
case 1:
printf("suspicious is indeed a bad pointer\n");
break;
default:
die("unexpected value returned by setjmp");
}
如果在某个地方检测到了这个指针危险,就可以返回到这里
就像goto语句,setjmp/longjmp也会导致程序难以理解难以调试,所以要尽量避免使用
UNIX下的栈段:
栈随着程序需要增长,程序员可以认为栈是无限大的
UNIX使用某种虚拟内存模式,当尝试获取超过分配的空间的空间的时候就会产生一个页错误,处理方式依赖于引用是否有效。内核处理非法引用的方式通常是向产生错误引用的程序发送一个信号。在栈顶之后有一个red zone,对这里的引用不会产生错误,操作系统相应的会增加栈空间的大小,虚拟地址空间会相应的增加
MS-DOS的栈段:
DOS中栈的大小是由可执行文件指定的,而且不能再运行时修改,对超过空间的访问会导致程序失效。如果打开了检查,就会产生栈溢出错误。如果超出了段的限制,也会在编译器产生这个错误。Turbo C如果数据段或代码段过大,会产生Segment overflowed maximum size <lsegname>,80x86架构限制为64Kbytes
C语言运行时数据结构的更多相关文章
- 关于使用动态语言运行时 (. net)
AutoCAD Managed .NET API允许您使用使用. NET 4.0 引入的动态语言运行时 (DLR). 使用DLR可以直接访问对象, 而无需: 打开一个对象进行读取或写入, 然后在完成后 ...
- 《精通C#》第十六章-动态类型和动态语言运行时-第一节至第四节
在.Net4.0中引入了一个关键字dynamic,这是一个动态类型关键字.Net中还有一个关键字是var,这是一个隐式类型,可以定义本地变量,此时var所代表的实际的数据类型有编译器在初次分配时决定, ...
- 内核运行时数据结构的操作(启用路由功能),sysctl内核设置命令
LINUX系统运行时,内核数据结构数据的修改,系统提供了统一抽象的文件操作接口(命名空间操作接口)比如启用路由功能echo 1 > proc/sys/net/ipv4/ip-forward// ...
- Visual C++中对运行时库的支持
原文地址:http://blog.csdn.net/wqvbjhc/article/details/6612099 一.什么是C运行时库 1)C运行时库就是 C run-time library,是 ...
- C运行时库(C Run-time Library)详解(提供的另一个最重要的功能是为应用程序添加启动函数。Visual C++对控制台程序默认使用单线程的静态链接库,而MFC中的CFile类已暗藏了多线程)
一.什么是C运行时库 1)C运行时库就是 C run-time library,是 C 而非 C++ 语言世界的概念:取这个名字就是因为你的 C 程序运行时需要这些库中的函数. 2)C 语言是所谓的“ ...
- VC++中的C运行时库浅析(控制台程序默认使用单线程的静态链接库,而MFC中的CFile类已暗藏了多线程)
1.概论 运行时库是程序在运行时所需要的库文件,通常运行时库是以LIB或DLL形式提供的.C运行时库诞生于20世纪70年代,当时的程序世界还很单纯,应用程序都是单线程的,多任务或多线程机制在此时还属于 ...
- [转帖]运行时库(runtime library)
运行时库(runtime library) https://blog.csdn.net/xitie8523/article/details/82712105 没学过这些东西 或者当时上课没听 又或者 ...
- C运行时库函数和API函数的区别和联系
C运行时库函数 C运行时库函数是指C语言本身支持的一些基本函数,通常是汇编直接实现的. API函数 API函数是操作系统为方便用户设计应用程序而提供的实现特定功能的函数,API函数也是C语言的函 ...
- C运行时库函数
C运行时库函数是指C语言本省支持的一些基本函数,通常是汇编直接实现的. API函数是操作系统提供给用户方便设计应用程序的函数,实现一些特定的功能,API函数也是C语言的函数实现的. 他们之间区别是: ...
随机推荐
- 编写高性能的javascript代码(持续更新)
参考资料: Vanilla JS——世界上最轻量的JavaScript框架(没有之一) http://segmentfault.com/a/1190000000355277 探索高效jQuery的奥秘 ...
- debian 9 添加源
1.将下面内容的添加入/etc/apt/sources.list(香港镜像) #For software deb http://mirrors.ustc.edu.cn/debian/ stretch ...
- 使用 satis 搭建 composer 本地仓库
环境 windows nginx php composer 安装 拉取 satis 项目包,并拉取项目依赖 composer create-project composer/satis --stabi ...
- 挖一挖python中的深浅拷贝问题
前几天在做面试题的时候,遇到一个与Python深浅拷贝的问题,今天总结出来一个方法,能够快速判断在对一个对象复制后,新对象与原来对象是否会互相影响的方法. 先抛出结论,然后我们对结论进行验证~~~ 先 ...
- poj 3311 Hie with the Pie (状压dp) (Tsp问题)
这道题就是Tsp问题,稍微加了些改变 注意以下问题 (1)每个点可以经过多次,这里就可以用弗洛伊德初始化最短距离 (2)在循环中集合可以用S表示更清晰一些 (3)第一维为状态,第二维为在哪个点,不要写 ...
- Vue入门教程(2)
小白入门学习vue和vue实例,vue总结 这就是我脑海中的 Vue 知识体系: 一句话概况了 Vue 通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件 Vue 的创建 我们的学习目的肯定 ...
- 【【henuacm2016级暑期训练】动态规划专题 H】Greenhouse Effect
[链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 原题意等价于:给你一个序列(实数的位置没用!)..你可以改变其中某些元素的位置(插入到某些位置中间. 然后让他变成有序的. (有序的 ...
- NYIST 1107 最高的奖励
最高的奖励 时间限制:1000 ms | 内存限制:65535 KB 难度:3 描述 请问:挖掘机技术哪家强?AC了告诉你! 给你N(N<=3*10^4)个任务,每个任务有一个截止完成时 ...
- cogs 2752. [济南集训 2017] 数列运算
2752. [济南集训 2017] 数列运算 ★★☆ 输入文件:sequenceQBXT.in 输出文件:sequenceQBXT.out 简单对比时间限制:1 s 内存限制:512 ...
- spring mvc拦截器interceptor
1. SpringMVC 中的Interceptor 拦截器也是相当重要和相当有用的,它的主要作用是拦截用户的请求并进行相应的处理.比如通过它来进行权限验证,或者是来判断用户是否登陆,或者是像123 ...