ELF链接以及结构
ELF文件结构
环境: 运行Linux的x86-64系统
一、编译与链接
假设存在两个cpp文件
- 使用
gcc -c test.cpp add.cpp
生成两个可重定位目标文件.o,此时还未进行链接,单独.o文件无法执行
-
使用
ld -e main -o out add.o test.o
链接并且-e main指定入口,此时依旧是无法运行,提示段错误,这是因为系统并不知道改在什么时候退出(我们并没有链接其他库帮我们进行退出),尝试在main下断点,gdb调试到ret即为返回时进行下一步
-
下一步可见直接跳到0x0的地址执行代码,显然这个地址是不正确的,所以导致段错误,下图中rip值确为0x1
解决方案:
加入自解决exit代码,然后在return之前调用exit即可,代码实际是实现了一个系统调用sys_exit
void exit() {
asm("movq $14,%rdi \n\t"
"movq $60,%rax \n\t"
"syscall \n\t");
}
- 使用objdump分别dump
链接前的test.o
和链接后输出的out
, 链接前test.o中无add
的汇编实现,而且地址从0
开始,链接后有add汇编实现,并且地址空间从0x401000
开始
二、ELF文件格式
典型的可重定位目标文件
的格式如下图所示,注意是可重定位目标文件,而非可执行文件
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
.rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言”任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。
这里着重说明.symtab .shstrtab .strtab
的不同
.strtab
即string table
保存普通的字符串,比如符号的名字,如下图有test.cpp _ZL13global_static 以及 main _Z4exitv Z3addii等等
这里说明c++函数签名(如下图)
.shstrtab
即为section header table
保存节的名字,如.data .text等
用readelf -s读
.symtab
可以看到输出的都是strtab中的一些符合和其他数据。
实际上.symtab
确定符号的名称与其值之间的关联,其中名称不是直接以字符串形式出现的,而是表示为某一字符串数组.strtab
的索引
下为symtable的结构体定义,name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字”value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。
例如符号表中的索引为3位置的entry,即 _ZL13global_static,为全局静态已初始化变量的符号,应当存储到.data节
其值距离目标节起始位置偏移为4,大小为4B,寻找对应位置,偏移量为4位置处的值为0x32,与源码中初始化值50一致
三、段Segment与节Section
- 根据下面的elf对比图可见,可执行文件比可重定位目标文件多一个program_header_table (describes the sections of the program that contain executable program code which will be mapped into the program address space as it is loaded)
并且section_header_table内结构不一样,可重定位目标文件多两个rela节和一个note.gnu_stack堆栈提示节
如下图,左边是ELF的链接视图,可以理解为是目标代码文件的内容布局。右边是ELF的执行视图,可以理解为可执行文件的内容布局。
Section称为节,是指在汇编源码中经由关键字section或segment修饰、逻辑划分的指令或数据区域,汇编器会将这两个关键字修饰的区域在目标文件中编译成节,也就是说"节"最初诞生于目标文件中。
Segment称为段,是链接器根据目标文件中属性相同的多个Section合并后的Section集合,这个集合称为Segment,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件中。我们平时所说的可执行程序内存空间中的代码段和数据段就是指的Segment。
从概念上来说,段和节的区别在于它们所代表的内存区域的不同。段代表的是可执行代码和数据等内存区域,而节则更加抽象,它代表的是文件中的一组相关数据。在ELF文件中,节是按照功能和目的来划分的,比如代码节、数据节、符号表节等等,而段则是按照内存区域来划分的,比如代码段、数据段、BSS段等等。总的来说,段和节在ELF文件中都是重要的概念,它们分别代表了可执行代码和数据在内存中的组织方式和文件中的逻辑组织方式。
-
使用readelf -S 分别读取test.o和out的节Section
-
关于标志位,说明如下(部分),例如flag为WA,那这节在执行过程中可写并且需要分配内存,例如在.o中rela.text就不需要分配内存,因为他与可执行文件的执行无关,只在链接阶段有用
-
使用readelf -l 分别读取test.o和out的段Segment,显然可重定位目标文件.o并没有Segment的概念,而可执行文件有
这里的标志位,比如R E就是可读可执行,但是不可写
四、数据的存储位置
-
第二节中提到了不同数据存放的节,这里进行验证,这里并未验证const数据,但是const数据是只读数据,存放在.rodata
-
.rodata 节保存了只读的数据,如一行 C 语言代码中的字符串。因为.rodata 节是只读的,所以只能存在于一个可执行文件的只读段中,只能在 text段(不是 data 段)中找到.rodata节。
-
.bss节存放未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量
-
.data节存放已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
参考
- 《深入理解计算机系统》(CSAPP)
- 《程序员的自我修养——链接、装载与库》
- 《ELF_Format》http://www.skyfree.org/linux/references/ELF_Format.pdf
- (Linux) 使用裸ld手动链接c程序 https://blog.csdn.net/yangzijiangac/article/details/123579502
- .symtab https://blog.csdn.net/weixin_34357436/article/details/94235567