《程序员的自我修养—链接装载于库》读书笔记
第一章 目标文件里有什么
1.1初探ELF
文章目录
前言
本章用来探索gcc编译器生成的.o文件里都有什么。
一、SimpleSection.o
在linux虚拟机中创建SimpleSection.c代码。代码如下
1 int printf(const char* format,...);
2
3 int global_init_var = 84;
4 int global_uninit_var;
5
6 void func1(int i){
7 printf("%d\n",i);
8 }
9 int main(void){
10 static int static_var = 85;
11 static int static_var2;
12 int a = 1;
13 int b;
14
15 func1(static_var+static_var2+a+b);
16
17 return a;
18 }
编译:
gcc -c SimpleSection.c
利用objdump工具来查看object的关键段
objdump -h SimpleSection.o
由上图可见SimpleSection文件含有:
- 代码段 .text
- 数据段 .data
- 未初始化段.dss
- 只读数据段.rodata
- 注释信息段.comment
- 堆栈提示段.note.GUN-stack
二、.o文件主要段分析
1. .text段(代码区,只读)
存放已编译程序的机器代码。
注意:程序加载运行时,.rodata段和.text段通常合并到一个Segment(Text Segment)中,操作系统将这个Segment的页面只读保护起来,防止意外的改写。
可以采用objdump -d查看反汇编文件
objdump -s -d SimpleSection.o
2. .data段(全局静态区)(可读可写)
程序编译时分配内存
主要存储程序中已初始化的的全局变量(定义在任何函数之外的变量)、局部static对象和类static数据成员。全局变量分配内存时在全局区从低地址到高地址查找空闲的内存区域进行分配。
例如:全局作用域中的int ival = 10,static int a = 30,以及局部作用域中的static int b = 30,这3个变量均存放在.data段中。注意,局部作用域中的static变量的生命周期和其余栈变量的生命周期是不同的。
全局和静态变量在编译阶段就会初始化,并且只初始化一次,但是可以通过赋值的方式多次修改他的值。
#include <stdio.h>
int nn = 1;
int f(void)
{
static int mm = 1;//在第二次调用时不会执行
nn++;
mm++;
printf("%d %d\n", mm, nn);//第一次输出2 2,第二次输出3 3.
return 0;
}
int main() {
f();
f();
return 0;
}
3. .rodata段(read only data) (常量区)
存放只读数据和字符串常量,比如printf语句中的格式字符串和开关语句(switch)的跳转表
例如,全局作用域中的 const int ival = 10,ival存放在.rodata段
再如,函数局部作用域中的printf(“Hello world %d\n”, c);语句中的格式字符串"Hello world %d\n",也存放在.rodata段
单独设.rodata段有很多好处:
- 在语义上支持了c++的const关键词
- 操作系统在加载的时候可以将“.rodata”段属性设置为只读,保证了程序安全
- 另外,在某些嵌入式平台下,有些存储器是采用只读存储器的,如ROM,这样将“.rodata”段放在该存储区域中就可以保证程序访问存储器的正确性
4 . .bss段
主要存储程序中未初始化的的全局变量(定义在任何函数之外的变量)、局部static对象和类static数据成员。
在目标文件中这个段不占据实际的空间,它仅仅是一个占位符,在加载时这个段用0填充。目标文件区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
5. 其他段
此外,ELF文件也可能是含有其他段。
常用段名 | 说明 |
---|---|
.comment | 编译器版本信息 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.plt和.got | 动态链接的跳转表和全局入口表 |
… | … |
6.指定变量所处段
如果你希望变量或者某些部分代码能够放到你所指定的段去,以实现某些特殊功能,例如为了满足某些硬件的内存和IO的地址布局,或者是像操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。GCC提供了一个扩展机制。__attribute__((section("name")))
__attribute__((section("FOO"))) int global_init_var = 84;
三、 为什么需要分代码段和程序段
- 当程序被装载后,数据和指令分别被映射到两个虚存区域。数据区域对进程来说是可读可写,而指令区域对进程来说是只读的,因此这两个虚存区域的权限不同,可以防止指令被改写
- 指令区和数据区的分离有助于提高程序局部性,提高cpu缓存命中率。
- 当系统中运行着多个该程序的副本时,他们的指令是一样的,所以内存中只需要保存一份该程序的指令部分。对于指令这种只读区域来说是进程间可以共享的,这个在有动态链接的系统中可以节省大量内存。
总结
本篇介绍了gcc编译器生成的.o文件里的主要段,下一篇将具体介绍.o文件的结构