计算机系统篇之链接(2):目标文件
Author: stormQ
Created: Saturday, 21. December 2019 11:08AM
Last Modified: Tuesday, 26. January 2021 11:32PM
本文描述了目标文件的基础知识,其中详细介绍了ELF-64
目标文件格式,并提供了 Linux 系统中目标文件处理工具——readelf
和objdump
的常见用法。
从技术角度来看,目标文件就是一个字节序列(Technically, object file is a sequence of bytes)。
目标文件可以分为三类:可重定位目标文件、可执行目标文件和可共享目标文件(可共享目标文件是一种特殊的可重定位目标文件)。
三者之间的关系:可重定位目标文件和可共享目标文件可用于生成可执行目标文件;可重定位目标文件可用于生成可共享目标文件。
目标文件类型 | 生成者(特指直接参与者) | 是否可以直接加载到内存中执行 | Linux 中的目标文件后缀(习惯上) |
---|---|---|---|
可重定位目标文件 | 汇编器 | 不可以 | .o |
可执行目标文件 | 链接器 | 可以 | 无后缀 |
可共享目标文件 | 链接器 | 不可以 | .so |
注:
对于 Linux 中的目标文件来说,从技术角度讲,文件后缀可以是任意的或无后缀,习惯上的文件后缀只是为了便于区别。实际上在 Linux 系统中,目标文件类型被定义在 ELF Header 中的某个字段中(下文详细描述),系统通过这个字段的值来判断目标文件的真正类型,而不是通过目标文件的扩展名。
上述表格中的生成者
特指与生成目标文件的过程直接相关的部分。比如:可执行目标文件的生成者为链接器,当然编译器和汇编器也参与了这一过程,但后两者属于间接参与。
注意: CS:APP(Third Edition) 7.3 节中所描述的是,可共享目标文件的生成者为编译器和汇编器。其原文为:
Compilers and assemblers generate relocatable object files (including shared object files). Linkers generate
executable object files.
但经过笔者实际验证(验证方法见上一篇:《计算机系统篇之链接(1):gcc / g++的编译流程》),链接器参与了生成可共享目标文件的过程。这一点需要注意。
1) 如何生成可重定位目标文件(On X86-64 Linux):
$ g++ -c main.cpp -o main.o
# 或 g++ -c main.cpp
# 这两个命令都会生成名称为 main.o 的可重定位目标文件
注:-c
选项表示只执行编译和汇编过程,但不执行链接过程(Compile or assemble the source files, but do not link.)。
查看 main.cpp 的内容:
$ cat main.cpp
int main()
{
return 0;
}
2) 如何生成可执行目标文件(On X86-64 Linux):
# 默认生成的可执行目标文件为 a.out
$ g++ main.cpp
# 指定生成的可执行目标文件为 main
$ g++ main.cpp -o main
3) 如何生成可共享目标文件(On X86-64 Linux):
# 默认生成的可共享目标文件为 a.out
$ g++ -shared -fPIC sum.cpp
# 指定生成的可共享目标文件为 sum.so
$ g++ -shared -fPIC sum.cpp -o sum.so
注:
-shared
选项表示生成可共享目标文件,属于链接器选项,默认名称也为 a.out。
-fPIC
选项表示生成地址无关代码(在后续文章中会详细介绍),属于编译器选项。
注意: 含有main
函数的源文件不能用于生成可共享目标文件,但可用于生成可重定位目标文件(即 .o 文件)。
查看 sum.cpp 的内容:
$ cat sum.cpp
int sum(int a, int b)
{
return a + b;
}
目标文件格式是指目标文件的组织方式。目标文件格式因系统而异。
系统 | 目标文件格式 |
---|---|
Windows | PE(Portable Executable) |
Mac OS-X | Mach-O(Mach Object) |
X86-64 Linux | ELF-64(Executable and Linkable Format) |
aarch64 Linux | ELF-64 |
注:aarch64 表示 ARM 64-bit 系统。
1) 如何生成目标文件格式为PE
的可执行目标文件(On X86-64 Linux):
$ /usr/bin/x86_64-w64-mingw32-g++ -o main_w64.exe main.cpp
在 Windows 64-bit 系统上的命令行窗口中执行 main_w64.exe 程序:
C:\Users\x\Desktop>main_w64.exe
g_val_1=0x0, g_val_2=0x10
查看 main.cpp 的内容:
$ cat main.cpp
#include <stdio.h>
int g_val_1;
int g_val_2 = 16;
const int g_val_3 = 2;
int main()
{
printf("g_val_1=0x%x, g_val_2=0x%x\n", g_val_1, g_val_2);
return 0;
}
注:要在 X86-64 Linux 系统上生成在 Windows 64-bit 系统上运行的可执行目标文件,需要使用交叉编译器,比如:g++-mingw-w64(GNU C++ compiler for MinGW-w64)。安装命令为sudo apt-get install g++-mingw-w64
。
2) 如何生成目标文件格式为ELF-64
的可重定位目标文件(On X86-64 Linux):
$ g++ -c sum.cpp -o sum.o
查看 sum.cpp 的内容:
$ cat sum.cpp
extern int g_val;
int g_val_1 = 0;
int g_val_2 = 1;
int g_val_3;
int sum(int a, int b)
{
static int val_1;
static int val_2 = 0;
static int val_3 = 1;
static int val_4 = 0;
static int val_5 = 2;
const static int val_6 = 0;
return a + b;
}
注:在 X86-64 Linux 上,g++ 默认的目标文件格式为 ELF-64(对应的缺省编译选项为-m64
)。如果要在 X86-64 Linux 上使用 g++ 生成 ELF-32 的目标文件格式,需要添加-m32
选项,如下所示:
$ g++ -c sum.cpp -o sum_elf32.o -m32
$ readelf -h sum_elf32.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 740 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 12
Section header string table index: 9
从上面的打印结果中可以看出,可重定位目标文件 sum_elf32.o 的目标文件格式为 ELF32。
典型的 ELF-64 可重定位目标文件格式:
组成部分 | 描述 | 如何查看 |
---|---|---|
ELF header | 描述生成该目标文件的系统的字大小和字节顺序、目标文件类型、机器类型等信息。 | readelf -h <object file> |
.text | 已编译程序的机器代码。 | objdump -d <object file> (只输出汇编,输出内容包括:.init、.plt、.plt.got、.text、.fini 等 sections)objdump -S <object file> (输出汇编和对应的源码。需要编译时加 -g 选项,才会打印源码。)objdump -d --section .text sum.so (只输出汇编,且只打印 .text section) |
.rodata | 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。 | 无 |
.data | 已初始化的且初始值非0的全局变量和静态变量。 | 无 |
.bss | 未初始化的或初始值为0的全局变量和静态变量。 | 无 |
.symtab | 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。 | readelf -s <object file> (注:删除目标文件中 .symtab section 的命令为:strip <object file> ) |
.rel<name> |
<name> section 的重定位信息。 |
readelf -r <object file> |
.debug | 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生) | readelf --debug-dump <object file> objdump -g <object file> strip --strip-debug <object file> (只删除调试信息相关的 sections)或 strip <object file> (除调试信息相关的 sections 以外,还删除 .symtab 和 .strtab) ) |
.comment | 版本控制信息。 | readelf -p .comment <object file> |
.shstrtab | 一个字符串表,其内容包括所有 section 的名称。 | readelf -p .shstrtab <object file> |
.strtab | 一个字符串表,其内容包括所有符号的名称。 | readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file> ) |
Section header table | 描述目标文件的所有节(sections)的位置和大小等信息。 | readelf -S <object file> |
注:
readelf 命令中缩写含义:-h
为--file-header
的缩写,-s
为--symbols
的缩写,-r
为--relocs
的缩写,-p
为--string-dump
的缩写,-S
为--section-headers
的缩写。
objdump 命令中缩写含义:-d
为--disassemble
的缩写,-S
为--source
的缩写,-g
为--debugging
的缩写。
注意: CS:APP(Third Edition) 7.4 节中所描述的是,节头表名称包含在.strtab
section 中。其原文为:
.strtab: A string table for the symbol tables in the .symtab and .debug sections, and for the section
names in the section headers.
但经过笔者实际验证,在 X86-64 Linux(Ubuntu 20.04)中,节头表名称包含在.shstrtab
而不是.strtab
section 中。这一点需要注意。
在可执行目标文件和可共享目标文件中,section 被分组成 segment 以供加载。每个 segment 中包含的 sections 是连续的。
典型的 ELF-64 可执行目标文件格式:
组成部分 | 描述 | 如何查看 | 属于哪个 Segment | Segment 的访问权限 | 程序执行时是否加载到内存 |
---|---|---|---|---|---|
ELF header | 描述生成该目标文件的系统的字大小和字节顺序、目标文件类型、机器类型等信息。 | readelf -h <object file> |
The First Segment | 只读 | 是 |
Program header table | 描述可执行目标文件中所有段(Segments)的位置和大小等信息。 | readelf -l <object file> objdump -p <object file> |
The First Segment | 只读 | 是 |
.interp | 该 section 中保存了可执行目标文件所需要的动态链接器的路径。 | readelf -p .interp <object file> readelf -l <object file> | grep interpreter objdump -s --section .interp <object file> |
The First Segment | 只读 | 是 |
.dynsym | 包含需要动态链接的符号 | readelf --dyn-syms <object file> readelf -s <object file> |
The First Segment | 只读 | 是 |
.dynstr | 动态链接符号表,仅包括需要动态链接的符号名称 | readelf -p .dynstr <object file> |
The First Segment | 只读 | 是 |
.rela.dyn | 动态链接中的数据重定位表。类似于静态链接中的 .rela.data。 | readelf -r <object file> |
The First Segment | 只读 | 是 |
.rela.plt | 动态链接中的代码重定位表。类似于静态链接中的 .rela.text。 | readelf -r <object file> |
The First Segment | 只读 | 是 |
.init | 该 section 中定义了一个名称为 _init 的函数,会被程序初始化代码调用。 | objdump -d <object file> |
Code Segment( The Second Segment) | 可读,可执行 | 是 |
.text | 已编译程序的机器代码。 | objdump -d <object file> (只输出汇编)objdump -S <object file> (输出汇编和对应的源码。需要编译时加-g选项,才会打印源码。)objdump -d --section .text sum.so (只输出汇编,且只打印 .text section) |
Code Segment | 可读,可执行 | 是 |
.rodata | 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。 | 无 | The Third Segment | 只读 | 是 |
.dynamic | 该 section 中保存了动态链接的基本信息。 | readelf -d <object file> objdump -p <object file> |
The Fouth Segment | 只读 | 是 |
.data | 已初始化的且初始值非0的全局变量和静态变量。 | 无 | Data Segment( The Fifth Segment) | 可读,可写 | 是 |
.bss | 未初始化的或初始值为0的全局变量和静态变量。 | 无 | Data Segment | 可读,可写 | 是 |
.symtab | 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。 | readelf -s <object file> (注:删除目标文件中 .symtab section 的命令为:strip <object file> ) |
不属于任何一个 Segment | 无 | 否 |
.debug | 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生) | readelf --debug-dump <object file> objdump -g <object file> |
不属于任何一个 Segment | 无 | 否 |
.comment | 版本控制信息。 | readelf -p .comment <object file> |
不属于任何一个 Segment | 无 | 否 |
.shstrtab | 一个字符串表,其内容包括所有 section 的名称。 | readelf -p .shstrtab <object file> |
不属于任何一个 Segment | 无 | 否 |
.strtab | 一个字符串表,其内容包括所有符号的名称。 | readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file> ) |
不属于任何一个 Segment | 无 | 否 |
Section header table | 描述目标文件的所有节(sections)的位置和大小等信息。 | readelf -S <object file> |
不属于任何一个 Segment | 无 | 否 |
注:
readelf 命令中缩写含义:-h
为--file-header
的缩写,-l
为--program-headers
的缩写,-d
为--dynamic
的缩写,-s
为--symbols
的缩写,-p
为--string-dump
的缩写,-S
为--section-headers
的缩写。
objdump 命令中缩写含义:-p
为--private-headers
的缩写,-d
为--disassemble
的缩写,-S
为--source
的缩写,-g
为--debugging
的缩写,-s
为--full-contents
的缩写。
注意: CS:APP(Third Edition) 7.8 节中所描述的是,ELF Header
、Program Header Table
和.rodata
section 都位于Code Segment
(代码段)中。但经过笔者实际验证,在 X86-64 Linux(Ubuntu 20.04)中,ELF Header
和Program Header Table
位于同一个段中(根据先后顺序,在这里称之为The First Segment
),但不是与.text
所在的代码段。另外,.rodata
section 所在的段(在这里称之为The Third Segment
)也不是代码段。这两点需要注意。
查看可重定位目标文件(ELF-64 格式)的ELF Header
的内容:
$ readelf -h sum.o
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: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 928 (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: 12
Section header string table index: 9
注:
ELF-64 目标文件格式中,ELF Header
对应的结构体名称为Elf64_Ehdr
,定义在/usr/include/elf.h
头文件中。
Magic
是 ELF Header 结构体中的第 1 个字段,占用 16 字节。
前 4 个字节用于标识该文件是 ELF 格式的目标文件,值(十六进制)固定为:7f(0x7f)、45(ASCII 码中大写字母 E 的十六机制表示)、4c(ASCII 码中大写字母 L 的十六机制表示)、46(ASCII 码中大写字母 F 的十六机制表示)。
Magic[4] 表示 ELF 目标文件类别,可选项:1(ELFCLASS32,32 位的 ELF 文件)、2(ELFCLASS64,64 位的 ELF 文件)。
Magic[5] 表示 ELF 目标文件采用的字节序,可选项:1(ELFDATA2LSB,小端)、2(ELFDATA2MSB,大端)。
Magic[6] 表示 ELF 目标文件格式的主版本号,可选项:1(当前版本)。
Magic[7] 表示操作系统及 ABI,可选项:0(ELFOSABI_SYSV,System V ABI)、1(ELFOSABI_HPUX,HP-UX operating system)、64(ELFOSABI_ARM_AEABI,ARM EABI)、97(ELFOSABI_ARM,ARM)、255(ELFOSABI_STANDALONE,Embedded application)等。
Type
是 ELF Header 结构体中的第 2 个字段,占用 2 字节,表示目标文件类型。常用可选项:0(ET_NONE,没有文件类型)、1(ET_REL,可重定位目标文件)、2(ET_EXEC,可执行目标文件)、3(ET_DYN,可共享目标文件)、4(ET_CORE,core 文件)。
Machine
是 ELF Header 结构体中的第 3 个字段,占用 2 字节,表示目标机器的体系结构,即该目标文件只能在哪种平台下使用。相关常量以EM_
开头,可选项:3(EM_386,目标机器是 x86-32)、62(EM_X86_64,目标机器是 x86-64)、64(EM_ARM,目标机器为 ARM)、183(EM_AARCH64,目标机器是 AARCH64)、243(EM_RISCV,目标机器是 RISC-V)等。
Version
是 ELF Header 结构体中的第 4 个字段,占用 4 字节,表示 ELF 目标文件格式的版本,可选项:1(当前版本)。
Entry point address
是 ELF Header 结构体中的第 5 个字段,占用 8 字节,表示程序入口点的虚拟地址。0 表示没有程序入口点。
Start of program headers
是 ELF Header 结构体中的第 6 个字段,占用 8 字节,表示 program header table 的起始位置在目标文件中的偏移量(字节)。
Start of section headers
是 ELF Header 结构体中的第 7 个字段,占用 8 字节,表示 section header table 的起始位置在目标文件中的偏移量(字节)。
Flags
是 ELF Header 结构体中的第 8 个字段,占用 4 字节,表示特定处理器中的标识。
Size of this header
是 ELF Header 结构体中的第 9 个字段,占用 2 字节,表示 ELF Header 的大小(字节)。
Size of this program headers
是 ELF Header 结构体中的第 10 个字段,占用 2 字节,表示 program header table 中每个条目的大小(字节),每个条目的大小是固定的。
Number of program headers
是 ELF Header 结构体中的第 11 个字段,占用 2 字节,表示 program header table 中条目的数量。
Size of section headers
是 ELF Header 结构体中的第 12 个字段,占用 2 字节,表示 section header table 中每个条目的大小(字节),每个条目的大小是固定的。
Number of section headers
是 ELF Header 结构体中的第 13 个字段,占用 2 字节,表示 section header table 中条目的数量。
Section header string table index
是 ELF Header 结构体中的第 14 个字段,占用 2 字节,表示 .strtab section 在 section header table 中的索引。0 表示没有 .strtab section。
查看可重定位目标文件(ELF-64 格式)的Section Header Table
的内容:
$ readelf -S sum.o
There are 12 section headers, starting at offset 0x3a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000014 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000054
000000000000000c 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 00000060
0000000000000014 0000000000000000 WA 0 0 4
[ 4] .rodata PROGBITS 0000000000000000 00000060
0000000000000004 0000000000000000 A 0 0 4
[ 5] .comment PROGBITS 0000000000000000 00000064
0000000000000033 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000097
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000328
0000000000000018 0000000000000018 I 10 7 8
[ 9] .shstrtab STRTAB 0000000000000000 00000340
000000000000005c 0000000000000000 0 0 1
[10] .symtab SYMTAB 0000000000000000 000000d0
00000000000001c8 0000000000000018 11 15 8
[11] .strtab STRTAB 0000000000000000 00000298
0000000000000090 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
注:
ELF-64 目标文件格式中,Section Header Table
对应的结构体名称为Elf64_Shdr
,定义在/usr/include/elf.h
头文件中。
There are 12 section headers, starting at offset 0x3a0
,表示 section header table 中有 12 个条目,section header table 的起始位置在目标文件中的偏移量为 0x3a0 字节。也就是说,section header table 的内容从第 0x3a1 字节(包括该字节)开始。
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 索引。比如:如果该条目表示.text
section,并且其重定位信息在.rela.text
section 中,那么该字段的值为.rela.text
section 的索引。
Info
是 Section Header 结构体中的第 8 个字段,占用 4 字节,表示该 section 的额外信息。
Align
是 Section Header 结构体中的第 9 个字段,占用 8 字节,表示该 section 的内存对齐要求。这个字段的值必须是 2 的幂。如果该字段的值为 0 或 1,那么表示该 section 没有内存对齐要求。
EntSize
是 Section Header 结构体中的第 10 个字段,占用 8 字节,表示该 section 中每个条目的大小(字节)。0 表示该 section 不包含任何条目。
查看可重定位目标文件的.comment
section 的内容:
$ readelf -p .comment sum.o
String dump of section '.comment':
[ 1] GCC: (Ubuntu 6.5.0-2ubuntu1~16.04) 6.5.0 20181026
注意: 这里.comment
section 的第一个字节的值为 0x00(二进制的零,\0
),但不代表.comment
section 在其他类型的目标文件中第一个字节的值也为 0x00(\0
)。比如:在可执行目标文件中该 section 的第一个字节的值不为 0x00。
查看可执行目标文件main
的.comment
section 的内容:
$ readelf -p .comment main
String dump of section '.comment':
[ 0] GCC: (Ubuntu 6.5.0-2ubuntu1~16.04) 6.5.0 20181026
查看可重定位目标文件的.shstrtab
section 的内容:
$ readelf -p .shstrtab sum.o
String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .text
[ 21] .data
[ 27] .bss
[ 2c] .rodata
[ 34] .comment
[ 3d] .note.GNU-stack
[ 4d] .rela.eh_frame
注:
[]
中的值,表示 section 名称的起始位置在.shstrtab
section 中的偏移量(字节)。
.shstrtab
section 的第一个字节的值固定为 0,表示一个空的或者不存在的 section 名称。
.shstrtab
section 的 section 名称是以\0
(对应的 ASCII 码十六机制为 0x00)结尾的字符串。
注意: 可重定位目标文件sum.o
中,其.shstrtab
section 的内容未显式地包含.eh_frame
section 的名称。从表面上看,这不符合“.shstrtab
section 的作用:一个字符串表,其内容包括所有 section 的名称”。实际上,.shstrtab
section 中隐式地包含.eh_frame
section 的名称,即复用了.rela.eh_frame
section 的后半部分的名称。
验证过程:
查看可重定位目标文件的.shstrtab
section 的内容(十六进制):
$ readelf -x .shstrtab sum.o
Hex dump of section '.shstrtab':
0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
0x00000010 002e7368 73747274 6162002e 74657874 ..shstrtab..text
0x00000020 002e6461 7461002e 62737300 2e726f64 ..data..bss..rod
0x00000030 61746100 2e636f6d 6d656e74 002e6e6f ata..comment..no
0x00000040 74652e47 4e552d73 7461636b 002e7265 te.GNU-stack..re
0x00000050 6c612e65 685f6672 616d6500 la.eh_frame.
最左侧一列为地址,后面的为 .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 的内容:
$ readelf -p .strtab sum.o
String dump of section '.strtab':
[ 1] sum.cpp
[ 9] _ZZ3sumiiE5val_1
[ 1a] _ZZ3sumiiE5val_2
[ 2b] _ZZ3sumiiE5val_3
[ 3c] _ZZ3sumiiE5val_4
[ 4d] _ZZ3sumiiE5val_5
[ 5e] _ZZ3sumiiE5val_6
[ 6f] g_val_1
[ 77] g_val_2
[ 7f] g_val_3
[ 87] _Z3sumii
注:
[]
中的值,表示符号名称的起始位置在.strtab
section 中的偏移量(字节)。
.strtab
section 的第一个字节的值固定为 0,表示一个空的或者不存在的符号名称。
.strtab
section 的符号名称是以\0
(对应的 ASCII 码十六机制为 0x00)结尾的字符串。
每个目标文件(包括可重定位目标文件、可共享目标文件和可执行目标文件)中都有一个符号表(.symtab
section),符号表中包含了被该目标文件定义和引用的符号的信息。
符号表(.symtab
section)是由汇编器产生的,跟编译时是否添加-g
选项无关。
符号表中的符号可以分为三类:
符号类型 | 定义者 | 可见性 | 哪些属于这类符号 |
---|---|---|---|
内部定义的全局符号(Global Symbols) | 本目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件定义的非静态函数和非静态全局变量 |
引用外部的全局符号(External Symbols) | 其他目标文件 | 本目标文件和其他目标文件都可见 | 由本目标文件引用的但定义在其他目标文件中的非静态函数和非静态全局变量 |
内部定义的局部符号(Local Symbols) | 本目标文件 | 仅本目标文件可见 | 由本目标文件定义的静态函数、静态全局变量和静态局部变量 |
注:严格地讲,静态局部变量不仅对于其他目标文件不可见,而且对于本目标文件的其他函数也不可见。
查看 sum.cpp 的内容:
$ cat sum.cpp
extern int g_val;
int g_val_1 = 0;
int g_val_2 = 1;
int sum(int a, int b)
{
static int val_1;
static int val_2 = 0;
static int val_3 = 1;
static int val_4 = 0;
static int val_5 = 2;
const static int val_6 = 0;
return a + b;
}
查看 sum.o 的.symtab
section 的内容:
$ readelf -s sum.o
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS sum.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_1
6: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_2
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 _ZZ3sumiiE5val_3
8: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_4
9: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 _ZZ3sumiiE5val_5
10: 0000000000000000 0 SECTION LOCAL DEFAULT 5
11: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 _ZZ3sumiiE5val_6
12: 0000000000000000 0 SECTION LOCAL DEFAULT 7
13: 0000000000000000 0 SECTION LOCAL DEFAULT 8
14: 0000000000000000 0 SECTION LOCAL DEFAULT 6
15: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 g_val_1
16: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 g_val_2
17: 0000000000000000 28 FUNC GLOBAL DEFAULT 1 _Z3sumii
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND g_val
注:
ELF-64 目标文件格式中,Symbol Table Entry
对应的结构体名称为Elf64_Sym
,定义在/usr/include/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 sum.o
输出结果中该 section [Nr]
列的值(同一个 section 在不同的 .o 文件中对应的数字不一定相同)。其他可选项: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
查看这些符号的完整名称。
GCC 对 C++ 语言中的函数进行名称改编时,所有的符号都以_Z
开头。我们可以通过命令c++filt
命令解析改编后的名称。比如:c++filt _Z41__static_initialization_and_destruction_0ii
的输出结果为__static_initialization_and_destruction_0(int, int)
。需要注意,不同的编译器采用不同的名称修饰方法。
注意: CS:APP(Third Edition) 7.5 节中所描述的是,伪节:ABS
、UND
和COM
只可能出现在可重定位目标文件中,不会出现在可执行目标文件中。其原文为:
Note that these pseudosections exist only in relocatable object files; they do not exist in executable object files.
但经过实际验证,在动态链接的可执行目标文件中,伪节ABS
和UND
可能出现。这一点需要注意。
验证过程:
1) 验证“在可重定位目标文件(不包括可共享目标文件)中,Value 列,表示符号在其 section 中的偏移量”
15: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 g_val_1
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_1
6: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_2
8: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 _ZZ3sumiiE5val_4
上述内容表示的含义:
在可重定位目标文件 sum.o 中,符号:g_val_1、_ZZ3sumiiE5val_1、_ZZ3sumiiE5val_2、_ZZ3sumiiE5val_4 都位于同一个 setion 中(该 section 在 Section header table 中的索引为 4)
符号 g_val_1 所关联的对象在该 section 中占用的空间为 0000000000000000~0000000000000003(即该 section 中第一个 4 字节的值为 g_val_1)
符号 _ZZ3sumiiE5val_1 所关联的对象在该 section 中占用的空间为 0000000000000004~0000000000000007(即该 section 中第二个 4 字节的值为 val_1)
符号 _ZZ3sumiiE5val_2 所关联的对象在该 section 中占用的空间为 0000000000000008~000000000000000b(即该 section 中第三个 4 字节的值为 val_2)
符号 _ZZ3sumiiE5val_4 所关联的对象在该 section 中占用的空间为 000000000000000c~000000000000000f(即该 section 中第四个 4 字节的值为 val_4)。
2) 验证“Ndx 列,表示符号所关联对象位于哪个 section”
$ readelf -S sum.o
There are 13 section headers, starting at offset 0x3c8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001c 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000330
0000000000000018 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 0000005c
000000000000000c 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000068
0000000000000010 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 00000068
0000000000000004 0000000000000000 A 0 0 4
[ 6] .comment PROGBITS 0000000000000000 0000006c
0000000000000033 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000009f
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000a0
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000348
0000000000000018 0000000000000018 I 11 8 8
[10] .shstrtab STRTAB 0000000000000000 00000360
0000000000000061 0000000000000000 0 0 1
[11] .symtab SYMTAB 0000000000000000 000000d8
00000000000001c8 0000000000000018 12 15 8
[12] .strtab STRTAB 0000000000000000 000002a0
000000000000008e 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
全局变量 g_val1,静态变量 val_1、val_2、val_4 要么未初始化,要么初始值为 0。所以,这些变量会存放到.bss
section 中。.bss
section 在 Section header table 中的索引为 4([Nr] 列),与Ndx
列的值相同。从而,验证了“readelf -s 输出结果中的Ndx
列即为上面结果中Nr
列的值,表示符号所关联对象所在的 section”。
3) 验证“Name 列,表示符号在.strtab
section 中的名称”
$ readelf sum.o -p .strtab
String dump of section '.strtab':
[ 1] sum.cpp
[ 9] _ZZ3sumiiE5val_1
[ 1a] _ZZ3sumiiE5val_2
[ 2b] _ZZ3sumiiE5val_3
[ 3c] _ZZ3sumiiE5val_4
[ 4d] _ZZ3sumiiE5val_5
[ 5e] _ZZ3sumiiE5val_6
[ 6f] g_val_1
[ 77] g_val_2
[ 7f] _Z3sumii
[ 88] g_val
从.strtab
section 中可以看到符号名称。
4) 验证“Size 列,表示符号所关联对象的大小”
a)测试代码 test.cpp
int g_val_1 = 1;
int g_val_2[] = {1, 2, 3};
int add(int a, int b)
{
return a + b;
}
b)生成可重定位目标文件 test.o
$ g++ -c -o test.o test.cpp
c)查看 test.o 的符号表
$ readelf -s test.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 g_val_1
9: 0000000000000008 12 OBJECT GLOBAL DEFAULT 2 g_val_2
10: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 _Z3addii
d)反汇编 add 函数
$ objdump -d test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3addii>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 8b 55 fc mov -0x4(%rbp),%edx
d: 8b 45 f8 mov -0x8(%rbp),%eax
10: 01 d0 add %edx,%eax
12: 5d pop %rbp
13: c3 retq
从上面可以看出:
符号_Z3addii
所关联的对象是一个函数,并且实现该函数的机器指令所占用的字节数为 20,而 size 字段的值也正好为 20。因此,验证了“如果符号关联的对象是函数,那么该字段的值为实现该函数的机器指令所占用的字节数。”。
符号g_val_1
所关联的对象是一个 int 类型的变量,而 size 字段的值也正好为 4。符号g_val_2
所关联的对象一个大小为 3 的 int 类型数组,而 size 字段的值也正好为 12。因此,验证了“如果符号关联的对象是数据,那么该字段的值为数据占用的字节数。”。
查看测试程序 main.cpp 的内容:
$ cat main.cpp
#include <stdio.h>
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;
}
查看可执行目标文件main
的Program Header Table
:
$ readelf -l main
Elf file type is EXEC (Executable file)
Entry point 0x400430
There are 9 program headers, starting at offset 64
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
0x000000000000071c 0x000000000000071c R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x000000000000022c 0x0000000000000238 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005f0 0x00000000004005f0 0x00000000004005f0
0x0000000000000034 0x0000000000000034 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
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 .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .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 .jcr .dynamic .got
注:
ELF-64 目标文件格式中,Program Header Table Entry
对应的结构体名称为Elf64_Phdr
,定义在/usr/include/elf.h
头文件中。
Entry point 0x400430
表示程序所执行的第一条指令的虚拟地址为 0x400430。
There are 9 program headers, starting at offset 64
,表示 Program Header Table Entry 中有 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 main
的输出结果进行佐证)。
验证过程:
1) 程序入口点
查看可执行目标文件main
的.text
section 的内容:
$ objdump -d main
# 省略...
Disassembly of section .text:
0000000000400430 <_start>:
400430: 31 ed xor %ebp,%ebp
400432: 49 89 d1 mov %rdx,%r9
400435: 5e pop %rsi
400436: 48 89 e2 mov %rsp,%rdx
400439: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40043d: 50 push %rax
40043e: 54 push %rsp
40043f: 49 c7 c0 c0 05 40 00 mov $0x4005c0,%r8
400446: 48 c7 c1 50 05 40 00 mov $0x400550,%rcx
40044d: 48 c7 c7 26 05 40 00 mov $0x400526,%rdi
400454: e8 b7 ff ff ff callq 400410 <__libc_start_main@plt>
400459: f4 hlt
40045a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
# 省略...
从上面结果中可以看出,程序入口点即为.text
section 的第一条指令。
程序执行时,程序入口点的地址仍为 0x400430,如下所示:
$ gdb -q ./main
Reading symbols from ./main...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x40052a
Starting program: /home/test/tx/main
Temporary breakpoint 1, 0x000000000040052a in main ()
(gdb) disas 0x400430
Dump of assembler code for function _start:
0x0000000000400430 <+0>: xor %ebp,%ebp
0x0000000000400432 <+2>: mov %rdx,%r9
0x0000000000400435 <+5>: pop %rsi
0x0000000000400436 <+6>: mov %rsp,%rdx
0x0000000000400439 <+9>: and $0xfffffffffffffff0,%rsp
0x000000000040043d <+13>: push %rax
0x000000000040043e <+14>: push %rsp
0x000000000040043f <+15>: mov $0x4005c0,%r8
0x0000000000400446 <+22>: mov $0x400550,%rcx
0x000000000040044d <+29>: mov $0x400526,%rdi
0x0000000000400454 <+36>: callq 0x400410 <__libc_start_main@plt>
0x0000000000400459 <+41>: hlt
End of assembler dump.
查看可执行目标文件的.interp
section 的内容:
$ readelf -p .interp main
String dump of section '.interp':
[ 0] /lib64/ld-linux-x86-64.so.2
在动态链接的 ELF 可执行目标文件中,其.interp
section 里面保存了可执行目标文件所需要的动态链接器的路径。
在 X86-64 Linux 下,上面结果中的动态链接器在笔者的机器上实际指向/lib/x86_64-linux-gnu/ld-2.31.so
,如下所示:
$ ll /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 32 Aug 18 04:02 /lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.31.so*
查看可执行目标文件的.dynsym
section 的内容:
$ readelf --dyn-syms main
Symbol table '.dynsym' contains 7 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
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
上述输出结果中各字段的含义与.symtab
section 中的相同。这里不再赘述。
查看可执行目标文件的.dynamic
section 的内容:
$ readelf -d main
Dynamic section at offset 0x2dc8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11f8
0x0000000000000019 (INIT_ARRAY) 0x3db8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3dc0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3a0
0x0000000000000005 (STRTAB) 0x470
0x0000000000000006 (SYMTAB) 0x3c8
0x000000000000000a (STRSZ) 132 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3fb8
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x5e8
0x0000000000000007 (RELA) 0x528
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x508
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4f4
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
注:
ELF-64 目标文件格式中,Dynamic Section Entry
对应的结构体名称为Elf64_Dyn
,定义在/usr/include/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 | 常见选项: |
加载对象的标志 |
0x6ffffffb | FLAGS_1 | 常见选项: |
状态标志 |
.symtab
和.dynsym
section 之间的区别为:
Section 名称 | 内容 | 是否会被加载到内存 | 是否可以通过 strip 命令删除该 section |
---|---|---|---|
.symtab | 包含所有的符号 | 否 | 是 |
.dynsym | 只包含需要动态链接的符号 | 是 | 否 |