这里介绍一下ELF文件格式,在linux下,我们用gcc编译出来的可执行文件的格式都是按照ELF文件格式摆放的,为了了解可执行文件怎么运行的,以及装载程序对他怎么进行装载的,我们很有必要了解一下他。
我们先看一下这张图,可以发现我们大概一个将一个ELF格式的文件分成四个部分:1.ELF文件的文件头。2.程序头表。3.主体内容。4.节头表。我们一个一个的看,先看文件头。
我们在linux下输入这条指令就可以查看一个具体elf文件文件头的信息。
readelf -h 文件名
这是我在我的虚拟机上随便查到的结果,我们可以获得这些信息。
对应这个结构体里存的数据。
#define EI_NIDENT 16
typedef struct{
/*ELF的一些标识信息,固定值*/
unsigned char e_ident[EI_NIDENT];
/*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/
Elf32_Half e_type;
/*文件的目标体系结构类型:3-intel 80386*/
Elf32_Half e_machine;
/*目标文件版本:1-当前版本*/
Elf32_Word e_version;
/*程序入口的虚拟地址,如果没有入口,可为0*/
Elf32_Addr e_entry;
/*程序头表(segment header table)的偏移量,如果没有,可为0*/
Elf32_Off e_phoff;
/*节区头表(section header table)的偏移量,没有可为0*/
Elf32_Off e_shoff;
/*与文件相关的,特定于处理器的标志*/
Elf32_Word e_flags;
/*ELF头部的大小,单位字节*/
Elf32_Half e_ehsize;
/*程序头表每个表项的大小,单位字节*/
Elf32_Half e_phentsize;
/*程序头表表项的个数*/
Elf32_Half e_phnum;
/*节区头表每个表项的大小,单位字节*/
Elf32_Half e_shentsize;
/*节区头表表项的数目*/
Elf32_Half e_shnum;
/*某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。*/
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
参考文献:https://blog.csdn.net/u014587123/article/details/115276998
其中有几个字段我觉得比较重要,1是e_entry,这个是可执行程序的入口地址,也就是main函数在内存中运行的虚拟地址,e_phoff和e_shoff都是偏移地址,是相对于我们这个文件的第一个字节的。
我们先解释两个概念,第一:Section头表,就是节头表,因为汇编程序在写的时候是以节作为划分的,也就是我们这个程序的经过编译与链接之后会被分成很多节,然后记录每一节的信息就是节头表。
readelf -S 文件名
通过上面的指令可以看见每一节的具体信息。
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8
0000000000000240 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000608 00000608
00000000000001a1 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000007aa 000007aa
0000000000000030 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000000007e0 000007e0
0000000000000060 0000000000000000 A 7 2 8
[10] .rela.dyn RELA 0000000000000840 00000840
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000900 00000900
00000000000001b0 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000130 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001150 00001150
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001160 00001160
0000000000000120 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001280 00001280
0000000000000535 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 00000000000017b8 000017b8
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
0000000000000041 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 0000000000002044 00002044
000000000000006c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 00000000000020b0 000020b0
00000000000001a8 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003d20 00002d20
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003d28 00002d28
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003d30 00002d30
0000000000000200 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003f30 00002f30
00000000000000d0 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004010 00003010
0000000000000010 0000000000000000 WA 0 0 8
[27] .comment PROGBITS 0000000000000000 00003010
000000000000002b 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003040
0000000000000858 0000000000000018 29 47 8
[29] .strtab STRTAB 0000000000000000 00003898
0000000000000408 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00003ca0
000000000000011a 0000000000000000 0 0 1
注意,我们可以看到我们这个程序一共被分成30节,里面有很多我们很熟悉的,比如名字叫.text的那节,我们的代码就放在里面。表头的地址表示的运行时会被放到内存的每一节开头的虚地址。偏移量表示相对于文件开头第一个字节我们这一节偏移了多少。以下是节头表每一项存的信息。
typedef struct{
/*节区名称*/
Elf32_Word sh_name;
/*节区类型:PROGBITS-程序定义的信息,NOBITS-不占用文件空间(bss),REL-重定位表项*/
Elf32_Word sh_type;
/*每一bit位代表一种信息,表示节区内的内容是否可以修改,是否可执行等信息*/
Elf32_Word sh_flags;
/*如果节区将出现在进程的内存影响中,此成员给出节区的第一个字节应处的位置*/
Elf32_Addr sh_addr;
/*节区的第一个字节与文件头之间的偏移*/
Elf32_Off sh_offset;
/*节区的长度,单位字节,NOBITS虽然这个值非0但不占文件中的空间*/
Elf32_Word sh_size;
/*节区头部表索引链接*/
Elf32_Word sh_link;
/*节区附加信息*/
Elf32_Word sh_info;
/*节区带有地址对齐的约束*/
Elf32_Word sh_addralign;
/*某些节区中包含固定大小的项目,如符号表,那么这个成员给出其固定大小*/
Elf32_Word sh_entsize;
}Elf32_Shdr;
这段代码转自链接:https://blog.csdn.net/u014587123/article/details/115276998。
然后是程序头表,我们的代码同时也可以被分段,我们在进程镜像里面看到的就是被分段的程序,其依据就是这个。我们可以用以下指令查ELF的分段情况。
readelf -l 文件名
以下是我随便找的程序的部分结果截图
其他字段都和节头表哪里差不多,我们要注意这个PhysAddr这个字段,他是物理地址没错。但我们早知道我知道我们在用readelf这指令的时候这个可执行文件还只是一个文件,他并没有被运行进入内存形成一个进程,所以这个PhysAddr是gcc建议装载程序将我们这些段放到内存的物理地址,但装载程序会不会放在这就不知道了(大概率不会)。下面是段头表每一项的信息。
typedef struct
{
/*segment的类型:PT_LOAD= 1 可加载的段*/
Elf32_Word p_type;
/*从文件头到该段第一个字节的偏移*/
Elf32_Off p_offset;
/*该段第一个字节被放到内存中的虚拟地址*/
Elf32_Addr p_vaddr;
/*在linux中这个成员没有任何意义,值与p_vaddr相同*/
Elf32_Addr p_paddr;
/*该段在文件映像中所占的字节数*/
Elf32_Word p_filesz;
/*该段在内存映像中占用的字节数*/
Elf32_Word p_memsz;
/*段标志*/
Elf32_Word p_flags;
/*p_vaddr是否对齐*/
Elf32_Word p_align;
} Elf32_phdr;
这段代码转自链接:https://blog.csdn.net/u014587123/article/details/115276998。
所以我们知道,只要一个ELF文件被生成,每一节和段的虚拟地址就是被定好的,所以每个变量的虚拟地址也是被定好的,但是物理地址要运行的时候才能确定。