LLVM Pass-PWN:从理论到实践,从入门到精通

内容包含LLVM Pass类PWN详细解读

环境搭建

我用kali本机只能下载有15和以上的,其他的缺少依赖项,懒得弄了,直接用docker搭建ubuntu20.04的可以下载

  1. sudo apt install clang-8
  2. sudo apt install llvm-8
  3. sudo apt install clang-10
  4. sudo apt install llvm-10
  5. sudo apt install clang-12
  6. sudo apt install llvm-12

基础知识

图片.png

图片.png

图片.png

LLVM IR

LLVM IR即代码的中间表示,有三种形式:

LLVM IR(Intermediate Representation,中间表示)是LLVM编译器框架中的一种中间代码表示形式。LLVM IR有三种主要的表示形式:

  1. .ll 格式:人类可读的文本格式。
  2. .bc 格式:适合机器存储和处理的二进制格式。
  3. 内存表示:LLVM编译器在运行时使用的内存中的数据结构。

实例

假设我们有一个简单的C语言代码:

  1. int add(int a, int b) {
  2. return a + b;
  3. }

编译这个代码时,LLVM会将其转换为LLVM IR。我们可以通过不同的方式查看和存储这个LLVM IR。

1. .ll 格式(人类可读的文本格式)

这是LLVM IR的文本表示形式,适合人类阅读和编辑。你可以通过命令行工具clangllvm-dis生成这个格式。

  1. ; ModuleID = 'example.c'
  2. source_filename = "example.c"
  3. define i32 @add(i32 %a, i32 %b) {
  4. entry:
  5. %0 = add i32 %a, %b
  6. ret i32 %0
  7. }

在这个例子中:

  • define i32 @add(i32 %a, i32 %b) 定义了一个返回类型为i32(32位整数)的函数add,它有两个参数ab,类型都是i32
  • entry: 是函数的入口基本块。
  • %0 = add i32 %a, %b 表示将ab相加,并将结果存储在临时变量%0中。
  • ret i32 %0 返回结果%0

2. .bc 格式(二进制格式)

这是LLVM IR的二进制表示形式,适合机器存储和处理。它通常比文本格式更紧凑,适合在编译器内部传递或存储在磁盘上。

你可以通过clangllvm-as工具生成这个格式:

  1. clang -emit-llvm -c example.c -o example.bc

生成的example.bc文件是二进制格式,无法直接阅读,但可以通过llvm-dis工具将其转换回人类可读的.ll格式:

  1. llvm-dis example.bc -o example.ll

3. 内存表示

当LLVM编译器在运行时处理代码时,LLVM IR会以内存中的数据结构形式存在。这种表示形式是LLVM编译器内部使用的,通常是通过C++对象和数据结构来表示的。

例如,函数add在内存中可能表示为一个llvm::Function对象,基本块entry表示为一个llvm::BasicBlock对象,指令%0 = add i32 %a, %b表示为一个llvm::Instruction对象。

这些内存中的对象和数据结构允许LLVM进行各种优化和代码生成操作。开发者可以通过LLVM的C++ API来操作这些内存表示,例如添加、删除或修改指令,或者进行各种优化。

  1. .c -> .llclang -emit-llvm -S a.c -o a.ll
  2. .c -> .bc: clang -emit-llvm -c a.c -o a.bc
  3. .ll -> .bc: llvm-as a.ll -o a.bc
  4. .bc -> .ll: llvm-dis a.bc -o a.ll
  5. .bc -> .s: llc a.bc -o a.s

LLVM IR的处理

LVM Pass可用于对代码进行优化或者对代码插桩(插入新代码),LLVM的核心库中提供了一些Pass类可以继承,通过实现它的一些方法,可以对传入的LLVM IR进行遍历并操作。

LLVM对IR中的函数、基本块(basic block)以及基本块内的指令的处理

示例函数

假设我们有一个简单的 C 语言函数:

  1. void example_function(int x, int y) {
  2. int result;
  3. if (x > y) {
  4. result = x + y;
  5. } else {
  6. result = x * y;
  7. }
  8. printf("The result is %d\n", result);
  9. }

编译为 LLVM IR

将上述函数编译为 LLVM IR 可能会产生类似以下的中间表示:

  1. define void @example_function(i32 %x, i32 %y) {
  2. entry:
  3. %result = alloca i32
  4. %x_gt_y = icmp sgt i32 %x, %y
  5. br i1 %x_gt_y, label %iftrue, label %iffalse
  6. iftrue:
  7. %add_result = add i32 %x, %y
  8. br label %after_if
  9. iffalse:
  10. %mul_result = mul i32 %x, %y
  11. br label %after_if
  12. after_if:
  13. %phi_result = phi i32 [ %add_result, %iftrue ], [ %mul_result, %iffalse ]
  14. store i32 %phi_result, i32* %result
  15. %print_result = call void @printf(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @.str, i32 0, i32 0), i32 %phi_result)
  16. ret void
  17. @.str = private unnamed_addr constant [17 x i8] c"The result is %d\n\00"

解释

  • 函数(Function): @example_function 是一个函数,它接受两个整数参数 xy 并打印它们的运算结果。
  • 基本块(Basic Block): 一个基本块是一段连续的指令序列,控制流不能从中中断。在这个示例中,我们有三个基本块:
  1. - `entry`: 函数的入口点,分配局部变量空间并进行条件跳转。
  2. - `iftrue`: `x > y` 时执行的代码块。
  3. - `iffalse`: `x <= y` 时执行的代码块。
  4. - `after_if`: 无论哪个条件分支被执行之后的合并点。
  • 基本块内的指令(Instructions): 每个基本块包含了一系列的指令。例如,在 entry 基本块中,我们有:
  1. - `%result = alloca i32`:分配一个整数类型的局部变量 `result`
  2. - `%x_gt_y = icmp sgt i32 %x, %y`:比较 `x` 是否大于 `y`
  3. - `br i1 %x_gt_y, label %iftrue, label %iffalse`:根据条件跳转到 `iftrue` 或者 `iffalse` 基本块。

