ELF可重定位目标文件

发布时间 2023-10-07 21:39:15作者: mjy66
1、简述

​ 一个main.c文件从源代码到可执行文件要通过四个步骤:预处理、编译、汇编、链接。可重定位目标文件出现在汇编处理之后,其包含二进制代码和数据,并能与其他可重定位目标文件合并,最终创建一个可执行目标文件。

​ 目标文件分为三种:可重定位目标文件、可执行目标文件、共享目标文件,其是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。Windows使用可移植可执行(Portable Executable,PE)格式,MacOS-X使用Mach-O格式,现代x86-64Linux和Unix系统使用可执行可链接(Executable and Linkable Format,ELF)格式,以下我们将讨论ELF格式的可重定位目标文件。

2、ELF可重定位目标文件格式

​ 典型的ELF可重定位目标文件如下图所示:

​ ELF头以一个16个字节的序列开始,以节头部表结尾,夹在中间的都是节,一个典型的ELF可重定位目标文件包含下面几个节:

  • .text:已编译程序的机器代码
  • .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
  • .data:已初始化的全局和静态C变量,局部的C变量在运行的时候保存在栈中,不出现在.data节中,也不出现在.bss节中
  • .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量,未初始化变量不需要占据任何实际的磁盘空间,在运行的时候,在内存中分配这些变量,初始值为0
  • .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,其不包含局部变量的条目
  • .rel.text:.text节的位置列表,任何调用外部函数或者引用全局变量的指令都需要修改这些位置
  • .rel.data:被模块引用或定义的所有全局变量的重定位信息
  • .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件,在编译时以-g选项调用编译器驱动程序,才会得到这张表
  • .line:原始C源程序中的行号和.text节中机器指令之间的映射,在编译时以-g选项调用编译器驱动程序,才会得到这张表
  • .strtab:字符串表,包含.symtab和.debug节中的符号表
3、分析工具

​ 分析ELF文件最常用的命令行工具是readelf和hexdump,readelf工具用来读取elf文件,hexdump工具用来读取二进制文件,下文仅做一个对.o文件的读取,因此只使用readelf工具。

4、示例程序
//main.c
int buf[2]={1,2};
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
    int temp;
    bufp1 = &buf[1];
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}

int main()
{
	swap();
	return 0;
}
5、分析

​ 输入命令行,使用gcc编译main.c文件,使其编译系统在汇编阶段完成之后停止,此时生成main.o文件。

[root@master test]gcc -Og -c main.c
  • ELF文件头部分
[root@master test]readelf -h main.o

输出:

​ elf文件头会包含一些关于本机操作系统类型、elf文件类型以及elf文件中各个部分、节的大小和数量等信息。

  • ELF文件Section部分
[root@master test]readelf -S main.o

输出:

输出显示ELF文件section部分共13节,其中第一个为NULL,每个节都有其名称、类型、地址、偏移等信息,最后一节的偏移量为0x330,大小为0x5e,0x330+0x5e=0x38E(910),由于节与节也要考虑内存对齐,默认情况下一般是4个字节对齐,所以section部分占用的内存大小为912字节,与ELF文件头中的信息均对应。

  • Section部分中的.symtab符号表
[root@master test]readelf -s main.o

输出:

​ 符号表中包含main.o文件定义和引用的符号的信息,包含三种不同的符号:

  • 由模块定义并能被其他模块引用的全局符号,其对应于非静态的C函数和全局变量
  • 由其他模块定义并被模块引用的全局符号,这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量
  • 只被模块定义和引用的局部符号,其对应于带static属性的C函数和全局变量

​ 其符号表中每个条目包含的内容可用Elf64_Symbol结构体表示:

typedef struct{
	int name; //字符串表中的字节偏移,指向符号的以null结尾的字符串名字
	char type; //表示是函数还是数据
	char binding; //表示是局部还是全局
	char reserved; 
	short section; //表示目标文件中的某个节
	long value; //符号的地址
	long size; //目标的大小
}Elf64_Symbol

​ 每个符号都有所归属的节,Ndx=1表示归属.text节,Ndx=3表示归属.data节,另外还有三个特殊的伪节可以来表示符号的其它属性:ABS代表不该被重定位的符号、UNDEF代表未定义的符号、COMMON表示未初始化的全局变量。在这个例子里,全局符号swap定义的条目,它是一个位于.text节中偏移量为0处的38字节函数,bufp1符号由于是还未进行初始化的全局变量,因此用COM来标明,其它的符号条目的属性均以此类推。