段(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 ,80x86架构限制为64Kbytes