iftrue 基本块中,我们有:

  • %add_result = add i32 %x, %y:如果 x > y,则计算 x + y
  • br label %after_if:无条件跳转到 after_if 基本块。

iffalse 基本块中,我们有:

  • %mul_result = mul i32 %x, %y:如果 x <= y,则计算 x * y
  • br label %after_if:无条件跳转到 after_if 基本块。

after_if 基本块中,我们有:

  • %phi_result = phi i32 [ %add_result, %iftrue ], [ %mul_result, %iffalse ]:选择 iftrueiffalse 中的结果作为最终结果。
  • store i32 %phi_result, i32* %result:将结果存储到局部变量 result 中。
  • %print_result = call void @printf(...):调用 printf 函数打印结果。
  • ret void:返回空值,结束函数。

流程

  • LLVM PASS就是去处理IR文件,通过opt利用写好的so库优化已有的IR,形成新的IR。
  • LLVM PASS类PWN就是opt加载pass.so文件,对IR代码进行转换和优化这个过程中存在的漏洞加以利用。这里需要注意的是.so文件是不会被pwn的,我们pwn的是加载.so文件的程序——opt。所以我们需要对opt进行常规的检查。

CTF题目一般会给出所需版本的opt文件(可用./opt —version查看版本)或者在README文档中告知opt版本。安装好llvm后,可在/usr/lib/llvm-xx/bin/opt路径下找到对应llvm版本的opt文件(一般不开PIE保护)。

搜索vtable定位到虚表,最下面的函数就是重写的虚函数runOnFunction

图片.png

调试

  1. clang-8 -emit-llvm -S exp.c -o exp.bc或者
  2. clang-8 -emit-llvm -S exp.c -o exp.ll
  1. opt-8 -load ./VMPass.so -VMPass ./exp.bc

调试opt然后跟进到so文件
图片.png

opt并不会一开始就将so模块加载进来,会执行一些初始化函数才会加载so模块。
调试的时候可以把断点下载llvm::Pass::preparePassManager,程序执行到这里的时候就已经加载了LLVMHello.so文件(或者到main+11507),我们就可以根据偏移进一步将断点下在LLVMHello.so文件里面
查看vmmap,发现已经加载进来,然后可以更加偏移断在runOnFunction上

图片.png

图片.png

成功
图片.png

使用脚本

  1. from pwn import *
  2. import sys
  3. import os
  4. os.system("clang-8 -emit-llvm -S exp.c -o exp.bc")
  5. p = gdb.debug(["./opt-8",'-load','./VMPass.so','-VMPass','./exp.bc'],"b llvm::Pass::preparePassManager\nc")
  6. p.interactive()

IR结构

LLVM IR数据结构分析

LLVMContext

  • 一个全局数据
  • 只能通过llvm::getGlobalContext();创建赋值给一个LLVMContext变量
  • 删除了拷贝构造函数和拷贝赋值函数

