mctoll
Table of Contents
1. mctoll
`This tool statically (AOT) translates (or raises) binaries to LLVM IR.`
https://github.com/Microsoft/llvm-mctoll
translating_x86_binaries_into_llvm_intermediate_representation
1.1. 测试
https://github.com/sunwayforever/llvm-toy/tree/mctoll
$> cd <path-to-llvm>/mctoll $> ./mctoll_build.sh $> make
原始的 c 代码:
#include <stdio.h> int main(int argc, char **argv) { int a = argc; int b = argc + 1; return b; }
原始的汇编:
# at&t 语法, `op src, dst` main: 1 pushq %rbp 2 movq %rsp, %rbp 3 movl $0, -4(%rbp) 4 movl %edi, -8(%rbp) 5 movq %rsi, -16(%rbp) 6 movl -8(%rbp), %eax 7 movl %eax, -20(%rbp) 8 movl -8(%rbp), %eax 9 addl $1, %eax 10 movl %eax, -24(%rbp) 11 movl -24(%rbp), %eax 12 popq %rbp 13 retq
用汇编生成 binary 后通过 mctoll 转换回来的 llvm ir:
define dso_local i32 @main(i32 %arg1, i64 %arg2) { entry: ; %rbp 识别为 stktop_4 (stack top), 后续针对 stack 的使用 (-4(%rbp), -8(%rbp)...) ; 都使用 stktop_4 这个 llvm 局部变量来表示 %stktop_4 = alloca i8, i32 32, align 1 ; RBP_N.20/16/8/4 对应 {-20, -16, -8, -4}(%rbp), 其中 _N 表示 negative? %tos = ptrtoint ptr %stktop_4 to i64 %0 = add i64 %tos, 12 %RBP_N.20 = inttoptr i64 %0 to ptr %1 = add i64 %tos, 16 %RBP_N.16 = inttoptr i64 %1 to ptr %2 = add i64 %tos, 24 %RBP_N.8 = inttoptr i64 %2 to ptr %3 = add i64 %tos, 28 %RBP_N.4 = inttoptr i64 %3 to ptr %4 = add i64 %tos, 0 ; RSP_P.0 对应 $rsp, _P 表示 positive? %RSP_P.0 = inttoptr i64 %4 to ptr ; 写了一个 `DEADBEEF` 到 0(RSP), 表示未初始化 store i64 3735928559, ptr %RSP_P.0, align 8 ; 1 pushq %rbp 是 prologue 生成的, 不需要翻译 ; 2 movq %rsp, %rbp %RBP = ptrtoint ptr %RSP_P.0 to i64 ; 3 movl $0, -4(%rbp) store i32 0, ptr %RBP_N.4, align 1 ; mctoll 知道 x86 system-v abi 里 edi 代表 arg1 ; 4 movl %edi, -8(%rbp) store i32 %arg1, ptr %RBP_N.8, align 1 ; mctoll 知道 rsi 代表 arg2 ; 5 movq %rsi, -16(%rbp) store i64 %arg2, ptr %RBP_N.16, align 1 ; 6 movl -8(%rbp), %eax %memload = load i32, ptr %RBP_N.8, align 1 ; 7 movl %eax, -20(%rbp) store i32 %memload, ptr %RBP_N.20, align 1 ; 8 movl -8(%rbp), %eax %memload1 = load i32, ptr %RBP_N.8, align 1 ; eax 对应 %EAX 这个 llvm 局部变量 ; 9 addl $1, %eax %EAX = add i32 %memload1, 1 ; x86 的 addl 指令会设置 cf, pf, zf, of, sf 这几个 flag 寄存器: ; https://en.wikipedia.org/wiki/FLAGS_register ; cf: carry, 表示无符号加法是否有进位 ; of: overflow, 表示有符号加法是否有溢出 ; zf: zero, 表示结果是否为 0 ; sf: sign, 表示正负 ; pf: parity, 表示结果低 8 位中 1 的个数是奇数还是偶数 ; 这里通过 llvm.{s,u}add.with.overflow.i32 intrinsic 重新计算了 `addl $1, %eax`, ; 并根据结果设置各个 flag %5 = call { i32, i1 } @llvm.uadd.with.overflow.i32(i32 %memload1, i32 1) ; 通过返回的 i1 类型的值判断是否有 cf %CF = extractvalue { i32, i1 } %5, 1 %6 = and i32 %EAX, 255 %7 = call i32 @llvm.ctpop.i32(i32 %6) %8 = and i32 %7, 1 ; 根据 eax 低 8 位 1 的个数 (llvm.ctpop.i32) 设置 pf %PF = icmp eq i32 %8, 0 ; 通过 %eax 的值是否为零设置 zf %ZF = icmp eq i32 %EAX, 0 ; -2147483648 为 0x80000000, 根据符号位设置 sf %highbit = and i32 -2147483648, %EAX %SF = icmp ne i32 %highbit, 0 ; OF 表示有符号加法溢出, 所以通过 llvm.sadd.with.overflow.i32 又计算了一次加法... %9 = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %memload1, i32 1) %OF = extractvalue { i32, i1 } %9, 1 ; 10 movl %eax, -24(%rbp) store i32 %EAX, ptr %stktop_4, align 1 ; 11 movl -24(%rbp), %eax %memload2 = load i32, ptr %stktop_4, align 1 ; mctoll 知道 systemv abi 通过 eax 保存返回值 ret i32 %memload2 }
1.2. 总结
想像中的 assembly lifting 的工作方式:
c -> llvm ir -> asm ---+ ^ | | | +---lifting----+
实现上 assembly lifting 的工作方式:
c -> llvm ir -> asm --lifting--> llvm ir
mctoll 的主要思路是:
- 汇编中的寄存器, stack 等均被转换为 llvm 局部变量
- 把汇编指令转换为 llvm 指令以及 llvm intrinsic
- 特别的, 需要根据调用约定识别出函数参数和返回值, 对应 llvm 的函数参数和返回值, 而不是普通的局部变量
由于 x86 指令的复杂性 (例如 addl 对各个 flag 寄存器的修改), 一条 x86 汇编往往需要生成多条 llvm 指令
mctoll 把寄存器做为 llvm 局部变量的做法, 和 QEMU TCG 类似. 不同的是, mctoll 可以使用 llvm opt 和 llc 对 llvm ir 进行优化和 lower, 最终这些局部变量极可能会重新变成寄存器. 另外, llvm opt 也可以去掉一些没有用到的 llvm ir (例如针对 flag 寄存器的处理)
例如, 通过 mctoll+opt+llc 生成的 riscv 汇编为:
main: addi sp, sp, -32 lui a2, 228023 slli a2, a2, 2 addi a2, a2, -273 sd a2, 0(sp) li a2, 0 sw a2, 28(sp) sw a0, 24(sp) sd a1, 16(sp) lw a0, 24(sp) sw a0, 12(sp) lw a0, 24(sp) addiw a0, a0, 1 addi sp, sp, 32 ret
1.3. 为什么使用 llvm ir
尝试把 x86 asm 直接转换为 riscv asm:
main: # prologue: # riscv 的 prologue 类似于下面这种: # addi sp, sp, xxx # sw ra, xx(sp) # sw ..... # 需要计算出 stack size, csr, 还要考虑 riscv 对 stack 对齐的要求 pushq %rbp movq %rsp, %rbp # 可以把 rbp 可以映射为 fp, 但是若 target 不支持 fp 怎么处理? 手工转换为 sp? movl $0, -4(%rbp) # edi, rsi 可以映射为 a0, a1, 但 riscv 的 calling convertion 与 x86 不同, 例 # 如 x86 前 6 个参数放在寄存器, 其它放在栈上, 但 riscv 前 8 个放在寄存器上, # 需要怎么转换 movl %edi, -8(%rbp) # 如果 riscv 不支持 rv64g, 无法直接映射 64 位的 rsi 到 riscv 寄存器 movq %rsi, -16(%rbp) # x86_64 只有 10 个 gpr, 但 riscv 有 32 个 gpr, 直接映射无法利用全部 riscv 寄存器 movl -8(%rbp), %eax movl %eax, -20(%rbp) movl -8(%rbp), %eax # x86 的 addl 需要修改多个 flag, riscv 需要生成多条指令, 这些指令分配寄存器 # 时怎么避免冲突? riscv 中如何检查 add 是否有溢出? addl $1, %eax movl %eax, -24(%rbp) movl -24(%rbp), %eax popq %rbp retq
上面注释中列出来的所有问题, 其实在实现后端时都已经处理过了…通过转换为 llvm ir, 可以避免重新实现一次后端.
另外, 利用 llvm opt, 还可以对生成的 llvm ir 进行优化, 可能生成比原来的汇编更好的代码.