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 的主要思路是:

  1. 汇编中的寄存器, stack 等均被转换为 llvm 局部变量
  2. 把汇编指令转换为 llvm 指令以及 llvm intrinsic
  3. 特别的, 需要根据调用约定识别出函数参数和返回值, 对应 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 进行优化, 可能生成比原来的汇编更好的代码.

Author: [email protected]
Date: 2023-08-02 Wed 10:41
Last updated: 2023-08-02 Wed 18:58

知识共享许可协议