LLVM
LLVM 简介
LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。
LLVM环境搭建
预编译包安装
编译安装
1 |
|
LLVM整体设计
llvm 与 gcc
- 架构和设计
- LLVM编译器是基于模块化、可扩展的设计,它将编译过程划分为多个独立的阶段,。并使用中间表示(IR)作为通用的数据结构进行代码优化和生成。而GCC编译器则是集成了多个前端和后端的传统编译器,其设计更加紧密一体化
- 开发语言和前端支持
- LLVM编译器使用C++开发,并提供了广泛的前端支持,可以处理多种编程语言(如C、C++、Rust等),这使得开发者能够使用统一的编译框架来处理不同的语言。GCC编译器则使用C语言开发,并对各种编程语言提供了广泛的前端支持。
- 优化功能
- LLVM编译器一起高度模块化的中间表示(IR)为基础,具备强大的代码优化能力。同时,LLVM的设计也使得优化部分可以在编译过程的各个阶段进行,从而实现更细粒度的优化。GCC编译器也提供了一系列的优化选项,但其优化能力相对较低。
- 社区支持和生态系统
- LLVM拥有庞大而活跃的开源社区,并且有很多基于LLVM的工具和项目,如Clang、LLDB等。GCC也有强大的开源社区支持,但相对于LLVM稍显逊色。
llvm 结构
- 前端解析源代码,检查错误,并构建特定于语言的抽象语法树(AST)来表示输入代码。AST 可以选择转换为新的表示形式以进行优化,并且优化器和后端在代码上运行
- 优化器负责进行各种转换以尝试提高代码的运行时间,例如消除冗余计算,并且通常或多或少独立于语言和目标
- 后端(也称为代码生成器)将代码映射到目标指令集。除了编写正确的代码之外,它还负责生成利用受支持架构的不寻常功能的良好代码。编译器后端的常见部分包括指令选择、寄存器分配和指令调度。
项目组成
- Clang:解析 C/C++代码
- MLIR:构建可重用和扩展编译器基础设施的新颖方法
- OpenMP:提供了一个OpenMP运行时库函数
- polly:使用多面体模型实现了一套缓存局部性优化以及自动并行和向量化
- LLDB、libc++、libc++ABI、compiler-rt、libclc、klee、LLD、BOLT
命令/工具
- llc - LLVM静态编译器
- lli - 直接从 LLVM 位码执行程序
- llvm-as - LLVM 编译器
- llvm-dis - LLVM 反编译器
- opt - LLVM 优化器
clang前端
- 预处理(Preprocessor):头文件以及宏的处理;
- 词法分析(Lexer):词法分析器的任务是从左向右逐行扫描程序的字符,识别出各个单词并确定单词的类型,将识别出的单词转换成统一的机内表示——词法单元(token)形式;
- 语法分析(Parser):主要任务是从词法分析器输出的token序列中识别出各类短语,并构造语法分析树。如果输入字符串的各个单词恰好自左至右地站在分析树的各个叶结点上,那么这个词串就是该语言的一个句子,语法分析树描述了句子的语法结构;
- 语义分析(Sema):搜集标识符的属性信息与语义检查。标识符的属性种属(kind)、类型(Type)、存储位置和长度、值、作用域、参数和返回值类型。语义检查包括变量或过程未经声明就使用、变量或过程名重复声明、运算分量类型不匹配、操作符与操作数之间的类型不匹配。
- 代码生成(CodeGen):将AST转换成相应的llvm代码。
LLVM IR
基本概念
高级语言经过clang前端会将代码解析为平台无关的中间表示(IR),使编译器能够在编译、链接、以及代码生成的各个阶段忽略语言特性,进行全面有效的优化和分析。
LLVM基于统一的中间表示来实现优化遍,中间表示采用静态单赋值形式,该形式的虚拟指令集能够高效的表示高级语言,具有灵活性好、类型安全、底层操作等特点。如图所示,当同一变量出现多次赋值时,通过SSA变量重命名的方式加以区分,可以避免出现多次定义的情况。
IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用 LLVM IR的各种编译优化、JIT支持、目标代码生成等功能。
IR表示
LLVM IR有三种形式:
- 内存中的表示形式
- bitcode形式
- LLVM汇编文件格式
IR表示:
- Module类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件。
- Function类,这个类顾名思义就是对应于一个函数单元。Function可以描述两种情况,分别是函数定义和函数声明。
- BasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,就相当于程序流程图中的基本过程。
- Instruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等。
控制流图CFG:
1 |
|
代码生成
基本概念
LLVM 目标无关代码生成器是一个框架,它提供了一套可重用组件,用于将 LLVM 内部表示转换为指定目标的机器代码,可以是汇编形式(适用于静态编译器),也可以是二进制机器代码格式(适用于JIT编译器)。
LLVM 后端的主要功能是代码生成,因此也叫代码生成器。后端包括若干个代码生成分析转换pass将 LLVM IR转换成特定目标架构的机器代码。
- 源码结构:
- 特定目标的抽象目标描述接口的实现。这些机器描述使用 LLVM 提供的组件,并可以选择提供定制的特定于目标的传递,为特定目标构建完整的代码生成器。目标描述位于lib/Target中。
- 用于实现本机代码生成的各个阶段(寄存器分配、调度、堆栈帧表示等)的目标无关算法。此代码位于lib/COdeGen中。
- 目标独立组件JIT组件。LLVM JIT 完全独立于目标(使用TargetJITInfo结构来处理特定于目标的问题。独立于目标的JIT的代码位于lib/ExecvtionEngine/JIT中)
LLVM Pass
pass 介绍
第一个 Pass:Hello Pass
1 |
|
使用cmake编译
1 |
|
1 |
|
命令行编译
命令行编译是最简单暴力的方法,以Hello Pass为例:
1 |
|
其中
llvm-config
提供了CXXFLAGS
与LDFLAGS
参数方便查找LLVM的头文件与库文件。 如果链接有问题,还可以用llvm-config --libs
提供动态链接的LLVM库。 具体llvm-config
打印了什么,请自行尝试或查找官方文档。-fPIC -shared
显然是编译动态库的必要参数。- 因为LLVM没用到RTTI,所以用
-fno-rtti
来让我们的Pass与之一致。 -Wl,-znodelete
主要是为了应对LLVM 5.0+中加载ModulePass引起segmentation fault的bug。如果你的Pass继承了ModulePass,还请务必加上。
现在,你手中应该有一份编译好的LLVMHello.so了。根据刚才阅读过的官方文档的介绍,你知道可以通过命令
1 |
|
来使用它。
自动使用Clang运行 Pass
当代码文件比较多的时候,你会觉得先把源代码编译成IR代码,然后用opt运行你的Pass实在麻烦且无趣。 恰好在你手头已有一些构建工具时,你可能会想,如果能把Pass集成到clang的参数中调用,那该多好啊。 因为这样你就可以做这样的事情 (假设你的构建工具是autotools):
1 |
|
下面这篇文章就告诉了你该怎么做,请仔细阅读。当你读完后,你可能会觉得,这魔法参数也太丑陋了吧。我也觉得。“Maybe this is life”。
现在回头看看前面Hello.cpp
,有留意到里面的两行注释吗? static RegisterPass<Hello> X
是给opt加载Pass用的, static RegisterStandardPasses Y
是给clang加载Pass用的, 有时候两者只要选一个就行了。希望在读完上面这篇文章后你能理解得更深入。
现在,你可以在clang中直接加载Hello Pass了
1 |
|
当然,你还觉得这不够优雅的话,也可以编写一个clang的wrapper程序hello-clang
。 它会读取命令行参数,然后加上-Xclang -load -Xclang path/to/LLVMHello.so
构造成新的命令行参数。 最后调用execvp()
执行clang
。
举例来说,如果输入hello-clang main.c -o main
, 那么它会调整参数,最终执行clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main
。
不用我说,你也能想到这个画面:
1 |
|
结合 Clnag 插桩的注意点
一般来说,插桩代码的时候,我们往往会在源代码中插入一些call指令来调用我们实现的函数。 举个例子,你可能会想写一个MemTrace Pass来监控运行时内存的访问。所以它会在所有访问内存的指令前插入一个call my_memlog(mem_addr)
指令来记录这次的内存访问。
假如MemTrace Pass编译在libmemtrace.so
中,my_memlog()
函数编译在libmemlog.a
中, 那么我们不要忘记在编译的时候链接它:
1 |
|
你也可以和上面的hello-clang
一样,把它封装到一个clang wrapper中。
更难一些的Pass
现在是仔细瞧瞧
LLVM Programmer’s Manualllvm.org/docs/ProgrammersManual.html
的时候了。 其中,结合
,读者可以再仔细去看看ProgrammersManual - The Core LLVM Class Hierarchy Reference这一小节,回顾一下LLVM IR在内存中的表示。 也记得看看Helpful Hints for Common Operations这一小节,学习一下怎么遍历IR、修改指令。 当你看完这些后,那个github项目你也肯定能看懂了。
参考项目
后言
参考链接:LLVM架构简介 - LLVM IR入门指南 (evian-zhang.github.io)
参考链接:LLVM编译器入门(一):LLVM整体设计
参考链接:基于LLVM-18,使用New Pass Manager,编写和使用Pass