moudle

  • 主要是包含函数和全局变量的两个链表
  • 创建一个Module,需要一个名字和一个LLVMContext
  • Module操作函数链表的成员函数 begin() end () size() empty() functions(),直接拿到函数链表的函数getFunctionList(),查找模块内函数链表中函数的函数getFunction
  1. iterator_range functions() {
  2. return make_range(begin(), end());
  3. }
  4. FunctionListType &amp;getFunctionList() { return FunctionList; }
  5. Function *getFunction(StringRef Name) const;
  • -Module操作全局变量的成员函数 global_begin() global_end () global_size() global_empty() globals()直接拿到全局变量链表的函数getGlobalList()
  1. iterator_range globals() {
  2. return make_range(global_begin(), global_end());
  3. }
  4. GlobalListType &amp;getGlobalList() { return GlobalList; }
  • 插入或查找函数 StringRef Name:函数名 Type *RetTy:返回值类型 ArgsTy… Args:每个参数的类型 FunctionType *T:函数类型(其实就是参数类型和返回值类型的集合),可以通过get方法构造 isVarArg:是是否支持可变参数
  1. FunctionCallee getOrInsertFunction(StringRef Name, FunctionType *T);
  2. template
  3. FunctionCallee getOrInsertFunction(StringRef Name, Type *RetTy,
  4. ArgsTy... Args);
  5. 返回值类型是FunctionCallee,成员为一个Value指针(就是具体的函数Function 指针)和一个FunctionType指针
  6. class FunctionCallee {
  7. private:
  8. FunctionType *FnTy = nullptr;
  9. Value *Callee = nullptr;
  10. };
  11. 可通过如下方法获得
  12. FunctionType *getFunctionType() { return FnTy; }
  13. Value *getCallee() { return Callee; }
  14. FunctionType 只是一个函数类型信息
  15. class FunctionType : public Type {
  16. static FunctionType *get(Type *Result,
  17. ArrayRef Params, bool isVarArg);
  18. Function 才是具体的函数
  19. FunctionCallee Module::getOrInsertFunction(StringRef Name, FunctionType *Ty,
  20. AttributeList AttributeList) {
  21. // See if we have a definition for the specified function already.
  22. GlobalValue *F = getNamedValue(Name);
  23. if (!F) {
  24. // Nope, add it
  25. Function *New = Function::Create(Ty, GlobalVariable::ExternalLinkage,
  26. DL.getProgramAddressSpace(), Name);
  27. if (!New->isIntrinsic()) // Intrinsics get attrs set on construction
  28. New->setAttributes(AttributeList);
  29. FunctionList.push_back(New);
  30. return {Ty, New}; // Return the new prototype.
  31. }
  32. 但使用由于原来是 Value *会需要转换
  33. Function *customFunc = dyn_cast(callee.getCallee());

Value,User和Use

刚看的时候真抽象

  • User用Value,这个使用的行为就是Use,同时Value里面有个Uselist记录了哪些User用过它们
  1. class Value {
  2. Use *UseList;
  3. ……
  4. }
  5. 使用ValueUse以双向链表连接起来
  6. class Use {
  7. private:
  8. Value *Val = nullptr;
  9. Use *Next = nullptr;
  10. Use **Prev = nullptr;
  11. User *Parent = nullptr;
  12. Val:指向被使用的Value
  13. Next:指向下一个Use
  14. Prev:指向上一个Prev
  15. Parent:指向User
  • Use会放在User结构体前,一种是以固定个数的Use,以数组的形式放在User前,另一种是不定个数的Use,一个Use放在User前,这个Use的Prev指针指向Use数组,然后可以通过一个Use找到其他Use,通过getOperandList的不同处理可以看出
  1. // HasHungOffUses是Value的成员
  2. const Use *getOperandList() const {
  3. return HasHungOffUses ? getHungOffOperands() : getIntrusiveOperands();
  4. }
  5. const Use *getHungOffOperands() const {
  6. return *(reinterpret_cast(this) - 1);
  7. }
  8. const Use *getIntrusiveOperands() const {
  9. return reinterpret_cast(this) - NumUserOperands;
  10. }

GlobalVariable

  • GlobalVariable由GlobalObject和ilist_node_base组成
  • GlobalObject是一个容器,其中的GlobalValue包含了Value、ValueType、Parent等属性
  • Globallist中的prev和next和GlobalVariable中的ilist_node_base双向连接

图片.png

  • 通过GlobalVariable创建,下面一种方式除了创建还会将该GlobalVariable添加到模块中,同时禁止了拷贝构造函数和赋值
  1. GlobalVariable(Type *Ty, bool isConstant, LinkageTypes Linkage,
  2. Constant *Initializer = nullptr, const Twine &amp;Name = "",
  3. ThreadLocalMode = NotThreadLocal, unsigned AddressSpace = 0,
  4. bool isExternallyInitialized = false);
  5. GlobalVariable(Module &amp;M, Type *Ty, bool isConstant, LinkageTypes Linkage,
  6. Constant *Initializer, const Twine &amp;Name = "",
  7. GlobalVariable *InsertBefore = nullptr,
  8. ThreadLocalMode = NotThreadLocal,
  9. Optional AddressSpace = None,
  10. bool isExternallyInitialized = false);
  11. GlobalVariable(const GlobalVariable &amp;) = delete;
  12. GlobalVariable &amp;operator=(const GlobalVariable &amp;) = delete;

Function

  • 包含GlobalObject和illst_node_base和一个Arguments指针,指向Argument数组,一个BasicBlock双链表

图片.png

  • BasicBlock迭代器:begin() end() size() empty() &amp;front() &amp;back() begin返回指向BasicBlock集合的第一个元素的迭代器。front是第一个元素的引用。end和back同理
  • Argument迭代器:arg_begin() arg_end() getArg(unsigned i) args()
  1. 返回一个迭代器范围
  2. iterator_range args() {
  3. return make_range(arg_begin(), arg_end());
  4. }
  • Function创建
  1. static Function *Create(FunctionType *Ty, LinkageTypes Linkage,
  2. const Twine &amp;N, Module &amp;M);
  3. static Function *Create(FunctionType *Ty, LinkageTypes Linkage,
  4. unsigned AddrSpace, const Twine &amp;N = "",
  5. Module *M = nullptr) {
  6. return new Function(Ty, Linkage, AddrSpace, N, M);
  7. }
  8. static Function *Create(FunctionType *Ty, LinkageTypes Linkage,
  9. const Twine &amp;N = "", Module *M = nullptr) {
  10. return new Function(Ty, Linkage, static_cast(-1), N, M);
  11. }
  • FunctionType是Type的子类,ReturnType和ParamType都存在Type类型的ContainedTys成员里,这是一个Type数组
  1. class Type {
  2. protected:
  3. unsigned NumContainedTys = 0;
  4. Type * const *ContainedTys = nullptr;

可以从getXXXType函数中看出来

  1. // Function
  2. Type *getReturnType() const { return getFunctionType()->getReturnType(); }
  3. // FunctionType
  4. FunctionType *getFunctionType() const {
  5. return cast(getValueType());
  6. }
  7. Type *getReturnType() const { return ContainedTys[0]; }
  8. Type *getParamType(unsigned i) const { return ContainedTys[i+1]; }

对于函数的入口的basicblock

  1. const BasicBlock &amp;getEntryBlock() const { return front(); }
  2. BasicBlock &amp;getEntryBlock() { return front(); }
  • 设置和获取函数调用规定 getCallingConv():返回Function的调用约定。 setCallingConv(CC):设置Function的调用约定为CC
    CallingConv::ID是一个枚举类型,表示不同的调用约定。
    getSubclassDataFromValue()是一个内部函数,用于从Function对象中提取一些数据。在这个例子中,它返回了Function的一些位掩码,其中的一部分表示调用约定。
    getCallingConv()通过移位和按位与运算从这些位掩码中提取出调用约定的值。
    setCallingConv(CC)首先将CC转换为无符号整数,然后检查它是否在一个有效的范围内。如果有效,它会更新Function的位掩码以反映新的调用约定。
  1. CallingConv::ID getCallingConv() const {
  2. return static_cast((getSubclassDataFromValue() >> 4) &amp;
  3. CallingConv::MaxID);
  4. }
  5. void setCallingConv(CallingConv::ID CC) {
  6. auto ID = static_cast(CC);
  7. assert(!(ID &amp; ~CallingConv::MaxID) &amp;&amp; "Unsupported calling convention");
  8. setValueSubclassData((getSubclassDataFromValue() &amp; 0xc00f) | (ID << 4));
  9. }

BasicBlock

  • BasicBlock内包括Value和ilist_node_base和Instlist和Parent,Instlist包含指令链表
  • 创建一个BasicBlock
    图片.png
  1. static BasicBlock *Create(LLVMContext &amp;Context, const Twine &amp;Name = "",
  2. Function *Parent = nullptr,
  3. BasicBlock *InsertBefore = nullptr) {
  4. return new BasicBlock(Context, Name, Parent, InsertBefore);
  5. }
  6. InsertBeforeNULL时默认插入Function末尾
  7. BasicBlock::BasicBlock(LLVMContext &amp;C, const Twine &amp;Name, Function *NewParent,
  8. BasicBlock *InsertBefore)
  9. : Value(Type::getLabelTy(C), Value::BasicBlockVal), Parent(nullptr) {
  10. if (NewParent)
  11. // Insert unlinked basic block into a function. Inserts an unlinked basic block into Parent. If InsertBefore is provided, inserts before that basic block, otherwise inserts at the end.
  12. insertInto(NewParent, InsertBefore);
  13. else
  14. assert(!InsertBefore &amp;&amp;
  15. "Cannot insert block before another block with no function!");
  16. setName(Name);
  17. }
  • Instruction迭代器:begin() end() rbegin() rend() size() empty() front() back()

    获取Instruction所属Function

  1. const Function *getParent() const { return Parent; }
  2. Function *getParent() { return Parent; }

Instruction

  • 包含Value和illist_node_base和Parent
    图片.png
  • 获取父BasicBlock
  1. inline const BasicBlock *getParent() const { return Parent; }
  2. inline BasicBlock *getParent() { return Parent; }
  • 获取指令的操作码
  1. unsigned getOpcode() const { return getValueID() - InstructionVal; }
  • 返回指令的另一个实例

    没有名字: 在LLVM中,值(包括指令)可以有名字,用于调试和识别。克隆的指令不会自动获得原指令的名字,所以它是匿名的。
    没有Parent: 克隆的指令不属于任何BasicBlock。在LLVM中,指令是包含在BasicBlock中的,而BasicBlock又属于Function。一个新克隆的指令还没有被插入到任何BasicBlock中,因此它的Parent(即它所在的BasicBlock)是nullptr。

    1. Instruction *clone() const;
  • 指令替换
  1. void ReplaceInstWithInst(BasicBlock::InstListType &amp;BIL, BasicBlock::iterator &amp;BI, Instruction *I);
  2. - `BIL`: 这是`BasicBlock`的指令列表,代表了指令所在的序列。
  3. - `BI`: 这是指向要被替换指令的迭代器。
  4. - `I`: 这是要替换成的新指令。
  1. void ReplaceInstWithInst(Instruction *From, Instruction *To); // 不更新迭代器,会段错误
  2. - **参数解释**:
  3. - `From`: 要被替换的旧指令。
  4. - `To`: 新的指令。

不同Instruction的创建

alloca

alloca命令是AllocaInst类型,继承关系是

  • alloca命令是AllocaInst类型,继承关系是
  1. AllocaInst->UnaryInstruction->Instruction

AllocaInst 是 LLVM 中用来表示栈上内存分配指令的类。它是继承自 UnaryInstructionInstruction 的一个子类。它的主要作用是在当前函数的栈帧上分配内存空间,通常用于存储局部变量。

下面是一些关于如何使用 AllocaInst 类的详细说明和示例:

类的继承关系

AllocaInstInstruction 的子类,具体继承关系如下:

  • AllocaInst -> UnaryInstruction -> Instruction -> User -> Value

AllocaInst 的构造函数

AllocaInst 提供了多个构造函数,允许你在创建对象时指定不同的参数。常用的构造函数包括:

  1. 类型和地址空间

    1. AllocaInst(Type *Ty, unsigned AddrSpace, const Twine &amp;Name, Instruction *InsertBefore);
    2. AllocaInst(Type *Ty, unsigned AddrSpace, const Twine &amp;Name, BasicBlock *InsertAtEnd);
  2. 类型、地址空间和数组大小

    1. AllocaInst(Type *Ty, unsigned AddrSpace, Value *ArraySize, Align Align, const Twine &amp;Name = "", Instruction *InsertBefore = nullptr);
    2. AllocaInst(Type *Ty, unsigned AddrSpace, Value *ArraySize, Align Align, const Twine &amp;Name, BasicBlock *InsertAtEnd);

参数说明

  • *`Type Ty`**: 要分配的内存类型。
  • unsigned AddrSpace: 地址空间,可以通过 ModulegetDataLayout().getAllocaAddrSpace() 方法获取。
  • *`Value ArraySize**: 数组大小,通常使用ConstantInt` 表示单个元素时大小为 1。
  • Align Align: 内存对齐,可以指定对齐值。
  • const Twine &amp;Name: 指令的名称。
  • *`Instruction InsertBefore`**: 将新指令插入到某个指令之前。
  • *`BasicBlock InsertAtEnd`**: 将新指令插入到基本块结束处。

使用示例

创建一个 AllocaInst

  1. // 创建一个上下文
  2. LLVMContext context;
  3. // 获取数据布局和地址空间
  4. Module *module = /* 获取或创建一个 Module */;
  5. unsigned addrSpace = module->getDataLayout().getAllocaAddrSpace();
  6. // 创建一个基本块
  7. BasicBlock *entryBlock = /* 获取或创建一个 BasicBlock */;
  8. // 创建一个常量整数,表示数组大小
  9. Value* intValue = ConstantInt::get(context, APInt(32, 1));
  10. // 创建 AllocaInst 实例
  11. AllocaInst *allocaInst = new AllocaInst(
  12. IntegerType::get(context, 32), // 类型为 int32
  13. addrSpace, // 地址空间
  14. intValue, // 数组大小
  15. Align(4), // 对齐
  16. "", // 名称
  17. entryBlock // 插入位置
  18. );
  19. // 设置对齐
  20. allocaInst->setAlignment(Align(4));

解释

  • 地址空间: 通过 ModuleDataLayout 获取到 AllocaAddrSpace
  • 数组大小: 使用 ConstantInt::get 创建 ConstantInt 表示分配单个元素。
  • 对齐: 可以在构造时指定对齐,也可以之后通过 setAlignment 设置。

这种用法在 LLVM IR 中生成如下指令:

  1. %1 = alloca i32, align 4

store

在LLVM中,StoreInst 是一种用于执行存储操作的指令,表示将一个值存储到指定的内存位置。它继承自 Instruction 类,并且比 Instruction 多了一个 SSID 成员,用于表示同步作用域(SyncScope::ID),这在多线程环境中非常重要。

1. StoreInst 的继承结构

StoreInst 继承自 Instruction,因此它包含了 Instruction 类的所有成员,并且额外增加了一个 SSID 成员。SSIDSyncScope::ID 类型的,它用于指定同步范围(如系统范围或单线程范围),在存储操作涉及到线程同步时,这个字段会派上用场。

2. StoreInst 的构造函数

StoreInst 提供了多个构造函数,允许用户创建包含不同参数的存储指令。常见的构造函数如下:

  • StoreInst(Value *Val, Value *Ptr, Instruction *InsertBefore);
  • StoreInst(Value *Val, Value *Ptr, BasicBlock *InsertAtEnd);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, Instruction *InsertBefore);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, BasicBlock *InsertAtEnd);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, Align Align, Instruction *InsertBefore = nullptr);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, Align Align, BasicBlock *InsertAtEnd);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, Align Align, AtomicOrdering Order, SyncScope::ID SSID = SyncScope::System, Instruction *InsertBefore = nullptr);
  • StoreInst(Value *Val, Value *Ptr, bool isVolatile, Align Align, AtomicOrdering Order, SyncScope::ID SSID, BasicBlock *InsertAtEnd);

