ELF目标文件概述

mac2024-06-24  47

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例程的第一条指令。

最新回复(0)