ELF文件(Executable Linkable Format)是一种文件存储格式。Linux下的目标文件和可执行文件都按照该格式进行存储,有必要做个总结。
1. 链接举例
2. ELF文件类型2.1 可重定位目标文件(.o文件)
2.2 可执行目标文件(a.out文件)
2.3 共享对象文件(.so文件)
3. ELF文件作用
4. ELF文件格式4.1 从编译和链接角度看ELF文件(可重定位目标文件)
4.2 从程序执行角度看ELF文件(可执行文件)
5.总结
1. 链接举例
在介绍ELF文件之前,我们先看下,一个.c程序是如何变成可执行目标文件的。下面举个例子。
该程序由main.c和sum.c两个模块组成。sum.c接收数组和数组长度两个参数,最后将数组求和的结果返回。main.c调用sum函数,并传递一个两元素的int数组array,将计算结果保存在val中。//main.c
intsum(int*a,intn);
intarray[2]={1,2};
intmain(intargc,char**argv){
intval=sum(array,2);
returnval;
}//sum.c
intsum(int*a,intn){
inti,s=0;
for(i=0;i
s+=a[i];
}
returns;
}
让我们来看看如果我们使用GCC编译两个模块会发生什么?
main.c和sum.c将分别通过翻译器将源文件处理为可重定位的目标文件main.o和sum.o。翻译器处理的过程包括了预处理(ccp)、编译(ccl)、汇编(as)三个过程。最后,链接器(ld)将可重定位的目标文件main.o和sum.o以及一些必要的系统文件组合起来,创建一个可执行目标文件prog。具体过程如下图所示。
链接过程
由上面的过程,我们可以看出在经过汇编器后会输出一个.o文件,这个叫做可重定位的目标文件。将main.o和sum.o输入链接器后,链接器输出的prog文件叫做可执行目标文件。那这两个目标文件有什么样的区别呢?
2. ELF文件类型
2.1 可重定位目标文件(.o文件)
包含二进制代码和数据,其形式可以和其他目标文件进行合并,创建一个可执行目标文件。例如lib*.o文件。
2.2 可执行目标文件(a.out文件)
包含二进制代码和数据,可直接被加载器加载执行。例如编译好的可执行文件a.out。
2.3 共享对象文件(.so文件)
用于和其他共享目标文件或者可重定位文件一起生成ELF目标文件或者和执行文件一起创建进程映像,例如lib*.so文件。
3. ELF文件作用
ELF文件参与程序的连接(建立一个程序)和程序的执行(运行一个程序),所以可以从不同的角度来看待ELF格式的文件:
1.如果用于编译和链接(可重定位文件),则编译器和链接器将把ELF文件看作是节头表描述的节的集合,程序头表可选。
2.如果用于加载执行(可执行文件),则加载器则将把ELF文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头表可选。
4. ELF文件格式
4.1 从编译和链接角度看ELF文件(可重定位目标文件)
从编译和链接角度看ELF文件
ELF头
每个ELF文件都必须存在一个ELF_He ader,这里存放了很多重要的信息用来描述整个文件的组织,如: 版本信息,入口信息,偏移信息等。程序执行也必须依靠其提供的信息。
段头表
段头表。存放的是所有不同段将在内存中的位置。
.text section
代码段。存放已编译程序的机器代码,一般是只读的。
.rodatasection
只读数据段。此段的数据不可修改,存放常量。比如,printf中的格式化语句。
.datasection
数据段。存放已初始化的全局变量、常量。
.bsssection
bss段。未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。目标文件格式区分初始化和非初始化是为了空间效率。
从编译和链接角度看ELF文件
.symtabsection
符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rel.txtsection
.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
.rel.datasection
.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息。
.debugsection
调试用的符号表。
.strtab section
包含 symtab和 debug节中符号及节名。
节头部表
每个节的节名、偏移和大小。
以下是32位系统对应的节头表数据结构,说明了每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。typedefstruct{
Elf32_Wordsh_name;//节名字符串在.strtab节(字符串表)中的偏移
Elf32_Wordsh_type;//节类型:无效/代码或数据/符号/字符串/...
Elf32_Wordsh_flags;//节标志:该节在虚拟空间中的访问属性
Elf32_Addrsh_addr;//虚拟地址:若可被加载,则对应虚拟地址
Elf32_Offsh_offset;//在文件中的偏移地址,对.bss节而言则无意义
Elf32_Wordsh_size;//节在文件中所占的长度
Elf32_Wordsh_link;//sh_link和sh_info用于与链接相关的节(如.rel.text节、.rel.data节、.symtab节等)
Elf32_Wordsh_info;
Elf32_Wordsh_addralign;//节的对齐要求
Elf32_Wordsh_entsize;//节中每个表项的长度,0表示无固定长度表项
}Elf32_Shdr;
使用readelf命令命令查看节头表内容[ubuntu@localhostinterpositioning]$readelf-Smain.o
Thereare13sectionheaders,startingatoffset0x3f8:
SectionHeaders:
[Nr]NameTypeAddressOffset
SizeEntSizeFlagsLinkInfoAlign
[0]NULL000000000000000000000000
00000000000000000000000000000000000
[1].textPROGBITS000000000000000000000040
00000000000000710000000000000000AX001
[2].rela.textRELA0000000000000000000002d0
00000000000000900000000000000018I1118
[3].dataPROGBITS0000000000000000000000b1
00000000000000490000000000000000WA001
[4].bssNOBITS0000000000000000000000b1
000000000000000c0000000000000000WA001
[5].rodataPROGBITS0000000000000000000000b1
00000000000000190000000000000000A001
[6].commentPROGBITS0000000000000000000000ca
00000000000000350000000000000001MS001
[7].note.GNU-stackPROGBITS0000000000000000000000ff
00000000000000000000000000000000001
[8].eh_framePROGBITS000000000000000000000100
00000000000000580000000000000000A008
[9].rela.eh_frameRELA000000000000000000000360
00000000000000300000000000000018I1188
[10].shstrtabSTRTAB000000000000000000000390
00000000000000610000000000000000001
[11].symtabSYMTAB000000000000000000000158
000000000000015000000000000000181298
[12].strtabSTRTAB0000000000000000000002a8
00000000000000230000000000000000001
KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings),l(large)
I(info),L(linkorder),G(group),T(TLS),E(exclude),x(unknown)
O(extraOSprocessingrequired)o(OSspecific),p(processorspecific)
可重定位目标文件中,每个可装入节的起始地址总是0。
.bss节应占0x0c大小,但只有装入内存时才会分配。
4.2 从程序执行角度看ELF文件(可执行文件)
从程序执行角度看ELF文件
与可重定位目标文件不同:
1.ELF头中,字段 e_entry给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0。
2.多一个init节,用于定义init函数,该函数用来进行可执行目标文件开始执行时的初始化工作。
3.少两个.rel节(无需重定位)。
4.多一个程序头表,也称段头表,是一个结构数组。
使用readelf命令查看ELF头的内容:[ubuntu@localhostinterpositioning]$readelf-hmain.o
ELFHeader:
Magic:7f454c4601000000000000000000
Class:ELF64
Data:2'scomplement,littleendian
Version:1(current)
OS/ABI:UNIX-SystemV
ABIVersion:0
Type:REL(Relocatablefile)
Machine:AdvancedMicroDevicesX86-64
Version:0x1
Entrypointaddress:0x0
Startofprogramheaders:0(bytesintofile)
Startofsectionheaders:1064(bytesintofile)
Flags:0x0
Sizeofthisheader:64(bytes)
Sizeofprogramheaders:32(bytes)//程序头表每项32B
Numberofprogramheaders:8//程序头表共8项
Sizeofsectionheaders:64(bytes)
Numberofsectionheaders:13
Sectionheaderstringtableindex:10//.strtab在节头表中的索引
装入内存时,ELF头、程序头表、.init节、.rodata节会被装入只读代码段。.data节和.bss节会被装入读写数据段。
段头表能够描述可执行文件中的节与虚拟空间中的存储段之间的映射关系。一个表项32B,说明虚拟地址空间中一个连续的片段或一个特殊的节。以下是32位系统对应的段头表数据结构:typedefstruct{
Elf32_Wordp_type;//此数组元素描述的段的类型,或者如何解释此数组元素的信息。
Elf32_Offp_offset;//此成员给出从文件头到该段第一个字节的偏移
Elf32_Addrp_vaddr;//此成员给出段的第一个字节将被放到内存中的虚拟地址
Elf32_Addrp_paddr;//此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
Elf32_Wordp_filesz;//此成员给出段在文件映像中所占的字节数。可以为0。
Elf32_Wordp_memsz;//此成员给出段在内存映像中占用的字节数。可以为0。
Elf32_Wordp_flags;//此成员给出与段相关的标志。
Elf32_Wordp_align;//此成员给出段在文件中和内存中如何对齐。
}Elf32_phdr;
使用readelf命令查看某可执行目标文件的程序头表。[ubuntu@localhostinterpositioning]$readelf-lmain
ElffiletypeisEXEC(Executablefile)
Entrypoint0x400550
Thereare9programheaders,startingatoffset64
ProgramHeaders:
TypeOffsetVirtAddrPhysAddr
FileSizMemSizFlagsAlign
PHDR0x00000000000000400x00000000004000400x0000000000400040
0x00000000000001f80x00000000000001f8RE8
INTERP0x00000000000002380x00000000004002380x0000000000400238
0x000000000000001c0x000000000000001cR1
[Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2]
LOAD0x00000000000000000x00000000004000000x0000000000400000
0x00000000000008ac0x00000000000008acRE200000
LOAD0x0000000000000e100x0000000000600e100x0000000000600e10
0x00000000000002400x0000000000000248RW200000
DYNAMIC0x0000000000000e280x0000000000600e280x0000000000600e28
0x00000000000001d00x00000000000001d0RW8
NOTE0x00000000000002540x00000000004002540x0000000000400254
0x00000000000000440x0000000000000044R4
GNU_EH_FRAME0x00000000000007800x00000000004007800x0000000000400780
0x00000000000000340x0000000000000034R4
GNU_STACK0x00000000000000000x00000000000000000x0000000000000000
0x00000000000000000x0000000000000000RW10
GNU_RELRO0x0000000000000e100x0000000000600e100x0000000000600e10
0x00000000000001f00x00000000000001f0R1
程序头表信息有9个表项,其中两个为可装入段(即Type=LOAD):
第一可装入段:第0x0000 0~0x0x8ab的长度为0x8ac字节的ELF头、程序头表、.init、.text和.rodata节,映射到虚拟地址0x400000开始长度为0x8ac字节的区域 ,按0x200000=2MB对齐,具有只读/执行权限(Flg=RE),是只读代码段。
第二可装入段:第0xe10 ~0x104f的长度为0x240字节的.data节和磁盘中不占存储空间的.bss节,映射到虚拟地址0x600e10开始长度为0x248字节的存储区域,在0x248=584B存储区中,前0x240= 576B用.data节内容初始化,后面584-576= 8B对应.bss节,初始化为0 ,按0x200000 =2MB对齐,具有可读可写权限(Flg=RW),是可读写数据段。
由此看出.bss节在文件中不占用磁盘空间,但在存储器中需要给它分配相应大小的空间。
5.总结
1.链接处理涉及到三种目标文件格式:可重定位目标文件、可执行目标文件和共享目标文件。共享库文件是一种特殊的可重定位目标。
2.ELF目标文件格式可以从编译链接角度和程序执行角度两个角度看,前者是可重定位目标格式,后者是可执行目标格式。从编译链接角度看,可重定位目标文件中包含ELF头、各个节以及节头表。可执行目标文件中包含ELF头、程序头表(段头表)以及各种节组成的段。
3.bss段在可执行目标文件中不会有它的空间,只有当可执行目标文件装载运行时,才会被分配内存(并且位于data段内存块之后),并且初始化为0。