llvm,全称Low-Level Virtual Machine,顾名思义,就是“底层虚拟机”。作为一个功能完备的编程语言后端,已经被clangghc等编译器支持(其实clang是完全依赖于llvm的)。通过一套LLVM IR中间字节码/语言,前端和后端(以及优化器/JIT等中间件)得以共享一套简洁易用的表示。

​ 关于llvm的架构以及与传统编译系统的区别,这里有一篇非常好的文章。我在此也不多做阐述了。

安装

​ 因为这里没有需求,我并没有下载llvm的源码进行编译,仅仅只是把相关的软件包和工具集进行了安装。

​ 体验llvm,最直接的方式就是先安装clang(这也是官网上推荐的方式 :wink: )

1
sudo apt install clang

clanggcc的用法差别不大,对应的也有支持C++clang++

​ 写一个测试程序

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc,char ** argv) {
int a, b, c;
scanf("%d%d", &a, &b);
c = a + b;
printf("%d + %d = %d\n", a, b, c);
return 0;
}

​ 用clang编译

1
2
3
4
phosphorus15@ubuntu:~/ir$ clang aplusb.c -o aplusb.bin
phosphorus15@ubuntu:~/ir$ ./aplusb.bin
3 5
3 + 5 = 8

也是一个挺正常的C编译器嘛。因为clang是用llvm作为后端的,此时系统里其实已经有了很多llvm的相关库了。下面,我们安装llvm主体和主要组件

1
2
sudo apt install llvm-runtime # 运行时
sudo apt install llvm # 编译/优化工具

基本使用

​ 前面提到LLVM IR及其字节码是llvm架构中前后端通用的形式,因为clang是以llvm为后端,不难想见,我们可以让clang生成LLVM IR供我们参考。命令如下:

1
phosphorus15@ubuntu:~/ir$ clang -emit-llvm aplusb.c -c -o aplusb.bc

​ 这样,目录下就多出了一个aplusb.bc,需要注意的是,*.bc文件是已经被编码之后的字节码形式,并不是LLVM IR代码,如果直接查看,是不能看到任何有意义的内容的。

​ 此时,如果file这个生成的*.bc文件,会有如下结果

1
2
phosphorus15@ubuntu:~/ir$ file aplusb.bc
aplusb.bc: LLVM IR bitcode

​ 显示这是一个字节码文件。

​ 和Java虚拟机一样,LLVM也可以直接执行字节码,使用lli命令就能运行一个字节码文件

1
2
3
phosphorus15@ubuntu:~/ir$ lli aplusb.bc
15 27
15 + 27 = 42

​ 那么,有了字节码文件,如何查看相应的LLVM IR呢?这时就要用到llvm-dis命令,在bash下键入

1
phosphorus15@ubuntu:~/ir$ llvm-dis < aplusb.bc > aplusb.ll

​ 这样,就生成了一个包含LLVM IR*.ll文件,查看文件,有以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
; ModuleID = '<stdin>'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [5 x i8] c"%d%d\00", align 1
@.str.1 = private unnamed_addr constant [14 x i8] c"%d + %d = %d\0A\00", align 1

; Function Attrs: nounwind uwtable
define i32 @main(i32 %argc, i8** %argv) #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i8**, align 8
%a = alloca i32, align 4
%b = alloca i32, align 4
%c = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 %argc, i32* %2, align 4
store i8** %argv, i8*** %3, align 8
%4 = call i32 (i8*, ...) @__isoc99_scanf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i32 0, i32 0), i32* %a, i32* %b)
%5 = load i32, i32* %a, align 4
%6 = load i32, i32* %b, align 4
%7 = add nsw i32 %5, %6
store i32 %7, i32* %c, align 4
%8 = load i32, i32* %a, align 4
%9 = load i32, i32* %b, align 4
%10 = load i32, i32* %c, align 4
%11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.1, i32 0, i32 0), i32 %8, i32 %9, i32 %10)
ret i32 0
}

declare i32 @__isoc99_scanf(i8*, ...) #1

declare i32 @printf(i8*, ...) #1

attributes #0 = { nounwind uwtable "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" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "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" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.ident = !{!0}

!0 = !{!"clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)"}

​ 暂且不关注这些IR内容的意义(其实我写到这里也不太清楚:joy:),先继续看看llvm的其它组件。

​ 既然可以把字节码反编译成IR,那相对的,应该也是可以进行编译的,我们有llvm-as命令进行编译(其实应该叫编码?)操作

1
2
3
4
5
6
7
8
phosphorus15@ubuntu:~/ir$ llvm-as < aplusb.ll > aplusb1.bc
phosphorus15@ubuntu:~/ir$ ls
aplusb1.bc aplusb.bin aplusb.ll test.ll
aplusb.bc aplusb.c test.bc test.s
phosphorus15@ubuntu:~/ir$ md5sum < aplusb.bc
ecfb8b7884fecf9e71c35b6a07afcd0c -
phosphorus15@ubuntu:~/ir$ md5sum < aplusb1.bc
ecfb8b7884fecf9e71c35b6a07afcd0c -

​ 可以看到,llvm-as生成的aplusb1.bc字节码和原来的aplusb.bc具有相同的MD5校验和,因此可以初步判断两个文件是完全一致的。

​ 从llvm的机制就知道,到字节码,一切都还没有结束,llc命令可以让我们更进一步,通过IR字节码生成目标平台下的汇编文件。

1
phosphorus15@ubuntu:~/ir$ llc aplusb.bc -o aplusb.s

​ 默认生成的汇编是当前平台的,如果想要生成别的目标平台的汇编文件,修改参数就能很轻易地改变llc的目标平台

1
phosphorus15@ubuntu:~/ir$ llc -march=arm aplusb.bc -o aplusb-arm.s # 生成 arm 平台的汇编

​ 我们可以看看两个文件的main代码段当中一些很明显的对比(其实就是想凑凑字数:smile:)

  • aplusb - x86_64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    main:                                   # @main
    .cfi_startproc
    # BB#0:
    pushq %rbp
    .Ltmp0:
    .cfi_def_cfa_offset 16
    .Ltmp1:
    .cfi_offset %rbp, -16
    movq %rsp, %rbp
    .Ltmp2:
    .cfi_def_cfa_register %rbp
    subq $32, %rsp
    ...
    callq __isoc99_scanf
    ...
    movl $.L.str.1, %edi
    xorl %eax, %eax
    callq printf
    ...
    retq
  • aplusb - arm

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    main:                                   @ @main
    .fnstart
    @ BB#0:
    push {r11, lr}
    mov r11, sp
    sub sp, sp, #32
    ...
    bl __isoc99_scanf
    ...
    str r3, [sp, #4]
    bl printf
    mov r0, #0
    mov sp, r11
    pop {r11, lr}
    mov pc, lr
    .align 2

​ 因为架构的差异,很多指令和操作的实现方式都风格迥异,但由此我们也能从侧面看出llvm的强大能力——把来自不同前端编译器(意味着不同语言、子语言)的IR输出成不同平台的汇编。其弹性和兼容性都是十分可观的。

​ 当得到汇编文件以后,就能够直接进行编译了,可以使用一些常见的汇编器,也可以直接用gcc来完成汇编和链接。

1
2
3
4
phosphorus15@ubuntu:~/ir$ gcc aplusb.s -o aplusb.native
phosphorus15@ubuntu:~/ir$ ./aplusb.native
233 666
233 + 666 = 899

​ 由此,已经大致介绍完了大部分常用的相关命令,更多内容可以参考官方文档