编译器生成的目标文件格式有axf和elf。区别是:ADS,armcc编译出来的是AXF文件;gcc编译出来的是ELF文件。两者虽然很像,但还是有差别的。这是文件格式的差别,不涉及调试格式。
参考:
ELF文件格式 - 知乎
ELF文件详解—初步认识-CSDN博客
【精选】readelf 和 objdump 例子详解及区别 (ELF文件说明)_objdump readelf-CSDN博客
一、简介
ELF文件全称 “Executable and Linkable Format”,即可执行可链接文件格式是一种用于二进制文件、可执行文件、目标代码、共享库和core转存格式文件。是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。
ELF文件主要有四种类型:
1)可重定位文件(Relocatable File)
包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。如 xxx.o,xxx.a 文件。
2)可执行文件(Executable File)
包含适合于执行的程序, 它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。 这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。
加载器:对于裸机来说jtag,操作系统来说是一段加载代码(通过execve函数调用加载代码)
加载器将可执行文件的片复制到代码段和数据段,然后跳转到程序入口点,也就是_start函数的位置,这个函数是在系统目标文件ctrl.o中定义的,_start函数调用系统启动函数,_libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数。
3)共享目标文件(Shared Object File) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像,即 xxx.so文件。
4)内核转储(core dumps),存放当前进程的执行上下文,用于dump信号触发。
二、ELF文件格式的两种视图
ELF文件格式提供了两种视图,分别是链接视图和执行视图:
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。
目标文件.o里的代码段.text 是section(汇编中.text同理),当多个可重定向文件最终要整合成一个可执行的文件的时候(链接过程),链接器把目标文件中相同权限属性的 section 整合成一个segment分配在同一空间,在程序运行的时候,方便加载器的加载。
使用readelf -l 命令可以查看一个链接后的elf可执行文件,Section to Segment 的映射关系:
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000007e4 0x00000000000007e4 R E 200000 LOAD 0x0000000000000e30 0x0000000000600e30 0x0000000000600e30 0x0000000000000210 0x0000000000000220 RW 200000 DYNAMIC 0x0000000000000e58 0x0000000000600e58 0x0000000000600e58 0x00000000000001a0 0x00000000000001a0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x000000000000005c 0x000000000000005c R 4 GNU_EH_FRAME 0x00000000000006d0 0x00000000004006d0 0x00000000004006d0 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e30 0x0000000000600e30 0x0000000000600e30 0x00000000000001d0 0x00000000000001d0 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .rodata .eh_frame_hdr .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.SuSE .note.gnu.build-id 06 .eh_frame_hdr 07 08 .ctors .dtors .jcr .dynamic .got 下面的段序号和上面程序头里的段一一对应。
type中只有load类型的segment是需要被映射的
三、ELF文件结构
一个ELF文件由以下三部分组成:
1、ELF头
ELF头(ELF header)位于文件的开始位置,它的主要目的是定位文件的其他部分。
typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf32_Half e_type; /* Object file type */ Elf32_Half e_machine; /* Architecture */ Elf32_Word e_version; /* Object file version */ Elf32_Addr e_entry; /* Entry point virtual address */可执行目标文件的入口虚拟地址 Elf32_Off e_phoff; /* Program header table file offset */ Elf32_Off e_shoff; /* Section header table file offset */ Elf32_Word e_flags; /* Processor-specific flags */ Elf32_Half e_ehsize; /* ELF header size in bytes */ Elf32_Half e_phentsize; /* Program header table entry size */ Elf32_Half e_phnum; /* Program header table entry count */ Elf32_Half e_shentsize; /* Section header table entry size */ Elf32_Half e_shnum; /* Section header table entry count */ Elf32_Half e_shstrndx; /* Section header string table index */ } Elf32_Ehdr;比较重要的成员有:e_ident(ELF文件幻数)、e_machine(比如可执行文件ET_EXEC)、e_entry(程序入口虚拟地址)等等,下面会在加载过程分析中结合源码分析各成员作用。
可通过readelf -h 读取ELF 头信息,包括文件信息
2、程序头表(Program header table)
列举了所有有效的段(segments)和他们的属性(执行视图,非可执行文件没有程序头表)。
程序头是一个结构的数组,每一个结构都表示一个段(segments)。程序头表的段内容是节头表中节(Sections)的重组表示,而非由节头表“直接组成”。它们是EL文件结构和加载机制的不同层次——程序头表服务于运行时进程构建,节头表服务于链接开发阶段。通过工具如readelf -l查看程序头表或readelf -S查看节头表,可验证这种映射(例如,readelf -l的输出会显示段包含的节)。
typedef struct elf32_phdr{ Elf32_Word p_type; /* 段类型 */ Elf32_Off p_offset; /* 该段在文件中的偏移 */ Elf32_Addr p_vaddr; /* 段在虚拟地址的起始位置 */ Elf32_Addr p_paddr; Elf32_Word p_filesz; /* segment在ELF文件中的所占空间的长度,值可能是0*/ Elf32_Word p_memsz; /* segment在进程虚拟地址空间中所占的长度,也可能是0*/ Elf32_Word p_flags; /* 权限 RWX*/ Elf32_Word p_align; /* segment的对齐属性,2^n*/ } Elf32_Phdr;程序头的索引地址(e_phoff)、段数量(e_phnum)、表项大小(e_phentsize)都是通过 ELF头部信息获取的。
可通过readelf -l 读取ELF程序头信息
3、节头表(Section header table)
包含对节(sections)的描述(链接视图,只有需要链接的文件才有)。由多个节头表描述符的数组结构组成,节头描述符包含这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。节头描述符如下:
typedef struct elf32_shdr{ Elf32_Word sh_name; //节区名是一个NULL结尾的字符串,sh_name是在.shtrtab中的偏移 Elf32_Word sh_type; //为节区类型 Elf32_Word sh_flags; //节区标志 Elf32_Addr sh_addr; //节区的第一个字节应处的位置。否则,此字段为 0。 Elf32_Off sh_offset; //此成员的取值给出节区的第一个字节与文件头之间的偏移。 Elf32_Word sh_size; //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。 Elf32_Word sh_link; //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。 Elf32_Word sh_info; //此成员给出附加信息,其解释依赖于节区类型。 Elf32_Word sh_addralign; //某些节区带有地址对齐约束. Elf32_Word sh_entsize; //给出每个表项的长度字节数。 }Elf32_Shdr;节区名存储在.shstrtab字符串表中,sh_name是表中偏移。
可通过readelf -S 或者objdump -h读取sections' header
4、section
主要section
以“.”作为前缀的节区是系统预订的,下面介绍下常见和比较重要的section:
| sh_name | sh_type | description |
|---|---|---|
| .text | SHT_PROGBITS | 代码段,包含程序的可执行指令 |
| .data | SHT_PROGBITS | 包含初始化了的数据,将出现在程序的内存映像中, |
| .bss | SHT_NOBITS | 未初始化数据,因为只有符号。如果初始化的值为0,那么将其保存在bss段(占空间的),如果没有初始化,则将其保存在common段(因为该弱符号最终所占空间是未知的),等到链接时再将其放入到BSS段(因为此时符号最终大小已经确定了)。关于第三点不同编译器行为会不同,有的编译器会把没有初始化的全局变量直接放到BSS段。 |
| .rodata | SHT_PROGBITS | 包含只读数据。字符串常量和const修饰的变量等。也有可能放在.data,编译器有差异 |
| .comment | SHT_PROGBITS | 包含版本控制信息 |
| .eh_frame | SHT_PROGBITS | 它生成描述如何unwind 堆栈的表 |
| .debug | SHT_PROGBITS | 此节区包含用于符号调试的信息 |
| .dynsym | SHT_DYNSYM | 此节区包含了动态链接符号表。使用strip时不会清除该节 |
| .dynamic | 包含可执行文件需要加载的库,动态链接信息 | |
| .shstrtab | SHT_STRTAB | 存放section名的字符串表。Section Header String Table |
| .strtab | SHT_STRTAB | 字符串表 |
| .symtab | SHT_SYMTAB | 保存所有符号,包括 .dynsym 中的符号。使用strip时会清除该节 |
| .got | SHT_PROGBITS | 全局偏移表 |
| .plt | SHT_PROGBITS | 过程链接表 |
| .rel.name | SHT_REL | 包含了重定位信息。 |
注:
1).text 代码段
可以通过objdump -d 反汇编,查看ELF文件代码段内容。
2).strtab / .shstrtab 字符串表
在ELF文件中,会用到很多字符串,比如节名,变量名等。所以ELF将所有的字符串集中放到一个表里,每一个字符串以’\0’分隔,然后使用字符串在表中的偏移来引用字符串。这样在ELF中引用字符串只需要给出一个数组下标即可。字符串表在ELF也以段的形式保存,shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符号的名字,存放的都是ASCII码。
3).symtab 符号表 & .dynsym动态符号表
在链接的过程中需要把多个不同的目标文件合并在一起,不同的目标文件相互之间会引用变量和函数。在链接过程中,我们将函数和变量统称为符号,函数名和变量名就是符号名。
每个定义的符号都有一个相应的值,叫做符号值(Symbol Value),对于变量和函数,符号值就是它们的地址。
符号表结构
typedef struct { Elf32_Word st_name; /* 符号名,字符串表中的下标 */ Elf32_Addr st_value; /* 符号值。分多种情况,1、符号位于所在段的偏移。2、符号属于common段,符号值代表对齐属性。3、在可执行文件中代表运行时虚拟地址 */ Elf32_Word st_size; /* 符号大小:数据类型大小,函数所占的大小*/ unsigned char st_info; /* 符号类型(变量、数组、函数、段、文件名)和绑定信息(局部、全局、弱引用) */ unsigned char st_other; Elf32_Half st_shndx; /* 符号定义在本目标文件中,表示符号所在段,其他特殊值,未初始化变量或者符号未定义(0)*/ } Elf32_Sym;可以使用下面命令查看:
1、readelf -s xxx.o
直接解析 ELF 文件结构,显示静态符号表(.symtab)和动态符号表(.dynsym)。
2、objdump -t
显示目标文件或可执行文件的符号表(主要是 .symtab)。
3、nm
专门用于显示符号表,默认同时显示静态和动态符号(除非用-D仅显示动态符号)
4).eh_frame / .eh_frame_hdr
在调试程序的时候经常需要进行堆栈回溯,早期使用通用寄存器(ebp)来保存每层函数调用的栈帧地址,但局限性很大。后来现代Linux操作系统在LSB(Linux Standard Base)标准中定义了一个.eh_frame section,用来描述如何去unwind the stack。gcc编译器默认打开,如果不想把.eh_frame section编入elf文件,可以通过gcc选项 -fno-asynchronous-unwind-tables 去除。
GAS(GCC Assembler)汇编编译器定义了一组伪指令来协助eh_frame生成调用栈信息CFI(Call Frame Information)。具体原理在后续《栈回溯》章节分析,这里不再阐述。
5)重定位表(.rel.name)
链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据中中那些绝对地址引用的位置。对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。比如”.rel.text”就是针对”.text”的重定位表,”.rel.data”就是针对”.data”的重定位表。
GOT是全局偏移表( Global Offset Table),用于存储外部符号地址;PLT是程序链接表(Procedure Link Table),用于存储记录定位信息的额外代码。
制作一个新的段
可以用 objcopy 命令将一个普通二进制文件,如图片,多媒体之类的东西作为目标文件中的一个段。制作方法如下:
[beyes@beyes ELF]$ objcopy -I binary -O elf32-i386 -B i386 mypic.jpg temp.o
[beyes@beyes ELF]$ objdump -t temp.o
temp.o: file format elf32-i386
SYMBOL TABLE:
00000000 l d .data 00000000 .data
00000000 g .data 00000000 _binary_mypic_jpg_start
00011855 g .data 00000000 _binary_mypic_jpg_end
00011855 g *ABS* 00000000 _binary_mypic_jpg_size
objcopy 命令用于将目标文件的的部分或全部内容拷贝到另一个目标文件中,并可以实现格式的变换。
上面的 objcopy 命令中:
-I 选项表示指定输入的目标文件格式。
-O 选项表示指定输出的目标文件格式。
-B 选项指定要欲转入目标文件的输入文件的适用平台。这里指定 i386 表示该图片格式适用于 i386 平台。但由于 objcopy 所使用的 BFD 库中已经识别了 i386 平台,所以这里不用 -B 选项也是可以的。
在使用 objcopy 将图片文件转换进目标文件 temp.o 中后,默认会用 _binary_objfile_start 和 _binary_objfile_end 来标识被转换文件在内存中的起始地址,结束地址;以及使用 _binary_objfile_size 来表示大小。
自定义段
通常,在 gcc 编译出来的目标文件中,代码一般是放到 .text 段,全局变量和静态变量被放到 .data 和 .bss 段。这只是默认行为,我们有时可能希望变量或某些代码放在一个自定义的段中去以实现某些特定功能,比如为了满足某些硬件的内存和 I/O 地址布局,或者像 Linux 内核中用来完成一些初始化和用户控件复制时出现的错误异常等。GCC 提供了一个扩展机制,使得我们可以将变量放在我们自定义的段中。如下代码示例:
__attribute__((section("myvarsection"))) int global_var = 18;
__attribute__((section("myfuncsection"))) void hello(void);
四、静态链接过程
1、 空间与地址分配。
将各个目标文件的相似段合并
2、符号解析与重定位,调整代码中的地址
符号地址的确定
当源码被编译成目标文件时,编译器不知道外部引用的变量和函数地址,编译器使用临时的假地址;链接器根据符号的地址对每个需要重定位的指令(内容)进行地址修正。
链接器如何知道哪些指令需要被调整? 答案是重定位表,在elf文件中往往是一个或多个段(比如”.rel.text”就是针对”.text”的重定位表,”.rel.data”就是针对”.data”的重定位表)。对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。
objdump -r a.o 查看目标文件的重定位表,如下
[344259@yanfa209_centos7-jk128:latest test]$ objdump -r elf.o
elf.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:(表示该重定位段是代码段)
OFFSET TYPE VALUE(重定位入口在该段中的偏移)
0000000000000006 R_X86_64_PC32 global_BB-0x0000000000000008
0000000000000010 R_X86_64_PC32 global_CC-0x0000000000000008
000000000000001e R_X86_64_PC32 func1-0x0000000000000004
0000000000000023 R_X86_64_32 testweak
000000000000002d R_X86_64_PC32 testweak-0x0000000000000004
0000000000000032 R_X86_64_32 .rodata
0000000000000037 R_X86_64_PC32 puts-0x0000000000000004
0000000000000048 R_X86_64_PC32 .data
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0000000000000040 R_X86_64_PC32 .text+0x0000000000000042
重定位表的结构是一个Elf32_Rel结构的数组,每个数组元素对应一个重定位入口。Elf32_Rel的定义如下
typedef struct { Elf32_Addr r_offset; /* 重定位入口偏移量,即该符号所作用的节加上该偏移量即为待重定位的地址 */ Elf32_Word r_info; /* 重定位入口的类型和符号,第8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表中的数组下标 */ } Elf32_Rel; 重定位类型: R_386_PC32 这种重定位类型对应着指令的相对寻址,叫做相对寻址修正,计算方式为 S + A - P R_386_32 这种重定位类型对应着指令的绝对寻址,叫做绝对寻址修正,计算方式为 S + A A : 保存在被修正位置的值,即待修正位置保存的值,绝对寻址修正时为0 P :被修正的位置(相对于节开始的偏移量或虚拟地址),可以通过 r_offset 计算得到 S :符号的实际地址,由 r_info 的高24位指定的符号的实际地址强符号弱符号
一、什么是强符号 什么是弱符号
强符号:函数和初始化(包括初始化为0)的全局变量
弱符号:未初始化的全局变量是弱符号(C++并没有将未初始化的全局符号视为弱符号)
显式定义:我们也可以通过GCC的"__attribute__((weak))"来定义任何一个强符号为弱符号。注意,强符号和弱符号都是针对定义来说的,不是针对符号的引用
void __attribute__((weak)) func(void) { } or __attribute__((weak)) void func(void) {} or void func(void)__attribute__((weak)) {}二、链接规则
1.【多个强符号】同名的强符号只能有一个,否则编译器报"重复定义"错误。
2.【一个强符号,多个弱符号】允许一个强符号和多个弱符号,但定义会选择强符号的
3.【多个弱符号】当有多个弱符号相同时,链接器选择最先出现那个,也就是与链接顺序有关(或者选占用空间较大的的符号,至少不会造成诸如溢出、越界等严重后果)。
4.【仅声明弱符号】可以编译通过,且值为0。
三、使用场景
1)提供默认实现(库中使用弱符号)
弱符号常用于库中提供可被用户覆盖的默认实现。用户可自定义同名强符号来替换库的默认行为。
2) 插件或模块化设计(调用方使用弱符号)
弱符号允许可选依赖。如果某模块未链接,弱符号指向空实现,避免链接错误。
强引用和弱引用
编译器默认所有的变量和函数为强引用,同时编程者可以使用__attribute__((weakref))来声明一个函数。如果该函数没有被定义,则该函数指针为0。
/* 定义 test_weakref() 函数弱引用 test_weak_ref() 函数, 调用test_weakref时弱引用test_weak_ref 必须加上static */ static void test_weakref(void) __attribute__ ((weakref("test_weak_ref")));1.强引用,在未定义该强引用的实现时,编译会报错误:未定义的引用
2.弱引用允许定义一个未实现(未实例化)的对象,这在编译的时候会将该对象处理成NULL,编译器并不会报错。通过使用弱引用可以实现后期优化代码的功能。而避免改动使用该函数的地方。使用弱函数可以实现类似“钩子(hook)"函数的功能。
注意:C标准里根本没有提到强、弱符号。这只是GCC这个实现定义的特性,在MS C编译器里是不存在这个概念的。
五、分析工具elf格式文件的工具
objdump工具
objdump -h 打印关键的section信息(长度,所在文件位置)
objdump -s 打印关键的section的具体内容
objdump -d 反汇编
objdump -p 可执行文件 | grep NEEDED 查看可执行文件的依赖动态库
objdump -t 查看符号表
readelf工具
readelf -h 读取elf头
readelf -l 读取Program Header Table(查看段信息,其实是多个section组成,可重定向文件没有这个表头)
readelf -S 读取Section Header Table(每个section在文件中的位置,大小)
readelf -s 读取符号表
readelf -a 打印所有信息
addr2line -e <filename> <addr0> <addr1> <addr2> //显示addr地址对应的代码行
size <可执行文件> // 查看elf文件代码段数据段bss段的长度
strings <可执行文件> | grep 字符串 //用于分析二进制文件,如可执行文件、库文件或核心转储文件,以查找其中包含的文本字符串。这对于调试、逆向工程或安全分析非常有用。
nm <可执行文件> 打印符号表运行时地址
依赖的动态库信息
ldd(是一个脚本) 查看可执行文件的依赖动态库
objdump -p 可执行文件 | grep NEEDED
已运行程序的链接动态库
pmap pid
pldd pid
也可以查看进程下面的smap 或者 maps文件
六、gdb调试需要哪些段
| GDB 能力 | 需要的段 | 典型file输出 |
|---|---|---|
| 行号、局部变量、类型、宏(源码级) | .debug_info+.debug_line+ 配套 | with debug_info |
函数名断点、函数级bt、全局变量 | .symtab/.dynsym | not stripped |
仅地址级bt(无任何符号) | .eh_frame | stripped但能看地址 |