ELF文件结构分析详解
ELF文件头
使用 readelf –h 命令查看 ELF 文件,可以看到原始的 ELF 文件头。 ELF 文件头从文件的 0 偏移量开始,是除了文件头之后剩余部分文件的一个映射。文件头主要标记了 ELF 类型、结构和程序开始执行的入口地址,并提供了其他 ELF 头(节头和程序头)的偏移量,稍后会细讲。一旦理解了节头和程序头的含义,就容易理解文件头了。通过查看 Linux 的 ELF(5)手册, 可以了解ELF 头部的结构:
#define EI_NIDENT 16 typedef struct{ unsigned char e_ident[EI_NIDENT]; //Magic,类别,数据,版本,OS/ABI,ABI uint16_t e_type; //类型 uint16_t e_machine; //系统架构 uint32_t e_version; //版本 ElfN_Addr e_entry; //入口点地址 ElfN_Off e_phoff; //start of program headers ElfN_Off e_shoff; //start of section headers uint32_t e_flags; //标志 uint16_t e_ehsize; //文件头的大小 uint16_t e_phentsize; //程序头大小 uint16_t e_phnum; //number of program headers uint16_t e_shentsize; //节头大小 uint16_t e_shnum; //number of program headers uint16_t e_shstrndx; //字符串表段索引 }ElfN_Ehdr;
魔数
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 7f 、45、4c、46分别对应ascii码的Del(删除)、字母E、字母L、字母F。这四个字节被称为ELF文件的魔数,操作系统在加载可执行文件时会确认魔数是否正确,如果不正确则拒绝加载。 第五个字节标识ELF文件是32位(01)还是64位(02)的。 第六个字节标识该ELF文件字节序是小端(01)还是大端(02)的。 第七个字节指示ELF文件的版本号,一般是01。 后九个字节ELF标准未做定义。一般为00.
文件类型:
- ET_NONE(0):未知类型。这个标记表明文件类型不确定,或者还未定义
- ET_REL(1):重定位文件。ELF 类型标记为 relocatable 意味着该文件被标记为了一段可重定位的代码,有时也称为目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码 (position independent code)。在编译完代码之后通常可以看到一个.o 格式的文件,这种文件包含了创建可执行文件所需要的代码和数据
- ET_EXEC(2):可执行文件。ELF 类型为executable,表明这个文件被标记为可执行文件。这种类型的文件也称为程序,是一个进程开始执行的入口
- ET_DYN(3):共享目标文件。ELF 类型为 dynamic,意味着该文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中
- ET_CORE(4):核心文件。在程序崩溃或者进程传递了一个 SIGSEGV 信 号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以 使用 GDB 读取这类文件来辅助调试并查找程序崩溃的原因
系统架构:
e_machine成员标识系统架构(机器类型),ELF定义了以下多种系统架构。可在“/usr/include/elf.h”头文件中查看,以下是其中的几种
常量标识 值 系统架构 EM_M32 1 AT&T WE 32100 EM_SPARC 2 SPARC EM_386 3 Intel 80386 EM_68K 4 Motorola m68k family EM_88K 5 Motorola m88k family EM_860 7 Intel 80860
实例( readelf 命令来读取ELF文件头部信息)
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: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x80484c0 Start of program headers: 52 (bytes into file) Start of section headers: 6552 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 9 Size of section headers: 40 (bytes) Number of section headers: 31 Section header string table index: 28
中文翻译版本
ELF 头: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 类别: ELF32 数据: 2 补码,小端序 (little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: EXEC (Executable file) 系统架构: Intel 80386 版本: 0x1 入口点地址: 0x80484c0 程序头起点: 52 (bytes into file) Start of section headers: 6552 (bytes into file) 标志: 0x0 本头的大小: 52 (字节) 程序头大小: 32 (字节) Number of program headers: 9 节头大小: 40 (字节) 节头数量: 31 字符串表索引节头: 28
ELF程序头
ELF 程序头是对二进制文件中段的描述,是程序装载必需的一部分。段(segment)是在内核装载时被解析的,描述了磁盘上可执行文件的内存布局以及如何映射到内存中。可以通过引用原始ELF头中名为e_phoff(程序头 表偏移量)的偏移量来得到程序头表
Elf32_Phdr结构体
- 一个可执行文件至少有一个PT_LOAD类型的段。这类程序头描述的是可 装载的段,也就是说,这种类型的段将被装载或者映射到内存中。
typedef struct { uint32_t p_type; (segment type) Elf32_Off p_offset; (segment offset) Elf32_Addr p_vaddr; (segment virtual address) Elf32_Addr p_paddr; (segment physical address) uint32_t p_filesz; (size of segment in the file) uint32_t p_memsz; (size of segment in memory) uint32_t p_flags; (segment flags, I.E execute|read|read) uint32_t p_align; (segment alignment in memory) } Elf32_Phdr;
PT_LOAD
一个可执行文件至少有一个PT_LOAD类型的段。这类程序头描述的是可 装载的段,也就是说,这种类型的段将被装载或者映射到内存中
例如,一个需要动态链接的ELF可执行文件通常包含以下两个可装载的段(类型为PT_LOAD):
存放程序代码的text段;
存放全局变量和动态链接信息的data段。
上面的两个段将会被映射到内存中,并根据p_align中存放的值在内存中对齐。
PT_DYNAMIC——动态段的 Phdr
动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些 信息。在动态段中包含了一些标记值和指针,包括但不限于以下内容:
- 运行时需要链接的共享库列表;
- 全局偏移表(GOT)的地址——ELF 动态链接部分
- 重定位条目的相关信息。
完整的标记名列表
标记名 描述 DT_HASH 符号散列表的地址 DT_STRTAB 字符串表的地址 DT_SYMTAB 符号表地址 DT_RELA 相对地址重定位表的地址 DT_RELASZ Rela 表的字节大小 DT_RELAENT Rela 表条目的字节大小 DT_STRSZ 字符串表的字节大小 DT_SYMENT 符号表条目的字节大小 DT_INIT 初始化函数的地址 DT_FINI 终止函数的地址 DT_SONAME 共享目标文件名的字符串表偏移量 DT_RPATH 库搜索路径的字符串表偏移量 DT_SYMBOLIC 修改链接器,在可执行文件之前的共享目标文件中搜索符号 DT_REL Rel relocs 表的地址 DT_RELSZ Rel 表的字节大小 DT_RELENT Rel 表条目的字节大小 DT_PLTREL PLT 引用的 reloc 类型(Rela 或 Rel) DT_DEBUG 还未进行定义,为调试保留 DT_TEXTREL 缺少此项表明重定位只能应用于可写段 DT_JMPREL 仅用于 PLT 的重定位条目地址 DT_BIND_NOW 指示动态链接器在将控制权交给可执行文件之前处理所有的重定位 DT_RUNPATH 库搜索路径的字符串表偏移量 动态段结构体定义
/* Dynamic section entry. */ typedef struct { Elf32_Sword d_tag; /* Dynamic entry type */ union { Elf32_Word d_val; /* Integer value */ Elf32_Addr d_ptr; /* Address value */ } d_un; } Elf32_Dyn; typedef struct { Elf64_Sxword d_tag; /* Dynamic entry type */ union { Elf64_Xword d_val; /* Integer value */ Elf64_Addr d_ptr; /* Address value */ } d_un; } Elf64_Dyn;
PT_NOTE
PT_NOTE类型的段可能保存了与特定供应商或者系统相关的附加信息。
有时供应商或系统构建者需要在目标文件上标记特定的信息,以便于其他程序对一致性、兼容性等进行检查。SHT_NOTE类型的节(section)和PT_NOTE类型的程序头元素就可以用于这一目的。节或者程序头元素中的备注信息可以有任意数量的条目,每个条目都是一个4字节的目标处理器格式的数组。下面的标签可以解释备注信息的组织结构,不过这些标签并不是规范中的内容。
事实上,这一个段中只是保存了操作系统的规范信息,在可执行文件运行时是不需要该段的,因此这个段极易被病毒感染。
PT_INTERP
- PT_INTERP段只将位置和大小信息存放在一个以null为终止符的字符串中,是对程序解释器的位置描述。
PT_PHDR
PT_PHDR段保存了程序头表本身的位置和大小。Phdr表保存了所有的Phdr对文件(以及内存镜像)中段的描述信息。
可以使用readelf -l
命令来查看文件的Phdr表。如: - readelf -l just_do_it
Elf file type is EXEC (Executable file) Entry point 0x80484c0 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00954 0x00954 R E 0x1000 LOAD 0x000f08 0x08049f08 0x08049f08 0x00138 0x001a8 RW 0x1000 DYNAMIC 0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x000858 0x08048858 0x08048858 0x0002c 0x0002c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 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 .rel.dyn .rel.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
- 可以看到程序的执行入口点,PT_LOAD段的权限和对齐等信息,以及其他各种重要信息。
ELF节头
在程序中,段(segment)并不等于节(section)。段是程序执行的必要组成部分,在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小描述,主要用于链接和调试。节头表对于程序的执行来说不是必需的,没有节头表,程序仍然可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局描述是程序头表的任务。
如果二进制文件中缺少节头,并不意味着节就不存在。只是没有办法通过节头来引用节,对于调试器或者反编译程序来说,只是可以参考的信息变少了而已。
每一个节都保存了某种类型的代码或者数据。数据可以是程序中的全局变量,也可以是链接器所需要的动态链接信息。正如前面所提到的,每个ELF目标文件都有节,但是不一定有节头,尤其是有人故意将节头从节头表中删除了之后。当然,默认是有节头的。
ELF文件节头结构如下:
/* Section header. */ typedef struct { Elf32_Word sh_name; /* Section name (string tbl index) */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to 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;
.text节
- .text节是保留了程序代码指令的代码节。一段可执行程序,如果存在Phdr,.text节就会存放在text段中。由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS。
.rodata节
- .rodata节保存了只读数据,因此只能存放于一个可执行文件的只读段中。也因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,因此节类型为SHT_PROGBITS。
.plt节
- 过程链接表(Procedure Linkage Table,PLT),.plt节中包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于其存在于text段中,同样保存了代码,因此节类型为SHT_PROGBITS。
.data节
- .data节存在于data段中,保存了初始化的全局变量等数据。由于其保存了程序的变量数据,因此节类型被标记为SHT_PROGBITS。
.bss节
- .bss节保存了未进行初始化的全局数据,是data段的一部分,占用空间不超过4字节,仅表示这个节本身的空间。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,因此节类型为SHT_PROGBITS。
.got
- GOT是Global Offset Table,用来实现位置无关代码(PIC: Position Independent Code),里面的条目指向位置本身就在ELF文件中。
.got.plt节
- .got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。如果攻击者获得了堆或者.bss漏洞的一个指针大小的写原语,就可以对该节任意进行修改。.got.plt节跟程序执行有关,因此节类型被标记为SHT_PROGBITS。
.dynsym节
- .dynsym节保存了从共享库导入的动态符号信息,该节保存在text段中,节类型被标记为SHT_PROGBITS。
.dynstr节
- .dynstr节保存了动态符号字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,以空字符作为终止符。
.rel.*节
- 重定位节保存了重定位相关的信息,这些信息描述;了在链接或者运行时,对ELF目标文件的某部分内容或者进程镜像进行补充或者修改。重定位节保存了重定位相关的数据,因此节类型被标记为SHT_REL。
.hash节
- .hash节有时也称为.gnu.hahs,保存了一个用于查找符号的散列表。
.symtab节
- .symtab节保存了ElfN_Sym类型的符号信息,因此节类型被标记为SHT_SYMTAB。
.strtab节
- .strtab节保存的是符号字符串表,表中的内容会被.symtab的ElfN_Sym结构中的st_name条目引用。由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。
.shstrtab节
- .shstrtab节保存节头字符串表,该表是一个以空字符终止的字符串的集合,字符串保存了每个节的节名,如.text、.data等。有一个名为e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。
.ctors和.dtors节
- .ctors(构造器)和.dtors(析构器)这两个节保存了指向构造函数和析构函数的指针,构造函数是在main函数执行之前需要执行的代码,析构函数是在main函数之后需要执行的代码。
在一个ELF文件中,节头如下:
使用readelf -S XXXXX来读取节头
There are 31 section headers, starting at offset 0x1998: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000030 04 A 5 0 4 [ 5] .dynsym DYNSYM 080481dc 0001dc 0000e0 10 A 6 1 4 [ 6] .dynstr STRTAB 080482bc 0002bc 00008f 00 A 0 0 1 [ 7] .gnu.version VERSYM 0804834c 00034c 00001c 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 08048368 000368 000030 00 A 6 1 4 [ 9] .rel.dyn REL 08048398 000398 000020 08 A 5 0 4 [10] .rel.plt REL 080483b8 0003b8 000040 08 AI 5 24 4 [11] .init PROGBITS 080483f8 0003f8 000023 00 AX 0 0 4 [12] .plt PROGBITS 08048420 000420 000090 04 AX 0 0 16 [13] .plt.got PROGBITS 080484b0 0004b0 000008 00 AX 0 0 8 [14] .text PROGBITS 080484c0 0004c0 0002b2 00 AX 0 0 16 [15] .fini PROGBITS 08048774 000774 000014 00 AX 0 0 4 [16] .rodata PROGBITS 08048788 000788 0000cf 00 A 0 0 4 [17] .eh_frame_hdr PROGBITS 08048858 000858 00002c 00 A 0 0 4 [18] .eh_frame PROGBITS 08048884 000884 0000d0 00 A 0 0 4 [19] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4 [20] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4 [21] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4 [22] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4 [23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4 [24] .got.plt PROGBITS 0804a000 001000 00002c 04 WA 0 0 4 [25] .data PROGBITS 0804a02c 00102c 000014 00 WA 0 0 4 [26] .bss NOBITS 0804a040 001040 000070 00 WA 0 0 32 [27] .comment PROGBITS 00000000 001040 000034 01 MS 0 0 1 [28] .shstrtab STRTAB 00000000 00188b 00010a 00 0 0 1 [29] .symtab SYMTAB 00000000 001074 000520 10 30 47 4 [30] .strtab STRTAB 00000000 001594 0002f7 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) 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符号
符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。在readelf -S命令的输出内容中,可以看到有两个节:.dynsym和.symtab。
.dynsym保存了引用来自外部文件符号的全局符号,如printf这样的库函数,.dynsym保存的符号是.symtab所保存符号的子集,.symtab中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。因此,.symtab保存了所有的符号,而.dynsym只保存动态/全局符号。
ELF文件符号项结构如下:
/* Symbol table entry. */ typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
C语言实现ELF文件解析
// // Created by root on 19-3-4. // #include <stdio.h> #include <string.h> #include <errno.h> #include <elf.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <stdint.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char **argv) { int fd, i; uint8_t *mem; struct stat st; char *StringTable, *interp; Elf32_Ehdr *ehdr; Elf32_Phdr *phdr; Elf32_Shdr *shdr; if (argc < 2) { printf("Usage: %s <executable> \n", argv[0]); exit(0); } if ((fd = open(argv[0], O_RDONLY)) < 0) { perror("open"); exit(-1); } if (fstat(fd, &st) < 0) { perror("fstat"); exit(-1); } /* Map the executable into memory */ mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (mem == MAP_FAILED) { perror("mmap"); exit(-1); } /* * The initial ELF Header starts at offset 0 of our mapped memory. */ ehdr = (Elf32_Ehdr *)mem; /* * The shdr table and phdr tabke offsets are given by e_shoff and e_phoff members of the Elf32_Ehdr */ phdr = (Elf32_Phdr *)&mem[ehdr->e_phoff]; shdr = (Elf32_Shdr *)&mem[ehdr->e_shoff]; /* * Check to see if the ELF magic (The first 4 bytes) match up as 0x7f E L F */ if (mem[0] != 0x7f && strcmp(&mem[1], "ELF")) { fprintf(stderr, "%s is not an ELF file\n", argv[1]); exit(-1); } /* * We are only parsing executables with this code. so ET_EXEC marks an executable. if (ehdr->e_type != ET_EXEC) { fprintf(stderr, "%s is not an executable\n", argv[1]); printf("ehdr->e_type = %x, ET_EXEC = %x\n",ehdr->e_type, ET_EXEC); //exit(-1); }*/ printf("Program Entry point: 0x%x\n",ehdr->e_entry); /* * We find the string table for the section header names with e_shstrndx which gives the index of which section holds the string table. */ StringTable = &mem[shdr[ehdr->e_shstrndx].sh_offset]; /* * Print each section header name and address. * Notice we get the index into the string table that contains each section header name with the shdr.sh_name member. */ printf("Section header list:\n\n"); for (i = 1; i < ehdr->e_shnum; i++) { printf("%s: 0x%x\n",&StringTable[shdr[i].sh_name], shdr[i].sh_addr); } /* * Print out each segment name, and address. * Except for PT_INTERP we print the path to the dynamic linker (Interpreter). */ printf("\n Program header list\n\n"); for (i = 0; i < ehdr->e_phnum; i++) { switch (phdr[i].p_type) { case PT_LOAD: /* * We know that text segment starts at offset 0. And only one other possible loadable segment exists which is the data segment. */ if (phdr[i].p_offset == 0) { printf("Text segment: 0x%x\n", phdr[i].p_vaddr); } else { printf("Data segment: 0x%x\n",phdr[i].p_vaddr); } break; case PT_INTERP: interp = strdup((char *)&mem[phdr[i].p_offset]); printf("Interpreter: %s\n", interp); break; case PT_NOTE: printf("Note segment:0x%x\n", phdr[i].p_vaddr); break; case PT_DYNAMIC: printf("Dynamic segment: 0x%x\n", phdr[i].p_vaddr); break; case PT_PHDR: printf("Phdr segment:0x%x\n",phdr[i].p_vaddr); break; } } exit(0); return 0; }
参考:
- 《Linux二进制分析》
- 《ELF手册》
- 《elf.h》
关于ELF文件分析较好的两篇文章:
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 787772394@qq.com
文章标题:ELF文件结构分析详解
本文作者:二豆子·pwnd0u
发布时间:2019-11-28, 18:07:17
最后更新:2019-11-28, 21:32:18
原始链接:http://blog.codefat.cn/2019/11/28/ELF%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E5%88%86%E6%9E%90%E8%AF%A6%E8%A7%A3/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。