今天我们介绍一种常见的文件格式:ELF
文件格式,标准称谓叫做可执行和可链接格式(Executable and Linkable Format
)。在维基百科中这样描述:
在计算机科学中,ELF文件是一种用于可执行文件、目标文件、共享库和核心转储(
core dump
)的标准文件格式。其中核心转储是指:操作系统在进程收到某些信号而终止时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试。
一般来说由汇编器和链接器生成的文件都属于ELF
文件。通常我们接触的ELF
文件主要有以下四类:
excutable file
):经过编译链接后,可以直接加载到内存中执行的二进制程序。relocatable file
) :可重定位文件即目标文件和静态库文件,是源文件编译后但未完成链接的半成品,被用于与其他目标文件合并链接,以构建出二进制可执行文件。shared object file
):一种特殊的可重定位目标文件,可以在加载或者运行时被ld-linux.so.x
动态的加载进内存并链接。Core dump file
):当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些信息转储到核心转储文件。总之,ELF
文件是一种文件格式。但凡是一种格式,总要有一些规则,下面我们来介绍ELF
文件的格式规则。
一个典型的ELF
文件宏观上包括 ELF Header
、Program Header Table
、Sections/Segments
和 Section Header Table
四部分。其作用简要描述如下:
ELF Header:用来描述整个文件的结构。
Program Header Table:描述文件中的各种Segments
。
Sections/Segments:Sections
是从链接角度描述ELF
,Segments
是从执行角度描述ELF
。
Section Header Table:包含了文件节区的信息,如大小,偏移。
ELF
文件结构分布如下图所示:
图1 ELF文件结构
程序中的段(Segment
)和节(Section
)是真正的程序体;段包括代码段和数据段等,段是由节组成的,多个节经过链接后被合并为一个段。
注意:段(
Segment
)与节(Section
)的区别。很多地方对两者有所混淆。段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。
段和节的信息通过Header
进行描述,程序头是Program Header
,节头是Section Header
;段和节的大小和数量都是不固定的,需要用专门的数据结构来描述,即程序头表(Program Header Table
)和节头表(Section Header Table
),这是两个数组,元素分别是程序头(Program Header
)和节头(Section Header
);程序头表(Program Header Table
)中的元素全是程序头(Program Header
),节头表(Section Header Table
)中的元素全是节头(Section Header
);程序头表是用来描述段(Segment
)的,称为段头表,段是程序本身的组成部分。
由于段和节的大小和数量都是不固定的,程序头表和节头表的大小也不固定,两个表在程序文件中的位置也不固定;需要在一个固定的位置,用一个固定大小的数据结构来描述程序头表和节头表的大小和位置信息,即位于文件最开始部分的ELF Header
。
【拓展】:ELF文件有两种视图形式:链接视图和执行视图
图2 链接视图和执行视图
链接视图:可以理解为目标文件的内容视图。静态链接器(即编译后参与生成最终
ELF
过程的链接器,如ld
)会以链接视图解析ELF
。编译时生成的.o
(目标文件)以及链接后的.so
(共享库)均可通过链接视图解析,链接视图可以没有段表(如目标文件不会有段表)。链接器只关心ELF Header, Sections
和Section Header Table
这 3 部分内容。Program Header Table
在汇编和链接过程中没有用到,所以是可有可无的。执行视图:可以理解为目标文件的内存视图。动态链接器(即加载器,如x86架构 linux下的 /lib/ld-linux.so.2或者安卓系统下的 /system/linker均为动态链接器)会以执行视图解析ELF并动态链接,执行视图可以没有节表。加载器只关心
ELF Header, Program Header Table
和Segments
这 3 部分内容。而Section Header Table
在加载过程中没有用到,所以是可有可无的。总结:链接视图是以节(
Section
)为单位,执行视图是以段(Segment
)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。实际上,在链接阶段,我们可以忽略Program Header Table
来处理此文件,在运行阶段可以忽略Section Header Table
来处理此程序(所以很多加固手段删除了Section Header Table
)。或者说链接视图是给链接编辑器(静态链接器)看的,执行视图是给程序解析器(动态连接器)看的。【为什么需要两种视图】
当
ELF
文件被加载到内存后,系统会将多个具有相同权限的Section
合并成一个Segment
。操作系统通常是以页为基本单位来管理内存分配,一般页的大小为4KB
。同时,内存的权限管理粒度也是以页为单位,页内的内存是具有同样的权限属性。ELF
文件被映射时,是以系统的页长度为单位,每个Section
在映射时的长度都是系统页长度的整数倍,若Section
的长度不是其整数倍,则多余部分会占一个页,然而一个ELF
文件具有多个Section
,会导致内存浪费严重,而将多个Section
合并后会以段为基准,减少了页面内部碎片,节省里空间,提高了内存利用率。
每个ELF
文件都存在一个ELF Header
用来描述其结构和组成。ELF
文件的最开始就是ELF文件头(ELF Header
),其中包含了描述整个文件的基本属性;ELF
文件分为文件头和文件体两部分;先用ELF Header
从文件全局概要出程序中程序头表、节头表的位置和大小等信息,然后从程序头表和节头表中分别解析出各个段和节的位置和大小等信息,程序头表对于可执行文件是必须的,而对于可重定位文件是可选的。
ELF Header
其实对应的是一个结构体,定义在include/uapi/linux/elf.h
中,分为32
位和64
位两种版本,分别为Elf32_Ehdr
和Elf_64_Ehdr
,内容如下:
#define EI_NIDENT 16
typedef struct elf32_hdr {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
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;
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
上述结构体的各成员意义概括如下:
成员 | 大小/字节(32位机器) | 大小/Z字节(64位机器) | 说明 |
---|---|---|---|
e_ident | 16 | 16 | 表示ELF字符等信息,开头四个字节是固定不变的ELF文件魔数,0x7f 0x45 0x4c 0x46;可用于确认文件类型是否正确; |
e_type | 2 | 2 | ELF目标文件的类型;表示该文件属于可执行文件、可重定位文件、core dump文件或者共享库; |
e_machine | 2 | 2 | ELF目标文件的体系结构类型,即要在哪种硬件平台运行; |
e_version | 4 | 4 | ELF文件的版本号,通常都是1; |
e_entry | 4 | 8 | 操作系统运行该程序时,程序的入口虚拟地址; |
e_phoff | 4 | 8 | 程序头表(Program Header Table)在文件内的字节偏移量(从文件开头开始算起的偏移量);如果没有程序头表,该值为0; |
e_shoff | 4 | 8 | 节头表(Section Header Table)在文件内的字节偏移量(从文件开头开始算起的偏移量);如果没有节头表,该值为0; |
e_flags | 4 | 4 | 与处理器相关的标志; |
e_ehsize | 2 | 2 | ELF Header的大小; |
e_phentsize | 2 | 2 | 程序头表(Program Header Table)中每个条目(entry)的大小,即每个用来描述段信息的数据结构的大小(struct Elf32_Phdr或者struct Elf64_Phdr); |
e_phnum | 2 | 2 | 程序头表中条目的数量,即段的个数; |
e_shentsize | 2 | 2 | 节头表(Section Header Table)中每个条目(entry)的大小,即每个用来描述节信息的数据结构的大小(struct Elf32_Shdr或者struct Elf64_Shdr); |
e_shnum | 2 | 2 | 节头表中条目的数量,即节的个数; |
e_shstrndx | 2 | 2 | 节头字符串表(Section Header String Table)在节头表(Section Header Table)中的索引; |
上表给出了结构体成员的概括性描述,但是个别结构体成员需要特别说明,如下:
16
字节大小的数组,用来表示ELF
字符等信息,开头四个字节是固定不变的ELF
文件魔数,对应四个宏,分别为ELFMAG0、ELFMAG1、ELFMAG2、ELFMAG3;其宏值分别为0x7f
数值和E
、L
、F
字符;根据ASCII
编码表可知E
、L
、F
字符对应的十六进制数值分别为0x45
、0x4c
、0x46
。因此,通过readelf
读取ELF
文件时,开头的四个字节始终为0x7f 0x45 0x4c 0x46
。e_ident数组下标宏 | 下标范围 | 说明 |
---|---|---|
e_ident[EI_MAG0~EI_MAG3] | 0~3 | EI_MAG0、EI_MAG1、EI_MAG2、EI_MAG3为e_ident的数组下标,对应的值为0,1,2,3;这四个字节为固定的ELF文件魔数,用于识别ELF文件类型是否正确;操作系统在加载可执行文件时会确认魔数是否正确,如果不正确则拒绝加载; |
e_ident[EI_CLASS] | 4 | 表示ELF文件的Class/体系结构,0:不可识别类型;1:32位架构;2:64位架构; |
e_ident[EI_DATA] | 5 | 该字节指定ELF的编码格式,即文件是大端或小端的,0:非法编码格式;1:小端LSB;2:大端MSB; |
e_ident[EI_VERSION] | 6 | 该字节规范ELF头的版本信息,默认为1,0:非法版本;1:当前版本; |
e_ident[EI_OSABI] | 7 | 指明 ELF 文件操作系统的二进制接口的版本标识符,值为 0:指明 UNIX System V ABI; |
e_ident[EI_PAD] | 8 | 标记e_ident中未使用字节的开始。这些字节被保留并设置为零;读取对象文件的程序应该忽略它们。如果当前未使用的字节被赋予意义,EI_PAD 的值将在将来发生变化; |
注意:第八个字节为EI_OSABI,即e_ident[EI_OSABI],全部类型定义如下:
#define ELFOSABI_NONE 0 /* UNIX System V ABI */
#define ELFOSABI_LINUX 3 /* Object uses GNU ELF extensions. */
e_type:2字节,用来指定ELF
目标文件的类型;全部类型定义如下:
//文件路径:include/uapi/linux/elf.h
/* These constants define the different elf file types */
#define ET_NONE 0 //未知目标文件格式
#define ET_REL 1 //可重定位文件
#define ET_EXEC 2 //可执行文件
#define ET_DYN 3 //动态共享目标文件
#define ET_CORE 4 //core文件,程序崩溃时内存映像的转储格式
#define ET_LOPROC 0xff00 //特定处理器文件的扩展下界
#define ET_HIPROC 0xffff //特定处理器文件的扩展上界
e_machine:2字节,用来描述ELF
目标文件的体系结构类型,即要在哪种硬件平台运行;全部平台结构类型定义如下:
//文件路径:include/uapi/linux/elf-em.h
/* These constants define the various ELF target machines */
#define EM_NONE 0
#define EM_M32 1
#define EM_SPARC 2
#define EM_386 3
#define EM_68K 4
#define EM_88K 5
#define EM_486 6 /* Perhaps disused */
#define EM_860 7
#define EM_MIPS 8 /* MIPS R3000 (officially, big-endian only) */
/* Next two are historical and binaries and
modules of these types will be rejected by
Linux. */
#define EM_MIPS_RS3_LE 10 /* MIPS R3000 little-endian */
#define EM_MIPS_RS4_BE 10 /* MIPS R4000 big-endian */
#define EM_PARISC 15 /* HPPA */
#define EM_SPARC32PLUS 18 /* Sun's "v8plus" */
#define EM_PPC 20 /* PowerPC */
#define EM_PPC64 21 /* PowerPC64 */
#define EM_SPU 23 /* Cell BE SPU */
#define EM_ARM 40 /* ARM 32 bit */
...
#define EM_IA_64 50 /* HP/Intel IA-64 */
#define EM_X86_64 62 /* AMD x86-64 */
...
#define EM_AARCH64 183 /* ARM 64 bit */
#define EM_TILEPRO 188 /* Tilera TILEPro */
#define EM_MICROBLAZE 189 /* Xilinx MicroBlaze */
#define EM_TILEGX 191 /* Tilera TILE-Gx */
#define EM_ARCV2 195 /* ARCv2 Cores */
#define EM_RISCV 243 /* RISC-V */
#define EM_BPF 247 /* Linux BPF - in-kernel virtual machine */
#define EM_CSKY 252 /* C-SKY */
#define EM_LOONGARCH 258 /* LoongArch */
#define EM_FRV 0x5441 /* Fujitsu FR-V */
e_version:4字节,用来标识ELF
文件的版本号。EV_CURRENT
是一个动态的数字,表示最新的版本。尽管当前最新的版本号就是“1”,但如果以后有更新的版本的话,EV_CURRENT
将被更新为更大的数字,而目前的“1”将成为历史版本。全部版本定义如下:
#define EV_NONE 0 /* e_version, EI_VERSION */
#define EV_CURRENT 1
#define EV_NUM 2
readelf
和xxd
查看ARM 64
位平台上的结果,显示程序的ELF
文件头:
# 1、显示 elf 格式的目标文件的信息
[root@localhost 8.3.1]# readelf -h nfp.ko
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 35305512 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 65
Section header string table index: 64
# 2、xxd查看文件对应的十六进制形式
[root@localhost 8.3.1]# xxd -u -a -g 1 -s 0 -l 0x100 nfp.ko
00000000: 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010: 01 00 B7 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 00 00 00 00 28 B8 1A 02 00 00 00 00 ........(.......
00000030: 00 00 00 00 40 00 00 00 00 00 40 00 41 00 40 00 ....@.....@.A.@.
00000040: 04 00 00 00 14 00 00 00 03 00 00 00 47 4E 55 00 ............GNU.
00000050: BB 3B E2 7D 74 42 4F 1D 0D 72 C7 85 34 10 34 C0 .;.}tBO..r..4.4.
00000060: D6 55 7D F7 00 00 00 00 FD 7B BE A9 FD 03 00 91 .U}......{......
00000070: F3 53 01 A9 F4 03 00 AA F3 03 01 AA E0 03 1E AA .S..............
00000080: 00 00 00 94 80 22 40 B9 62 22 40 B9 1F 00 02 6B ....."@.b"@....k
00000090: A0 00 00 54 00 00 02 4B F3 53 41 A9 FD 7B C2 A8 ...T...K.SA..{..
000000a0: C0 03 5F D6 61 26 40 B9 80 26 40 B9 F3 53 41 A9 .._.a&@..&@..SA.
000000b0: 00 00 01 4B FD 7B C2 A8 C0 03 5F D6 1F 20 03 D5 ...K.{...._.. ..
000000c0: FD 7B BF A9 FD 03 00 91 E0 03 1E AA 00 00 00 94 .{..............
000000d0: FD 7B C1 A8 C0 03 5F D6 FD 7B BD A9 FD 03 00 91 .{...._..{......
000000e0: F3 53 01 A9 F5 13 00 F9 F5 03 00 AA F3 03 02 AA .S..............
000000f0: E0 03 1E AA F4 03 01 AA 00 00 00 94 E0 03 15 AA ................
readelf -h
显示的信息,就是 ELF header
中描述的所有内容。且上述内容与结构体 Elf64_Ehdr
中的成员变量是一一对应的!
细心的你可能发现显示结果第 15
行显示的内容:Size of this header: 64 (bytes)
,表明ELF header
部分的内容,一共是 64
个字节。可以通过od
命令将字节码显示出来,如下:
[root@localhost 8.3.1]# od -Ax -t x1 -N 64 nfp.ko
000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
000010 01 00 b7 00 01 00 00 00 00 00 00 00 00 00 00 00
000020 00 00 00 00 00 00 00 00 28 b8 1a 02 00 00 00 00
000030 00 00 00 00 40 00 00 00 00 00 40 00 41 00 40 00
000040
说明:
-Ax: 显示地址的时候,用十六进制来表示。如果使用 -Ad,意思就是用十进制来显示地址;
-t -x1: 显示字节码内容的时候,使用十六进制(x),每次显示一个字节(1);
-N 64:只需要读取 64 个字节;
这 64
个字节的内容,可以对照上面的结构体Elf64_Ehdr
中每个字段来解释,为方便记忆,制作图片如下:
图4 ELF Header 16-31字节解析
图5 ELF Header 32-47字节解析
图6 ELF Header 48-63字节解析
另外,对照上面的Elf64_Ehdr
结构体中每个字段,详细解释如下:
1、00000000~0000000F
是e_ident
数组,16个字节
【00000000~00000003】
:这4个字节处是固定的ELF
魔数:0x7f, 0x45, 0x4c, 0x46;表示0x7f E L F
;
【00000004】
:该位置处的值02
表示文件是64
位的ELF
文件;
【00000005】
:该位置处的值01
表示文件是小端字节序;
【00000006】
:该位置处的值01
表示默认版本;
【00000007】
:该位置处的值00
表示UNIX System V ABI;
【00000008~0000000F】
:这8个字节为保留位,全部置为0。
2、00000010~00000011
是e_type,2个字节
0x01 0x00
,因为是小端字节序,故0x01 0x00
组合后的值为0x0001
,即低字节处的值放置于低位,对应十进制值为1
,即ET_REL
;表示类型是可重定位文件。3、00000012~00000013
是e_machine,2个字节
0xB7 0x00
,因为是小端字节序,故0xB7 0x00
组合后的值为0x00B7
,即低字节处的值放置于低位,对应十进制值为183
,即EM_AARCH64
;表示类型是Arm 64
平台。4、00000014~00000017
是e_version,4个字节
0x01 0x00 0x00 0x00
,因为是小端字节序,故0x01 0x00 0x00 0x00
组合后的值为0x00000001
,即低字节处的值放置于低位,对应十进制值为1
,即ELF
文件的版本号为1
;此字段(4 字节)指明目标文件的版本。5、00000018~0000001F
是e_entry,8个字节(64位ELF文件该字段是8字节)
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
,组合后的值为0x00
,对应的十进制值为0
,表示程序的虚拟入口地址。即当文件被加载到进程空间里后,入口程序在进程地址空间里的地址。对于可执行程序文件来说,当 ELF
文件完成加载之后,程序将从这里开始运行;而对于其它文件来说,这个值应该是 0
。6、00000020~00000027
是e_phoff,8个字节(64位ELF文件该字段是8字节)
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
,组合后的值为0x00
,对应的十进制值为0
,表示程序头表开始处在文件中的偏移量,即相对于 ELF
文件初始位置的偏移量;程序头表又称为段头表,上面介绍过 ELF
的执行试图中涉及到若干的段,而程序头表包含这些段的一个总览的信息。如果没有程序头表,该值应设为 0
。7、00000028~0000002F
是shoff,8个字节(64位ELF文件该字段是8字节)
【00000028~0000002F】:该位置处的值为0x28 0xB8 0x1A 0x02 0x00 0x00 0x00 0x00
,因为是小端字节序,故0x28 0xB8 0x1A 0x02 0x00 0x00 0x00 0x00
组合后的值为0x00000000021AB828
,对应的十进制值为35305512
,表示节头表在文件内的偏移量,即相对于ELF
文件初始位置的偏移量;
8、00000030~00000033
是e_flags,4个字节
【00000030~00000033】:该位置处的值为0x00 0x00 0x00 0x00
,故组合后的值为0x00
,对应的十进制值为0
。此字段(4
字节)含有处理器特定的标志位。对于 Intel
架构的处理器来说,它没有定义任何标志位,所以 e_flags
应该值为 0
。
9、00000034~00000035
是e_ehsize,2个字节
【00000034~00000035】:该位置处的值为0x40 0x00
,故组合后的值为0x40
,对应十进制值为64
。此字段(2
字节)表明 ELF
文件头的大小,以字节为单位。分析文件时注意进制转换。
10、00000036~00000037
是e_phentsize,2个字节
【00000036~00000037】:该位置处的值为0x00 0x00
,故组合后的值为0x00
,对应十进制值为0
。此字段(2
字节)表明在程序头表中每一个表项的大小(即每个用来描述段信息的数据结构的大小),以字节为单位。在 ELF
文件的其他数据结构中也有相同的定义方式,如果一个结构由若干相同的子结构组成,则这些子结构就称为入口。因这里nfp.ko
为目标文件,不可直接执行,故以其链接视图解析,程序头表可为0
。
11、00000038~00000039
是e_phnum,2个字节
【00000038~00000039】:该位置处的值为0x00 0x00
,故组合后的值为0x00
,对应十进制值为0
。此字段(2
字节)表明程序头表中总共有多少个表项。如果一个目标文件中没有程序头表,该值应设为 0
。
12、0000003A~0000003B
是e_shentsize,2个字节
【0000003A~0000003B】:该位置处的值为0x40 0x00
,故组合后的值为0x40
,对应十进制值为64
。此字段(2
字节)表明在节头表中每一个表项的大小(即每个用来描述节信息的数据结构的大小),以字节为单位。
13、0000003C~0000003D
是e_shnum,2个字节
【0000003C~0000003D】:该位置处的值为0x41 0x00
,故组合后的值为0x41
,对应十进制值为65
。此字段(2
字节)表明节头表中总共有多少个表项。如果一个目标文件中没有节头表,该值应设为 0
。
14、0000003E~0000003F
是e_shstrndx,2个字节
【0000003E~0000003F】:该位置处的值为0x40 0x00
,故组合后的值为0x40
,对应十进制值为64
,表示节区头部字符串表在节头表中的索引为64
。此字段(2 字节)表明节区头部字符串表在节头表中的索引。如果文件没有节头字符串表,此值应设置为 SHN_UNDEF(Section Header Name-Undefinition)
。
【引入该数据成员的原因】:节头表中每个表项的大小是固定的,但它们的名称不可能一样长,如果将节的名字分别存入每个节头表中,节头表过短可能发生名字的失真,节头表过长就可能会出现大量地址未被利用,所以需要将每个节头表的名字统一存到一个专门存储节头表名字的一个节中,即为
.shstrtab
节,而e_shstrndx
就表明了.shstrtab
节在节头表的索引值,也就是.shstrtab
节在节头表中的位置。如下所示即为.shstrtab在节头表中的索引值:图7 .shstrtab在节头表中的索引
Program Header Table
,即程序头表(也称为段表)是一个描述文件中各个段的数组,程序头表描述了文件中各个段在文件中的偏移位置及段的属性等信息;从程序头表里可以得到每个段的所有信息,包括代码段和数据段等;各个段的内容紧跟Program Header Table
保存;程序头表中各个段用Elf32_Phdr
或Elf64_Phdr
结构体表示,定义在include/uapi/linux/elf.h
中,分为32
位和64
位两种版本,分别为Elf32_Phdr
和Elf64_Phdr
,内容如下:
typedef struct elf32_phdr {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;
Elf32_Phdr
或Elf64_Phdr
结构体用来描述位于磁盘上的程序中的一个段;其成员描述总结如下:
成员 | 大小(32位) | 大小(64位) | 说明 |
---|---|---|---|
p_type | 4 | 4 | 程序中该段的类型; |
p_offset | 4 | 8 | 本段在文件内的起始偏移; |
p_vaddr | 4 | 8 | 本段在内存中的起始虚拟地址; |
p_paddr | 4 | 8 | 用于与物理地址相关的系统中; |
p_filesz | 4 | 8 | 本段在文件中的大小; |
p_memsz | 4 | 8 | 本段在内存中的大小; |
p_flags | 4 | 4 | 与本段相关的标志; |
p_align | 4 | 8 | 本段在文件和内存中的对齐方式;如果为0或1,表示不对齐,否则应该为2的幂次数; |
上表给出了结构体成员的概括性描述,但是个别结构体成员需要特别说明,如下:
p_type:4字节,用来指明程序中该段的类型;
// 文件路径:include/uapi/linux/elf.h
/* These constants are for the segment types stored in the image headers */
#define PT_NULL 0 //忽略
#define PT_LOAD 1 //可加载程序段
#define PT_DYNAMIC 2 //动态链接信息
#define PT_INTERP 3 //动态加载器的路径名称
#define PT_NOTE 4 //辅助的附加信息
#define PT_SHLIB 5 //保留
#define PT_PHDR 6 //程序头表
#define PT_TLS 7 /* Thread local storage segment */
#define PT_LOOS 0x60000000 /* OS-specific */
#define PT_HIOS 0x6fffffff /* OS-specific */
#define PT_LOPROC 0x70000000
#define PT_HIPROC 0x7fffffff
#define PT_GNU_RELRO (PT_LOOS + 0x474e552)
p_flags:4字节,用来指明与本段相关的标志;
// 文件路径:include/uapi/linux/elf.h
/* These constants define the permissions on sections in the program
header, p_flags. */
#define PF_R 0x4
#define PF_W 0x2
#define PF_X 0x1
在ELF
文件中,数据和代码分开存放的,这样可以按照其功能属性分成一些区域,比如程序、数据、符号表等。这些分离存放的区域在ELF
文件中反映成section
。ELF文件中典型的section
如下:
.text节
.text
节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr
,则.text
节就会存在于text
段中。由于.text
节保存了程序代码,所以节类型为SHT_PROGBITS
。
.rodata节
rodata
节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata
节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text
段(不是data
段)中找到.rodata
节。由于.rodata
节是只读的,所以节类型为SHT_PROGBITS
。
.comment节
存放编译器版本信息,如字符串 "GCC: (GNU) 8.3.1"。
.debug节(调试符号表)
存放调试信息的一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs
),程序中定义和引用的全局变量,以及原始的 C
源文件。(只有带 -g
编译时才会产生)。
.plt节(过程链接表)
.plt
节也称为过程链接表(Procedure Linkage Table),其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt
节保存了代码,所以节类型为SHT_PROGBITS
。
.data节
.data
节存在于data
段中,其保存了初始化的全局变量等数据。由于.data
节保存了程序的变量数据,所以节类型为SHT_PROGBITS
。
.bss节
本节中包含目标文件中未初始化的全局变量。一般情况下,可执行程序在开始运行的时候,系统会把这一段内容清零。但是,在运行期间的 bss 段是由系统初始化而成的,在目标文件中.bss 节并不包含任何内容,其长度为 0,所以它的节类型为 SHT_NOBITS
。
.got节(全局偏移表)
.got
节保存了全局偏移表。.got
节和.plt
节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got
节与程序执行有关,所以节类型为SHT_PROGBITS
。
.got.plt
实际上其本质是从.got表中拆除来的一部分,当开启延迟绑定(Lazy Binding)时,会将plt表中的长跳转(函数)的重定位信息单独放到此表中,以满足后续实际的延迟绑定。
.dynamic节
本节里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
.dynsym节(动态链接符号表)
.dynsym
节保存在text
段中。其保存了从共享库导入的动态符号表,节类型为SHT_DYNSYM
。
.dynstr节(动态链接字符串表)
.dynstr
保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。
*.rel.节(重定位表)
重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF
目标文件的某部分或者进程镜像进行补充或修改,由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL
。
.hash节
.hash
节也称为.gnu.hash
,其保存了一个用于查找符号的散列表。
.symtab节(符号表)
.symtab
节是一个ElfN_Sym(N为32或64)
的数组,保存了符号信息,节类型为SHT_SYMTAB
。
.strtab节(字符串表)
.strtab
节是一个符号字符串表,保存的内容包括所有符号的名称,表中的内容会被.symtab
的ElfN_Sym
结构中的st_name
引用,节类型为SHT_STRTAB
。
.shstrtab(节区头部符串表)
本节是“节区头部符串表(节头名字符串表)”,其内容包括所有 section
的名称。
.ctors节和.dtors节
ctors
(构造器)节和.dtors
(析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main
函数执行之前需要执行的代码;析构函数是在main
函数之后需要执行的代码。
.interp节
本节中保存了可执行目标文件所需要的动态链接器的路径。如: /lib64/ld-linux-x86-64.so.2
,Linux
的可执行文件加载时会去寻找可执行文件所需的动态链接器。
.init节
此节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进入主函数之前执行这一节中的代码。
.fini节
此节包含进程终止时要执行的程序指令。当程序正常退出时,系统会执行这一节中的代码。
注意:以点号”.”为前缀的节名字是为系统保留的。应用程序也可以构造自己的段,但最好不要取与上述系统已定义的节相同的名字,也不要取以点号开头的名字,以避免潜在的冲突。另外,目标文件中节的名字并不具有唯一性,可以存在多个相同名字的节。
节头表中各个节用Elf32_Shdr
或Elf64_Shdr
结构体表示,定义在include/uapi/linux/elf.h
中,分为32
位和64
位两种版本,内容如下:
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
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;
typedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
Elf32_Shdr
或Elf64_Shdr
结构体用来描述位于磁盘上的程序中的一个节;其成员描述总结如下:
成员 | 大小(32位) | 大小(64位) | 说明 |
---|---|---|---|
sh_name | 4 | 4 | 是一个索引值,在.shstrtable(section header string table,包含section name的字符串表,也是一个section)中的索引。ELF文件头里面专门有一个字e_shstrndx,其含义就是.shstrtable对应的section header在section header table中的索引。 |
sh_type | 4 | 4 | 节的类型; |
sh_flags | 4 | 8 | 节的标志; |
sh_addr | 4 | 8 | 节在内存中的起始地址,指定节映射到虚拟地址空间中的位置;如果该节可以被加载,则sh_addr为该节被加载后在进程地址空间中的虚拟地址;否则sh_addr为0 |
sh_offset | 4 | 8 | 节在文件中的起始位置; |
sh_size | 4 | 8 | 节的大小; |
sh_link | 4 | 4 | 引用另一个节头表项,根据节类型有不同的解释; |
sh_info | 4 | 4 | 节的附加信息,与sh_link联合使用; |
sh_addralign | 4 | 8 | 节数据在内存中的对齐方式;sh_entsize指定节头中各Entry的长度,各Entry长度要相同; |
sh_entsize | 4 | 8 | 节Entry大小; |
上表给出了结构体成员的概括性描述,但是个别结构体成员需要特别说明,如下:
sh_name:节名称sh_name的值是节名字符串的一个索引,节名称字符串以’\0’结尾,字符串统一存放在.shstrtab表中,使用sh_name的值作为节区头部字符串表的索引,找到对应的字符串即为节名称;字符串表中包含多个以’\0’结尾的字符串;在目标文件中,这些字符串通常是符号的名字或节的名字,需要引用某些字符串时,只需要提供该字符串在节区头部字符串表中的序号即可;节区头部字符串表中的第一个字符串(序号为0)是空串,即’\0’,可以用于表示没有名字或一个空的名字;如果节区头部字符串表为空,节头中的sh_size值为0。
注意:节区头部字符串表是给节区头部表专门准备的字符串表,ELF文件中通常存在两个字符串表:
* 一个是代码中所有使用到的字符串的表,名称为.strtab
* 一个是记录所有节区名的字符串表,名称为.shstrtab
二者通常是没有关系的,节区头部字符串表中只记录节区名称,如下:
[root@localhost 8.3.1]# readelf -p .shstrtab nfp.ko
String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .note.gnu.build-id
[ 2e] .rela.text
[ 39] .rela.text.unlikely
[ 4d] .rela.init.text
[ 5d] .rela.exit.text
[ 6d] .rela__ksymtab
[ 7c] .rela__kcrctab
[ 8b] .rela.rodata
[ 98] .rela.altinstructions
[ ae] .altinstr_replacement
[ c4] .rodata.str
[ d0] .modinfo
[ d9] .rodata.str1.8
[ e8] .rela__param
[ f5] .rela__mcount_loc
[ 107] __ksymtab_strings
[ 119] .rela.eh_frame
[ 128] __versions
[ 133] .rela__jump_table
[ 145] .rela.data
[ 150] .rela__bug_table
[ 161] .rela__verbose
[ 170] .data.once
[ 17b] .data..read_mostly
[ 18e] .rela.gnu.linkonce.this_module
[ 1ad] .init.plt
[ 1b7] .text.ftrace_trampoline
[ 1cf] .bss
[ 1d4] .comment
[ 1dd] .note.GNU-stack
[ 1ed] .rela.debug_aranges
[ 201] .rela.debug_info
[ 212] .debug_abbrev
[ 220] .rela.debug_line
[ 231] .rela.debug_frame
[ 243] .debug_str
[ 24e] .rela.debug_loc
[ 25e] .rela.debug_ranges而字符串表则记录符号表中符号相关的字符串信息,如:
[root@localhost 8.3.1]# readelf -p .strtab nfp.ko | head -n 10
String dump of section '.strtab':
[ 1] nfp6000_pcie.c
[ 10] $x
[ 13] bar_cmp
[ 1b] nfp6000_area_cleanup
[ 30] nfp6000_explicit_get
[ 45] nfp6000_explicit_put
[ 5a] nfp6000_explicit_do
[ 6e] $d
#省略...
sh_type:节的类型;
// 文件路径:include/uapi/linux/elf.h
/* sh_type */
#define SHT_NULL 0 //标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。
#define SHT_PROGBITS 1 //此节区包含程序定义的信息,其格式和含义都由程序来解释。
#define SHT_SYMTAB 2 //此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。
#define SHT_STRTAB 3 //此节区包含字符串表。目标文件可能包含多个字符串表节区。
#define SHT_RELA 4 //此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。
#define SHT_HASH 5 //此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。
#define SHT_DYNAMIC 6 //此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。
#define SHT_NOTE 7 //此节区包含以某种方式来标记文件的信息。
#define SHT_NOBITS 8 //这种类型的节区不占用文件中的空间,其他方面和SHT_PROGBITS相似。尽管此节区不包含任何字节,表示该节在文件中没有内容。如.bss节。
#define SHT_REL 9 //此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
#define SHT_SHLIB 10 //此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。
#define SHT_DYNSYM 11 //作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
#define SHT_NUM 12
#define SHT_LOPROC 0x70000000 //这一段(包括两个边界),是保留给处理器专用语义的。
#define SHT_HIPROC 0x7fffffff //这一段(包括两个边界),是保留给处理器专用语义的。
#define SHT_LOUSER 0x80000000 //此值给出保留给应用程序的索引下界。
#define SHT_HIUSER 0xffffffff //此值给出保留给应用程序的索引上界。
sh_flags:节的标志;标志着此节区是否可以修改,是否可以执行,具体定义如下:
// 文件路径:include/uapi/linux/elf.h
/* sh_flags */
#define SHF_WRITE 0x1 //表示该节在进程空间中可写
#define SHF_ALLOC 0x2 //表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。
#define SHF_EXECINSTR 0x4 //表示该节在进程空间中可以被执行
#define SHF_RELA_LIVEPATCH 0x00100000
#define SHF_RO_AFTER_INIT 0x00200000
#define SHF_MASKPROC 0xf0000000 //所有被此值所覆盖的位都是保留做特殊处理器扩展用的。
sh_link与sh_info:如果节的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表等,则sh_link
、sh_info
两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该节所使用的字符串表在节头表中的下标 | 0 |
SHT_HASH | 该节所使用的符号表在节头表中的下标 | 0 |
SHT_REL | 该节所使用的相应符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_RELA | 该节所使用的相应符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_SYMTAB | 操作系统相关 | 操作系统相关 |
SHT_DYNSYM | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
Linux
系统中常用目标文件处理工具有:readelf
、od
、xxd
以及 objdump
。为了进一步加深对ELF
文件整体结构的理解,我们使用一个可重定位的nfp.ko
文件来解释说明前文所讲。
俗话讲得好:“工欲善其事,必先利其器”;让我们首先了解一下上述处理ELF
文件的工具的用法。
功能:用于显示ELF
格式文件的信息。
描述:readelf
命令用来显示一个或者多个elf格式的目标文件的信息,可以通过它的选项来控制显示哪些信息。这里的elf-file(s)
就表示那些被检查的文件。可以支持32位,64位的elf格式文件,也支持包含elf
文件的文档(这里一般指的是使用ar
命令将一些elf文件打包之后生成的例如lib*.a
之类的“静态库”文件)。
这个程序和objdump
提供的功能类似,但是它显示的信息更为具体,并且它不依赖BFD
库(BFD
库是一个GNU
项目,它的目标就是希望通过一种统一的接口来处理不同的目标文件),所以即使BFD
库有什么bug
存在的话也不会影响到readelf
程序。
运行readelf
的时候,除了-v
和-H
之外,其它的选项必须有一个被指定的ELF
文件。
选项:
-a --all 相当于:-h -l -S -S -r -d -V -A -IA -I
-h --file-header 显示ELF文件头
-l --program-headers 显示程序头
--segments --program-headers的别名
-S --section-headers 显示节头
--sections --section-headers的别名
-g --section-groups 显示节组
-t --section-details 显示节详细信息
-e --headers 相当于: -h -l -S
-s --syms 显示符号表
--symbols --syms的别名
--dyn-syms 显示动态符号表
-n --notes 显示核心注释(如果存在)
-r --relocs 显示重定位(如果存在)
-u --unwind 显示unwind节信息(如果存在)
-d --dynamic 显示动态节(如果存在)
-V --version-info 显示版本节(如果存在)
-A --arch-specific 显示特定于架构的信息(如果有的话)
-c --archive-index 显示存档中的符号/文件索引
-D --use-dynamic 在显示符号时使用动态section信息
-x --hex-dump=
将section 的内容转储为字节
-p --string-dump=
将section 的内容转储为字符串
-R --relocated-dump=
将section 的内容转储为重新定位的字节
-z --decompress 在转储之前解压缩节
-w[lLiaprmfFsoRtUuTgAckK] or
--debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
=frames-interp,=str,=loc,=Ranges,=pubtypes,
=gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
=addr,=cu_index,=links,=follow-links]
显示DWARF调试节的内容
--dwarf-depth=N 不显示深度为N或更大的DIEs
--dwarf-start=N 显示以N开头的DIEs, 在相同或更深的深度
-I --histogram 显示bucket list长度的直方图
-W --wide 允许输出宽度超过80个字符
-U --unicode=[locale|escape|hex|highlight|invalid]
显示由当前语言环境(默认)确定的unicode字符,转义序列,"",突出显示转义序列,或将其视为无效并显示为"{hex序列}"
@ 从读取选项
-H --help 显示帮助信息
-v --version 显示readelf的版本号
拓展:DWARF的全称是"Debugging With Attributed Record Formats(带属性记录格式的调试)",遵从GNU FDL授权。现在已经有dwarf1,dwarf2,dwarf3三个版本。
功能:以八进制和其他格式转储文件。
描述:od
命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或 ASCII
编码字符方式显示,通常用于显示或查看文件中不能直接显示在终端的字符。od
命令系统默认的显示方式是八进制,名称源于Octal Dump
。
常见的文件为文本文件和二进制文件。od
命令主要用来查看保存在二进制文件中的值,按照指定格式解释文件中的数据并输出,不管是 IEEE754
格式的浮点数还是 ASCII
码,od
命令都能按照需求输出它们的值。
选项:
-A, --address-radix=RADIX
在输出中以何种格式来表示地址偏移,其中RADIX的可选值有[doxn]。d表示Decimal,o表示Octal,x表示Hex,n表示None
-j, --skip-bytes=BYTES
表示跳过开头的BYTES个字节
-N, --read-bytes=BYTES
表示只dump BYTES个字节
-S BYTES, --strings[=BYTES]
输出长度不小于指定字节数的字符串
-w[BYTES], --width[=BYTES]
设置每行显示的字节数,od默认每行显示32字节
-t, --format=TYPE
指定输出格式,格式包括a、c、d、f、o、u和x,各含义如下:
1) a: named character, ignoring high-order bit
2) c: select printable characters or backslash escapes
3) d[SIZE]:十进制,正负数都包含,SIZE字节组成一个十进制整数;
4) f[SIZE]:浮点,SIZE字节组成一个浮点数;
5) o[SIZE]:八进制,SIZE字节组成一个八进制数;
6) u[SIZE]:无符号十进制,只包含正数,SIZE字节组成一个无符号十进制整数;
7) x[SIZE]:十六进制,SIZE字节为单位以十六进制输出,即输出时一列包含SIZE字节
此外,我们还有一些简写的方式来输出相应格式:
-a same as -t a, select named characters, ignoring high-order bit
-b same as -t o1, select octal bytes
-c same as -t c, select printable characters or backslash escapes
-d same as -t u2, select unsigned decimal 2-byte units
-f same as -t fF, select floats
-i same as -t dI, select decimal ints
-l same as -t dL, select decimal longs
-o same as -t o2, select octal 2-byte units
-s same as -t d2, select decimal 2-byte units
-x same as -t x2, select hexadecimal 2-byte units
功能:用于以十六进制形式显示文件内容,亦可以将十六进制内容转换回原始二进制的形式。
描述:XXD创建给定文件或标准输入的十六进制转储。它还可以将十六进制转储转换回其原始二进制形式。与uuencode(1)和uudcode(1)一样,它允许以“邮件安全”的ASCII表示传输二进制数据,但具有解码为标准输出的优点。此外,它还可以用于执行二进制文件修补。
选项:
-a 切换自动换行,默认关闭
-b 转换为二进制格式,默认为十六进制
-c 每行显示几个字节,默认为16
-g 指定几个字节为1组,默认为2
-i 转换为C语言include头文件格式
-l 指定显示前几个字节
-p 转换为纯十六进制格式
-r 将十六进制转换成二进制
-s 指定从第几个字节开始转换,正数表示跳过前几个字节,负数表示只显示后几个字节
-u 使用大写十六进制字母
功能:显示目标文件的二进制信息。
描述:objdump是GNU Binutils工具集中的一员,用于显示目标文件的内容。它可以反汇编二进制文件,显示二进制指令、符号表、调试信息等,为程序员和系统开发者提供了深入分析和调试的能力。
选项:
-a
--archive-header 假如任何一个objfile是库文件,则显示相应的header信息(输出格式类似于ls -l命令)。除了可以列出 ar tv所能展示的信息,objdump -a还可以显示lib文件中每一个对象文件的格式。
--adjust-vma=offset 当使用objdump命令来dump信息的时候,将section addresses都加上offset。此选项主要用于section addresses与符号表不对应的情形下,比如我们需要将相应的section放到某一个特殊的地址处时。
-b bfdname
--target=bfdname 为obj文件指定对象码(object-code)格式。本选项是非必需的,objdump命令可以自动的识别许多种格式。
例如:
objdump -b oasys -m vax -h fu.o
上面的命令用于fu.o的头部摘要信息,并明确指出了fu.o这个对象文件是vax平台上由oasys编译器编译而来。
我们可以使用-i选项来列出所支持的所有平台格式
-C
--demangle[=style] 将底层(low-level)的符号名解码成用户级(user-level)的名称。除了会去掉由系统添加的头部下划线之外,还使得C++的函数名以便于理解的方式显示出来。
-g
--debugging 用于显示调试信息。这会使得objdump命令尝试解析STABS和IEEE格式的调试信息,然后以C语言语法格式将相应的调试信息进行输出。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。
-e
--debugging-tags 类似于-g选项。但是产生的输出信息格式兼容ctags工具。
-d
--disassemble 从objfile中对机器指令进行反汇编。本选项只对那些包含指令的section进行反汇编。
-D
--disassemble-all 类似于-d,但是本选项会对所有的sections进行反汇编,而不仅仅是那些包含指令的sections。本选项会微妙的影响代码段的反汇编。当使用-d选项的时候,objdump会假设代码中出现的所有symbols都在对应的boundary范围之内,并且不会跨boundary来进行反汇编;而当使用-D选项时,则并不会有这样的假设。这就意味着-d与-D选项在反汇编时,可能输出结果会有些不同,比如当数据存放在代码段的情况下。
--prefix-addresses 反汇编的时候,显示每一行的完整地址。这是一种比较老的反汇编格式
-EB
-EL
--endian={big|little} 指定目标文件的大小端。这仅仅会影响到反汇编。这在对某一些并未指定大小端信息的obj文件进行反汇编时很有用,比如S-records。
-f
--file-headers 显示每一个obj文件的整体头部摘要信息
-F
--file-offsets 当在对sections进行反汇编时,无论是否显示相应的symbol,都会显示其在文件内的偏移(offset)。
-h
--section-headers 显示obj文件各个sections的头部摘要信息。
--headers obj文件中segments可能会被relocate,比如在ld时通过使用-Ttext、-Tdata或者-Tbss选项。然而,有一些
对象文件格式,比如a.out,其本身并没有保存起始地址信息。在这种情况下,尽管ld可以正确的对这些sections
进行relocate,但是使用objdump -h来查看各sections的头部摘要信息时则不能正确的显示地址信息。
-H
--help objdump的帮助信息
-i
--info 显示objdump所支持的所有arch以及obj格式。-m和-b选项可用到这
-j name
--section=name 仅仅显示指定名称为name的section的信息
-l
--line-numbers 用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用时有效。通常要求具有调试信息,即编译时使用了-g之类的选项。
-m machine
--architecture=machine 指定反汇编目标文件时使用的架构。当待反汇编的目标文件其本身并没有包含arch信息时(如S-records文件),我们就可以使用此选项来进行指定。我们可以使用objdump -i来列出所支持的arch。
-p
--private-headers 显示objfile文件格式的专属信息。具体的输出取决于object file的格式,对于某一些格式,可能并没有一些额外的信息输出。
-r
--reloc 显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来
-R
--dynamic-reloc 显示文件的动态重定位入口,仅仅对于动态目标文件有意义,比如某些共享库。
-s
--full-contents 显示指定section的所有内容。默认情况下,对于所有非空section都会显示。
-S
--source 将反汇编代码与源代码交叉显示。通常在调试版本能够较好的显示尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
--show-raw-insn 在进行反汇编时,显示每条汇编指令对应的机器码。默认情况下会显示,除非使用了--prefix-addresses。
--no-show-raw-insn 反汇编时,不显示汇编指令的机器码。当使用了--prefix-addresses时,默认就不会显示机器码。
--start-address=address从指定地址开始显示数据,该选项影响-d、-r和-s选项的输出。
--stop-address=address 显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出。
-t
--syms 显示文件的符号表入口。类似于nm -s提供的信息
-T
--dynamic-syms 显示文件的动态符号表入口,仅仅对动态目标文件有意义,比如某些共享库。它显示的信息类似于 nm -D(--dynamic)显示的信息。
-V
--version 打印objdump的版本信息
-x
--all-headers 显示所有可用的header信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。
-z
--disassemble-zeroes 一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编。
@file
可以将选项集中到一个文件中,然后使用这个@file选项载入。
查看可重定位目标文件(ELF-64
格式)的ELF Header
的内容:
[root@localhost 8.3.1]# readelf -h nfp.ko
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 35305512 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 65
Section header string table index: 64
具体解析见前面ELF Header
节,此处不再赘述。
查看可重定位目标文件(ELF-64
格式)的Program Header Table
:
[root@localhost 8.3.1]# readelf -l nfp.ko
There are no program headers in this file.
上述结果证明relocatable
文件没有程序头部表;一般只有动态库或可执行文件拥有程序头部表。
因此,这里通过一个测试程序test.c
来进行说明,源码如下:
//文件名:test.c
#include
int g_val_1;
int g_val_2 = 16;
const int g_val_3 = 2;
int main()
{
printf("g_val_1=%x, g_val_2=%x\n", g_val_1, g_val_2);
return 0;
}
编译生成可执行文件test
,查看可执行目标文件test
的Program Header Table
:
[root@localhost 1010]# readelf -lW test
Elf file type is EXEC (Executable file)
Entry point 0x4004b0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R 0x8
INTERP 0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000798 0x000798 R E 0x200000
LOAD 0x000e00 0x0000000000600e00 0x0000000000600e00 0x000228 0x000230 RW 0x200000
DYNAMIC 0x000e10 0x0000000000600e10 0x0000000000600e10 0x0001d0 0x0001d0 RW 0x8
NOTE 0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x000674 0x0000000000400674 0x0000000000400674 0x00003c 0x00003c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x000e00 0x0000000000600e00 0x0000000000600e00 0x000200 0x000200 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .dynamic .got
Program Header Table
布局如下图所示:
上述内容解析如下:
ELF-64 目标文件格式中,Program Header Table Entry
对应的结构体名称为Elf64_Phdr
,定义在include/uapi/linux/elf.h
头文件中。
Entry point 0x4004b0
表示程序所执行的第一条指令的虚拟地址为 0x4004b0
。
There are 9 program headers, starting at offset 64
,表示 Program Header Table
中有 9
个条目,其起始位置在可执行目标文件中的偏移量为 64
字节(可执行目标文件的前 64
字节被 ELF Header
占用)。
Type
是 Program Header Table Entry
结构体中的第 1
个字段,占用 4
字节,表示 Segment
的类型。常用可选项:
0
(PT_NULL
,未使用的条目);
1
(PT_LOAD
,可加载的 segment
);
2
(PT_DYNAMIC
,动态链接信息);
3
(PT_INTERP
,动态链接器的路径名称);
4
(PT_NOTE
,辅助性信息);
5
(PT_SHLIB
,保留);
6
(PT_PHDR
,Program header table
);
0x6474e552
(PT_GNU_RELRO
,重定位后访问权限变为只读)。
Flags
是 Program Header Table Entry
结构体中的第 2
个字段,占用 4
字节,表示 Segment
的属性。常用可选项:0x1
(PF_X
,可执行权限);0x2
(PF_W
,可写权限);0x4
(PF_R
,可读权限)。
Offset
是 Program Header Table Entry
结构体中的第 3
个字段,占用 8
字节,表示该 Segment
的起始位置在可执行目标文件中的偏移量(字节)。
VirtAddr
是 Program Header Table Entry
结构体中的第 4
个字段,占用 8
字节,表示该 Segment
的虚拟地址,即该 Segment
中第一个字节在进程虚拟地址空间的虚拟地址。
PhysAddr
是 Program Header Table Entry
结构体中的第 5
个字段,占用 8
字节,为物理寻址的系统保留。
FileSiz
是 Program Header Table Entry
结构体中的第 6
个字段,占用 8
字节,表示该 Segment
在可执行目标文件中的大小(字节)。
MemSiz
是 Program Header Table Entry
结构体中的第 7
个字段,占用 8
字节,表示该 Segment
在内存中的大小(字节)。
Align
是 Program Header Table Entry
结构体中的第 8
个字段,占用 8
字节,表示该 Segment
的内存对齐要求。该字段的值必须是 2
的幂。上面打印结果中该字段的值是十六进制的(可以用objdump -p test
的输出结果进行佐证)。
注意:可以看到这个可执行文件中共有9个Segments,每个段(Segment)包含多少个节应该是根据sections的地址排序确定的,从装载的角度看,我们只关心两个“LOAD”型的Segment,因为只有它是需要被映射的,其他诸如“NOTE”,"GNU_STACK"都是在装载时起辅助作用的。下面的0到8分别对应着上面的一个Segment,两个LOAD类型的Segment分别对应着02和03,可以看到每个LOAD类型的Segment里面都包含了许多的sections。
ELF将相同或者相似属性的section合并为一个Segment并映射到一个VMA中,是为了减少页面内部碎片,以节省内存空间的使用。因为在有了虚拟存储机制以后,装载的时候采用页映射的方式。Intel系列的处理器,页尺寸最小是4096个字节,也就是4KB。当写的程序很小的时候,每个section可能只有几十或者几百个字节,如果每个section都占用一个页的话,对内存的浪费是海量的。所以在将目标文件链接成可执行文件的时候,链接器会尽量把相同或相似权限属性的section分配在同一空间,在程序头表中,将一个或多个属性类似的section合并为一个Segment,然后在装载的时候,将这个Segment映射到进程虚拟地址空间中的一个VMA中。
上述Segments含义如下:
INTERP: 此表项的offset实际上指向.interp节的内容,也就是动态链接器的全路径的字符串,如/lib64/ld-linux-x86-64.so.2;
LOAD: 代表可加载的segment,只有此类型的Segment在运行时会载入内存;
DYNAMIC:对于动态链接的二进制,此Segment记录动态链接器信息段(.dynamic)的指针;
NOTE: 记录辅助信息,如对于core dump文件,此Segment包含core dump文件创建时的进程状态信息,如导致crash的signal,pending & held signals, 进程/父进程UID,nice,寄存器值(包括当前pc)等;
GNU_STACK: 此段并没有任何内容,但其属性用来代表当前进程的stack是否可执行;
GNU_RELRO: 此段代表重定位之后哪些段需要设置为RO的(若.got.plt没有在这里,代表其最后不受到RO保护,因为延迟绑定,是正常的,解决方法是关闭延迟绑定);
GNU_EH_FRAME: 保存栈帧的unwind信息,若存在此段通常指向.eh_fram_hdr(eh = Exception Handling);
TLS: 代表线程局部存储的信息。
验证过程:
1) 程序入口点
查看可执行目标文件test
的.text section
的内容:
[root@localhost 1010]# objdump -d test
test: file format elf64-x86-64
# 省略...
Disassembly of section .text:
00000000004004b0 <_start>:
4004b0: f3 0f 1e fa endbr64
4004b4: 31 ed xor %ebp,%ebp
4004b6: 49 89 d1 mov %rdx,%r9
4004b9: 5e pop %rsi
4004ba: 48 89 e2 mov %rsp,%rdx
4004bd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
4004c1: 50 push %rax
4004c2: 54 push %rsp
4004c3: 49 c7 c0 30 06 40 00 mov $0x400630,%r8
4004ca: 48 c7 c1 c0 05 40 00 mov $0x4005c0,%rcx
4004d1: 48 c7 c7 96 05 40 00 mov $0x400596,%rdi
4004d8: ff 15 0a 0b 20 00 callq *0x200b0a(%rip) # 600fe8 <__libc_start_main@GLIBC_2.2.5>
4004de: f4 hlt
# 省略...
从上面结果中可以看出,程序入口点即为.text section
的第一条指令。
程序执行时,程序入口点的地址仍为 0x4004b0
,如下所示:
[root@localhost 1010]# gdb -q ./test
Reading symbols from ./test...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x40059a
Starting program: /home/lxy/1010/test
Temporary breakpoint 1, 0x000000000040059a in main ()
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-236.el8.x86_64
(gdb)
(gdb) disas 0x4004b0
Dump of assembler code for function _start:
0x00000000004004b0 <+0>: endbr64
0x00000000004004b4 <+4>: xor %ebp,%ebp
0x00000000004004b6 <+6>: mov %rdx,%r9
0x00000000004004b9 <+9>: pop %rsi
0x00000000004004ba <+10>: mov %rsp,%rdx
0x00000000004004bd <+13>: and $0xfffffffffffffff0,%rsp
0x00000000004004c1 <+17>: push %rax
0x00000000004004c2 <+18>: push %rsp
0x00000000004004c3 <+19>: mov $0x400630,%r8
0x00000000004004ca <+26>: mov $0x4005c0,%rcx
0x00000000004004d1 <+33>: mov $0x400596,%rdi
0x00000000004004d8 <+40>: callq *0x200b0a(%rip) # 0x600fe8
0x00000000004004de <+46>: hlt
End of assembler dump.
查看可重定位目标文件(ELF-64
格式)的Section Header Table
的内容:
[root@localhost 8.3.1]# readelf -S -W nfp.ko
There are 65 section headers, starting at offset 0x21ab828:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000000000 000040 000024 00 A 0 0 4
[ 2] .text PROGBITS 0000000000000000 000068 04be60 00 AX 0 0 8
[ 3] .rela.text RELA 0000000000000000 130f858 035460 18 I 62 2 8
[ 4] .text.unlikely PROGBITS 0000000000000000 04bec8 0000bc 00 AX 0 0 4
[ 5] .rela.text.unlikely RELA 0000000000000000 1344cb8 000240 18 I 62 4 8
[ 6] .init.text PROGBITS 0000000000000000 04bf84 0001ac 00 AX 0 0 4
[ 7] .rela.init.text RELA 0000000000000000 1344ef8 000420 18 I 62 6 8
[ 8] .exit.text PROGBITS 0000000000000000 04c130 00003c 00 AX 0 0 4
[ 9] .rela.exit.text RELA 0000000000000000 1345318 0000f0 18 I 62 8 8
[10] __ksymtab PROGBITS 0000000000000000 04c170 000480 00 A 0 0 8
[11] .rela__ksymtab RELA 0000000000000000 1345408 000d80 18 I 62 10 8
[12] __kcrctab PROGBITS 0000000000000000 04c5f0 000120 00 A 0 0 1
[13] .rela__kcrctab RELA 0000000000000000 1346188 0006c0 18 I 62 12 8
[14] .rodata PROGBITS 0000000000000000 04c710 006618 00 A 0 0 8
[15] .rela.rodata RELA 0000000000000000 1346848 002c40 18 I 62 14 8
[16] .altinstructions PROGBITS 0000000000000000 052d28 000d80 00 A 0 0 1
[17] .rela.altinstructions RELA 0000000000000000 1349488 003600 18 I 62 16 8
[18] .altinstr_replacement PROGBITS 0000000000000000 053aa8 000754 00 A 0 0 4
[19] .rodata.str PROGBITS 0000000000000000 0541fc 0015cb 01 AMS 0 0 1
[20] .modinfo PROGBITS 0000000000000000 0557c8 000c32 00 A 0 0 8
[21] .rodata.str1.8 PROGBITS 0000000000000000 056400 0080c2 01 AMS 0 0 8
[22] __param PROGBITS 0000000000000000 05e4c8 000230 00 A 0 0 8
[23] .rela__param RELA 0000000000000000 134ca88 000540 18 I 62 22 8
[24] __mcount_loc PROGBITS 0000000000000000 05e6f8 002110 00 A 0 0 8
[25] .rela__mcount_loc RELA 0000000000000000 134cfc8 006330 18 I 62 24 8
[26] __ksymtab_strings PROGBITS 0000000000000000 060808 0005a1 00 A 0 0 1
[27] .eh_frame PROGBITS 0000000000000000 060db0 014340 00 A 0 0 8
[28] .rela.eh_frame RELA 0000000000000000 13532f8 006360 18 I 62 27 8
[29] __versions PROGBITS 0000000000000000 0750f0 006240 00 A 0 0 8
[30] __jump_table PROGBITS 0000000000000000 07b330 0005e8 00 WA 0 0 8
[31] .rela__jump_table RELA 0000000000000000 1359658 0011b8 18 I 62 30 8
[32] .data PROGBITS 0000000000000000 07b918 000a48 00 WA 0 0 8
[33] .rela.data RELA 0000000000000000 135a810 000468 18 I 62 32 8
[34] __bug_table PROGBITS 0000000000000000 07c360 0006f0 00 WA 0 0 4
[35] .rela__bug_table RELA 0000000000000000 135ac78 001bc0 18 I 62 34 8
[36] __verbose PROGBITS 0000000000000000 07ca50 000690 00 WA 0 0 8
[37] .rela__verbose RELA 0000000000000000 135c838 000b40 18 I 62 36 8
[38] .data.once PROGBITS 0000000000000000 07d0e0 00000f 00 WA 0 0 1
[39] .data..read_mostly PROGBITS 0000000000000000 07d0ef 000001 00 WA 0 0 1
[40] .gnu.linkonce.this_module PROGBITS 0000000000000000 07d100 000380 00 WA 0 0 64
[41] .rela.gnu.linkonce.this_module RELA 0000000000000000 135d378 000030 18 I 62 40 8
[42] .plt NOBITS 0000000000000380 07d480 000001 00 WA 0 0 1
[43] .init.plt NOBITS 0000000000000381 07d480 000001 00 WA 0 0 1
[44] .text.ftrace_trampoline NOBITS 0000000000000382 07d480 000001 00 WA 0 0 1
[45] .bss NOBITS 0000000000000000 07d480 0000a0 00 WA 0 0 8
[46] .comment PROGBITS 0000000000000000 07d480 000bf4 01 MS 0 0 1
[47] .note.GNU-stack PROGBITS 0000000000000000 07e074 000000 00 0 0 1
[48] .debug_aranges PROGBITS 0000000000000000 07e074 000cc0 00 0 0 1
[49] .rela.debug_aranges RELA 0000000000000000 135d3a8 000cc0 18 I 62 48 8
[50] .debug_info PROGBITS 0000000000000000 07ed34 a2862c 00 0 0 1
[51] .rela.debug_info RELA 0000000000000000 135e068 e165d0 18 I 62 50 8
[52] .debug_abbrev PROGBITS 0000000000000000 aa7360 033121 00 0 0 1
[53] .debug_line PROGBITS 0000000000000000 ada481 09f8bd 00 0 0 1
[54] .rela.debug_line RELA 0000000000000000 2174638 000660 18 I 62 53 8
[55] .debug_frame PROGBITS 0000000000000000 b79d40 0165d0 00 0 0 8
[56] .rela.debug_frame RELA 0000000000000000 2174c98 00c6c0 18 I 62 55 8
[57] .debug_str PROGBITS 0000000000000000 b90310 5cfb1e 01 MS 0 0 1
[58] .debug_loc PROGBITS 0000000000000000 115fe2e 123a2c 00 0 0 1
[59] .rela.debug_loc RELA 0000000000000000 2181358 022d28 18 I 62 58 8
[60] .debug_ranges PROGBITS 0000000000000000 128385a 0717e0 00 0 0 1
[61] .rela.debug_ranges RELA 0000000000000000 21a4080 007530 18 I 62 60 8
[62] .symtab SYMTAB 0000000000000000 12f5040 0108c0 18 63 1860 8
[63] .strtab STRTAB 0000000000000000 1305900 009f55 00 0 0 1
[64] .shstrtab STRTAB 0000000000000000 21ab5b0 000271 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
上述表头字段解析如下:
ELF-64 目标文件格式中,Section Header Table
对应的结构体名称为Elf64_Shdr
,定义在include/uapi/linux/elf.h
头文件中。
There are 65 section headers, starting at offset 0x21ab828
,表示 section header table
中有 65
个条目,section header table
的起始位置在目标文件中的偏移量为 0x21ab828
字节。也就是说,section header table
的内容从第 0x21ab829
字节(包括该字节)开始。
Name
是 Section Header
结构体中的第 1
个字段,占用 4
字节,表示该 section
名称的起始位置在.shstrtab section
中的偏移量(字节)。
Type
是 Section Header
结构体中的第 2
个字段,占用 4
字节,表示 section
类型。常用可选项:
0
(SHT_NULL
,表示无效 section
);
1
(SHT_PROGBITS
,表示该 section
包含由程序定义的信息,比如:.text
、.data sections
);
2
(SHT_SYMTAB
,表示链接器符号表);
3
(SHT_STRTAB
,表示字符串表);
4
(SHT_RELA
,表示 "Rela" 类型的重定位表);
5
(SHT_HASH
,表示符号表的哈希表);
6
(SHT_DYNAMIC
,表示动态链接信息);
7
(SHT_NOTE
,表示提示性信息);
8
(SHT_NOBITS
,表示未初始化的空间,不占用目标文件的任何空间,比如:.bss section
);
9
(SHT_REL
,表示 "Rel" 类型的重定位表);
10
(SHT_SHLIB
,保留);
11
(SHT_DYNSYM
,表示动态链接的符号表)等;
Flags
是 Section Header
结构体中的第 3
个字段,占用 8
字节,表示该 section
在进程虚拟地址空间中的属性。常用可选项:
1
(SHF_WRITE
,简写为 W
,表示该 section
在进程空间中可写);
2
(SHF_ALLOC
,简写为 A
,表示该 section
在进程空间中需要被分配空间,即执行程序时会将该 section
加载到内存中);
4
(SHF_EXECINSTR
,简写为 X
,表示该 section
在进程空间中可以被执行);
Address
是 Section Header
结构体中的第 4
个字段,占用 8
字节,表示该 section
的起始位置在被加载后所对应的在进程地址空间中的虚拟地址。0
表示该 section
不会被加载到内存中。
Offset
是 Section Header
结构体中的第 5
个字段,占用 8
字节,表示该 section
的起始位置在目标文件中偏移量(字节)。
Size
是 Section Header
结构体中的第 6
个字段,占用 8
字节,表示该 section
在目标文件中占用的空间大小(字节),除 SHT_NOBITS section
以外(SHT_NOBITS section
不占用目标文件(被存储在磁盘上)的任何空间)。
Link
是 Section Header
结构体中的第 7
个字段,占用 4
字节,表示该 section
相关联的 section
的 section
索引。比如:如果该条目表示.rela.gnu.linkonce.this_module
,该字段的值为.gnu.linkonce.this_module
的索引。
Info
是 Section Header 结构体中的第 8
个字段,占用 4
字节,表示该 section
的额外信息。
Align
是 Section Header
结构体中的第 9
个字段,占用 8
字节,表示该 section
的内存对齐要求。这个字段的值必须是 2
的幂。如果该字段的值为 0
或1
,那么表示该 section
没有内存对齐要求。
ES
是 Section Header
结构体中的第 10
个字段,占用 8
字节,表示该 section
中每个条目的大小(字节)。0
表示该 section
不包含任何条目。
拓展:系统预定义了一些节名(以
.
开头),这些节的含义解析如下:
.bss:包含程序运行时未初始化的数据(全局变量和静态变量)。当程序运行时,这些数据初始化为0。其类型为 SHT_NOBITS
,表示不占文件空间。标志为SHF_ALLOC
+SHF_WRITE
,运行时要占用内存的。.comment 包含版本控制信息(是否包含程序的注释信息?不包含,注释在预处理时已经被删除了)。类型为 SHT_PROGBITS
。.data和.data1:包含初始化的全局变量和静态变量。类型为 SHT_PROGBITS
,标志为SHF_ALLOC
+SHF_WRITE
(占用内存,可写)。.debug:包含了符号调试用的信息,我们要想用 gdb
等工具调试程序,需要该类型信息,类型为SHT_PROGBITS
。.dynamic:类型 SHT_DYNAMIC
,包含了动态链接的信息。标志SHF_ALLOC
,是否包含SHF_WRITE
和处理器有关。.dynstr: SHT_STRTAB
,包含了动态链接用的字符串,通常是和符号表中的符号关联的字符串。标志SHF_ALLOC
。.dynsym:类型 SHT_DYNSYM
,包含动态链接符号表, 标志SHF_ALLOC
。.fini:类型 SHT_PROGBITS
,程序正常结束时,要执行该section
中的指令。标志SHF_ALLOC + SHF_EXECINSTR
(占用内存可执行)。现在ELF
还包含.fini_array section
。.got:类型 SHT_PROGBITS
,全局偏移表(global offset table
),以后会重点讲。.hash:类型 SHT_HASH
,包含符号hash
表,以后细讲。标志SHF_ALLOC
。.init: SHT_PROGBITS
,程序运行时,先执行该节中的代码。SHF_ALLOC + SHF_EXECINSTR
,和.fini
对应。现在ELF
还包含.init_array section
。.interp
:SHT_PROGBITS
,该节内容是一个字符串,指定了程序解释器的路径名。如果文件中有一个可加载的segment
包含该节,属性就包含SHF_ALLOC
,否则不包含。.line: SHT_PROGBITS
,包含符号调试的行号信息,描述了源程序和机器代码的对应关系。gdb
等调试器需要此信息。.note: Note Section
, 类型SHT_NOTE
,以后单独讲。.plt:过程链接表( Procedure Linkage Table
),类型SHT_PROGBITS
,以后重点讲。.relNAME:类型 SHT_REL
, 包含重定位信息。如果文件有一个可加载的segment
包含该section
,section
属性将包含SHF_ALLOC
,否则不包含。NAME
,是应用重定位的节的名字,比如.text
的重定位信息存储在.rel.text
中。.relaname :类型 SHT_RELA
,和.rel
相同。SHT_RELA
和SHT_REL
的区别,会在讲重定位的时候说明。.rodata和.rodata1:类型 SHT_PROGBITS
, 包含只读数据,组成不可写的段。标志SHF_ALLOC
。.shstrtab:类型 SHT_STRTAB
,包含section
的名字。有读者可能会问:section header
中不是已经包含名字了吗,为什么把名字集中存放在这里?sh_name
包含的是.shstrtab
中的索引,真正的字符串存储在.shstrtab
中。那么section names
为什么要集中存储?我想是这样:如果有相同的字符串,就可以共用一块存储空间。如果字符串存在包含关系,也可以共用一块存储空间。.strtab: SHT_STRTAB
,包含字符串,通常是符号表中符号对应的变量名字。如果文件有一个可加载的segment
包含该section
,属性将包含SHF_ALLOC
。字符串以\0
结束,section
以\0
开始,也以\0
结束。一个.strtab
可以是空的,它的sh_size
将是0
。针对空字符串表的非0
索引是允许的。symtab:类型 SHT_SYMTAB
,Symbol Table
,符号表。包含了定位、重定位符号定义和引用时需要的信息。符号表是一个数组,Index 0
第一个入口,它的含义是undefined symbol index
,STN_UNDEF
。如果文件有一个可加载的segment
包含该section
,属性将包含SHF_ALLOC
。
查看可重定位目标文件的.comment section
的内容:
[root@localhost 8.3.1]# readelf -p .comment nfp.ko
String dump of section '.comment':
[ 1] GCC: (GNU) 8.3.1 20190311 (Red Hat 8.3.1-3)
[ 2e] GCC: (GNU) 8.3.1 20190311 (Red Hat 8.3.1-3)
[ 5b] GCC: (GNU) 8.3.1 20190311 (Red Hat 8.3.1-3)
#省略...
查看可重定位目标文件的.shstrtab section
的内容:
[root@localhost 8.3.1]# readelf -p .shstrtab nfp.ko
String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .note.gnu.build-id
[ 2e] .rela.text
[ 39] .rela.text.unlikely
[ 4d] .rela.init.text
[ 5d] .rela.exit.text
[ 6d] .rela__ksymtab
[ 7c] .rela__kcrctab
[ 8b] .rela.rodata
[ 98] .rela.altinstructions
[ ae] .altinstr_replacement
[ c4] .rodata.str
[ d0] .modinfo
[ d9] .rodata.str1.8
[ e8] .rela__param
[ f5] .rela__mcount_loc
[ 107] __ksymtab_strings
[ 119] .rela.eh_frame
[ 128] __versions
[ 133] .rela__jump_table
[ 145] .rela.data
[ 150] .rela__bug_table
[ 161] .rela__verbose
[ 170] .data.once
[ 17b] .data..read_mostly
[ 18e] .rela.gnu.linkonce.this_module
[ 1ad] .init.plt
[ 1b7] .text.ftrace_trampoline
[ 1cf] .bss
[ 1d4] .comment
[ 1dd] .note.GNU-stack
[ 1ed] .rela.debug_aranges
[ 201] .rela.debug_info
[ 212] .debug_abbrev
[ 220] .rela.debug_line
[ 231] .rela.debug_frame
[ 243] .debug_str
[ 24e] .rela.debug_loc
[ 25e] .rela.debug_ranges
上述内容解析如下:
[]
中的值,表示 section
名称的起始位置在.shstrtab section
中的偏移量(字节)。
.shstrtab section
的第一个字节的值固定为 0,表示一个空的或者不存在的 section
名称。
.shstrtab section
的 section
名称是以\0
(对应的 ASCII 码十六机制为 0x00
)结尾的字符串。
注意: 可重定位目标文件
nfp.ko
中,其.shstrtab section
的内容未显式地包含.eh_frame section
的名称。从表面上看,这不符合“.shstrtab section
的作用:一个字符串表,其内容包括所有section
的名称”。实际上,.shstrtab section
中隐式地包含.eh_frame section
的名称,即复用了.rela.eh_frame section
的后半部分的名称。
验证过程:
查看可重定位目标文件的.shstrtab section
的内容(十六进制):
[root@localhost 8.3.1]# readelf -x .shstrtab nfp.ko
Hex dump of section '.shstrtab':
0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
0x00000010 002e7368 73747274 6162002e 6e6f7465 ..shstrtab..note
0x00000020 2e676e75 2e627569 6c642d69 64002e72 .gnu.build-id..r
0x00000030 656c612e 74657874 002e7265 6c612e74 ela.text..rela.t
0x00000040 6578742e 756e6c69 6b656c79 002e7265 ext.unlikely..re
0x00000050 6c612e69 6e69742e 74657874 002e7265 la.init.text..re
0x00000060 6c612e65 7869742e 74657874 002e7265 la.exit.text..re
0x00000070 6c615f5f 6b73796d 74616200 2e72656c la__ksymtab..rel
0x00000080 615f5f6b 63726374 6162002e 72656c61 a__kcrctab..rela
0x00000090 2e726f64 61746100 2e72656c 612e616c .rodata..rela.al
0x000000a0 74696e73 74727563 74696f6e 73002e61 tinstructions..a
0x000000b0 6c74696e 7374725f 7265706c 6163656d ltinstr_replacem
0x000000c0 656e7400 2e726f64 6174612e 73747200 ent..rodata.str.
0x000000d0 2e6d6f64 696e666f 002e726f 64617461 .modinfo..rodata
0x000000e0 2e737472 312e3800 2e72656c 615f5f70 .str1.8..rela__p
0x000000f0 6172616d 002e7265 6c615f5f 6d636f75 aram..rela__mcou
0x00000100 6e745f6c 6f63005f 5f6b7379 6d746162 nt_loc.__ksymtab
0x00000110 5f737472 696e6773 002e7265 6c612e65 _strings..rela.e
0x00000120 685f6672 616d6500 5f5f7665 7273696f h_frame.__versio
0x00000130 6e73002e 72656c61 5f5f6a75 6d705f74 ns..rela__jump_t
0x00000140 61626c65 002e7265 6c612e64 61746100 able..rela.data.
0x00000150 2e72656c 615f5f62 75675f74 61626c65 .rela__bug_table
0x00000160 002e7265 6c615f5f 76657262 6f736500 ..rela__verbose.
0x00000170 2e646174 612e6f6e 6365002e 64617461 .data.once..data
0x00000180 2e2e7265 61645f6d 6f73746c 79002e72 ..read_mostly..r
0x00000190 656c612e 676e752e 6c696e6b 6f6e6365 ela.gnu.linkonce
0x000001a0 2e746869 735f6d6f 64756c65 002e696e .this_module..in
0x000001b0 69742e70 6c74002e 74657874 2e667472 it.plt..text.ftr
0x000001c0 6163655f 7472616d 706f6c69 6e65002e ace_trampoline..
0x000001d0 62737300 2e636f6d 6d656e74 002e6e6f bss..comment..no
0x000001e0 74652e47 4e552d73 7461636b 002e7265 te.GNU-stack..re
0x000001f0 6c612e64 65627567 5f617261 6e676573 la.debug_aranges
0x00000200 002e7265 6c612e64 65627567 5f696e66 ..rela.debug_inf
0x00000210 6f002e64 65627567 5f616262 72657600 o..debug_abbrev.
0x00000220 2e72656c 612e6465 6275675f 6c696e65 .rela.debug_line
0x00000230 002e7265 6c612e64 65627567 5f667261 ..rela.debug_fra
0x00000240 6d65002e 64656275 675f7374 72002e72 me..debug_str..r
0x00000250 656c612e 64656275 675f6c6f 63002e72 ela.debug_loc..r
0x00000260 656c612e 64656275 675f7261 6e676573 ela.debug_ranges
0x00000270 00 .
最左侧一列为地址,后面的为 .shstrtab section
中的内容。从上面结果中可以看出:
.shstrtab section
的第一个字节的值为 0x00
,即\0
。打印结果中用.
表示这个字符。8
字节的内容为:2e7379 6d746162 00
,对应的 ASCII
码依次为 .
、s
、y
、m
、t
、a
、b
、'\0'
。也就是说,.symtab section
名称的起始位置在.shstrtab section
中的偏移量为 1
(字节),正好对应其在[]
中的值。其余 sections
名称的验证过程与之类似,此处不再赘述。查看可重定位目标文件的.strtab section
的内容:
[root@localhost 8.3.1]# readelf -p .strtab nfp.ko | head -n 10
String dump of section '.strtab':
[ 1] nfp6000_pcie.c
[ 10] $x
[ 13] bar_cmp
[ 1b] nfp6000_area_cleanup
[ 30] nfp6000_explicit_get
[ 45] nfp6000_explicit_put
[ 5a] nfp6000_explicit_do
[ 6e] $d
上述内容解析如下:
[]
中的值,表示符号名称的起始位置在.strtab section
中的偏移量(字节)。.strtab section
的第一个字节的值固定为 0
,表示一个空的或者不存在的符号名称。.strtab
section 的符号名称是以\0
(对应的 ASCII
码十六机制为 0x00
)结尾的字符串。每个目标文件(包括可重定位目标文件、可共享目标文件和可执行目标文件)中都有一个符号表(.symtab section
),符号表中包含了被该目标文件定义和引用的符号的信息。
符号表(.symtab section
)是由汇编器产生的,跟编译时是否添加-g
选项无关。
符号表中的符号可以分为三类:
符号类型 | 定义者 | 可见性 | 哪些属于这类符号 |
---|---|---|---|
内部定义的全局符号(Global Symbols) | 本目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件定义的非静态函数和非静态全局变量 |
引用外部的全局符号(External Symbols) | 其他目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件引用的但定义在其他目标文件中的非静态函数和非静态全局变量 |
内部定义的局部符号(Local Symbols) | 本目标文件 | 仅本目标文件可见 | 由本目标文件定义的静态函数、静态全局变量和静态局部变量 |
注意:严格地讲,静态局部变量不仅对于其他目标文件不可见,而且对于本目标文件的其他函数也不可见。
[root@localhost 8.3.1]# readelf -s nfp.ko
Symbol table '.symtab' contains 2824 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 SECTION LOCAL DEFAULT 6
5: 0000000000000000 0 SECTION LOCAL DEFAULT 8
6: 0000000000000000 0 SECTION LOCAL DEFAULT 10
7: 0000000000000000 0 SECTION LOCAL DEFAULT 12
8: 0000000000000000 0 SECTION LOCAL DEFAULT 14
#省略...
2815: 000000000003c1c0 72 FUNC GLOBAL DEFAULT 2 nfp_flower_compile_meta
2816: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND system_wq
2817: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND disable_irq
2818: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND alloc_pages_current
2819: 0000000000015b90 1196 FUNC GLOBAL DEFAULT 2 nfp_nfd3_ctrl_poll
2820: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND flush_work
2821: 000000000001fa40 92 FUNC GLOBAL DEFAULT 2 nfp_net_rss_write_key
2822: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND devlink_port_register
2823: 00000000000266b0 152 FUNC GLOBAL DEFAULT 2 nfp_port_get_port_parent_
上述内容解析如下:
ELF-64 目标文件格式中,Symbol Table Entry
对应的结构体名称为Elf64_Sym
,定义在include/uapi/linux/elf.h
头文件中。
符号表的第一个条目被保留,并且该条目中的值必须全部为零。符号常量 STN_UNDEF
用于引用这个条目。
Num
列,表示符号表条目在符号表中的索引,索引从 0
开始。
Name
是 Symbol Table Entry
结构体中的第 1
个字段,占用 4
字节,表示符号名称的起始位置在.strtab section
中的偏移量(字节)。
Bind
和Type
是 Symbol Table Entry
结构体中的第 2
个字段,占用 1
字节(Bind
占用高四位,Type
占用低四位)。
Bind
,表示符号所关联对象的的绑定属性(即符号的作用范围)。常见可选项:
0
(STB_LOCAL
,局部符号,仅定义该符号的目标文件可见);
1
(STB_GLOBAL
,全局符号,所有目标文件可见);2
(STB_WEAK
,弱引用,全局作用域,但其优先级低于全局符号);Type
,表示符号所关联对象的类型。常见可选项:0
(STT_NOTYPE
,未指定类型,比如一个绝对符号);1
(STT_OBJECT
,表示数据对象,比如:变量、数组等);2
(STT_FUNC
,表示该符号是个函数或其他可执行代码);3
(STT_SECTION
,符号与 section
相关联,意味着该符号表示一个 section
,这种符号必须是局部符号);4
(STT_FILE
,与目标文件关联的源文件,意味着该符号表示文件名,一般都是该目标文件所对应的源文件名,该符号一定是局部符号,并且其 Ndx
列的值一定是SHN_ABS
);6
(STT_TLS
,表示该符号是线程私有存储对象);Symbol Table Entry
结构体中的第 3
个字段作为保留字段,占用 1
字节。
Ndx
是 Symbol Table Entry
结构体中的第 4
个字段,占用 2
字节,表示符号所关联对象位于哪个 section
。该值为 section
在 Section header table
中的索引,即readelf -S nfp.ko
输出结果中该 section
[Nr]
列的值(同一个 section
在不同的 .ko
文件中对应的数字不一定相同)。其他可选项:UND
(SHN_UNDEF
,0
,表示未定义符号)、ABS
(SHN_ABS
,0xfff1
,表示绝对符号,即不需要重定位的符号)、COM
(SHN_COMMON
,0xfff2
,表示未初始化的全局变量)。
Value
是 Symbol Table Entry
结构体中的第 5
个字段,占用 8
字节,表示符号所关联对象的地址。在可重定位目标文件(不包括可共享目标文件)中,如果该符号不是COMMON
,那么该值表示符号所关联对象在其 section
中的偏移量(即符号所关联对象在其 section
中的起始地址);否则,该值表示该符号的对齐属性。在可执行目标文件和可共享目标文件中,该值表示符号所关联对象的虚拟地址。
Size
是 Symbol Table Entry
结构体中的第 6
个字段,占用 8
字节,表示符号所关联对象的大小,单位:字节。如果符号没有关联的大小或大小未知,则此字段的值为 0
。如果符号关联的对象是函数,那么该字段的值为实现该函数的机器指令所占用的字节数。如果符号关联的对象是数据,那么该字段的值为数据占用的字节数。比如:符号关联的对象是一个int
类型的变量,那么该字段的值为 4
;符号关联的对象是一个int
类型,大小为 3
的数组,那么该字段的值为 12
。
另外,关于readelf
输出结果的Name
列的值,有两点需要注意:
SECTION
类型的符号,它们的符号名称(即Name
列的值)为下标为Ndx
的section
的名称。比如:上面结果中Num
为 2
的符号,其Name
列的值应该为.text
。我们可以通过命令objdump -t
看到这些类型的符号名称。readelf
输出结果中的Name
列默认只显示该符号名称的部分。我们可以通过命令readelf -s -W
(-W --wide Allow output width to exceed 80 characters
)或者objdump -t
查看这些符号的完整名称。因可重定位目标文件中不存在.interp
节,故只能以上面第2)节中的test
可执行程序进行验证。
[root@localhost 8.3.1]# readelf -p .interp nfp.ko
readelf: nfp.ko: Warning: Section '.interp' was not dumped because it does not exist!
查看可执行文件test
的.interp section
的内容:
[root@localhost 1010]# readelf -p .interp test
String dump of section '.interp':
[ 0] /lib64/ld-linux-x86-64.so.2
在可执行目标文件中,其.interp section
里面保存了可执行目标文件所需要的动态链接器的路径。
在 ARM-64 Linux
下,上面结果中的动态链接器在笔者的机器上实际指向ld-2.28.so
,如下所示:
[root@localhost 1010]# ll /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 10 Aug 15 23:11 /lib64/ld-linux-x86-64.so.2 -> ld-2.28.so
因nfp.ko
文件中无.dynsym section
,故只能查看可执行文件test
的.dynsym section
的内容:
[root@localhost 1010]# readelf --dyn-syms test
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
上述输出结果中各字段的含义与.symtab section
中的相同。这里不再赘述。
因nfp.ko
文件中无.dynamic section
,故只能查看可执行文件test
的.dynamic section
的内容:
[root@localhost 1010]# readelf -d test
Dynamic section at offset 0xe10 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x400468
0x000000000000000d (FINI) 0x400638
0x0000000000000019 (INIT_ARRAY) 0x600e00
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e08
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400348
0x0000000000000006 (SYMTAB) 0x4002b8
0x000000000000000a (STRSZ) 117 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400450
0x0000000000000007 (RELA) 0x4003f0
0x0000000000000008 (RELASZ) 96 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4003d0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4003be
0x0000000000000000 (NULL) 0x0
上述内容解析如下:
Dynamic Section Entry
对应的结构体名称为Elf64_Dyn
,定义在include/uapi/linux/elf.h
头文件中。Tag
是 Dynamic Section Entry
结构体中的第 1
个字段,占用 8
字节,表示动态条目类型。最左侧一列表示Tag
的值,中间一列为Tag
的动态条目类型,对应elf.h
中以DT_
开头的宏定义。Name/Value
是 Dynamic Section Entry
结构体中的第 2
个字段,占用 8
字节,根据Tag
类型的不同,该字段表示的含义也不同。常见选项见下表。Tag 的值 | Tag 的类型 | Name/Value 的值 | Name/Value 的含义 |
---|---|---|---|
1 | NEEDED | 所依赖的共享库名称在 .dynstr section 中的下标 | 依赖的共享库名称(仅包含直接依赖的) |
5 | STRTAB | .dynstr section 在目标文件中的偏移量(字节) | 动态链接字符串表的地址 |
6 | SYMTAB | .dynsym section 在目标文件中的偏移量(字节) | 动态链接符号表的地址 |
7 | RELA | .rela.dyn section 在目标文件中的偏移量(字节) | 动态链接重定位表(特指 rela 类型的)的地址 |
8 | RELASZ | .rela.dyn section 在目标文件中占用的空间大小(字节) | 动态链接重定位表(特指 rela 类型的)的大小 |
9 | RELAENT | .rela.dyn section 中每个条目的大小(字节) | 动态链接重定位表(特指 rela 类型的)中每个条目的大小 |
10 | STRSZ | .dynstr section 在目标文件中占用的空间大小(字节) | 动态链接字符串表的大小 |
11 | SYMENT | .dynsym section 中每个条目的大小(字节) | 动态链接符号表中每个条目的大小 |
12 | INIT | .init section 在目标文件中的偏移量(字节) | 初始化代码地址 |
13 | FINI | .fini section 在目标文件中的偏移量(字节) | 结束代码地址 |
14 | SONAME | 本共享库名称在 .dynstr section 中的下标 | 本共享对象的“SO-NAME”,即本共享库的名称 |
30 | FLAGS | 常见选项: - SYMBOLIC,符号解析从这开始; -TEXTREL,本目标文件包含 .text 的重定位表; - BIND_NOW,本目标文件不采用延迟绑定; | 加载对象的标志 |
0x6ffffffb | FLAGS_1 | 常见选项: - NOW,为本目标文件设置了 RTLD_NOW 标志位; - GLOBAL,为本目标文件设置了 RTLD_GLOBAL 标志位; | 状态标志 |
拓展:.symtab 和 .dynsym 之间的区别
Section 名称 内容 是否会被加载到内存 是否可以通过 strip 命令删除该 section .symtab 包含所有的符号 否 是 .dynsym 只包含需要动态链接的符号 是 否
上一节给出了通过readelf
直接读取Section
各节内容的方法。本节将提供通过od
命令读取Section Header Entry & Section
的方法。下面以节区头部字符串表为例进行讲解,其他节类似。
在一个 ELF
文件中,存在很多字符串,例如:Section
名称、变量名、函数名、链接器加入的符号等等,这些字符串的长度都是不固定的,因此用一个固定的结构来表示这些字符串,肯定是不现实的。人们想到一种解决办法:把这些字符串集中起来,统一放在一起,作为独立的 Section
来进行管理。于是,ELF
文件设计为存在两个字符串表:一个是代码中所有使用到的字符串的表,名称为.strtab
;另一个是记录所有节区名的字符串表,名称为.shstrtab
。
既然.shstrtab
是一个 Section
,那么在 Section header table
中,就一定有一个表项 Entry
来描述它,那么如何找到这个表项呢?
现在,咱们一起看一下这个 nfp.ko
文件中的节区头部字符串表;ELF Header
节我们讲过,可以通过readelf -h
读取ELF
文件的文件头获取节区头部字符串表在节头表(Section header table
)中的索引。
[root@localhost 8.3.1]# readelf -d nfp.ko
There is no dynamic section in this file.
[root@localhost 8.3.1]# readelf -h nfp.ko
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 35305512 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 65
Section header string table index: 64
根据最后一行Section header string table index
的值64
可知,节区头部字符串表在节头表中的索引为64
,也就是Entry 64
。这里,我们还可以用指令 readelf -S nfp.ko
来看一下这个 ELF
文件中所有的 Section
信息:
[root@localhost 8.3.1]# readelf -W -S nfp.ko
There are 65 section headers, starting at offset 0x21ab828:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000000000 000040 000024 00 A 0 0 4
[ 2] .text PROGBITS 0000000000000000 000068 04be60 00 AX 0 0 8
[ 3] .rela.text RELA 0000000000000000 130f858 035460 18 I 62 2 8
[ 4] .text.unlikely PROGBITS 0000000000000000 04bec8 0000bc 00 AX 0 0 4
[ 5] .rela.text.unlikely RELA 0000000000000000 1344cb8 000240 18 I 62 4 8
[ 6] .init.text PROGBITS 0000000000000000 04bf84 0001ac 00 AX 0 0 4
[ 7] .rela.init.text RELA 0000000000000000 1344ef8 000420 18 I 62 6 8
[ 8] .exit.text PROGBITS 0000000000000000 04c130 00003c 00 AX 0 0 4
[ 9] .rela.exit.text RELA 0000000000000000 1345318 0000f0 18 I 62 8 8
[10] __ksymtab PROGBITS 0000000000000000 04c170 000480 00 A 0 0 8
[11] .rela__ksymtab RELA 0000000000000000 1345408 000d80 18 I 62 10 8
[12] __kcrctab PROGBITS 0000000000000000 04c5f0 000120 00 A 0 0 1
[13] .rela__kcrctab RELA 0000000000000000 1346188 0006c0 18 I 62 12 8
[14] .rodata PROGBITS 0000000000000000 04c710 006618 00 A 0 0 8
[15] .rela.rodata RELA 0000000000000000 1346848 002c40 18 I 62 14 8
[16] .altinstructions PROGBITS 0000000000000000 052d28 000d80 00 A 0 0 1
[17] .rela.altinstructions RELA 0000000000000000 1349488 003600 18 I 62 16 8
[18] .altinstr_replacement PROGBITS 0000000000000000 053aa8 000754 00 A 0 0 4
[19] .rodata.str PROGBITS 0000000000000000 0541fc 0015cb 01 AMS 0 0 1
[20] .modinfo PROGBITS 0000000000000000 0557c8 000c32 00 A 0 0 8
[21] .rodata.str1.8 PROGBITS 0000000000000000 056400 0080c2 01 AMS 0 0 8
[22] __param PROGBITS 0000000000000000 05e4c8 000230 00 A 0 0 8
[23] .rela__param RELA 0000000000000000 134ca88 000540 18 I 62 22 8
[24] __mcount_loc PROGBITS 0000000000000000 05e6f8 002110 00 A 0 0 8
[25] .rela__mcount_loc RELA 0000000000000000 134cfc8 006330 18 I 62 24 8
[26] __ksymtab_strings PROGBITS 0000000000000000 060808 0005a1 00 A 0 0 1
[27] .eh_frame PROGBITS 0000000000000000 060db0 014340 00 A 0 0 8
[28] .rela.eh_frame RELA 0000000000000000 13532f8 006360 18 I 62 27 8
[29] __versions PROGBITS 0000000000000000 0750f0 006240 00 A 0 0 8
[30] __jump_table PROGBITS 0000000000000000 07b330 0005e8 00 WA 0 0 8
[31] .rela__jump_table RELA 0000000000000000 1359658 0011b8 18 I 62 30 8
[32] .data PROGBITS 0000000000000000 07b918 000a48 00 WA 0 0 8
[33] .rela.data RELA 0000000000000000 135a810 000468 18 I 62 32 8
[34] __bug_table PROGBITS 0000000000000000 07c360 0006f0 00 WA 0 0 4
[35] .rela__bug_table RELA 0000000000000000 135ac78 001bc0 18 I 62 34 8
[36] __verbose PROGBITS 0000000000000000 07ca50 000690 00 WA 0 0 8
[37] .rela__verbose RELA 0000000000000000 135c838 000b40 18 I 62 36 8
[38] .data.once PROGBITS 0000000000000000 07d0e0 00000f 00 WA 0 0 1
[39] .data..read_mostly PROGBITS 0000000000000000 07d0ef 000001 00 WA 0 0 1
[40] .gnu.linkonce.this_module PROGBITS 0000000000000000 07d100 000380 00 WA 0 0 64
[41] .rela.gnu.linkonce.this_module RELA 0000000000000000 135d378 000030 18 I 62 40 8
[42] .plt NOBITS 0000000000000380 07d480 000001 00 WA 0 0 1
[43] .init.plt NOBITS 0000000000000381 07d480 000001 00 WA 0 0 1
[44] .text.ftrace_trampoline NOBITS 0000000000000382 07d480 000001 00 WA 0 0 1
[45] .bss NOBITS 0000000000000000 07d480 0000a0 00 WA 0 0 8
[46] .comment PROGBITS 0000000000000000 07d480 000bf4 01 MS 0 0 1
[47] .note.GNU-stack PROGBITS 0000000000000000 07e074 000000 00 0 0 1
[48] .debug_aranges PROGBITS 0000000000000000 07e074 000cc0 00 0 0 1
[49] .rela.debug_aranges RELA 0000000000000000 135d3a8 000cc0 18 I 62 48 8
[50] .debug_info PROGBITS 0000000000000000 07ed34 a2862c 00 0 0 1
[51] .rela.debug_info RELA 0000000000000000 135e068 e165d0 18 I 62 50 8
[52] .debug_abbrev PROGBITS 0000000000000000 aa7360 033121 00 0 0 1
[53] .debug_line PROGBITS 0000000000000000 ada481 09f8bd 00 0 0 1
[54] .rela.debug_line RELA 0000000000000000 2174638 000660 18 I 62 53 8
[55] .debug_frame PROGBITS 0000000000000000 b79d40 0165d0 00 0 0 8
[56] .rela.debug_frame RELA 0000000000000000 2174c98 00c6c0 18 I 62 55 8
[57] .debug_str PROGBITS 0000000000000000 b90310 5cfb1e 01 MS 0 0 1
[58] .debug_loc PROGBITS 0000000000000000 115fe2e 123a2c 00 0 0 1
[59] .rela.debug_loc RELA 0000000000000000 2181358 022d28 18 I 62 58 8
[60] .debug_ranges PROGBITS 0000000000000000 128385a 0717e0 00 0 0 1
[61] .rela.debug_ranges RELA 0000000000000000 21a4080 007530 18 I 62 60 8
[62] .symtab SYMTAB 0000000000000000 12f5040 0108c0 18 63 1860 8
[63] .strtab STRTAB 0000000000000000 1305900 009f55 00 0 0 1
[64] .shstrtab STRTAB 0000000000000000 21ab5b0 000271 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
其中的Entry 64
,描述的正是节区头部字符串表 Section
:
[64] .shstrtab STRTAB 0000000000000000 21ab5b0 000271 00 0 0 1
可以看出来:这个 Section
在 ELF
文件中的偏移地址是 0x21ab5b0
,长度是 0x000271
个字节。
下面,我们从 ELF header
的二进制数据中,来推断这信息。
如何通过 ELF header
中提供的信息,把节区头部字符串表这个 Section
给找出来?要想打印节区头部字符串表 Section
的内容,就必须知道这个 Section
在 ELF
文件中的偏移地址。要想知道偏移地址,只能从 Section header table
中Entry 64
的描述信息中获取。要想知道Entry 64
的地址,就必须知道 Section header table
在 ELF
文件中的开始地址,以及每一个Entry
的大小。正好最后这 2
个需求信息,在 ELF header
中都告诉我们了,因此我们倒着推算,就一定能成功。
Section header table
在 ELF
文件中的开始地址为35305512
字节处(ELF
文件头中给出);知道了开始地址,再来算一下Entry 64
的地址。
ELF
文件头中Size of section headers
给出了Section header table
每个表项(每个Section header
)的长度是 64
个字节。
注意这里的计算都是从 0
开始的,因此Entry 64
的开始地址就是:35305512 + 64 * 64 = 35309608
,也就是说用来描述节区头部字符串表这个 Section
的表项,位于 ELF
文件的 35309608
字节的位置。为了更好的理解,让我们通过示意图来看一下:
图9 .shstrtab节示意图
既然知道了这个表项 Entry
的地址,那么就扒开来看一下其中的二进制内容:
执行指令:od -Ax -t x1 -j 35309608 -N 64 nfp.ko
。
其中的 -j 35309608
选项,表示跳过前面的 35309608
个字节,也就是我们从 nfp.ko
这个 ELF
文件的 35309608
字节处开始读取,一共读 64
个字节。
[root@localhost 8.3.1]# od -Ax -t x1 -j 35309608 -N 64 nfp.ko
21ac828 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00
21ac838 00 00 00 00 00 00 00 00 b0 b5 1a 02 00 00 00 00
21ac848 71 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00
21ac858 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
21ac868
这 64
个字节的内容,就对应了 Elf64_Shdr
结构体中的每个成员变量:
图10 .shstrtab节内容与结构体对应关系图
这里主要关注一下上图中标注出来的 4
个字段:
sh_name: 表示节区头部字符串表这个 Section
本身的名字在.shstrtab
节中的索引,值为17
;
sh_type:表示这个 Section
的类型,3
表示这是一个 string table
;
sh_offset: 表示这个 Section
,在 ELF
文件中的偏移量。0x21ab5b0 = 35304880
,意思是节区头部字符串表这个 Section
的内容,从 ELF 文件的 35304880
个字节处开始;
sh_size:表示这个 Section
的长度。0x271 = 625
个字节,意思是节区头部字符串表这个 Section
的内容,一共有 625
个字节。
还记得前面我们使用 readelf -S
工具,读取到节区头部字符串表 Section
在 ELF
文件中的偏移地址是 0x21ab5b0
,长度是 0x271
个字节吗?刚好和这里对应起来。
既然知道了节区头部字符串表这个 Section
在 ELF
文件中的偏移量以及长度,那么就可以把它的字节码内容读取出来。
执行指令: od -Ax -t c -j 35304880 -N 625 nfp.ko
,所有这些参数应该不用再解释了吧?!
[root@localhost 8.3.1]# od -Ax -t c -j 35304880 -N 625 nfp.ko
21ab5b0 \0 . s y m t a b \0 . s t r t a b
21ab5c0 \0 . s h s t r t a b \0 . n o t e
21ab5d0 . g n u . b u i l d - i d \0 . r
21ab5e0 e l a . t e x t \0 . r e l a . t
21ab5f0 e x t . u n l i k e l y \0 . r e
21ab600 l a . i n i t . t e x t \0 . r e
21ab610 l a . e x i t . t e x t \0 . r e
21ab620 l a _ _ k s y m t a b \0 . r e l
21ab630 a _ _ k c r c t a b \0 . r e l a
21ab640 . r o d a t a \0 . r e l a . a l
21ab650 t i n s t r u c t i o n s \0 . a
21ab660 l t i n s t r _ r e p l a c e m
21ab670 e n t \0 . r o d a t a . s t r \0
21ab680 . m o d i n f o \0 . r o d a t a
21ab690 . s t r 1 . 8 \0 . r e l a _ _ p
21ab6a0 a r a m \0 . r e l a _ _ m c o u
21ab6b0 n t _ l o c \0 _ _ k s y m t a b
21ab6c0 _ s t r i n g s \0 . r e l a . e
21ab6d0 h _ f r a m e \0 _ _ v e r s i o
21ab6e0 n s \0 . r e l a _ _ j u m p _ t
21ab6f0 a b l e \0 . r e l a . d a t a \0
21ab700 . r e l a _ _ b u g _ t a b l e
21ab710 \0 . r e l a _ _ v e r b o s e \0
21ab720 . d a t a . o n c e \0 . d a t a
21ab730 . . r e a d _ m o s t l y \0 . r
21ab740 e l a . g n u . l i n k o n c e
21ab750 . t h i s _ m o d u l e \0 . i n
21ab760 i t . p l t \0 . t e x t . f t r
21ab770 a c e _ t r a m p o l i n e \0 .
21ab780 b s s \0 . c o m m e n t \0 . n o
21ab790 t e . G N U - s t a c k \0 . r e
21ab7a0 l a . d e b u g _ a r a n g e s
21ab7b0 \0 . r e l a . d e b u g _ i n f
21ab7c0 o \0 . d e b u g _ a b b r e v \0
21ab7d0 . r e l a . d e b u g _ l i n e
21ab7e0 \0 . r e l a . d e b u g _ f r a
21ab7f0 m e \0 . d e b u g _ s t r \0 . r
21ab800 e l a . d e b u g _ l o c \0 . r
21ab810 e l a . d e b u g _ r a n g e s
21ab820 \0
根据上述结果可知,这个 Section
中存储的全部是字符串。现在我们来数一下节区头部字符串表 Section
内容中,第 17
个字节开始的地方,存储的是什么?是不是看到了:“.shstrtab” 这个字符串(\0是字符串的分隔符)。讲到这里,有关节区头部字符串节的内容就完全讲完了。
前面章节讲了很多内容,可谓是干货满满,相信对于没有具体了解过ELF
文件的朋友,也能有一个基本的认识,然后在工作中结合实际问题,再回过头来查看本文,相信会消化的更加深入。
最后,总结一下本文主要讲述内容,其实只要抓住下面 3
个重点即可:
ELF Header
描述了文件的总体信息,以及两个 Table
的相关信息(偏移地址,表项个数,表项长度)。
每一个 Table
中,包括很多个表项 Entry
,每一个表项都描述了一个 Section/Segment
的具体信息。
解析ELF
文件的工具readelf
、od
、xxd
和objdump
的使用和实战。
计算机系统篇之链接(2):目标文件 https://csstormq.github.io/blog/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%AF%87%E4%B9%8B%E9%93%BE%E6%8E%A5%EF%BC%882%EF%BC%89%EF%BC%9A%E7%9B%AE%E6%A0%87%E6%96%87%E4%BB%B6
Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索 https://mp.weixin.qq.com/s/ZOvHG_ofiU6iWtoSR9bFow
计算机那些事(4)——ELF文件结构 http://chuquan.me/2018/05/21/elf-introduce/
linux中ELF格式二进制程序 https://mshrimp.github.io/2020/09/13/linux%E4%B8%ADELF%E6%A0%BC%E5%BC%8F%E4%BA%8C%E8%BF%9B%E5%88%B6%E7%A8%8B%E5%BA%8F/