本文的demo是在linux环境下编译解析的,cpu是x86-64。本文大量参考《程序员的自我修养》一书
从一个demo说起
首先我们先写一个功能简单的demo-SimpleSection.c。这个demo中有一个func1
函数用来打印数据,一个已经初始化的全局变量global_init_var
和未初始化的全局变量global_uninit_var
,一个已初始化的局部静态变量static_var
和一个未初始化的局部静态变量static_var2
。代码如下:
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
下一步我们把代码文件编译成目标文件: gcc -c SimpleSection.c
接着使用objdump
工具查看目标文件的段表信息:objdump -h SImpleSection.o
。对应截图如下:
从上述截图中可以看出,objdump得到的 目标文件信息共有8项,前六项0~5分别是:
- 代码段
- 数据段
- BSS段
- 只读数据段
- 注释信息段
- 堆栈提示段
信息项共有5列,分别为Size
、VMA
、LMA
、File off
和 Algn
。其中Size
为段的长度,File off
为段的偏移也就是段的位置。
下面我们用一张图来标识这几个段的相对位置:
从图中可以清晰的看到各个段在目标文件中的分布。值得注意的是.bss
段并不存在于目标文件中,我们在上一张截图中就可以看到Size一列中.bss
显示的是ALLOC
而不是CONTENTS
。这表示在ELF文件中不存在内容。因此.bss
段中只记录需要分配的空间大小,而不是真正存在于ELF文件的空间大小。还有就是.text
段的起始地址是0x 0000 0040
,段size为0x5f
,但是奇怪的是0x40+0x5f=0x9f
而不是0xa0
,这是因为对齐的缘故。
为了更加清晰的查看.text
段的信息,我们使用objdump -s -d SimpleSection.o
命令去反汇编ELF文件,从而与上面的表述相印证。
如上面截图所示,.text
段的最后一个字节的偏移量,也就是位置是0x5f
。这与前面我们看到的.text
段的大小一致。
对于.data
段,一共有8个字节,对应了代码中的int global_init_var
和static int static_var
变量。前4个字节为0x 54 00 00 00 00
,其中第一个字节为0x54
,相应的十进制为84,对应了代码中int global_init_var
变量。但是需要注意的是这里使用了小端字节序,所以0x54
字节才会在第一个。类比一下就知道后4个字节表示的是static int static_var
变量。
通常编译器会把字符串常量和其他的一些常量(在c++中用const修饰)放在.rodata
段中,这样有一下几个好处:
- 在语义上支持c++的const关键字
- 可以在操作系统加载程序的时候将
.rodata
段的属性映射成只读,保证程序的安全性 - 在某些嵌入式平台下,有些存储区采用只读存储器,如ROM,保证了程序访问存储器的正确性
但是需要注意的是,某些编译器会把字符串常量放到.data
段,如MSVC编译器。
段表
段表,保存各个段的基本属性的结构,是ELF文件中非常重要的结构。它描述了各个段的信息,如段名、段长度、在文件中的偏移、读写权限以及其他属性。
我们使用 readelf
工具来查看目标文件的段表结构:readelf -S SimpleSection.o
从截图中我们可以看到,改段表中共有14个元素,其中第一个是NULL,为无效的,因此该段表中只有13个有效的元素。在这个段表中的每个元素都代表一个段,这样的元素对应
Elf64_Shdr
结构体,被称为段描述符。
段描述符Elf64_Shdr
被定义在/usr/include/elf.h
中,代码如下:
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
段描述符中的每一项含义如下表:
成员 | 含义 |
---|---|
sh_name | 真正的段名是一个字符串,它位于一个叫做“.shstrtab"的字符串表中。sh_name是段名在“.shstrtab"中的偏移 |
sh_type | 段类型,对于系统来说真正决定段的属性的是段的类型和段的标志位 |
sh_flags | 段标志位,对于系统来说真正决定段的属性的是段的类型和段的标志位 |
sh_addr | 段虚拟地址 |
sh_offset | 段偏移 |
sh_size | 段长度 |
sh_link & sh_info | 段的链接信息 |
sh_addralign | 段地址对齐 |
sh_entsize | Section Entry Size项的长度 |