LLVM Basics
资料
LLVM-Techniques-Tips-and-Best-Practies(中文版)
如何操作LLVM IR:
- https://mukulrathi.com/create-your-own-programming-language/llvm-ir-cpp-api-tutorial/
- 可以使用
llc -march=cpp
: https://stackoverflow.com/questions/7787308/how-can-i-declare-a-global-variable-in-llvm - https://llvm.org/docs/LangRef.html 查询IR
中端优化:
- 实现自己的mem2reg(SSA construction):https://www.zzzconsulting.se/2018/07/16/llvm-exercise.html
- 中端优化为什么这么神奇:https://blog.matthieud.me/2020/exploring-clang-llvm-optimization-on-programming-horror/
YouTube的LLVM频道里有很多不错的视频。
-
2019 LLVM Developers’ Meeting: E. Christopher & J. Doerfert “Introduction to LLVM”
果然后端的水很深啊。
-
2019 EuroLLVM Developers’ Meeting: V. Bridgers & F. Piovezan “LLVM IR Tutorial - Phis, GEPs and other things, oh my! - Vince Bridgers (Intel Corporation)”》
讲getelementptr指令讲得特别好,要是当时理解debug info metadata的时候看了这个就好了。
-
2019 LLVM Developers’ Meeting: J. Paquette & F. Hahn “Getting Started With LLVM: Basics”
前半 讲了LLVM IR Pass需要考虑到的一些东西,users的概念,讲了移除基本块和指令时需要注意的。 后半部分讲Backend Pass也非常不错。
-
2019 LLVM Developers’ Meeting: S. Haastregt & A. Stulova “An overview of Clang ”
讲了clang前端的架构。和之前想象中有些不一样。先是driver,然后是前端:词法分析语法分析,生成AST后codegen生成IR。
-
2017 LLVM Developers’ Meeting: D. Michael “XRay in LLVM: Function Call Tracing and Analysis ”
非常不错的Hook介入的框架
-
2018 EuroLLVM Developers’ Meeting: C. Hubain & C. Tessier “Implementing an LLVM based Dynamic Binary Instrumentation framework - Charles Hubain
是Quarkslab的那个QBDI,有时间真得好好学学。
-
2017 LLVM Developers’ Meeting: “Challenges when building an LLVM bitcode Obfuscator ”
汇编混淆确实需要考虑很多情况。。
-
2014 LLVM Developers’ Meeting: “Debug Info Tutorial ”
-
2019 LLVM Developers’ Meeting: S. Haastregt & A. Stulova “An overview of Clang ”
讲了clang前端的架构。和之前想象中有些不一样。先是driver,然后是前端:词法分析语法分析,生成AST后codegen生成IR。
-
2017 LLVM Developers’ Meeting: D. Michael “XRay in LLVM: Function Call Tracing and Analysis
非常不错的Hook介入的框架
-
2018 EuroLLVM Developers’ Meeting: C. Hubain & C. Tessier “Implementing an LLVM based Dynamic Binary Instrumentation framework - Charles Hubain
是Quarkslab的那个QBDI,有时间真得好好学学。
-
2017 LLVM Developers’ Meeting: “Challenges when building an LLVM bitcode Obfuscator ”
汇编混淆确实需要考虑很多情况。
其他:
- language server
- 《Getting Started with LLVM Core Libraries》前端代码转换的部分
debug
打印使用llvm::outs() << xxx;
直接打印llvm Value
声明全局变量
Global variable definitions must be initialized.
Global variables in other translation units can also be declared, in which case they don’t have an initializer.
dso_local
看作是C语言的static,在同一个编译单元内
LinkageTypes
external
是默认的,如果没有initializer就会带一个external,如果有就没有修饰符,正常的符号。
嵌套的指令 nested instrucitons
https://lists.llvm.org/pipermail/llvm-dev/2015-October/091467.html
使用IRBuilder创建的这个其实不是嵌套的指令,LLVM也不支持嵌套的指令,而是创建了GEP constant expression。这种表达式比指令更好,同时蕴含着没有副作用的语义。
PassManger
LLVM PASS的管理有两种实现
- LegacyPass Manger
- NewPass Manger
传统的LegacyPM有一些不足,例如Analysis Pass不能缓存分析的信息导致重复分析等。NewPM将Analysis和Pass做了区分。在LegacyPM中存在过多的全局变量与registries,每个Pass都需要通过宏来注册,NewPM改进了这一点,当然还有内联函数分析等其他优化。LLVM12使用的是LegacyPassManager,13之后默认使用的是NewPassManager,本项目也使采用NewPassManager。
运行与管理
LLVM API允许在应用程序中嵌入LLVM Pass,并将其作为库调用。
调试
我们基于 vscode开发,使用codelldb插件。发现无法下条件断点。
- 首先下普通断点
- 使用
breakpoint list
查看断点编号 - 使用
breakpoint modify 1 -c "((int64_t) (ci->getSExtValue()) < -100)"
这样的命令给断点增加条件
更多使用方式见lldb使用教程
一些非常实用的调试技巧
- 打印指令,值,函数等:
expr llvm::errs() << I
- 打印模块:使用定义的工具函数:
expr notdec::printModule(M, "out.ll")
InstVisitor
class MyVisitor
: public llvm::InstVisitor<MyVisitor> {
访问逻辑:
- 名字仅有visit的为入口函数。访问模块或者函数 (
Visitor.visit(M)
) 会帮你访问里面的指令 - 默认继承的父类的行为:
- 指令的入口函数 (
void visit(Instruction &I)
): (一般不会override)根据指令类型调用细分的visit方法 - 各种其他类型的细分visit方法调用对应的大类visit方法 例如
visitLoadInst -> visitUnaryInstruction
。 - 最后一个大类visit方法
visitInstruction
: 为空。
- 指令的入口函数 (
总结:细分visit方法是我们要override的,如果没有override实现,则会因为继承的默认实现继续分流到大类方法,最终到最通用的visitInstruction
。反过来说如果override了,(如果没有主动去调用)则会截断这类指令的访问,使得这类指令不会主动调用 visitInstruction
。
需要注意,如果想要visitor有个返回值类型,需要在父类模板的第二个参数指定类型,同时必须实现visitInstruction方法,因为至少要给你的返回类型指定一个默认返回值。
修改LLVM值的类型
LLVM值的类型,基于def-use关系,其实利用类型转换还是可以灵活变动。比如一个整数,虽然可能有一些加法运算,但是你还是可以强制把它设成指针类型,然后在每个使用点插入ptrtoint指令转回去。
有些LLVM值的类型,修改起来非常麻烦。尤其是函数的参数和返回值类型。函数也是GlobalValue,甚至是Constant。
打印Pass前后IR
LLVM提供了方便的 --print-after-all
等选项。首先在创建PassBuilder的时候,创建插桩的类并注册CallBack。注册所有的标准插桩后很多选项都可以生效,包括计算Pass的运行时间,是否在每个前后打印Pass,展示IR的变化diff,甚至生成HTML的报告。
// add instrumentations.
PassInstrumentationCallbacks PIC;
StandardInstrumentations SI(::llvm::DebugFlag, false, PrintPassOptions());
SI.registerCallbacks(PIC, &FAM);
现在新的Pass都继承自 PassInfoMixin
类,这个类会增加一个name函数,在运行时可以动态获取类的名称(包含namespace)。如果不确定可以在--print-after-all
后在log里查看。这个名称称为 PassID 。
命令行参数里指定的是通过PassID查找得到的缩略名称。需要提前在 PassInstrumentationCallbacks
中注册:
PIC.addClassToPassName("notdec::LinearAllocationRecovery",
"linear-allocation-recovery");
PIC.addClassToPassName("notdec::PointerTypeRecovery",
"pointer-type-recovery");
此时就可以在命令行中指定相关的pass打印了。比如 --print-before=pointer-type-recovery
。
Clang
AST handling
- 创建AST时有两种方式
- 使用Create方法,如
clang::FunctionDecl::Create(...)
- 没有Create方法时,使用ASTContext的new操作符。如:
new (ASTCtx) clang::GotoStmt(...)
- 使用Create方法,如
CFG handling
CFGBlock包含:
-
LabelStatement,用于生成Goto语句。
-
一系列语句
-
CFGTerminator
- 对于return/unreachable:保存对应的返回语句或函数调用语句。
- 对于br/switch,保存对应的条件表达式。同时保证successor的顺序:本来CFG保证的是If-Then-Else顺序,但是如果是条件跳转,我们使用True-False的顺序,和IR里的successor()顺序一致。
-
CFGBuilder在LLVM中是逆序创建的,即逆向遍历AST,逆向创建语句。在打印或者遍历CFG块的时候,其中
CFGBlock::ElementList
把正常的iterator改用reverse_iterator实现。存储的时候是逆向存储,但是后续读取每个CFGBlock的时候也被偷偷改成逆向读取。但是我们算法如果不是逆向的话,想要在末尾继续插入语句,反而需要插入到开头,内存开销增加了。- 如果将结构恢复算法弄成逆向的,首先找到所有的没有后继节点的结束块,每个结束块分治。
-
对于
if (A && B && C)
,CFGBlock->getTerminatorStmt()
会在C处返回整个A && B && C
表达式。调用getLastCondition()
,会把CFGBlock最后一个statement转换成Expr返回。
BumpVector Memory Allocation
Clang的内存分配都是通过ASTContext进行的,内部使用了一个BumpAllocator,Bump是一种简单的线性内存分配,内存不会真正被释放,放弃内存复用。或者等一整块分配结束之后,再整块全部释放。根据这里,ASTContext解构时释放所有存储AST节点的内存。
- 根据这里,从这里分配的相关的数据结构内部不能有std::vector或者SmallVector这种会在堆上分配内存的数据结构。因为不会调用destructor,导致堆上的内存不会被free,导致内存泄露。
BumpVectorContext存储了一个llvm::PointerIntPair<llvm::BumpPtrAllocator*, 1> Alloc;
,这是一个指针低位复用的数据结构,可以理解为一个指针加一个bool值。用户传入外部的allocator,比如ASTContext的,完全不释放内存。也可以让它新建一个allocator,并在解构的时候释放所有内存。
Clang CFG内置了一个BumpVectorContext,解构时会释放所有内存。因此在结构分析过程中,CFG相关的基本块虽然出现创建后又删除的情况,但不会导致内存泄露。 TODO: 一些临时的AST节点,如一些临时的Goto语句,它们的内存释放怎么办?
这次在BumpVector里增加的erase函数,仅把元素移动到末尾然后解构,和内存泄露应该无关。
clang::QualType
QualType 本质上就是一个指针,同时复用了低位的bit位,存储一些qualifier信息,如volatile等修饰符。
- QualType大部分情况下可以看作是clang::Type*
- QualType有可能为空指针,同时也可以直接创建空的QualType
clang::IdentifierInfo
IdentifierInfo除了名字字符串之外,仅仅存储了一些名字的种类信息,例如是不是define的,是不是keyword,是不是variable或者function name。
- 变量重名:它似乎并不对应作用域,并不用于检测变量重名。
Sema::LookupName
内部手动遍历检测了重名情况。