这些构造函数的参数通常包括:

  • Val:要存储的值。
  • Ptr:存储目标(即内存地址)。
  • isVolatile:是否为易失性存储。若为 true,编译器在优化过程中不能移除或重新排序此存储操作。
  • Align:对齐方式。
  • AtomicOrdering:原子操作的顺序(如顺序一致性、获取-释放等)。
  • SSID:同步作用域的ID,默认是系统范围的 SyncScope::System
  • InsertBeforeInsertAtEnd:指定指令插入的位置,可以是插入到某一指令之前或某个基本块的末尾。

3. 代码示例

  1. StoreInst *st0 = new StoreInst(param1, ptr4, false, entryBlock);
  2. st0->setAlignment(Align(4));
  • param1: 是一个指向 Value 的指针,表示要存储的值。
  • ptr4: 是一个指向 Value 的指针,表示存储的目标地址,也就是 param1 将被存储到这个地址中。
  • false: 表示这个存储操作不是易失性的(非 volatile)。
  • entryBlock: 是一个 BasicBlock,表示指令将插入到这个基本块的末尾。
  • st0->setAlignment(Align(4)): 设置存储操作的对齐方式为 4 字节。

4. 解释

这段代码的作用是创建一个 StoreInst 对象,该对象表示将 param1 的值存储到 ptr4 指向的内存地址中,并且这个存储操作将被插入到基本块 entryBlock 的末尾。

  • StoreInst *st0 = new StoreInst(param1, ptr4, false, entryBlock);: 这行代码创建了一个新的 StoreInst 实例,表示将 param1 存储到 ptr4 中,并将其插入到 entryBlock 的末尾。
  • st0->setAlignment(Align(4));: 这行代码设置了存储操作的对齐方式为 4 字节。对齐方式在内存操作中很重要,特别是在处理不同架构和优化时。

