LLVM IR
LLVM 提供了一个详细的汇编语言表示(参阅 参考资料 获取相关的链接)。在开始编写我们之前讨论的自己的 Hello World 程序版本之前,有几个需知事项:
- LLVM 汇编语言中的注解以分号 (
;
) 开始,并持续到行末。 - 全局标识符要以
@
字符开始。所有的函数名和全局变量都必须以@
开始。 - LLVM 中的局部标识符以百分号 (
%
) 开始。标识符典型的正则表达式是[%@][a-zA-Z$._][a-zA-Z$._0-9]*
。 - LLVM 拥有一个强大的类型系统,这也是它的一大特性。LLVM 将整数类型定义为
i*N*
,其中 N 是整数占用的字节数。您可以指定 1 到 223- 1 之间的任意位宽度。 - 您可以将矢量或阵列类型声明为
[no. of elements X size of each element]
。对于字符串 "Hello World!",可以使用类型[13 x i8]
,假设每个字符占用 1 个字节,再加上为 NULL 字符提供的 1 个额外字节。 - 您可以对 hello-world 字符串的全局字符串常量进行如下声明:
@hello = constant [13 x i8] c"Hello World!\00"
。使用关键字constant
来声明后面紧跟类型和值的常量。我们已经讨论过类型,所以现在让我们来看一下值:您以c
开始,后面紧跟放在双引号中的整个字符串(其中包括\0
并以0
结尾)。不幸的是,关于字符串的声明为什么需要使用c
前缀,并在结尾处包含 NULL 字符和 0,LLVM 文档未提供任何解释。如果您有兴趣研究更多有关 LLVM 的语法怪现象,请参阅 参考资料,获取语法文件的链接。 - LLVM 允许您声明和定义函数。而不是仔细查看 LLVM 函数的整个特性列表,我只需将精力集中在基本要点上即可。以关键字
define
开始,后面紧跟返回类型,然后是函数名。返回 32 字节整数的main
的简单定义类似于:define i32 @main() { ; some LLVM assembly code that returns i32 }
。 - 函数声明,顾名思义,有着重大的意义。这里提供了
puts
方法的最简单声明,它是printf
:declare i32 puts(i8*)
的 LLVM 等同物。该声明以关键字declare
开始,后面紧跟着返回类型、函数名,以及该函数的可选参数列表。该声明必须是全局范围的。 - 每个函数均以返回语句结尾。有两种形式的返回语句:
ret <type> <value>
或ret void
。对于您简单的主例程,使用ret i32 0
就足够了。 - 使用
call <function return type> <function name> <optional function arguments>
来调用函数。注意,每个函数参数都必须放在其类型的前面。返回一个 6 位的整数并接受一个 36 位的整数的函数测试的语法如下:call i6 @test( i36 %arg1 )
。
这只是一个开始。您还需要定义一个主例程、一个存储字符串的常量,以及处理实际打印的puts
方法的声明。清单 3 显示第一次尝试创建的程序。
- 第一次尝试创建手动编写的 Hello World 程序
declare i32 @puts(i8*)
@global_str = constant [13 x i8] c"Hello World!\00"
define i32 @main {
call i32 @puts( [13 x i8] @global_str )
ret i32 0
}
这里提供了来自 lli
的日志:
lli: test.s:5:29: error: global variable reference must have pointer type
call i32 @puts( [13 x i8] @global_str )
^
程序并未按预期的运行。发生了什么?如之前所提及的,LLVM 拥有一个强大的类型系统。因为 puts
期望提供一个指向 i8
的指针,并且您能传递一个 i8
矢量,这样 lli
才能快速指出错误。该问题的常用解决方法(来自 C
编程背景)是使用类型转换。这将您引向了 LLVM 指令 getelementptr
。请注意,您必须将 清单 3 中的 puts
调用修改为与 call i32 @puts(i8* %t)
类似,其中 %t
是类型 i8*
,并且是 [13 x i8] to i8*
的类型转换结果。(请参阅 参考资料,获取 getelementptr
的详细描述的链接。)在进一步探讨之前,清单 4 提供了可行的代码。
- 使用 getelementptr 正确地将类型转换为指针
declare i32 @puts (i8*)
@global_str = constant [13 x i8] c"Hello World!\00"
define i32 @main() {
%temp = getelementptr [13 x i8]* @global_str, i64 0, i64 0
call i32 @puts(i8* %temp)
ret i32 0
}
getelementptr
的第一个参数是全局字符串变量的指针。要单步执行全局变量的指针,则需要使用第一个索引,即 i64 0
。因为 getelementptr
指令的第一个参数必须始终是 pointer
类型的值,所以第一个索引会单步调试该指针。0 值表示从该指针起偏移 0 元素偏移量。我的开发计算机运行的是 64 位 Linux®,所以该指针是 8 字节。第二个索引 (i64 0
) 用于选择字符串的第 0 个元素,该元素是作为 puts
的参数来提供的。
创建一个自定义的 LLVM IR 代码生成器
LLVM 提供了一款出色的工具,叫做 llvm-config
(参阅 参考资料)。运行 llvm-config –cxxflags
,获取需要传递至 g++ 的编译标志、链接器选项的 llvm-config –ldflags
以及 llvm-config –ldflags
,以便针对正确的 LLVM 库进行链接。在 清单 5 的样例中,所有的选项均需要传递至 g++。
一个自动生成函数流程控制图(Control Flow Graph
, CFG
)和调用关系图(Call Graph
,CG
)的脚本
clang $1.c -emit-llvm -S
opt -dot-cfg $1.ll > /dev/null
opt -dot-callgraph $1.ll > /dev/null
dot -Tpng -o $1.png cfg.main.dot
rm cfg.main.dot
dot -Tpng -o $1.callgraph.png callgraph.dot
rm callgraph.dot
使用sh gen_bc2dot.sh 1
,文件名不带拓展名即可。
可视化LLVM IR控制流图
使用LLC生成可视化SelectionDAG
https://blog.csdn.net/qq_27885505/article/details/80366525
依赖安装graphviz
:
sudo apt-get install graphviz
llvm:debug版本(我看网上有人说只有debug版本才能够使用可视化的命令,其他release版本没有尝试过)默认前提已经装好了llvm,就不再讲述llvm编译过程。
- 展示第一次优化PASS前的DAG
llc -view-dag-combine1-dags mul.ll
- 展示合法化之前的DAG
llc -view-legalize-dags test.ll
- 展示第二次优化PASS前的DAG
llc -view-dag-combine2-dags mul.ll
- 展示在执行指令选择(ISel InstructionSelect)阶段之前的DAG
llc -view-isel-dags mul.ll
- 展示在执行指令调度之前的DAG
llc -view-sched-dags test.ll
- 展示指令调度器的依赖图
llc -view-sunit-dags test.ll
参考资料
https://www.ibm.com/developerworks/cn/opensource/os-createcompilerllvm1/index.html
LLVM Cookbook