注意:包含系统编程。 是的,实质上,它不包含任何其他内容。
假设您被赋予编写幻想游戏的任务。 好吧,关于精灵。 关于虚拟现实。 从小开始,您就梦想着写这样的东西,并且毫不犹豫地同意。 很快,您就会意识到,您已经从古老的bashorgh和其他不同来源的笑话中了解了大多数精灵。 糟糕,是个问题。 好吧,我们的生活并没有消失……凭借丰富的编程经验,您可以去Google,输入“ Elf规范”,然后点击链接。 喔! 这导致了某种PDF ...因此我们在这里拥有了... Elf32_Sword
精灵之剑-看起来像您所需要的。 32显然是角色的等级,并且以下各列中的四个四分之一可能是损坏的。 正是您需要的,以及系统化的方式!
正如在一项奥运会编程任务中所述,在有关日本,武士和艺妓的主题的详细文本的几段之后:“正如您已经了解的那样,该任务根本不会与之有关。” 哦,是的,比赛当然是有一段时间了。 总的来说,我宣布坚持5分钟。
今天,我将尝试讨论解析64位ELF格式的文件。 原则上,它们只是不存储在其中,它们是本机程序,静态库,动态库,特定于实现的所有内容,例如崩溃转储...例如,在Linux和许多其他类似Unix的系统上使用它们,是的,他们说,甚至在电话上也是如此。以前,它的支持被积极地填充到修补的固件中。 似乎很难支持用于存储来自严重操作系统的程序的格式。 所以我想。 是的,可能是。 但是我们将支持一个非常特定的用例:从.o
文件中加载eBPF字节码。 为什么这样 只是为了进行进一步的实验,我需要一些严肃的(即不是膝盖高的 )跨平台字节码,可以从C获取而不是手动编写,因此eBPF很简单,并且有一个LLVM后端。 我只需要将ELF解析为编译器将此字节码放入其中的容器。
为了以防万一,我将澄清一下:该文章是探索性编程,并不声称是详尽的指南。 最终目标是制作一个引导加载程序,使您可以阅读使用Clang(我拥有的Clang)在eBPF中编译的C程序,其数量足以继续进行实验。
标题
标头位于ELF的零偏移处。 它包含字母E,L,F,如果您尝试使用文本编辑器打开它,可以看到它们,以及一些全局变量。 实际上,标头是文件中位于固定偏移量处的唯一结构,并且它包含查找结构其余部分的信息。 (此后,我受32位格式文档elf.h
和elf.h
了解,他们了解64位。因此,如果您发现错误,请随时进行纠正)
在文件中遇到的第一件事是unsigned char e_ident[16]
。 还记得“以下所有陈述都是错误的”系列中的这些有趣的文章吗? 两者大致相同:ELF可以包含32位或64位代码,Little Endian或Big Endian,甚至是十几种处理器体系结构。 您将在Little endian下将其阅读为Elf64-好吧,祝您好运...这个字节数组是内部内容以及如何解析它的一种签名。
对于前四个字节,一切都很简单-它是[0x7f, 'E', 'L', 'F']
。 如果它们不匹配,则有理由相信它们是某种错误的蜜蜂。 下一个字节包含类 性格 文件: ELFCLASS32
或ELFCLASS64
位深度。 为简单起见,我们仅处理64位文件(是否有32位eBPF?)。 如果该类最终是ELFCLASS32
,我们将简单地退出并返回一个错误:都一样,结构将“浮动”,并且进行健全性检查不会有任何伤害。 在此结构中,我们感兴趣的最后一个字节指示文件的字节序-我们将仅使用处理器的本机字节顺序。
为了以防万一,我将澄清一下:在C语言中使用ELF格式时,您不应该通过巧妙地计算出的偏移量减去每个int- elf.h
包含必要的结构,甚至在e_ident
包含字节数: EI_MAG0
, EI_MAG1
, EI_MAG2
, EI_MAG3
, EI_CLASS
, EI_DATA
...指向从文件读取或映射到内存的数据的指针,从文件到指向结构的指针并读取。
除了e_ident
标头还包含其他字段,我们将仅检查其中一些字段,而某些字段将用于进一步分析,但稍后。 即,我们检查e_machine == EM_BPF
(也就是说,它是“在eBPF处理器的体系结构之下”), e_type == ET_REL
, e_shoff != 0
。 最后的检查具有以下含义:文件可以包含用于链接(节表和节),启动(程序表和段)或两者的信息。 经过最后两次检查,我们确认文件中需要我们所需要的信息(好像用于链接)。 还要检查格式的版本是否为EV_CURRENT
。
立即进行预订,假设我们将其加载到进程中,那么我们将不信任该文件的有效性。 在处理不受信任文件的内核或其他程序的代码中,自然不可能在任何情况下都这样做 。
截面表
如我所说,我们对文件的链接视图(即节表和节本身)感兴趣。 标头中包含有关在何处查找节表的信息。 它的大小以及一个元素的大小也都在sizeof(Elf64_Shdr)
-它可以大于sizeof(Elf64_Shdr)
(因为它影响格式的版本号,老实说,我不知道)。 一些主要的节号是保留的,实际上不在表中。 引用它们具有特殊含义。 显然,我们只对SHN_UNDEF
感兴趣(零也保留-缺少的部分;顺便说一句,如您所知,表中的标题仍然存在) SHN_ABS
。 字符“在SHN_UNDEF
部分中定义”实际上是未定义的,而在SHN_ABS
它实际上具有绝对值并且不会重定位。 但是,似乎也不SHN_ABS
。
行表
在这里,我们第一次遇到字符串表-文件中使用的字符串表。 实际上,如果const char *strtab
是一个字符串表,则名称sh_name
只是strtab + sh_name
。 是的,它只是一行,从某个索引开始,一直到零字节。 线可能相交(更确切地说,一条可能是另一条的后缀)。 节可以有名称,然后在ELF标题中, e_shstrndx
字段将指向行表的节(如果有多个,则为节名称),节标题中的sh_name
字段指向特定行。
行表的第一个(零)和最后一个字节包含空字符。 后者是可以理解的原因:价值小时,结束最后一行。 但是零偏移量指定了一个不存在或为空的名称-取决于上下文。
载入部分
每个节的标头中都有两个地址:一个, sh_addr
是加载地址(该节将放置在内存中),另一个, sh_offset
是此节所在文件的偏移量。 我不知道这两个值是多少,但是每个值都可以分别为0:在一种情况下,“保留在磁盘上”部分是因为存在某种服务信息。 在另一部分中,该部分未从磁盘加载 ,例如,您只需要选择该部分并用零( .bss
)对其打分即可。 老实说,虽然我不必处理下载地址-上传地址,但上传地址是:)但是,坦率地说,我们也有特定的程序。
搬迁
现在有趣的事情是:根据安全措施,如您所知,如果没有操作员留在基地,它们就不会进入矩阵。 由于这里我们仍然有幻想,因此与运营商的联系将是心灵感应。 哦,是的,我宣布了五分钟的坚韧期。 通常,我们将简要讨论链接过程。
对于我的实验,我需要将一段代码编译成一个常规的so-boot,并用常规的libdl
加载。 在这里,我什至不详细介绍-只是打开dlopen
,通过dlsym
提取字符,在程序dlclose
时用dlclose
将其关闭。 但是,即使这些是与我们的 ELF文件加载器都不相关的实现细节。 仅存在一些上下文 :按名称获取指针的能力。
通常,eBPF指令集是对齐的机器代码的胜利:一条指令始终占用8个字节,并具有一个结构
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
此外,可能不会使用每个特定指令中的许多字段-为“机器”代码节省空间与我们无关。
实际上,第一条指令可以紧跟第二条指令,第二条指令不包含任何操作码,而只是将立即数字段从32位扩展到64位。 这是称为R_BPF_64_64
的复合指令的R_BPF_64_64
。
为了执行重定位,我们将再次查看sh_type == SHT_REL
的节表。 标头的sh_link
字段将指示我们正在修补的部分,以及sh_link
从哪个表获取字符的描述。
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
实际上,有两种类型的重定位部分: REL
和RELA
第二部分显式包含一个附加术语,但是我还没有看到它,因此我们仅对断言不符合这一事实添加断言,然后对其进行处理。 接下来,我将在指令中写入的值添加符号的地址。 在哪里得到呢? 正如我们已经知道的,在这里可以选择:
- 该符号指的是
SHN_ABS
部分。 然后就取st_value
- 字符指的是`SHN_UNDEF部分。 然后拉外线符号
- 在其他情况下,只需将链接修补到同一文件的另一部分即可。
如何自己尝试
首先,要读什么? 除了已经指定的规范外,读取该文件也是有意义的,iovisor团队在其中收集了通过eBPF从Linux内核提取的信息。
其次,实际上每个人应该如何处理? 首先,您需要从某处获取ELF文件。 如StackOverfow所述,团队将为我们提供帮助。
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
其次,您需要以某种方式对文件进行参考分析。 在正常情况下, objdump
命令将帮助我们:
$ objdump : objdump <> <()> <()>. : -a, --archive-headers Display archive header information -f, --file-headers Display the contents of the overall file header -p, --private-headers Display object format specific file header contents -P, --private=OPT,OPT... Display object format specific contents -h, --[section-]headers Display the contents of the section headers -x, --all-headers Display the contents of all headers -d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections --disassemble=<sym> Display assembler contents from <sym> -S, --source Intermix source code with disassembly -s, --full-contents Display the full contents of all sections requested -g, --debugging Display debug information in object file -e, --debugging-tags Display debug information using ctags style -G, --stabs Display (in raw form) any STABS info in the file -W[lLiaprmfFsoRtUuTgAckK] or --dwarf[=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] Display DWARF info in the file -t, --syms Display the contents of the symbol table(s) -T, --dynamic-syms Display the contents of the dynamic symbol table -r, --reloc Display the relocation entries in the file -R, --dynamic-reloc Display the dynamic relocation entries in the file @<file> Read options from <file> -v, --version Display this program's version number -i, --info List object formats and architectures supported -H, --help Display this information
但是在这种情况下,它是无能为力的:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
更确切地说,它将显示各个部分,但是拆卸是个问题。 在这里,我们回想起使用LLVM收集的内容。 LLVM具有来自binutils的实用程序扩展类,其名称形式为llvm-< >
。 例如,他们了解LLVM位代码。 而且他们也了解eBPF-可以肯定的是,它取决于编译选项,但是由于它已经编译,因此应该始终对其进行解析。 因此,为方便起见,我建议创建一个脚本:
vim test-bpf.c
然后对于这样的来源:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
会有这样的结果:
$ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 func: 0: bf 20 00 00 00 00 00 00 r0 = r2 1: 0f 10 00 00 00 00 00 00 r0 += r1 2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 0000000000000010: R_BPF_64_64 z 4: 79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0) 5: 0f 10 00 00 00 00 00 00 r0 += r1 6: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 g F .text 00000038 func 0000000000000000 *UND* 00000000 z
代号
第1部分。QInst:最好先输掉一天,然后在五分钟内飞起来(编写文书非常琐碎)