load

在LLVM中,LoadInst 是一种用于从内存中加载值的指令。与 StoreInst 类似,LoadInst 继承自 Instruction,并且比 Instruction 多了一个 SSID 成员,用于表示同步作用域(SyncScope::ID)。这个成员在多线程环境中非常重要,因为它用于指定加载操作的同步范围。

1. LoadInst 的继承结构

LoadInst 继承自 UnaryInstruction,而 UnaryInstruction 继承自 InstructionLoadInstInstruction 多了一个 SSID 成员,它用于指定同步作用域。这在处理多线程程序时非常关键,特别是涉及到原子操作和内存序列时。

2. LoadInst 的构造函数

LoadInst 提供了多种构造函数,允许根据不同的需求来创建加载指令。常见的构造函数包括:

  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, Instruction *InsertBefore);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, BasicBlock *InsertAtEnd);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, Instruction *InsertBefore);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, BasicBlock *InsertAtEnd);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, Align Align, Instruction *InsertBefore = nullptr);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, Align Align, BasicBlock *InsertAtEnd);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, Align Align, AtomicOrdering Order, SyncScope::ID SSID = SyncScope::System, Instruction *InsertBefore = nullptr);
  • LoadInst(Type *Ty, Value *Ptr, const Twine &amp;NameStr, bool isVolatile, Align Align, AtomicOrdering Order, SyncScope::ID SSID, BasicBlock *InsertAtEnd);

这些构造函数的参数意义如下:

  • Ty: 加载数据的类型。
  • Ptr: 指向要从中加载数据的内存位置的指针。
  • NameStr: 指令的名称(通常用于调试)。
  • isVolatile: 是否为易失性加载操作。若为 true,编译器在优化过程中不能移除或重新排序此加载操作。
  • Align: 对齐方式。
  • AtomicOrdering: 原子操作的顺序(如顺序一致性、获取-释放等)。
  • SSID: 同步作用域的ID,默认是系统范围的 SyncScope::System
  • InsertBeforeInsertAtEnd: 指定指令插入的位置,可以是插入到某一指令之前或某个基本块的末尾。

3. 代码示例

  1. LoadInst *ld0 = new LoadInst(IntegerType::get(context, 32), ptr4, "",false, entryBlock);
  2. ld0->setAlignment(Align(4));
  • IntegerType::get(context, 32): 创建一个32位宽的整数类型。
  • ptr4: 是一个指向 Value 的指针,表示要从中加载数据的内存地址。
  • "": 表示指令的名称为空字符串。
  • false: 表示这个加载操作不是易失性的(非 volatile)。
  • entryBlock: 是一个 BasicBlock,表示指令将插入到这个基本块的末尾。

ld0->setAlignment(Align(4)): 设置加载操作的对齐方式为4字节。

add

在LLVM中,BinaryOperator 是一个用于表示二元算术操作(如加法、减法、乘法等)的类。它继承自 Instruction 类,并且不引入额外的数据成员(即没有自己的 data 域)。BinaryOperator 主要用于创建和管理二元运算指令。

1. BinaryOperator 的创建

