一、ELF文件是什么二、ELF可重定位文件2.1 .shstrtab和.strtab2.2 .rel.text和.symtab2.3 .text节三、ELF可执行文件四、 参考
原创 嵌入一下? 谦益行
在一文了解ECU软件刷写文件HEX和S19格式 (qq.com)中介绍汽车控制器的刷写软件的格式,不管是编译生成的目标软件格式是哪种,同时都会生成ELF文件,ELF文件是什么?下文将进行详细介绍:
一、ELF文件是什么
ELF(Executable Linkble Format)是一种Unix-like系统上的二进制文件格式标准。
ELF标准中定义的采用 ELF 格式的文件分为4类:
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合;而执行一个ELF文件时,在加载器(Loader)看来,它是由Program Header Table描述的一系列Segment的集合 ,如下所示:
左边是从汇编器和链接器的视角来看这个文件,开头的 ELF Header 描述了体系结构和操作系统等基本信息,并指出 Section Header Table 和 Program Header Table 在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table 中保存了所有 Section 的描述信息。
右边是从加载器的视角来看这个文件,开头是 ELF Header,Program Header Table 中保存了所有 Segment 的描述信息,Section Header Table 在加载过程中没有用到,所以是可有可无的。注意 Section Header Table 和 Program Header Table 并不是一定要位于文件开头和结尾的,其位置由 ELF Header 指出,上图这么画只是为了清晰。
我们在汇编程序中用 .section 声明的 Section 会成为目标文件中的Section,此外汇编器还会自动添加一些 Section(比如符号表)。
Segment 是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个 Section 组成,比如有两个 Section 都要求加载到内存后可读可写,就属于同一个 Segment。有些 Section 只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内存,那么就不属于任何Segment。
目标文件需要链接器做进一步处理,所以一定有 Section Header Table;可执行文件需要加载运行,所以一定有 Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table 又有 Program Header Table。
二、ELF可重定位文件
下面用 readelf 工具读出目标文件 max.o 的 ELF Header 和 Section Header Table,然后我们逐段分析。
接下来我们来看 Section Header Table 格式
从 Section Header 中读出各 Section 的描述信息,其中:
.text 和 .data 是我们在汇编程序中声明的 Section,而其它 Section 是汇编器自动添加的。
Addr是这些段加载到内存中的地址(我们讲过程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。
Off和Size两列指出了各Section的文件地址,比如.data从文件地址0x60开始,一共0x38个字节,回去翻一下程序,.data中定义了14个4字节的整数,一共是56个字节,也就是0x38个。
根据以上信息可以描绘出整个目标文件的布局。
这个文件不大,我们直接用hexdump或者使用010 Editor工具把目标文件的字节全部打印出来看,如下所示:
2.1 .shstrtab和.strtab
.shstrtab
和 .strtab
这两个 Section 中存放的都是 ASCII 码:
可见 .shstrtab中保存着各Section的名字, .strtab中保存着程序中用到的符号的名字 。每个名字都是以’\0
’结尾的字符串。
我们知道,C语言的全局变量如果在代码中没有初始化,就会在程序加载时用0
初始化。这种数据属于.bss
段,在加载时它和.data
段一样都是可读可写的数据,但是在ELF文件中 .data
段需要占用一部分空间保存初始值,而 .bss
段则不需要。
也就是说,.bss
段在文件中只占一个 Section Header 而没有对应的 Section,程序加载时 .bss
段占多大内存空间在 Section Header 中描述。在我们这个例子中没有用到 .bss 段,以后我们会看到这样的例子。
2.2 .rel.text和.symtab
我们继续分析 readelf 输出的最后一部分,是从 .rel.text
和 .symtab
这两个 Section 中读出的信息。
.rel.text 告诉链接器指令中的哪些地方需要重定位,我们在下一节讨论。
.symtab 是符号表。Ndx 列是每个符号所在的 Section 编号,例如 data_items 在第 3 个 Section 里(也就是 .data ),各 Section 的编号见 Section Header Table 。Value 列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址,比如 data_items 位于 .data 段的开头,所以地址是0,_start 位于 .text 段的开头,所以地址也是0,但是 start_loop 和 loop_exit 相对于 .text 段的地址就不是0了。从 Bind 这一列可以看出 _start 这个符号是 GLOBAL 的,而其它符号是 LOCAL 的,GLOBAL 符号是在汇编程序中用 .globl 指示声明过的符号。
2.3 .text节
通过使用 objdump 工具可以把程序中的机器指令进行反汇编(Disassemble),得到其汇编代码,如下所示:
三、ELF可执行文件
先看可执行文件header的变化:
在看section header的变化:
.text和.data的加载地址分别改成了 0x08048074 和 0x080490a0 。
.bss段没有用到,所以被删掉了。
.rel.text段就是用于链接过程的,链接完了就没用了,所以也删掉了。
在看多出来的两个program header
多出来的Program Header Table描述了两个Segment的信息。
.text段和前面的 ELFHeader、Program Header Table 一起组成一个 Segment(FileSiz指出总长度是0x9e);
.data段组成另一个Segment(总长度是0x38)。
VirtAddr列指出第一个Segment加载到虚拟地址0x0804 8000(注意在x86平台上后面的PhysAddr列是没有意义的),第二个Segment加载到地址0x0804 90a0。
Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。
最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页,对应关系如下图所示。
这个可执行文件很小,总共也不超过一页大小,但是两个Segment 必须加载到内存中两个不同的页面,因为 MMU 的权限保护机制是以页为单位的,一个页面只能设置一种权限。
此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,比如第二个Segment在文件中的偏移是0xa0
,在内存页面0x0804 9000
中的偏移仍然是0xa0
,所以是从0x0804 90a0开始,这样规定是为了简化链接器和加载器的实现。
从上图也可以看出.text
段的加载地址应该是0x0804 8074,也正是_start
符号的地址和程序的入口地址。
原来目标文件符号表中的Value都是相对地址,现在都改成绝对地址了。此外还多了三个符号 __bss_start
、 _edata
和 _end
,这些是在链接过程中添进去的,加载器可以利用这些信息把 .bss
段初始化为0
。
再看一下反汇编的结果:
四、 参考
[1] ELF文件详解—初步认识_code&poetry的博客-CSDN博客
[2] 词法分析、语法分析、语义分析 - JackYang - 博客园 (cnblogs.com)
本文引自:C编译过程 以及 ELF文件(学习笔记),作者: 嵌入一下?