LLVM 后端:为 x86 后端替换间接 jmps
LLVM Backend : Replacing indirect jmps for x86 backend
我想将代码中的间接 jmp *(eax)
指令替换为 x86 可执行文件的 mov *(eax),ebx; jmp *ebx
。
在实现这个之前,我想制作 LLVM 编译器,通过添加一些打印语句,在每次检测到 jmp *(eax)
指令时记录输出。
然后我想继续替换间接序列。
根据我在 google 搜索和文章中看到的内容,我可能可以通过修改 llvm 后端中的 x86asmprinter 来实现这一点。但我不知道该怎么做。
任何帮助或阅读将不胜感激。
注意:我的实际要求涉及间接跳转和弹出,但我想从这里开始,以便在深入了解更多内容之前更多地了解后端。
我完成了我的项目。发布我的方法以造福他人。
LLVM backend的主要功能是转换Intermediate Representation
到最终的可执行文件,具体取决于目标体系结构和其他
规格。 LLVM 后端本身由几个阶段组成
目标特定优化、指令选择、调度和指令
发射。这些阶段是必需的,因为 IR 是一种非常通用的表示,并且
需要大量修改才能最终将它们转换为目标特定的可执行文件。
1)每次编译器生成时记录jmp *(eax)
我们可以通过在 Instruction Emitting/Printing 阶段添加 print 语句来实现这一点。在 IR 的大部分主要转换完成后,有一个 AsmPrinter pass,它通过每个函数的基本块中的每个机器指令。此主循环位于 lib/CodeGen/AsmPrinter/AsmPrinter.cpp:AsmPrinter::EmitFunctionBody()
。还有其他相关函数,如 EmitFunctionEpilogue、EmitFunctionPrologue。这些函数最终会针对特定架构调用 EmitInstruction,例如:lib/Target/X86/X86AsmPrinter.cpp
。如果您稍微修改一下,可以调用 MI.getOpcode() 并将其与定义的架构枚举进行比较以打印日志。
比如X86中使用寄存器的跳转,就是X86::JMP64r。您可以使用 MI.getOperand(0) 等获取关联的寄存器
if(MI->getOpcode() == X86::JMP64r)
dbgs() << "Found jmp *x instruction\n";
2)替换指令
所需的更改因您需要的更换类型而异。如果您需要有关寄存器的更多上下文或以前的说明,我们将需要在 Pass 链中更高层实施更改。有一种称为选择 DAG(有向无环图)的指令表示,它存储每条指令对先前指令的依赖关系。例如,在序列
mov myvalue,%rax
jmp *rax
由于 rax 的值取决于 mov 指令,DAG 会将 jmp 指令指向 move 指令(可能还有它之前的其他节点)。您可以将此处的节点替换为您需要的节点。如果做得正确,它应该最终改变最终的指令。
SelectionDAG 代码位于 lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp
。总是最好先四处看看,找出理想的改变地点。在对 DAG 进行拓扑排序之前,每个 IR 语句都会经历多次更改,以便指令处于线性序列中。可以查看图表
使用 llc --help-hidden
中的 -view-dag* 选项。
在我的例子中,我只是在 EmitInstruction 中添加了一个特定的检查并添加了代码来发出我想要的两条指令。
LLVM 文档始终存在,但我发现 Eli Bendersky 的两篇文章比任何其他资源都更有帮助。 Life of LLVM Instruction and Deeper look into LLVM Code Generation。这些文章讨论了非常复杂的 TableGen 描述和指令匹配过程,如果您有兴趣,这很酷。
我想将代码中的间接 jmp *(eax)
指令替换为 x86 可执行文件的 mov *(eax),ebx; jmp *ebx
。
在实现这个之前,我想制作 LLVM 编译器,通过添加一些打印语句,在每次检测到 jmp *(eax)
指令时记录输出。
然后我想继续替换间接序列。
根据我在 google 搜索和文章中看到的内容,我可能可以通过修改 llvm 后端中的 x86asmprinter 来实现这一点。但我不知道该怎么做。 任何帮助或阅读将不胜感激。
注意:我的实际要求涉及间接跳转和弹出,但我想从这里开始,以便在深入了解更多内容之前更多地了解后端。
我完成了我的项目。发布我的方法以造福他人。
LLVM backend的主要功能是转换Intermediate Representation 到最终的可执行文件,具体取决于目标体系结构和其他 规格。 LLVM 后端本身由几个阶段组成 目标特定优化、指令选择、调度和指令 发射。这些阶段是必需的,因为 IR 是一种非常通用的表示,并且 需要大量修改才能最终将它们转换为目标特定的可执行文件。
1)每次编译器生成时记录jmp *(eax)
我们可以通过在 Instruction Emitting/Printing 阶段添加 print 语句来实现这一点。在 IR 的大部分主要转换完成后,有一个 AsmPrinter pass,它通过每个函数的基本块中的每个机器指令。此主循环位于 lib/CodeGen/AsmPrinter/AsmPrinter.cpp:AsmPrinter::EmitFunctionBody()
。还有其他相关函数,如 EmitFunctionEpilogue、EmitFunctionPrologue。这些函数最终会针对特定架构调用 EmitInstruction,例如:lib/Target/X86/X86AsmPrinter.cpp
。如果您稍微修改一下,可以调用 MI.getOpcode() 并将其与定义的架构枚举进行比较以打印日志。
比如X86中使用寄存器的跳转,就是X86::JMP64r。您可以使用 MI.getOperand(0) 等获取关联的寄存器
if(MI->getOpcode() == X86::JMP64r)
dbgs() << "Found jmp *x instruction\n";
2)替换指令 所需的更改因您需要的更换类型而异。如果您需要有关寄存器的更多上下文或以前的说明,我们将需要在 Pass 链中更高层实施更改。有一种称为选择 DAG(有向无环图)的指令表示,它存储每条指令对先前指令的依赖关系。例如,在序列
mov myvalue,%rax
jmp *rax
由于 rax 的值取决于 mov 指令,DAG 会将 jmp 指令指向 move 指令(可能还有它之前的其他节点)。您可以将此处的节点替换为您需要的节点。如果做得正确,它应该最终改变最终的指令。
SelectionDAG 代码位于 lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp
。总是最好先四处看看,找出理想的改变地点。在对 DAG 进行拓扑排序之前,每个 IR 语句都会经历多次更改,以便指令处于线性序列中。可以查看图表
使用 llc --help-hidden
中的 -view-dag* 选项。
在我的例子中,我只是在 EmitInstruction 中添加了一个特定的检查并添加了代码来发出我想要的两条指令。
LLVM 文档始终存在,但我发现 Eli Bendersky 的两篇文章比任何其他资源都更有帮助。 Life of LLVM Instruction and Deeper look into LLVM Code Generation。这些文章讨论了非常复杂的 TableGen 描述和指令匹配过程,如果您有兴趣,这很酷。