BinaryOperator 提供了静态工厂方法 Create,用于创建具体的二元操作指令。常用的两个 Create 方法如下:

  1. static BinaryOperator *Create(BinaryOps Op, Value *S1, Value *S2,
  2. const Twine &amp;Name = Twine(),
  3. Instruction *InsertBefore = nullptr);
  4. static BinaryOperator *Create(BinaryOps Op, Value *S1, Value *S2,
  5. const Twine &amp;Name, BasicBlock *InsertAtEnd);
  • Op: 表示要执行的二元操作类型,这个类型是 BinaryOps 枚举值。例如,加法操作是 Instruction::Add
  • S1S2: 这是两个操作数,通常是指向 Value 对象的指针,表示要参与运算的值。
  • Name: 是一个 Twine 类型的对象,用于设置操作指令的名称。这个参数是可选的,通常用于生成更具可读性的LLVM IR代码。
  • InsertBefore: 指定将这条指令插入到现有指令之前。
  • InsertAtEnd: 指定将这条指令插入到基本块的末尾。

2. BinaryOps 枚举

BinaryOps 是一个枚举类型,它定义了所有的二元操作指令类型。这个枚举包括了各种常见的操作符,如加法 (Add)、减法 (Sub)、乘法 (Mul)、除法 (Div) 等。

枚举的定义通常在 Instruction.def 文件中通过宏展开生成,例如:

  1. enum BinaryOps {
  2. #define FIRST_BINARY_INST(N) BinaryOpsBegin = N,
  3. #define HANDLE_BINARY_INST(N, OPC, CLASS) OPC = N,
  4. #define LAST_BINARY_INST(N) BinaryOpsEnd = N+1
  5. #include "llvm/IR/Instruction.def"
  6. };

其中 Instruction::Add 是一个 BinaryOps 枚举值,表示加法操作。

3. 代码示例解析

  1. BinaryOperator *add1 = BinaryOperator::Create(Instruction::Add, ld0, ld1, "", entryBlock);
  • Instruction::Add: 这是一个 BinaryOps 枚举值,表示加法操作。
  • ld0ld1: 这两个值分别是两个操作数,通常是 Value* 类型的指针,表示要相加的两个值。这些值可能是从之前的 LoadInst 或其他指令中获取的。
  • "": 这是指令的名称,用于生成的LLVM IR中。如果没有特别指定,可以留空字符串。
  • entryBlock: 这是一个指向 BasicBlock 的指针,表示将指令插入到该基本块的末尾。

4. 解释

这段代码的作用是创建一个加法操作的 BinaryOperator 对象,表示将 ld0ld1 这两个操作数相加,并将生成的加法指令插入到 entryBlock 基本块的末尾。

  • 创建加法指令: BinaryOperator::Create(Instruction::Add, ld0, ld1, "", entryBlock) 创建了一个加法指令,将 ld0ld1 相加。
  • 插入基本块: 这个加法指令会被插入到 entryBlock 的末尾。

这条指令对应的LLVM IR可能类似于:

  1. %add1 = add i32 %ld0, %ld1
  • %add1 是生成的加法指令的结果。
  • i32 表示操作数是32位整数。
  • %ld0%ld1 是要相加的两个操作数。

icmp

在LLVM中,ICmpInst 是用于表示整数比较操作的指令。它继承自 CmpInst,并且没有增加额外的数据成员。ICmpInst 使用 Predicate 来指定具体的比较类型,例如等于、不等于、大于、小于等。

1. ICmpInst 的构造函数

ICmpInst 提供了多个构造函数,用于创建整数比较指令。这些构造函数的作用是生成一条比较指令,将其插入到指定的位置(例如某个基本块的末尾)。

以下是构造函数原型的详细说明:

  1. ICmpInst(
  2. Instruction *InsertBefore, // 要插入的指令之前的位置
  3. Predicate pred, // 比较谓词,表示比较类型
  4. Value *LHS, // 左操作数
  5. Value *RHS, // 右操作数
  6. const Twine &amp;NameStr = "" // 指令名称
  7. )
  • InsertBefore: 指定将比较指令插入到哪条现有指令之前。
  • pred: 指定比较的类型,例如 ICMP_EQ 表示等于,ICMP_SGT 表示有符号大于等。
  • LHS: 左操作数(通常是一个 Value 指针)。
  • RHS: 右操作数(通常是一个 Value 指针)。
  • NameStr: 可选参数,指令的名称,通常用于调试或生成的LLVM IR的可读性。

2. Predicate 枚举

Predicate 枚举定义了所有可能的比较操作符。对于整数比较,常见的值包括:

  • ICMP_EQ: 等于
  • ICMP_NE: 不等于
  • ICMP_UGT: 无符号大于
  • ICMP_UGE: 无符号大于或等于
  • ICMP_ULT: 无符号小于
  • ICMP_ULE: 无符号小于或等于
  • ICMP_SGT: 有符号大于
  • ICMP_SGE: 有符号大于或等于
  • ICMP_SLT: 有符号小于
  • ICMP_SLE: 有符号小于或等于

这些枚举值用于指定在比较操作中应该执行哪种类型的比较。

3. 代码示例解析

  1. ICmpInst *icmp = new ICmpInst(
  2. *entryBlock, // 指令插入的基本块
  3. ICmpInst::ICMP_SGT, // 使用有符号大于的比较
  4. add1, // 左操作数
  5. ConstantInt::get(context, APInt(32, 100)), // 右操作数,常量100
  6. ""
  7. );
  • *`entryBlock**: 这表示要将指令插入到entryBlock` 基本块的末尾。
  • ICmpInst::ICMP_SGT: 这里指定了有符号大于 (signed greater than) 的比较谓词,表示将比较 add1100,判断 add1 是否大于 100
  • add1: 这是左操作数,通常是一个 Value* 类型的指针,它可能是之前某个计算指令的结果。
  • ConstantInt::get(context, APInt(32, 100)): 这是右操作数,表示一个32位宽、值为100的常量整数。ConstantInt::get 生成一个 ConstantInt 对象,这是LLVM中表示常量整数的方式。

4. 解释

