ELF目标文件概述
目标文件分为三种:可重定位目标文件,可执行目标文件和共享目标文件。Linux中使用ELF格式
(举例的两个.c文件:)
/* main.c */
/* $begin main */
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
/* $end main */
/* sum.c */
/* $begin sum */
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
/* $end sum */
- 可重定位目标文件
- 编译生成可重定位目标文件:
$ gcc -c main.c sum.c
由于main.c模块与sum.c模块的ELF可重定位目标文件的格式类似,这里只给出main.o的ELF头,节头部表,符号表里的信息。 - 看ELF头:
$ readelf -h main.o
$ readelf -h sum.o
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。还描述了ELF头的大小,目标文件的类型(REL),机器类型(x86-64),节头部表的文件偏移(11),以及节头部表中条目的大小(64字节)和数量(12)。 - 看节头表:
$ readelf -S main.o
$ readelf -S sum.o
节头部表描述了不同节的位置和大小,其中目标文件的每个节都有一个固定大小的条目。
.text:已编译程序的机器代码。
.rodata:只读数据,如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局和静态C变量。局部变量运行时被保存在栈中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量,目标文件中这个节不占实际的空间。
.symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个位置的列表,链接器把这个目标文件和其他目标文件组合时需要修改这些位置 。任何调用外部函数或者引用全局变量的指令都需修改。
.rel.data:引用或定义的所有全局变量的重定位信息。
(只有可重定位目标文件才有,可执行目标文件没有)节头部表中有三个特殊的伪节在节头部表中是没有条目的:ABS,UNDEF,COMMON。
区分COMMON和.bss:未初始化的全局变量被分配在COMMON中,未初始化的静态变量,以及初始化为0的全局或静态变量被分配在.bss中。 - 看符号表:
$ readelf -s main.o
$ readelf -s sum.o
符号表示汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。
.symtab节中包含ELF符号表,这张符号表中包含一个条目的数组。
开始的8个条目是链接器内部使用的局部符号。(Bind为LOCAL)
value:距定义目标的节的起止位置的偏移,对于可执行目标文件来说这里是一个绝对运行时的地址。
size:目标的大小(字节)。
type:数据或函数。
bind:表示符号是本地还是全局的。
name:字符串表(.strtab)中的字节偏移。
看到全局符号main定义的条目,它是一个位于.text节(Ndx=1)中偏移量为0(value的值)处的33字节(size)的函数。array是一个位于.data节(Ndx=3)中偏移量为0(value的值)的8字节(size)条目。最后一个条目是对外部符号sum的引用,它不在该模块被定义(UND)。 - 反汇编:
$objdump -dx main.o
$objdump -dx -j .data main.o
-j name
–section=name
仅仅显示指定名称为name的section的信息
- 可执行目标文件
可执行目标文件的格式类似与可重定位目标文件格式。它还包括程序的入口点,也就是程序运行时要执行第一条指令的地址。相比于可重定位目标文件能看到它含有.init节不含.rel节。
- 链接生成可执行目标文件:
$ gcc -o main main.o sum.o/gcc -o main main.c sum.c
- 看ELF头:
$ readelf -h main
- 看节头表:
$ readelf -S main - 看符号表:
$ readelf -s main - 看程序头表:
$ readelf -l main
- 反汇编:
$objdump -dx main(仅贴出main和sum部分)
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段。程序头部表描述了这种映射关系。是由OBJDUMP显示的。
可执行目标文件的内容初始化两个内存段。
r-x说明代码段有读/执行访问权限,开始于内存地址0x000000处,总共的内存大小是0x850字节,并且被初始化可执行目标文件的头0x850个字节,其中包括ELF头、程序头部表以及.init、.text和.rodata节。
rw-说明数据段由读/写访问权限,开始于内存地址0x200df0处,总的内存大小为0x230个字节,并用从目标文件中偏移的0xdf0处开始的.data节中的0x228个字节初始化。
$objdump -dx -j .data main
3.重定位
当汇编器生成一个目标模块时,它并不知道数据和代码最终放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或全局变量的位置。
所以当汇编器遇到对未知位置的引用时就会生成一个重定位条目,告诉链接器在目标文件合并时如何修改这个引用。
代码的重定位条目在.rel.text中,已初始化数据的重定位条目在.rel.data中。
看重定位条目:
$ readelf -r main.o
$ readelf -r sum.o
offset时需要被修改的引用的节偏移。
type告知链接器如何修改新的引用(R_X86_64_PC32是重定位PC相对地址的引用,R_X86_64_32是重定位绝对地址的引用)。这里显示的PLT32。
symbol标识被修改引用应该指向的符号。
addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。反汇编main.o也能看到这部分数据。
重定位相对引用:
$ objdump -dx main
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: 48 83 ec 10 sub $0x10,%rsp
602: be 02 00 00 00 mov $0x2,%esi
607: 48 8d 3d 02 0a 20 00 lea 0x200a02(%rip),%rdi # 201010 <array>
60e: e8 08 00 00 00 callq 61b <sum>
613: 89 45 fc mov %eax,-0x4(%rbp)
616: 8b 45 fc mov -0x4(%rbp),%eax
619: c9 leaveq
61a: c3 retq
000000000000061b <sum>:
61b: 55 push %rbp
61c: 48 89 e5 mov %rsp,%rbp
61f: 48 89 7d e8 mov %rdi,-0x18(%rbp)
623: 89 75 e4 mov %esi,-0x1c(%rbp)
626: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
62d: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
634: eb 1d jmp 653 <sum+0x38>
636: 8b 45 f8 mov -0x8(%rbp),%eax
639: 48 98 cltq
63b: 48 8d 14 85 00 00 00 lea 0x0(,%rax,4),%rdx
642: 00
643: 48 8b 45 e8 mov -0x18(%rbp),%rax
647: 48 01 d0 add %rdx,%rax
64a: 8b 00 mov (%rax),%eax
64c: 01 45 fc add %eax,-0x4(%rbp)
64f: 83 45 f8 01 addl $0x1,-0x8(%rbp)
653: 8b 45 f8 mov -0x8(%rbp),%eax
656: 3b 45 e4 cmp -0x1c(%rbp),%eax
659: 7c db jl 636 <sum+0x1b>
65b: 8b 45 fc mov -0x4(%rbp),%eax
65e: 5d pop %rbp
65f: c3 retq
objdump -dx main.o
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: be 02 00 00 00 mov $0x2,%esi
d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 14 <main+0x14>
10: R_X86_64_PC32 array-0x4
14: e8 00 00 00 00 callq 19 <main+0x19>
15: R_X86_64_PLT32 sum-0x4
19: 89 45 fc mov %eax,-0x4(%rbp)
1c: 8b 45 fc mov -0x4(%rbp),%eax
1f: c9 leaveq
20: c3 retq
r.offset = 0x15
r.symbol = sum
r.type = R_X86_64_PLT32(书上的是PC32)
r.addend = -4
- 计算引用的运行时地址
ADDR(s) = ADDR(.text) ; ADDR(r.symbol) = ADDR(sum) - 更新该引用,使它在运行时指向sum程序
refaddr = ADDR(s) + r.offset = 0x5fa + 0x15 = 0x60f
*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr)
= (unsigned)(0x61b + (-4) - 0x60f)
= (unsigned)0x8 - 在得到的可执行文件中,call指令的重定位形式:
60e: e8 08 00 00 00 callq 61b < sum >
在运行时,call指令将存放在0x60e处,当CPU执行call指令时,PC的值为0x613(0x5fa+19)即call指令的下一条指令的地址。为执行这条指令,CPU将PC压入栈中,然后PC<—PC+0x8 = 0x61b,这样下一条指令就是sum例程的第一条指令。