5.6 LLVM

简介

LLVM 是当今炙手可热的编译器基础框架。它从一开始就采用了模块化设计的思想,使得每一个编译阶段都被独立出来,形成了一系列的库。LLVM 使用面向对象的 C++ 语言开发,为编译器开发人员提供了易用而丰富的编程接口和 API。

初步使用

首先我们通过著名的 helloWorld 来熟悉下 LLVM 的使用。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. printf("hello, world\n");
  5. }

将 C 源码转换成 LLVM 汇编码:

  1. $ clang -emit-llvm -S hello.c -o hello.ll

生成的 LLVM IR 如下:

  1. ; ModuleID = 'hello.c'
  2. source_filename = "hello.c"
  3. target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
  4. target triple = "x86_64-unknown-linux-gnu"
  5. @.str = private unnamed_addr constant [14 x i8] c"hello, world\0A\00", align 1
  6. ; Function Attrs: noinline nounwind optnone sspstrong uwtable
  7. define i32 @main() #0 {
  8. %1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
  9. ret i32 0
  10. }
  11. declare i32 @printf(i8*, ...) #1
  12. attributes #0 = { noinline nounwind optnone sspstrong uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
  13. attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
  14. !llvm.module.flags = !{!0, !1, !2}
  15. !llvm.ident = !{!3}
  16. !0 = !{i32 1, !"wchar_size", i32 4}
  17. !1 = !{i32 7, !"PIC Level", i32 2}
  18. !2 = !{i32 7, !"PIE Level", i32 2}
  19. !3 = !{!"clang version 5.0.1 (tags/RELEASE_501/final)"}

该过程从词法分析开始,将 C 源码分解成 token 流,然后传递给语法分析器,语法分析器在 CFG(上下文无关文法)的指导下将 token 流组织成 AST(抽象语法树),接下来进行语义分析,检查语义正确性,最后生成 IR。

LLVM bitcode 有两部分组成:位流,以及将 LLVM IR 编码成位流的编码格式。使用汇编器 llvm-as 将 LLVM IR 转换成 bitcode:

  1. $ llvm-as hello.ll -o hello.bc

结果如下:

  1. $ file hello.bc
  2. hello.bc: LLVM IR bitcode
  3. $ xxd -g1 hello.bc | head -n5
  4. 00000000: 42 43 c0 de 35 14 00 00 05 00 00 00 62 0c 30 24 BC..5.......b.0$
  5. 00000010: 49 59 be 66 ee d3 7e 2d 44 01 32 05 00 00 00 00 IY.f..~-D.2.....
  6. 00000020: 21 0c 00 00 4d 02 00 00 0b 02 21 00 02 00 00 00 !...M.....!.....
  7. 00000030: 13 00 00 00 07 81 23 91 41 c8 04 49 06 10 32 39 ......#.A..I..29
  8. 00000040: 92 01 84 0c 25 05 08 19 1e 04 8b 62 80 10 45 02 ....%......b..E.

反过来将 bitcode 转回 LLVM IR 也是可以的,使用反汇编器 llvm-dis:

  1. $ llvm-dis hello.bc -o hello.ll

其实 LLVM 可以利用工具 lli 的即时编译器(JIT)直接执行 bitcode 格式的程序:

  1. $ lli hello.bc
  2. hello, world

接下来使用静态编译器 llc 命令可以将 bitcode 编译为特定架构的汇编语言:

  1. $ llc -march=x86-64 hello.bc -o hello.s

也可以使用 clang 来生成,结果是一样的:

  1. $ clang -S hello.bc -o hello.s -fomit-frame-pointer

结果如下:

  1. .text
  2. .file "hello.c"
  3. .globl main # -- Begin function main
  4. .p2align 4, 0x90
  5. .type main,@function
  6. main: # @main
  7. .cfi_startproc
  8. # BB#0:
  9. pushq %rbp
  10. .Lcfi0:
  11. .cfi_def_cfa_offset 16
  12. .Lcfi1:
  13. .cfi_offset %rbp, -16
  14. movq %rsp, %rbp
  15. .Lcfi2:
  16. .cfi_def_cfa_register %rbp
  17. movabsq $.L.str, %rdi
  18. movb $0, %al
  19. callq printf
  20. xorl %eax, %eax
  21. popq %rbp
  22. retq
  23. .Lfunc_end0:
  24. .size main, .Lfunc_end0-main
  25. .cfi_endproc
  26. # -- End function
  27. .type .L.str,@object # @.str
  28. .section .rodata.str1.1,"aMS",@progbits,1
  29. .L.str:
  30. .asciz "hello, world\n"
  31. .size .L.str, 14
  32. .ident "clang version 5.0.1 (tags/RELEASE_501/final)"
  33. .section ".note.GNU-stack","",@progbits

参考资料