这段代码的作用是创建一条整数比较指令,它会判断 add1 是否大于100。假设 add1 的类型是32位整数,那么生成的LLVM IR指令可能类似于:

  1. %cmp = icmp sgt i32 %add1, 100
  • %cmp 是生成的比较指令的结果。
  • icmp sgt i32 表示这是一个有符号大于(signed greater than)的比较操作。
  • %add1 是左操作数,100 是右操作数。

branch

在LLVM中,BranchInst 是用于表示分支(跳转)操作的指令。BranchInst 没有额外的数据成员(即没有自己的 data 域),它的所有状态和行为都通过继承自 Instruction 类来实现。BranchInst 可以表示无条件分支(跳转到一个目标块)或者有条件分支(根据条件选择跳转到不同的目标块)。

1. BranchInst 的创建方法

BranchInst 提供了多个静态方法 Create,用于创建分支指令。具体的方法如下:

  • 无条件分支: 只跳转到一个目标块。

    1. static BranchInst *Create(BasicBlock *IfTrue, Instruction *InsertBefore = nullptr);
    2. static BranchInst *Create(BasicBlock *IfTrue, BasicBlock *InsertAtEnd);
  1. - **`IfTrue`**: 目标基本块,指定无条件跳转的目的地。
  2. - **`InsertBefore`**: 将指令插入到指定的现有指令之前。
  3. - **`InsertAtEnd`**: 将指令插入到指定基本块的末尾。
  • 有条件分支: 根据条件跳转到不同的目标块。

    1. static BranchInst *Create(BasicBlock *IfTrue, BasicBlock *IfFalse,
    2. Value *Cond, Instruction *InsertBefore = nullptr);
    3. static BranchInst *Create(BasicBlock *IfTrue, BasicBlock *IfFalse,
    4. Value *Cond, BasicBlock *InsertAtEnd);
  1. - **`IfTrue`**: 条件为真的时候跳转的目标块。
  2. - **`IfFalse`**: 条件为假的时候跳转的目标块。
  3. - **`Cond`**: 条件值(通常是一个比较结果,例如 `ICmpInst` 的结果)。
  4. - **`InsertBefore`**: 将指令插入到指定的现有指令之前。
  5. - **`InsertAtEnd`**: 将指令插入到指定基本块的末尾。

2. 代码示例解析

  1. BranchInst::Create(block10, block19, icmp, entryBlock);
  • block10: 这是条件为真时将要跳转的目标基本块。
  • block19: 这是条件为假时将要跳转的目标基本块。
  • icmp: 这是条件值,通常是一个 Value*,表示分支条件。在这个例子中,它可能是一个 ICmpInst 的结果。
  • entryBlock: 这是一个 BasicBlock*,指定将该分支指令插入到的基本块。在这个例子中,分支指令会插入到 entryBlock 的末尾。

3. 解释

这段代码的作用是创建一条有条件的分支指令,它将根据 icmp 计算的条件跳转到 block10block19

  • 条件判断与跳转: 如果 icmp 结果为真(即条件成立),则跳转到 block10。如果结果为假(即条件不成立),则跳转到 block19
  • 插入位置: 这条指令会被插入到 entryBlock 基本块的末尾。

生成的LLVM IR代码可能类似于:

  1. br i1 %icmp, label %block10, label %block19
  • br 表示分支指令。
  • i1 %icmp 表示条件值,i1 是1位的布尔值类型,%icmp 是条件值。
  • label %block10 是条件为真时跳转的目标基本块。
  • label %block19 是条件为假时跳转的目标基本块。

ret

在LLVM中,ReturnInst 是用于表示函数返回操作的指令。ReturnInst 是一个没有额外数据成员的类(即无data域),它主要通过继承自 Instruction 类来实现功能。ReturnInst 可以表示带有返回值的返回操作(例如从函数返回一个整数)或不带返回值的返回操作(返回 void 类型)。

1. ReturnInst 的创建方法

ReturnInst 提供了一些静态方法 Create,用于创建返回指令。具体的创建方法如下:

  1. static ReturnInst* Create(LLVMContext &amp;C, Value *retVal = nullptr,
  2. Instruction *InsertBefore = nullptr) {
  3. return new(!!retVal) ReturnInst(C, retVal, InsertBefore);
  4. }
  5. static ReturnInst* Create(LLVMContext &amp;C, Value *retVal,
  6. BasicBlock *InsertAtEnd) {
  7. return new(!!retVal) ReturnInst(C, retVal, InsertAtEnd);
  8. }
  9. static ReturnInst* Create(LLVMContext &amp;C, BasicBlock *InsertAtEnd) {
  10. return new(0) ReturnInst(C, InsertAtEnd);
  11. }
  • LLVMContext &amp;C: LLVM上下文对象,管理LLVM中的全局数据。
  • *`Value retVal**: 可选参数,表示要返回的值。如果返回void,则这个值为nullptr`。
  • *`Instruction InsertBefore`**: 可选参数,指示将返回指令插入到某条现有指令之前。
  • *`BasicBlock InsertAtEnd`**: 可选参数,指示将返回指令插入到某个基本块的末尾。

2. 代码示例解析

  1. ReturnInst::Create(context, ld20, block15);
  • context: 这是LLVM的上下文对象(LLVMContext),用于管理LLVM中的全局数据。
  • ld20: 这是一个Value*类型的指针,表示要返回的值。在这个例子中,ld20 可能是一个加载指令(LoadInst)的结果,表示从某个内存位置加载的值。
  • block15: 这是一个指向 BasicBlock 的指针,表示将返回指令插入到 block15 基本块的末尾。

3. 解释

这段代码的作用是创建一条返回指令,它将 ld20 作为返回值,并将这条返回指令插入到 block15 基本块的末尾。

  • 返回值: 如果 ld20 表示一个整数值(假设是一个 i32 类型的值),那么这条返回指令将返回这个整数值。
  • 插入位置: 返回指令会被插入到 block15 基本块的末尾。

生成的LLVM IR代码可能类似于:

  1. ret i32 %ld20
  • ret i32 %ld20 表示返回一个32位整数,%ld20 是返回的值。

如果没有返回值(即返回 void 类型),代码会是:

  1. ReturnInst::Create(context, block15);

