x86 反汇编器
反汇编器使二进制分析工作。借助可靠的反汇编器,您可以解决高级问题,例如追溯程序调用堆栈或分析基于样本的配置文件,以及低级问题,例如弄清楚编译器如何展开一个紧凑的浮点循环或声明一个变量 const 实际上在优化链的另一端带来的优势。可靠的反汇编器,它将字节序列转换为人类可读的指令助记符,是任何开发平台的重要组成部分。您将开始进行 LLVM 反汇编器的快速之旅:为什么应该存在一个反汇编器,这个反汇编器有什么优点,以及如何使用它。基于 LLVM 的反汇编器
反汇编器无处不在。您可能熟悉的反汇编器是来自 GNU gdb 的反汇编器 (源代码)。实际上,任何调试器都需要一个:Sun mdb 也包含一个反汇编器 (源代码)。一些专门的应用程序,例如 Dtrace,也需要反汇编器 (源代码)。因此,由于这是一个老生常谈的领域,您应该期望反汇编器具有一些共同的特性。- 一个大型库可能包含数十万条指令,因此反汇编必须快速。
- 具有大型内存占用的反汇编器会从更需要它们的分析算法中窃取内存和缓存,因此其代码和数据应紧凑。
- 因为反汇编器用于各种应用程序,所以它们应该以通用的形式提供有关指令的信息,最好是体系结构无关的。
- 为了将来代码维护人员的利益,反汇编器应该尽可能地表驱动。
MCInst
类表示 (include/llvm/MC/MCInst.h
)。MC 指令和机器代码之间的转换由预先存在的 TableGen 表定义 (lib/Target/X86/X86.td
用于 x86 平台)。在 MC 框架内编写反汇编器是有意义的,因为它自然解决了通用性和表驱动的难题,但我们仍然需要解决两个问题:速度和紧凑性。反汇编器的快速测试
llvm-mc 工具提供了一个简单的命令行包装器,我们主要用于测试(例如 test/MC/Disassembler/simple-tests.txt)。它读取包含输入字节的文本文件,并打印出与这些字节对应的指令。例如,以下是一次使用它的简单记录。
$ echo '1 2' | llvm-mc -disassemble -triple=x86_64-apple-darwin9
addl %eax, (%rdx)
$ echo '0x0f 0x1 0x9' | llvm-mc -disassemble -triple=x86_64-apple-darwin9
sidt (%rcx)
$ echo '0x0f 0xa2' | llvm-mc -disassemble -triple=x86_64-apple-darwin9
cpuid
$ echo '0xd9 0xff' | llvm-mc -disassemble -triple=i386-apple-darwin9
fcos
解码过程的设计
快速反汇编器可以分为两类,具体取决于指令格式。在具有固定长度指令的平台上,可以一次性提取指令的所有位,并根据这些位的任意范围进行过滤。相反,具有可变长度指令的平台要求逐字节解析指令。在本文中,我将讨论可变长度情况,特别是包括 i386
和 x86_64
目标的 x86 的情况。x86 指令的结构由几个重要因素决定,每个因素在解码时都至关重要。
- 指令的上下文决定指令的含义及其操作数的大小。上下文包括指令的地址和操作数大小,以及前缀的存在(和位置!),例如
x86_64
目标上的REX.w
前缀和具有 SSE 的体系结构上的f3
前缀。 - 指令的操作码大小可变,并确定需要哪些操作数。操作码有四种类型:形式为
xx
的单字节操作码、形式为0f
xx
的双字节操作码、形式为0f
38
xx
的三字节操作码,以及形式为0f
3a
xx
的三字节操作码。 - 指令的寻址字节决定指令内存操作数的寻址模式(只有一个内存操作数,可以选择模式)。寻址字节包括 ModR/M(修饰符 - 寄存器/内存)字节和 SIB(比例 - 索引 - 基址)字节。
其他前缀? | 强制前缀? | REX 前缀? | 0f [38 /3a ]? | 操作码 | ModR/M 字节? | SIB 字节? |
lib/Target/X86/Disassembler/X86DisassemblerDecoderCommon.h
来理解这个讨论,以下步骤的颜色与它们在表 1 中访问的数据一致。- 第 1 阶段
- 记录所有前缀,但不要使用它们。确定操作码的类型,并在此基础上获取一个
ContextDecision
:ONEBYTE_SYM
、TWOBYTE_SYM
、THREEBYTE38_SYM
和THREEBYTE3A_SYM
。 - 第 2 阶段
- 根据存在的前缀和要解码的机器体系结构开发一个上下文掩码。在查找表 (
CONTEXTS_SYM
) 中查找此掩码以获取上下文 ID。查阅ContextDecision
以找到与上下文对应的OpcodeDecision
。如标题中的注释所指出的,许多可能的上下文被归结为实际解码时相关的IC_max
个不同上下文 ID。这节省了大量空间。 - 第 3 阶段
- 读取操作码,并使用它来查阅
OpcodeDecision
以找到正确的ModRMDecision
。 - 第 4 阶段
- ModR/M 字节不仅指定寻址模式,而且有时也用于识别要执行的特定指令。例如,扩展操作码和转义操作码(通常在 SSE 中看到)使用 ModR/M 字节中的 Reg/Opcode 字段作为操作码的一部分。您可以在英特尔指令手册第 2 卷 B 章 A.4 和 A.5 章 (大型 PDF) 中看到这些奇特之处。给定 ModR/M 字节的值,在
ModRMDecision
中查找解码指令的 LLVM 操作码。 - 第 5 阶段
- 如果 ModR/M 字节指示需要 SIB 字节,则读取 SIB 字节。此阶段在读取操作数时发生。
在实际代码中使用反汇编器
如果您想在自己的代码中使用反汇编器,那么tools/llvm-mc/Disassembler.cpp
是一个很好的使用示例。您可以使用以下代码根据目标实例化一个反汇编器。llvm::OwningPtr<const llvm::MCDisassembler>此反汇编器与
disassembler(target.createMCDisassembler());
MemoryObject
s (include/llvm/Support/MemoryObject.h
) 一起使用,您需要子类化 MemoryObject
来执行正确的读取函数。一个非常 简单 MemoryObject
子类可能如下所示class BufferMemoryObject : public llvm::MemoryObject {给定一个
private:
const uint8_t *Bytes;
uint64_t Length;
public:
BufferMemoryObject(const uint8_t *bytes, uint64_t length) :
Bytes(bytes), Length(length) {
}
uint64_t getBase() const { return 0; }
uint64_t getExtent() const { return Length; }
int readByte(uint64_t addr, uint8_t *byte) const {
if (addr > getExtent())
return -1;
*byte = Bytes[addr];
return 0;
}
};
BufferMemoryObject
,您只需调用之前获取的反汇编器的 getInstruction
方法即可提取 MCInst
对象。llvm::MCInst Inst;最后一个参数是可选的诊断流,
uint64_t Size;
disassembler->getInstruction(Inst, Size, BufferMObj, 0, llvm::nulls()));
0
表示反汇编器应该从缓冲区中的地址 0 开始。在哪里查找更多文档
有关如何从lib/Target/X86/X86.td
生成反汇编器解码表的常规信息,请访问 utils/TableGen/DisassemblerEmitter.cpp
,它提供了 TableGen 代码方面的概述。如果您对反汇编器如何逐位分解各种指令字节感兴趣,您可以直接转到 lib/Target/X86/Disassembler/X86Disassembler.h
,它更详细地描述了解码过程,并提供了实现文件的指南。