ELF文件里有什么
一.文件头:
ELF文件的开头是一个“文件头”,它描述了整个文件的
- 魔数:用来描述文件的类型、字节序、ELF的主版本号,其余的暂时没有使用。
- 文件属性,
- 包括文件是否可执行、
- 是静态链接还是动态链接及入口地址(如果是可执行文件)、
- 目标硬件、
- 目标操作系统等信息,
- 还包括段表,段表是一个描述文件中各个段的数组,描述各个段在文件中的偏移位置及段的属性等。
二.分段的意义:
- 数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚拟区域的权限可以被分别设置成可读写和只读,防止程序的指令被有意或无意地改写;
- 提高缓存命中率,指令区和数据区的分离有利于提高程序的局部性;
- 当系统运行着多个该程序的副本时,它的指令是一样的,只需要保存一份,节约资源。
三.段:
-
代码段:程序源代码编译后的机器指令经常被放在代码段,常见的名字:.code .text
-
数据段和只读数据段:.data段保存的是哪些已经初始化了的全局静态变量和局部静态变量。.rodata段存放的是只读数据,一般都是程序里面的只读变量(如const修饰的变量)和字符串常量。
-
BSS段:.bss段存放的是未初始化的全局变量和局部静态变量。
-
.comment:存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”
-
.debug:调试信息
-
.dynamic:动态链接信息
-
.hash:符号哈希表
-
.line:调试时的行号表,即源代码行号与编译后指令的对应表
-
.note:额外的编译器信息,比如程序的公司名、发布版本号等
-
.strtab:String Table字符串表,用于存储ELF文件中用到的各种字符串
-
symtab:Symbol Table符号表
-
.shstrtab:Section String Table段名表,保存段表中用到的字符串,最常见的就是段名
-
.plt .got:动态链接的跳转表和全局入口表
-
.init .fini:程序初始化与终结代码段
-
.rel**:带有rel的段是重定位表,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。
四.自定义段:
“.”作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。我们可以将变量或某些部分代码放到指定的段中去。在全局变量或函数之前加上“_attribute_((section(“name”)))”属性就可以把相应的变量或函数放到以“name”作为段名的段中。
五.命令:
objdump:
- -s:可以将所有段的内容以十六进制的方式打印出来
- -d:可以将所有包含指令的段反汇编
- -h:把ELF文件的各个段的基本信息打印出来,只把ELF文件中关键的段显示出来,省略了其他的辅助性的段。要显示全部段的信息,使用readelf -S xxx
- -x:把ELF文件各个段的更多的信息打印出来,多而复杂
- -t:可以将所有符号,已经其对应的段的段名
size:
可以用来查看ELF文件的代码段、数据段和BSS段的长度
readelf:
- -h:查看ELF文件的文件头
- -S:查看ELF文件中真正的段表结构
- -a:把ELF文件各个段的更多的信息打印出来,多而复杂,类似于命令objdump -x
nm:
可以用来查看目标文件中的所有符号
c++filt:
可以解析被修饰过后的函数名称,如:
$ c++filt _ZN1N1C4funcEi
N::C::func(int)
strip:
用来去掉ELF文件中的调试信息。GCC编译时,加上“-g”参数,编译器就会在生成的目标文件里面加上调试信息,通过readelf可以看到很多“debug”相关的段。
六.符号:
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)
目标文件中都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。其他的类型:
- 定义在本目标文件的全局符号,可以被其他目标文件引用
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这个叫做外部符号(External Symbol),也叫符号引用
- 段名,这种符号往往由编译器成圣,它的值就是该段的起始地址
- 局部符号,这类符号只在编译单元内部可见。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件,这些局部符号对于链接过程没有作用,链接器往往也忽略它们
- 行号信息,即目标文件指令与源代码中代码行的对应关系,是可选的
6.1 符号表的结构:
对于c文件:
#include<stdio.h>
int printf(const char * format, ...);
int a;
int main(){
add();
printf("a=%d", a);
return 0;
}
xxx$ readelf -s b.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a
10: 0000000000000000 46 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND add
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
符号表的属性:
- name:符号名。在符号表结构体中其实并非真正的名字,而是该符号在字符串表中的下标
- value:符号相对应的值,这个值跟符号有关,可能是一个绝对值,也可能是一个地址等
- size:符号大小,对于包含数据的符号,这个值就是该数据类型的大小
- type:符号类型
- bind:绑定信息
- ndx:符号所在的段
绑定信息:
- LOCAL:局部符号,对于目标文件的外部不可见
- GLOBAL:全局符号,外部可见
- WEAK:弱引用
符号类型:
- NOTYPE:未知类型符号
- OBJECT:该符号是个数据对象,比如变量、数组等
- FUNC:该符号是个函数或其他可执行代码
- SECTION:该符号表示一个段,这个符号必须是LOCAL的
- FILE:该符号表示文件名,一般都是该目标文件所对应的源文件名,它一定是LOCAL类型的,并且它的ndx一定是ABS
nds:
符号所在段特殊常量:
- ABS:表示该符号包含一个绝对的值,比如表示文件名的符号就是属于这种类型的
- COMMON:表示该符号是一个“COMMON块”类型的符号,一般来说,未初始化的全局符号定义就是这种类型
- UNDEF:表示该符号未定义,这个符号表示该符号在本目标文件被引用,但是定义在其他目标文件中
符号值(value):
- 符号在对应的段中的偏移:针对的是不属于“COMMON块”类型的符号
- 对齐属性:针对的是属于“COMMON块”类型的符号
- 虚拟地址:针对的是动态链接时,符号对应的虚拟地址
6.2 特殊符号:
当使用ld作为链接器来链接生成可执行文件上时,它会为我们定义很多特殊的符号,这些符号并没有在程序中定义,但是我们可以直接声明并且引用它,这些符号的定义在ld链接器的链接脚本中,因此只有使用ld作为链接器的时候这些符号才可以使用,这些符号的值都是程序被装载后的虚拟地址,我们可以在程序中直接使用这些值:
- __executable_start:程序起始地址,注意不是入口地址,是程序的最开始的地址
- __etext或_etext或etext:代码段结束地址
- _edata或edata:数据段结束地址
- _end或end:程序结束地址
6.3 弱符号与强符号:
对于c、c++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(包括初始化为0的符号)。也可以通过GCC的“_attribute_((weak))”来定义任何一个强符号为弱符号。 强符号和弱符号都是针对定义来说的,而不是针对符号的引用,如:extern int a;那么a既不是强符号也不是弱符号,因为它是一个外部变量的引用。
规则:
- 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号)
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
6.4 弱引用与强引用:
- 强引用:本目标文件对外部目标文件的符号引用在目标文件最终链接成可执行文件时,它们必须被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误
- 弱引用:本目标文件对外部目标文件的符号引用在目标文件最终链接成可执行文件时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器不会报错,一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值。
在GCC中,可以通过使用“_attribute_((weakref))”来声明对一个外部函数的引用为弱引用,如:
__attribute__((weakref)) void foo();
int main()
{
if (foo) foo();
}
在链接的时候不会发生错误,但是在运行的时候如果foo没有定义,那么foo的值为0,直接使用会导致程序崩溃。
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号覆盖,从而使得程序可以使用自定义版本的库函数。