生成的LLVM IR代码将类似于:

  1. ret void

call

在LLVM中,CallInst 是用来表示函数调用指令的类。它继承自 CallBase,而 CallBase 本身继承自 InstructionCallInst 主要用于表示调用函数的操作,并且可以选择性地传递参数。由于函数调用在程序中的重要性,CallInst 提供了丰富的构造函数来适应不同的使用场景。

1. CallInst 的内部结构

CallInst 继承自 CallBase,而 CallBase 具有自己的数据成员。这些数据成员包括:

  • CalledOperandOpEndIdx: 这是一个静态成员,通常用于管理操作数的索引。
  • Attrs: 它是存储调用属性(如参数属性)的数据结构。
  • FTy: 指向函数类型 (FunctionType) 的指针,表示被调用函数的类型。

这些成员帮助管理与函数调用相关的元数据、参数和操作数。

2. CallInst 的创建方法

你提供的代码片段展示了LLVM中的CallInst类的多个静态工厂方法的声明。CallInst是LLVM IR(Intermediate Representation)中的一种指令类型,用于表示函数调用。这个类有许多重载的Create方法,用于创建CallInst对象。重载方法的存在使得在不同的上下文和需求下,可以灵活地创建函数调用指令。

重载方法的分类与解释

这些重载方法大致可以分为几类,主要根据参数的不同来进行分类。下面是对每一类的解释:

1. 基本创建方法

  • 方法签名:

    1. static CallInst *Create(FunctionType *Ty, Value *F, const Twine &amp;NameStr = "", Instruction *InsertBefore = nullptr);
  • 作用:
  1. - 这是最基本的创建方法,用于创建一个简单的函数调用指令。
  2. - 参数`Ty`表示函数的类型,`F`是实际的函数(或函数指针),`NameStr`是可选的名称字符串,`InsertBefore`是可选的参数,用于指定将指令插入到哪条指令之前。
  • 用例:
  1. - 适用于不需要传递参数的函数调用场景,或者函数没有参数。

2. 带参数的创建方法

  • 方法签名:

    1. static CallInst *Create(FunctionType *Ty, Value *Func, ArrayRef Args, const Twine &amp;NameStr, Instruction *InsertBefore = nullptr);
  • 作用:
    • 这个方法用于创建带有参数的函数调用指令。
    • 参数ArgsValue的数组,表示调用函数时传递的参数。
  • 用例:
    • 适用于函数有参数的场景。

3. 带参数和操作数包的创建方法

  • 方法签名:

    1. static CallInst *Create(FunctionType *Ty, Value *Func, ArrayRef Args, ArrayRef Bundles = None, const Twine &amp;NameStr = "", Instruction *InsertBefore = nullptr);
  • 作用:
  1. - 这个方法不仅可以传递参数,还可以传递“操作数包”(`OperandBundleDef`),这些包可以包含额外的元数据或附加信息。
  • 用例:
  1. - 适用于需要传递额外元数据或操作数包的高级场景。

4. 插入到基本块末尾的创建方法

  • 方法签名:

    1. static CallInst *Create(FunctionType *Ty, Value *F, const Twine &amp;NameStr, BasicBlock *InsertAtEnd);
  • 作用:
  1. - 这个方法用于创建一个调用指令,并将其插入到指定基本块的末尾。
  2. - 参数`InsertAtEnd`是一个`BasicBlock`,表示要插入的基本块。
  • 用例:
  1. - 适用于在基本块末尾插入指令的场景。

5. 使用FunctionCallee类型的创建方法

  • 方法签名:

    1. static CallInst *Create(FunctionCallee Func, const Twine &amp;NameStr = "", Instruction *InsertBefore = nullptr);
  • 作用:
  1. - `FunctionCallee`LLVM中的一个便利类型,封装了函数类型和函数的指针,简化了函数调用指令的创建。
  • 用例:
  1. - 适用于使用`FunctionCallee`类型的场景,降低手动获取函数类型的复杂度。

6. 带操作数包和替换操作数包的创建方法

  • 方法签名:

    1. static CallInst *Create(CallInst *CI, ArrayRef Bundles, Instruction *InsertPt = nullptr);
    2. static CallInst *CreateWithReplacedBundle(CallInst *CI, OperandBundleDef Bundle, Instruction *InsertPt = nullptr);
  • 作用:
  1. - 这些方法用于创建一个新的调用指令,基于已有的调用指令`CI`,并添加或替换操作数包。
  • 用例:
  1. - 适用于需要复制现有指令并修改其操作数包的场景。

实例

  1. Function* myAddFunc = module->getFunction("myadd");
  2. Value *arg[] = {old_ope->getOperand(0), old_ope->getOperand(1)};
  3. CallInst *myaddCall = CallInst::Create(myAddFunc, arg, "");
  1. 从当前 LLVM 模块中获取名为 "myadd" 的函数指针。
  2. 通过从某个现有操作(old_ope)中提取两个操作数,作为调用 "myadd" 函数的参数。
  3. 创建一个新的函数调用指令,调用 "myadd" 函数,并传递提取的两个参数。

最后,myaddCall 是创建的调用指令对象,它可以在 LLVM IR 中被插入到合适的位置,以便在生成的代码中执行这个函数调用。

参考

https://bbs.kanxue.com/thread-274259.htm#msg_header_h2_1

http://www.blackbird.wang/2022/08/30/LLVM-PASS%E7%B1%BBpwn%E9%A2%98%E6%80%BB%E7%BB%93/

LLVM基础知识

https://buaa-se-compiling.github.io/miniSysY-tutorial/pre/design_hints.html

https://blog.csdn.net/qq_45323960/article/details/129691707?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171928250416800178515048%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171928250416800178515048&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-2-129691707-null-null.nonecase&utm_term=LLVM&spm=1018.2226.3001.4450

https://xz.aliyun.com/t/11762?time__1311=mqmx0DBDcD2DuiCD%2FQbKBKFxr73BPhD&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F

  • 发表于 2024-11-08 09:00:01
  • 阅读 ( 6307 )
  • 分类:其他

0 条评论

麦当当
麦当当

4 篇文章

站长统计