文章目录
- 示例
- 编译
- 运行
- ELF文件格式
- ELF Header
- ELF Section Header Table (节头表)
- sh_type
- sh_flags
- sh_link、sh_info 节链接信息
- ELF Sections
- 节的分类
- .text节
- .rodata节
- .plt节(过程链接表)
- .data节
- .bss节
- .got.plt节(全局偏移表-过程链接表)
- .dynsym节(动态链接符号表)
- .dynstr节(动态链接字符串表)
- .rel.*节(重定位表)
- .hash节
- .symtab节(符号表)
- .strtab节(字符串表)
- .ctors节和.dtors节
- 字符串表
- 符号表
- Elf64_Sym
- st_info
- st_shndx
- st_value
- 重定位表
- r_offset
- r_info
- r_addend
- ELF Program Header Table (程序头表)
- p_type
- p_offset
- p_vaddr
- p_paddr
- p_filesz
- p_memsz
- p_flags
- p_align
这里结合实例来 认识一些ELF文件,可以先通过示例代码,生成文件libpart.so 和 main,本文主要目的是让你认识和阅读ELF文件,而不是ELF格式的文档,不会涉及到所有字段和各个值的含义
示例
项目地址:
编译
如何配置编译环境?
开发环境是Mac,以android为目标平台,根据 NDK ndk-build 和 CMake构建方式介绍 来配置NDK的交叉编译。 交叉编译会了,那编译为本机平台的可执行文件就轻而易举
提供的项目中,已经配置好了,把相关特性参数改为自己本地的路径,在build路径中,执行以下命令生产,libpart.so 和 main
cmake ../
cmake -build .
运行
既然是编译出Android平台的可执行文件,就需要放到Android上去执行。
发送文件
adb push main /data/local/tmp/
adb push libpart.so /data/local/tmp/
为什么要放在/data/local/tmp/目录下, 不放在更方便操作的 /sdcard/呢?
The filesystem on the /sdcard/ directory is usually a type of FAT filesystem which doesn’t support UNIX file permissions. As such, even if you push a file with execute permissions to /sdcard/, those permissions will not be preserved.
If you need to execute a file on an Android device, you’ll need to push it to a location on the device that supports executable files. A common location that allows for executables is /data/local/tmp/, which typically allows for file execution and is readable/writable by the shell user.
CANNOT LINK EXECUTABLE “./main”: library “libpart.so” not found: needed by main executable
This error is indicating that the shared library libpart.so needed by your main executable cannot be found. The Android linker looks for shared libraries in specific locations, and it might not be looking in the same directory as your main file.
One approach to solve this is to update the LD_LIBRARY_PATH environment variable, which specifies the directories where the linker should look for shared libraries. If your libpart.so file is in the same directory as main (say, /data/local/tmp/), you can run:
export LD_LIBRARY_PATH=/data/local/tmp:$LD_LIBRARY_PATH
This command adds /data/local/tmp/ to the list of directories where the system looks for shared libraries.
However, note that this change to LD_LIBRARY_PATH only persists for the current session. If you exit and start a new session, you’ll need to run the export command again.
If you need the change to be permanent, you’ll have to modify the startup scripts of the shell. This process varies depending on the shell and might require root privileges.
Remember that modifying LD_LIBRARY_PATH can potentially affect the loading of libraries for other programs as well, so make sure to understand the implications before making the change.
It’s also a good idea to ensure that your main executable and libpart.so are compiled to be compatible with your Android device’s architecture (ARM, ARM64, x86, etc.).
到此,./main
可以成功运行了
ELF文件格式
ELF,即 Executable and Linking Format,译为“可
执行可连接格式”,具有这种格式的文件称为 ELF 文件。
ELF 文件主要分为以下三种类型:
• 可重定位文件(relocatable file),用于与其它目标文件进行连接以构建可执行
文件或动态链接库。可重定位文件就是常说的目标文件,由源文件编译而成,但还没有连接成可执行文件。如果引用到其它目标文件或库文件中定义的符号(变量或者函数)的话,只是给出一个名字,其具体的地址是什么。需要在连接的过程中,把对这些外部符号的引用重新定位到其真正定义的位置上。(这里分为静态链接和动态链接)
• 共享目标文件(shared object file),即动态连接库文件。它在以下两种情况下
被使用:第一,在连接过程中与其它动态链接库或可重定位文件一起构建新的目标文件;第二,在可执行文件被加载的过程中,被动态链接到新的进程中,成为运行代码的一部分。
• 可执行文件(executable file),经过连接的,可以执行的程序文件。
目标文件是由汇编器(assembler)和连接编辑器(link editor)生成的,内容是二进
制,而非可读的文本形式,是可以直接在处理器上运行的代码。
动态链接库:Windows的.dll、Linux的.so
静态链接库:Windows的.lib、Linux的.a
ELF文件都是二进制的形式,在生成文件时,会有对应的数据结构来规定,有哪些字段(顺序是固定的)、各字段的长度、各字段的值代表什么含义。最后生成一个二进制文件。
如图所示,为ELF文件的基本结构,其主要由四部分组成:
-
ELF Header 指出了Program Header Table、Section Header Table的位置、大小、数量等
-
ELF Program Header Table (程序头表)
一个可执行文件或共享目标文件的程序头表(program header table)是一个数组,数组中的每一个元素称为“程序头(program header)”,每一个程序头描述了一个“段(segment)”或者一块用于准备执行程序的信息。一个目标文件中的“段(segment)”包含一个或者多个“节(section)”。程序头只对可执行文件或共享目标文件有意义,对于可重定位文件,该信息可以忽略。 -
ELF Sections
在每个“节”中包含有指令数据、符号数据、重定位数据等等。 -
ELF Section Header Table (节头表)
含有文件中所有“节”的信息。文件里的每一个“节”都需要在“节头表”中有一个对应的注册项,这个注册项描述了节的名字、大小、节的位置等等。
本文的示例,是64位的ELF文件,对应的变量类型如下,上面这4部分的结构体,会用到这些类型
Name | Size | Alignment | Purpose |
---|---|---|---|
Elf64_Addr | 8 | 8 | Unsigned program address |
Elf64_Off | 8 | 8 | Unsigned file offset |
Elf64_Half | 2 | 2 | Unsigned medium integer |
Elf64_Word | 4 | 4 | Unsigned integer |
Elf64_Sword | 4 | 4 | Signed integer |
Elf64_Xword | 8 | 8 | Unsigned long integer |
Elf64_Sxword | 8 | 8 | Signed long integer |
unsigned char | 1 | 1 | Unsigned small integer |
ELF Header
直观的看一下ELF Header
ELF Header 的数据结构
#define EI_NIDENT 16
typedef struct {
// 文件标识,本例指 [0x0:0x10)范围的数据,
// 7F 45 4C 46 ,表示文件标识 (.ELF),例如:0x45 = 69,对应的ascii码是E
// 02, 表示文件类型是64位
// 01, 表示小端编码,0x12345678 在二进制文件中存储为78 56 34 12
// 01, 表示版本,必须是 EV_CURRENT(1)
// 其余值的可对照文档分析
unsigned char e_ident[EI_NIDENT];
// 文件类型, 本例指 [0x10:0x12)范围的数据,03 00,因为是小端编码,所以是0x0003,表示 Shared object file(ET_DYN = 3)
Elf64_Half e_type;
//ELF 文件的 CPU 平台属性, 本例指[0x12:0x14)之间的数据,因为是小端编码,所以是0x00B7 该数值在文档中,没有说明
Elf64_Half e_machine;
//ELF 版本号,本例指[0x14:0x18)之间的数据,0x00000001
Elf64_Word e_version;
// 入口地址,规定 ELF 程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令
// 本例指[0x18:0x20)之间的数据,因为是小端编码,所以是 0x00000000000007E0
Elf64_Addr e_entry;
// Program header 表的文件偏移字节,本例指[0x20:0x28)之间的数据 0x0000000000000040
Elf64_Off e_phoff;
// 段表在文件中的偏移,本例指[0x28:0x30)之间的数据 0x000000000000E4B8
Elf64_Off e_shoff;
//ELF 标志位,用来标识一些 ELF 文件平台相关的属性。,本例指[0x30:0x34) 相关常量格式一般为 EF_machine_flag,machine 为平台,flag 为标志
Elf64_Word e_flags;
// ELF 文件头本身的大小,本例指[0x34:0x36) 0x0040 = 64个字节,上图中的Head字节共4行,一行16个字节
Elf64_Half e_ehsize;
// Program header 表的大小,本例指[0x36:0x38) 0x0038 = 56字节
Elf64_Half e_phentsize;
// Program header 表中的项数量,本例指[0x38:0x3A) 0x0007 = 7个
Elf64_Half e_phnum;
//Section header entry table size(节头表的大小),本例指[0x3A:0x3C) 0x0040 = 64字节
Elf64_Half e_shentsize;
//Section header table entry count,本例指[0x3C:0x3E) 0x0021 = 33个
Elf64_Half e_shnum;
//Section header string table index, 字符串节的 节头表项 在 Section Header Table中的索引,,本例指[0x3E:0x40) 0x001E = 30
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
本例是使用NDK编译,所以在查看ELF文件时,需要使用NDK对应版本的工具,例如readelf,路径在 Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf
readelf
用于解析 ELF 文件的工具,可以详细的输出 ELF 文件的信息。常用选项如下:
-a 等效于:-h -l -S -s -r -d -V -A -I
-h --file-header 显示 ELF 文件头
-l --program-headers 显示程序头
-s --syms 显示符号表
--dyn-syms 显示动态符号表
-n --notes 显示核心注释
-r --relocs 显示重定位
-u --unwind 显示展开信息
-d --dynamic 显示动态部分
➜ build /Users/01407714/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -a libpart.so
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: 0x0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x7E0
Start of program headers: 64 (bytes into file)
Start of section headers: 58504 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 33
Section header string table index: 30
ELF Section Header Table (节头表)
ELF文件中可以包含很多“节”(section),所有这些“节”都登记在一张称为
“节头表”(section header table)的数组里。节头表的每一个表项是一个 Elf64_Shdr
结构,通过每一个表项可以定位到对应的节。
下面是 节头表中的.shstrtb节项,.shstrtb节中的字符串是用于 节头表的,代码中的字符串.strtab节中
每个表项的数据结构是:
struct Elf64_Shdr {
// 节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值
// 本例指[0xEC08:0xEC0C) 0x00000011 , 字符串".shstrtab"是在.shstrtab节中的偏移位置0x11上
Elf64_Word sh_name;
//节类型(SHT_*)本例指[0xEC0C:0xEC10) 0x00000003, 表示该节的内容为字符串表
Elf64_Word sh_type;
// 节标志位 表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。
// 本例指[0xEC10:0xEC18) 0x0000000000000000, (这个值在文档中没有找到)
Elf64_Xword sh_flags;
//If the section will appear in the memory image of a process, this member gives the address at which the section's first byte should reside. Otherwise, the member contains 0.
// 本例指[0xEC18:0xEC20) 0x0000000000000000,
Elf64_Addr sh_addr;
//节偏移,如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS 节来说是没有意义的
// 本例指[0xEC20:0xEC28) 0x000000000000E347, .shstrtab 节位于文件偏移0xE347
Elf64_Off sh_offset;
// 节的长度 本例指[0xEC28:0xEC30) 0x000000000000013F = 319,
Elf64_Xword sh_size;
// 节的链接信息 本例指[0xEC30:0xEC34) 0x00000000
Elf64_Word sh_link;
// 节的额外信息 本例指[0xEC34:0xEC38) 0x00000000
Elf64_Word sh_info;
// 节地址对齐, only 0 and positive integral powers of two are allowed. Values 0 and 1 mean the section has no alignment constraints. 本例指[0xEC38:0xEC40) 0x00000000
Elf64_Xword sh_addralign;
//有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize表示每个项的大小。如果为0,则表示该节不包含固定大小的项。
Elf64_Xword sh_entsize; // 项的长度
};
通过上述分析,.shstrtab 节位于文件偏移0xE347,字符串".shstrtab"是在.shstrtab节中的偏移位置0x11
0xE347 + 0x11 = 0xE358,在0xE358的位置时确实是字符串".shstrtab"
sh_type
常量 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段、代码段、数据段都是这种类型 |
SHT_SYMTAB | 2 | 表示该段的内容为符号表 |
SHT_STRTAB | 3 | 表示该段的内容为字符串表 |
SHT_RELA | 4 | 重定位表,该段包含了重定位信息 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没有内容,比如 .bss 段 |
SHT_REL | 9 | 该段包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
sh_flags
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 表示该节在进程空间中可写 |
SHF_ALLOC | 2 | 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。 |
SHF_EXECINSTR | 4 | 表示该节在进程空间中可以被执行 |
sh_link、sh_info 节链接信息
如果节的类型是与链接相关的(无论是动态链接还是静态链接),如重定位表、符号表、等,则sh_link、sh_info两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。ELF存在多个字符串表
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该节所使用的字符串表在节头表中的下标 | 0 |
SHT_HASH | 该节所使用的符号表在节头表中的下标 | 0 |
SHT_REL | 该节所使用的符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_RELA | 该节所使用的符号表在节头表中的下标 | 该重定位表所作用的节在节头表中的下标 |
SHT_SYMTAB | 操作系统相关 | 操作系统相关 |
SHT_DYNSYM | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF | 0 |
使用readelf工具来查看节头表
➜ build /Users/01407714/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -S libpart.so
There are 33 section headers, starting at offset 0xe488:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .hash HASH 00000000000001c8 0001c8 0000a0 04 A 3 0 8
[ 2] .gnu.hash GNU_HASH 0000000000000268 000268 000064 00 A 3 0 8
[ 3] .dynsym DYNSYM 00000000000002d0 0002d0 0001f8 18 A 4 3 8
[ 4] .dynstr STRTAB 00000000000004c8 0004c8 000158 00 A 0 0 1
[ 5] .gnu.version VERSYM 0000000000000620 000620 00002a 02 A 3 0 2
[ 6] .gnu.version_r VERNEED 0000000000000650 000650 000020 00 A 4 1 8
[ 7] .rela.dyn RELA 0000000000000670 000670 0000a8 18 A 3 0 8
[ 8] .rela.plt RELA 0000000000000718 000718 000060 18 AI 3 19 8
[ 9] .plt PROGBITS 0000000000000780 000780 000060 10 AX 0 0 16
[10] .text PROGBITS 00000000000007e0 0007e0 0000fc 00 AX 0 0 4
[11] .rodata PROGBITS 00000000000008dc 0008dc 000028 01 AMS 0 0 1
[12] .eh_frame_hdr PROGBITS 0000000000000904 000904 00002c 00 A 0 0 4
[13] .eh_frame PROGBITS 0000000000000930 000930 000080 00 A 0 0 8
[14] .note.android.ident NOTE 00000000000009b0 0009b0 000098 00 A 0 0 4
[15] .init_array INIT_ARRAY 0000000000001d78 000d78 000008 08 WA 0 0 8
[16] .fini_array FINI_ARRAY 0000000000001d80 000d80 000010 08 WA 0 0 8
[17] .data.rel.ro PROGBITS 0000000000001d90 000d90 000008 00 WA 0 0 8
[18] .dynamic DYNAMIC 0000000000001d98 000d98 000210 10 WA 4 0 8
[19] .got PROGBITS 0000000000001fa8 000fa8 000058 08 WA 0 0 8
[20] .data PROGBITS 0000000000002000 001000 000004 00 WA 0 0 4
[21] .bss NOBITS 0000000000002008 001004 000038 00 WA 0 0 8
[22] .comment PROGBITS 0000000000000000 001004 0000b5 01 MS 0 0 1
[23] .debug_info PROGBITS 0000000000000000 0010b9 004708 00 0 0 1
[24] .debug_abbrev PROGBITS 0000000000000000 0057c1 0004d4 00 0 0 1
[25] .debug_line PROGBITS 0000000000000000 005c95 0010ca 00 0 0 1
[26] .debug_str PROGBITS 0000000000000000 006d5f 0064ec 01 MS 0 0 1
[27] .debug_loc PROGBITS 0000000000000000 00d24b 000427 00 0 0 1
[28] .debug_macinfo PROGBITS 0000000000000000 00d672 000001 00 0 0 1
[29] .debug_ranges PROGBITS 0000000000000000 00d673 000160 00 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00e347 00013f 00 0 0 1
[31] .symtab SYMTAB 0000000000000000 00d7d8 0008a0 18 32 74 8
[32] .strtab STRTAB 0000000000000000 00e078 0002cf 00 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 Sections
节的分类
.text节
.text节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,则.text节就会存在于text段中。由于.text节保存了程序代码,所以节类型为SHT_PROGBITS。
.rodata节
rodata节保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,所以节类型为SHT_PROGBITS。
.plt节(过程链接表)
.plt节也称为过程链接表(Procedure Linkage Table),其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt节保存了代码,所以节类型为SHT_PROGBITS。
.data节
.data节存在于data段中,其保存了初始化的全局变量等数据。由于.data节保存了程序的变量数据,所以节类型为SHT_PROGBITS。
.bss节
.bss节存在于data段中,占用空间不超过4字节,仅表示这个节本省的空间。.bss节保存了未进行初始化的全局数据。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS。
.got.plt节(全局偏移表-过程链接表)
.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt节与程序执行有关,所以节类型为SHT_PROGBITS。
.dynsym节(动态链接符号表)
.dynsym节保存在text段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM。
.dynstr节(动态链接字符串表)
.dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。
.rel.*节(重定位表)
重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对ELF目标文件的某部分或者进程镜像进行补充或修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL。
.hash节
.hash节也称为.gnu.hash,其保存了一个用于查找符号的散列表。
.symtab节(符号表)
.symtab节是一个ElfN_Sym的数组,保存了符号信息。节类型为SHT_SYMTAB。
.strtab节(字符串表)
.strtab节保存的是符号字符串表,表中的内容会被.symtab的ElfN_Sym结构中的st_name引用。节类型为SHT_STRTAB。
.ctors节和.dtors节
.ctors(构造器)节和.dtors(析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码;析构函数是在main函数之后需要执行的代码。
字符串表
节头表中的 每项都有标志位(sh_flags),可取值A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。
.strtab
- 本模块代码中用到的全局变量、函数字符串,局部变量由栈来管理。
- .symtab节在 节头表 中的项的sh_flags值None,运行时不加载到内存中
- 只是用来进行调试和链接的。
.dynstr
- 其他模块的全局符号。
- .dynstr节在 节头表 中的项的sh_flags值 ALLOC,ALLOC表示有该标记的节会在运行时分配并装载进入内存
- 其中的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号。对于动态链接可执行文件的执行是必需的
.shstrtab
- .shstrtab 是节头字符串表,用于保存节头表中用到的字符串,可通过sh_name进行索引。
来看一下.strtab长什么样子:
.dynstr 长这样子:
符号表
.strtab、 .dynstr 中的字符串也不是直接被使用,而是通过符号表中的索引来指向。
.strtab对应.symtab, .dynstr对应.dynsym, .symtab 和 .dynsym 的区别 和上面.strtab、 .dynstr 的区别基本一样。
这里以.dynsym来分析一下,首先通过节头表找到.dynsym节项
.dynsym
sh_type 值为 11,即 SHT_DYNSYM 官方文档。
sh_offset,即符号表在文件中的偏移量,值为 0x2D0。
sh_size,符号表的大小,值为 504Byte。
sh_entsize,符号表中每一个表项的大小为24Byte。
数据结构,来自官方文档Symbol Table,网上大部分文章给出的结构是32位的,数据格式大小和顺序都不一样
下面以.dynsym节中的第9项为例,来分析,第9项的起始位置时 0x2D0 + 9*24 = 0x3A8
Elf64_Sym
struct Elf64_Sym {
// 是一个指向字符串表的索引值,在字符串表中对应位置上的字符串就是该符号名字的实际文本。
// 如果此值为 0,那么此符号无名字。
// 此例指[0x3A8:0x3AC) 值是0xC1, 在.dynstr节的偏移0xC1处的值是 0x4C8+0xC1 = 0x589, 是字符串g_int1的开始
Elf64_Word st_name;
//符号的类型和绑定信息,共8个bit
// 此例指[0x3AC] 值是0x11 = 0b00010001, 表示这是一个全局的变量
unsigned char st_info;
// 目前为 0,保留。 此例指[0x3AD]
unsigned char st_other;
// 符号所在段的下标
// 此例指[0x3AE:0x3AF] 0x0015 =21 , 在上面的节头表 中得知,
// 指向的是 [21] .bss ,因为该变量是未初始化的全局变量,所以放在.bss中
Elf64_Half st_shndx;
// 符号相对应的值,是一个绝对值,或地址等。不同的符号,含义不同
// 此例指[0x3B1:0x3B8] 0x0000000000002008
Elf64_Addr st_value;
// 符号的大小
// 此例指[0x3B8:0x3BF] 0x4 , 在64位下,int类型占4字节
Elf64_XWord st_size;
};
st_info
st_info 的高4位 绑定 属性,低4位是 类型 属性
st_info 的 符号绑定 属性
- STB_LOCAL = 0
表明本符号是一个本地符号。它只出现在本文件中,在本文件外该符号无效。所以在不同的文件中可以定义相同的符号名,它们之间不会互相影响。 - STB_GLOBAL = 1
表明本符号是一个全局符号。当有多个文件被连接在一起时,在所有文件中该符号都是可见的。正常情况下,在一个文件中定义的全局符号,一定是在其它文件中需要被引用,否则无须定义为全局。 - STB_WEAK = 2
类似于全局符号,但是相对于 STB_GLOBAL,它们的优先级更低
st_info 的 符号类型 属性
- STT_NOTYPE = 0
本符号类型未指定。 - STT_OBJECT = 1
本符号是一个数据对象,比如变量、数组等。 - STT_FUNC = 2
本符号是一个函数,或者其它的可执行代码。函数符号在共享目标文件中有特殊的意义。当另外一个目标文件引用一个共享目标文件中的函数符号时,连接编辑器为被引用符号自动创建一个连接表项。非 STT_FUNC
类型的共享目标符号不会通过这种连接表项被自动引用。 - STT_SECTION = 3
本符号与一个节相关联,用于重定位,通常具有 STB_LOCAL 属性。 - STT_FILE = 4
本符号是一个文件符号,它具有 STB_LOCAL 属性,它的节索引值是 - SHN_ABS。在符号表中如果存在本类符号的话,它会出现在所有
- STB_LOCAL 类符号的前部。
st_shndx
任何一个符号表项的定义都与某一个“节”相联系,因为符号是为节而定义,在节中被引用。
本数据成员是一个索引值,它指向相关联的节在节头表中的索引。在重定位过程中,节的位置会改变,本数据成员的值也随之改变,继续指向节的新位置。
当本数据成员指向下面三种特殊的节索引值时,本符号具有如下特别的意义:
- SHN_ABS
符号的值是绝对的,具有常量性,在重定位过程中,此值不需要改变。 - SHN_COMMON
本符号所关联的是一个还没有分配的公共节,本符号的值规定了其内容的字节对齐规则,与 sh_addralign 相似。也就是说,连接器会为本符号分配存储空间,而且其起始地址是向 st_value 对齐的。本符号的值指明了要分配的字节数。 - SHN_UNDEF
当一个符号指向第 1 节(SHN_UNDEF)时,表明本符号在当前目标文件中未定义,在连接过程中,连接器会找到此符号被定义的文件,并把这些文件连接在一起。本文件中对该符号的引用会被连接到实际的定义上去。
st_value
符号的值。这个值其实没有固定的类型,它可能代表一个数值,也可以是一
个地址,具体是什么要看上下文。
对于不同的目标文件类型,符号表项的 st_value 的含义略有不同:
-
在重定位文件中,如果一个符号对应的节的索引值是
SHN_COMMON,st_value 值是这个节内容的字节对齐数。 -
在重定位文件中,如果一个符号是已定义的,那么它的 st_value 值
是该符号的起始地址在其所在节中的偏移量,而其所在的节的索引由
st_shndx 给出。 -
在可执行文件和共享库文件中,st_value 不再是一个节内的偏移量,
而是一个虚拟地址,直接指向符号所在的内存位置。这种情况下,st_shndx
就不再需要了。
如果一个可执行文件中含有一个函数的引用,而这个函数是定义在一个共享目标文件中,那么在可执行文件中,针对那个共享目标文件的符号表应该含有这个函数的符号。
符号表的 st_shndx 成员值为 SHN_UNDEF,这就告诉了动态连接器,这个函数的符号定义并不在可执行文件中。如果已经在可执行文件中给这个符号申请了一个函数连接表项,而且符号表项的 st_value 成员不是 0,那么 st_value值就将是函数连接表项中第一条指令的地址。否则,st_value 成员是 0。这个函数
连接表项地址被动态连接器用来解析函数地址。
使用readelf查看.dynsym的节信息,和上面的分析是完全吻合的
Symbol table '.dynsym' contains 21 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000000007e0 0 SECTION LOCAL DEFAULT 10 .text
2: 0000000000001d90 0 SECTION LOCAL DEFAULT 17 .data.rel.ro
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@LIBC
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZdlPv
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC
7: 0000000000002040 0 NOTYPE GLOBAL DEFAULT ABS _bss_end__
8: 00000000000008c8 20 FUNC WEAK DEFAULT 10 _ZNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED2Ev
9: 0000000000002008 4 OBJECT GLOBAL DEFAULT 21 g_int1
10: 0000000000002010 24 OBJECT GLOBAL DEFAULT 21 g_str1
11: 0000000000002004 0 NOTYPE GLOBAL DEFAULT ABS _edata
12: 0000000000002040 0 NOTYPE GLOBAL DEFAULT ABS _end
13: 00000000000008a0 40 FUNC GLOBAL DEFAULT 10 _Z6func_2NSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
14: 000000000000088c 20 FUNC GLOBAL DEFAULT 10 _Z6func_1i
15: 0000000000002004 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
16: 0000000000002000 4 OBJECT GLOBAL DEFAULT 20 g_int2
17: 0000000000002028 24 OBJECT GLOBAL DEFAULT 21 g_str2
18: 0000000000002040 0 NOTYPE GLOBAL DEFAULT ABS __bss_end__
19: 0000000000002040 0 NOTYPE GLOBAL DEFAULT ABS __end__
20: 0000000000002004 0 NOTYPE GLOBAL DEFAULT ABS __bss_start__
重定位表
重定位(relocation)是把符号引用与符号定义连接在一起的过程。比如,当程序
调用一个函数时,将从当前运行的指令跳转到一个新的指令地址去执行。在编写程
序的时候,我们只需指明所要调用的函数名(即符号引用),在重定位的过程中,
函数名会与实际的函数所在地址(即符号定义)联系起来,使程序知道应该跳转到
哪里去。
重定位文件必须知道如何修改其所包含的“节”的内容,在构建可执行文件或
共享目标文件的时候,把节中的符号引用换成这些符号在进程空间中的虚拟地址。
包含这些转换信息的数据也就是“重定位项(relocation entries)”。
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
r_offset
r_offset 是重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享目标文件来说,此值是受重定位作用的存储单元的虚拟地址。
r_info
r_info 既给出了重定位所作用的符号表索引,也给出了重定位的类型。
比如,如果是一个函数的重定位,r_info将要持有被调用函数所对应的符号表索引。
如果索引值为 STN_UNDEF,即未定义索引,那么重定位过程中将使用 0 作
为符号值。
重定位类型依处理器不同而不同,各种处理器将分别定义自己的类型。如果一种处理器规定自己引用了一个重定位项的类型或者符号表索引,表明这种处理器应用了 ELF32_R_TYPE 或 ELF32_R_SYM 到其重定位项的 r_info 成员
r_addend
r_addend指定了一个加数,这个加数用于计算需要重定位的域的值。
Elf64_Rela 与 Elf64_Rel 在结构上只有一处不同,就是前者有 r_addend。
Elf64_Rela 中是用 r_addend 显式地指出加数;
而对 Elf64_Rel 来说,加数是隐含在被修改的位置里的。Elf32_Rel 中加数的形式这里并不定义,它可以依处理器架构的不同而自行决定。在特定处理器上如何实现,可以指定一种固定的格式,也可以不指定格式而依据上下文来解析。
一个“重定位节(relocation section)”需要引用另外两个节:一个是符号表节,一个是被修改节。
在重定位节中,节头的 sh_info 和 sh_link 成员分别指明了引用关系。不同的目标文件中,重定位项的 r_offset 成员的含义略有不同。
-
在重定位文件中,r_offset 成员含有一个节偏移量。也就是说,重定位节本身描述的是如何修改文件中的另一个节的内容,重定位偏移量(r_offset)指向了另一个节中的一个存储单元地址。
-
在可执行文件或共享目标文件中,r_offset 含有的是符号定义在进程空间中的虚拟地址。可执行文件和共享目标文件是用于运行程序而不是构建程序的,所以对它们来说更有用的信息是运行期的内存虚拟地址,而不是某个符号定义在文件中的位置。尽管对于不同类型的目标文件,r_offset 的含义不同,但其重定位的作用是不变
的。
ELF Program Header Table (程序头表)
一个可执行文件或共享目标文件的程序头表(program header table)是一个数组,数组中的每一个元素称为“程序头(program header)”,每一个程序头描述了一个“段(segment)”或者一块用于准备执行程序的信息。
一个目标文件中的“段(segment)”包含一个或者多个“节(section)”。
程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。在目标文件的文件头(elf header)中,e_phentsize 和 e_phnum 成员指定了程序头的大小。
该示例共有7个程序头,下面以第0个来分析一下,程序头结构。关于程序具体如何被加载,哪段加载到哪个地址,放在下篇文章分析
typedef struct {
// 段类型
// [0x40:0x44) = 0x1 本程序头 指向一个可装载的段
Elf64_Word p_type;
//段的权限属性,比如可读 "R",可写 "W" 和可执行 "X"
// [0x44:0x48) = 0x5 可读,可执行
Elf64_Word p_flags;
// 段在文件中的偏移
// [0x48:0x50) = 0x0000000000000000
Elf64_Off p_offset;
// 段的第一个字节在虚拟地址空间的起始位置
// [0x50:0x58) = 0x0000000000000000
Elf64_Addr p_vaddr;
// 段的物理装载地址,即 LMA(Load Memory Address),一般情况下 p_paddr 和 p_vaddr 是相同的
// [0x58:0x60) = 0x0000000000000000
Elf64_Addr p_paddr;
// 段在 ELF 文件中所占空间的长度,可能为 0
// [0x60:0x68) = 0xA48 = 2632
Elf64_Xword p_filesz;
// 段在进程虚拟空间中所占空间的长度,可能为 0
// [0x68:0x70) = 0xA48 = 2632
Elf64_Xword p_memsz;
// 段的对齐属性,实际对齐字节等于 2 的 p_align 次方
// [0x70:0x78) = 0x1000
Elf64_Xword p_align;
} Elf64_Phdr;
p_type
此数据成员说明了本程序头所描述的段的类型,或者如何解析本程序头的信息
- PT_NULL = 0
此类型表明本程序头是未使用的,本程序头内的其它成员值均无意义。具有此种类型的程序头应该被忽略。 - PT_LOAD = 1
此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到内存中。如前所述,段在文件中的大小是 p_filesz,在内存中的大小是p_memsz。如果 p_memsz 大于 p_filesz,在内存中多出的存储空间应填 0 补充,也就是说,段在内存中可以比在文件中占用空间更大; - PT_DYNAMIC = 2
此类型表明本段指明了动态连接的信息。 - PT_INTERP = 3
本段指向了一个以”null”结尾的字符串,这个字符串是一个 ELF 解析器的路径。这种段类型只对可执行程序有意义,当它出现在共享目标文件中时,是一个无意义的多余项。在一个 ELF 文件中它最多只能出现一次,而且必须出现在其它可装载段的表项之前。 - PT_NOTE = 4
本段指向了一个以”null”结尾的字符串,这个字符串包含一些附加的信息。 - PT_SHLIB = 5
该段类型是保留的,而且未定义语法。UNIX System V 系统上的应用程序不会包含这种表项。
- PT_PHDR = 6
此类型的程序头如果存在的话,它表明的是其自身所在的程序头表在文件或内存中的位置和大小。这样的段在文件中可以不存在,只有当所在程序头表所覆盖的段只是整个程序的一部分时,才会出现一次这种表项,而且这种表项一定出现在其它可装载段的表项之前。
p_offset
此数据成员给出本段内容在文件中的位置,即段内容的开始位置相对于文件开头的偏移量。
p_vaddr
此数据成员给出本段内容的开始位置在进程空间中的虚拟地址。
p_paddr
此数据成员给出本段内容的开始位置在进程空间中的物理地址。对于目前大多数现代操作系统而言,应用程序中段的物理地址事先是不可知的,所以目前这个成员多数情况下保留不用,或者被操作系统改作它用。
p_filesz
此数据成员给出本段内容在文件中的大小,单位是字节,可以是 0。
p_memsz
此数据成员给出本段内容在内容镜像中的大小,单位是字节,可以是 0。
p_flags
此数据成员给出了本段内容的属性。具体有哪些标志位请参见下文。
p_align
对于可装载的段来说,其 p_vaddr 和 p_offset 的值至少要向内存页面大小对齐。此数据成员指明本段内容如何在内存和文件中对齐。如果该值为 0 或 1,表明没有对齐要求;否则,p_align 应该是一个正整数,并且是 2 的幂次数。p_vaddr 和p_offset 在对 p_align 取模后应该相等
参考:
计算机那些事(4)——ELF文件结构
ELF文件格式的详细文档:
https://paper.seebug.org/papers/Archive/refs/elf/Understanding_ELF.pdf
System V Application Binary Interface - DRAFT - 24 April 2001
Android so(ELF) 文件解析