LLVM IRBuilder and pass:2/2.通过llvm提供的api和IRBuilder生成LLVM-IR


7.算术运算语句Arithmetic Statement

一个基本的逻辑块(BasicBlock)是由一系列的指令(Instruction)所组成的。一条指令主要是为了完成某个任务(操作),比如一个算术运算操作。为了简单起见,我们在这里可以把一个简单的算术运算语句理解为一条指令。

在LLVM中,一个简单的算舒运算操作需要用到操作符和操作数。例如一个乘法操作用到了一个乘法操作符和两个乘数。所以,要创建爱你一条乘法运算指令,我们首先要得到两个操作数,他们可以来自于函数的参数、数值变量、数值常量等等。乘法操作是一个二元操作,IRBuilder里面提供了很多的API,可以用来方便地创建二元操作指令。

本节,我们就在一个函数里创建一条简单的算术运算指令。

为了简单起见,假定这个函数带有两个整数型参数,他们的名称分别为a 和b。我们把第一个参数乘以3,其结果就是函数的返回值。代码如下(示例):

// HelloArithmeticStatement.cpp

#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/GlobalVariable.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"

#include <vector>

using namespace llvm;

int main(int argc, char* argv[])
{
    LLVMContext context;
    IRBuilder<> builder(context);

    // Create a module
    Module* module = new Module("HelloModule", context);

    // Add a global variable
    module->getOrInsertGlobal("helloGlobalVariable", builder.getInt32Ty());
    GlobalVariable* globalVariable = module->getNamedGlobal("helloGlobalVariable");
    globalVariable->setLinkage(GlobalValue::CommonLinkage);
    globalVariable->setAlignment(MaybeAlign(4));

    // Add a function with parameters
    std::vector<Type*> parameters(2, builder.getInt32Ty());
    FunctionType* functionType = FunctionType::get(builder.getInt32Ty(), parameters, false);
    Function* function = Function::Create(functionType, GlobalValue::ExternalLinkage, "HelloFunction", module);

    // Set arguments for the function
    function->getArg(0)->setName("a");
    function->getArg(1)->setName("b");

    // Create a block
    BasicBlock* block = BasicBlock::Create(context, "entry", function);
    builder.SetInsertPoint(block);

    // Create an arithmetic statement
    Value* arg1 = function->getArg(0);
    ConstantInt* three = builder.getInt32(3);
    Value* result = builder.CreateMul(arg1, three, "multiplyResult");

    // Add a return
    builder.CreateRet(result);

    // Print the IR
    verifyFunction(*function);
    module->print(outs(), nullptr);

    return 0;
}

7.1编译

clang++ -O3 HelloArithmeticStatement.cpp -o HelloArithmeticStatement `llvm-config --cflags --ldflags` `llvm-config --libs` `llvm-config --system-libs`

此时在当前路径下生成了一个HelloFunctionArguments的可执行程序

7.2运行
运行HelloArithmeticStatement(示例):

./HelloArithmeticStatement

输出结果如下(示例):

; ModuleID = 'HelloModule'
source_filename = "HelloModule"

@helloGlobalVariable = common global i32, align 4

define i32 @HelloFunction(i32 %a, i32 %b) {
entry:
  %multiplyResult = mul i32 %a, 3
  ret i32 %multiplyResult
}

以上的IR代码中,变量%multiplyResult就是乘法运算的结果了,也是函数的返回值。

8.控制流语句if-else

我们知道,一个if-else语句包含了一个条件判断和两个逻辑分支,至于最终会运行哪个分支的代码,取决于条件判断的结果为真还是假。而“条件”则一般是一个比较表达式。

为了简单起见,我们来为以下这个简单的C函数生成IR代码(示例):

// Test.c

int Test(int a)
{
    int b;
    if (a > 33)
    {
        b = 66;
    }
    else
    {
        b = 77;
    }

    return b;
}

上述代码中存在一个可变变量b,当然它也是一个局部变量,我们可以在栈上创建一个可变变量。LLVM IR提供了一个指令,可以让我们在栈上申明变量,即alloca指令。使用方法如下:

%variable.address = alloca i32

LLVM也提供了相应的C++ API用于在栈上申明变量:

AllocaInst * llvm::IRBuilderBase::CreateAlloca(Type * Ty, Value * ArraySize = nullptr, const Twine & Name = "")

注意用alloca指令申明的变量,其实得到的是变量的地址。如果要访问他,我们需要用store和load指令,store指令可以把变量值写入改地址(示例):

store i32 66, i32* %variable.address, align 4

而load指令则可以把变量值从改地址读取出来(示例):

%actual.value = load i32, i32* %variable.address, align 4

以下是通过调用LLVM C++ API来生成IR的代码(示例):

// HelloIfElse.cpp

#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"

#include <vector>

using namespace llvm;

int main(int argc, char* argv[])
{
    LLVMContext context;
    IRBuilder<> builder(context);

    // Create a module
    Module* module = new Module("Test.c", context);

    // Add a function
    std::vector<Type*> parameters(1, builder.getInt32Ty());
    FunctionType* functionType = FunctionType::get(builder.getInt32Ty(), parameters, false);
    Function* function = Function::Create(functionType, GlobalValue::ExternalLinkage, "Test", module);

    // Add an argument to the function
    Value* arg = function->getArg(0);
    arg->setName("a");

    // Add some basic blocks to the function
    BasicBlock* entryBlock = BasicBlock::Create(context, "entry", function);
    BasicBlock* thenBlock = BasicBlock::Create(context, "if.then", function);
    BasicBlock* elseBlock = BasicBlock::Create(context, "if.else", function);
    BasicBlock* returnBlock = BasicBlock::Create(context, "if.end", function);

    // Fill the "entry" block (1):
    //   int b;
    builder.SetInsertPoint(entryBlock);
    Value* bPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "b.address");

    // Fill the "entry" block (2):
    //   if (a > 33)
    ConstantInt* value33 = builder.getInt32(33);
    Value* condition = builder.CreateICmpSGT(arg, value33, "compare.result");
    builder.CreateCondBr(condition, thenBlock, elseBlock);

    // Fill the "if.then" block:
    //   b = 66;
    builder.SetInsertPoint(thenBlock);
    ConstantInt* value66 = builder.getInt32(66);
    builder.CreateStore(value66, bPtr);
    builder.CreateBr(returnBlock);

    // Fill the "if.else" block:
    //   b = 77;
    builder.SetInsertPoint(elseBlock);
    ConstantInt* value77 = builder.getInt32(77);
    builder.CreateStore(value77, bPtr);
    builder.CreateBr(returnBlock);

    // Fill the "if.end" block:
    //   return b;
    builder.SetInsertPoint(returnBlock);
    Value* returnValue = builder.CreateLoad(builder.getInt32Ty(),bPtr, "return.value");
    builder.CreateRet(returnValue);

    // Print the IR
    verifyFunction(*function);
    module->print(outs(), nullptr);

    return 0;
}

8.1编译

clang++ -O3 HelloIfElse.cpp -o HelloIfElse `llvm-config --cflags --ldflags` `llvm-config --libs` `llvm-config --system-libs`

此时在当前路径下生成了一个HelloIfElse的可执行程序

8.2运行
运行HelloIfElse(示例):

./HelloIfElse

输出结果如下(示例):

; ModuleID = 'Test.c'
source_filename = "Test.c"

define i32 @Test(i32 %a) {
entry:
  %b.address = alloca i32, align 4
  %compare.result = icmp sgt i32 %a, 33
  br i1 %compare.result, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  store i32 66, i32* %b.address, align 4
  br label %if.end

if.else:                                          ; preds = %entry
  store i32 77, i32* %b.address, align 4
  br label %if.end

if.end:                                           ; preds = %if.else, %if.then
  %return.value = load i32, i32* %b.address, align 4
  ret i32 %return.value
}

9.控制流语句if-else-phi

本节中依然是在处理if-else语句。不过这一次我们用一个特殊的指令来生成IR代码,即phi指令。

一个if-else语句包含了一个条件判断和两个逻辑分支。最终会运行哪个分支的代码,取决于条件判断结果是真还是假。而“条件”则一般是一个比较表达式。

在很多情况下,控制流只是为了给某一个变量赋值,而phi指令,可以根据控制流来选择合适的值。它的用法如下(示例):

%value = phi i32 [66, %branch1], [77, %branch2], [88, %branch3] 

可以看到phi指令可以接收多个输入参数,参数的个数也是不固定的。第一个参数表示的是phi指令的返回值类型,如在以上示例中为i32。接下来的每一个参数都是一个数组,代表了每一个分支及其对应的返回值。例如,如果前一步执行的是branch1分支,则返回值为66;当执行的是branch2,则返回值为77;以此类推…

LLVM也提供了相应的C++API用于创建phi指令:

PHINode * llvm::IRBuilderBase::CreatePHI(Type * Ty, unsigned NumReservedValues, const Twine & Name = "")

以及向phi指令中添加条件返回值:

void llvm::PHINode::addIncoming(Value * V, BasicBlock * BB)

本节,我们利用phi指令来处理简单的if-else控制流语句。

为了简单起见,用一个简单的C函数生成IR代码。C函数如下(示例):

// Test.c

int Test(int a)
{
    int b;

    if (a > 33)
    {
        b = 66;
    }
    else
    {
        b = 77;
    }

    return b;
}

以下就是我们调用LLVM C++ API来生成IR的代码(示例):

// HelloIfElsePhi.cpp

#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"

#include <vector>

using namespace llvm;

int main(int argc, char* argv[])
{
    LLVMContext context;
    IRBuilder<> builder(context);

    // Create a module
    Module* module = new Module("Test.c", context);

    // Add a function
    std::vector<Type*> parameters(1, builder.getInt32Ty());
    FunctionType* functionType = FunctionType::get(builder.getInt32Ty(), parameters, false);
    Function* function = Function::Create(functionType, GlobalValue::ExternalLinkage, "Test", module);

    // Add an argument to the function
    Value* arg = function->getArg(0);
    arg->setName("a");

    // Add some basic blocks to the function
    BasicBlock* entryBlock = BasicBlock::Create(context, "entry", function);
    BasicBlock* thenBlock = BasicBlock::Create(context, "if.then", function);
    BasicBlock* elseBlock = BasicBlock::Create(context, "if.else", function);
    BasicBlock* returnBlock = BasicBlock::Create(context, "if.end", function);

    // Fill the "entry" block (1):
    //   int b;
    builder.SetInsertPoint(entryBlock);
    Value* b = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "b.address");

    // Fill the "entry" block (2):
    //   if (a > 33)
    ConstantInt* value33 = builder.getInt32(33);
    Value* condition = builder.CreateICmpSGT(arg, value33, "compare.result");
    builder.CreateCondBr(condition, thenBlock, elseBlock);

    // Fill the "if.then" block:
    //   b = 66;
    builder.SetInsertPoint(thenBlock);
    ConstantInt* value66 = builder.getInt32(66);
    builder.CreateBr(returnBlock);

    // Fill the "if.else" block:
    //   b = 77;
    builder.SetInsertPoint(elseBlock);
    ConstantInt* value77 = builder.getInt32(77);
    builder.CreateBr(returnBlock);

    // Fill the "if.end" block with phi instruction:
    //   return b;
    builder.SetInsertPoint(returnBlock);
    PHINode* phi = builder.CreatePHI(builder.getInt32Ty(), 2);
    phi->addIncoming(value66, thenBlock);
    phi->addIncoming(value77, elseBlock);
    builder.CreateStore(phi, b);
    Value* returnValue = builder.CreateLoad(b, "return.value");
    builder.CreateRet(returnValue);

    // Print the IR
    verifyFunction(*function);
    module->print(outs(), nullptr);

    return 0;
}

9.1编译

clang++ -O3 HelloIfElsePhi.cpp -o HelloIfElsePhi `llvm-config --cflags --ldflags` `llvm-config --libs` `llvm-config --system-libs`

此时在当前路径下生成了一个HelloIfElsePhi的可执行程序

9.2运行
运行HelloIfElsePhi(示例):

./HelloIfElsePhi

输出结果如下(示例):

; ModuleID = 'Test.c'
source_filename = "Test.c"

define i32 @Test(i32 %a) {
entry:
  %b.address = alloca i32, align 4
  %compare.result = icmp sgt i32 %a, 33
  br i1 %compare.result, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  br label %if.end

if.else:                                          ; preds = %entry
  br label %if.end

if.end:                                           ; preds = %if.else, %if.then
  %0 = phi i32 [ 66, %if.then ], [ 77, %if.else ]
  store i32 %0, i32* %b.address, align 4
  %return.value = load i32, i32* %b.address, align 4
  ret i32 %return.value
}

10.控制流语句for

我们知道一个for语句包含了条件判断以及循环执行的任务。具体循环多少次,取决于条件判断的结果。而“条件”则包含了循环的起点、终止条件、步进长度。

这里的for循环语句IR,跟前面的if-else语句有些相似的地方。他们都需要用到比较指令、跳转指令、栈上的变量等。

简单起见,我们以下面这个简单的C函数生成IR代码(示例):

//Test.c
int Test(int a)
{
	int b = 0;
	for(int i=0; i<a; i++)
	{
		b = b + i;	
	}
	return b;
}

以下就是我们调用LLVM C++ API来生成IR的代码(示例):

// HelloLoop.cpp

#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"

#include <vector>

using namespace llvm;

int main(int argc, char* argv[])
{
    LLVMContext context;
    IRBuilder<> builder(context);

    // Create a module
    Module* module = new Module("Test.c", context);

    // Add a function with argument
    //   int Test(int a)
    std::vector<Type*> parameters(1, builder.getInt32Ty());
    FunctionType* functionType = FunctionType::get(builder.getInt32Ty(), parameters, false);
    Function* function = Function::Create(functionType, GlobalValue::ExternalLinkage, "Test", module);
    Value* a = function->getArg(0);
    a->setName("a");

    // Add some basic blocks to the function
    BasicBlock* entryBlock = BasicBlock::Create(context, "entry", function);
    BasicBlock* forConditionBlock = BasicBlock::Create(context, "for.condition", function);
    BasicBlock* forBodyBlock = BasicBlock::Create(context, "for.body", function);
    BasicBlock* forIncrementBlock = BasicBlock::Create(context, "for.increment", function);
    BasicBlock* forEndBlock = BasicBlock::Create(context, "for.end", function);

    // Fill the "entry" block (1):
    //   int b = 0;
    builder.SetInsertPoint(entryBlock);
    Value* bPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "b.address");
    builder.CreateStore(builder.getInt32(1), bPtr);

    // Fill the "entry" block (2):
    //   for (int i = 0; ...)
    Value* iPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "i.address");
    builder.CreateStore(builder.getInt32(0), iPtr);
    builder.CreateBr(forConditionBlock);

    // Fill the "for.condition" block:
    //   for (... i < a; ...)
    builder.SetInsertPoint(forConditionBlock);
    Value* i0 = builder.CreateLoad(iPtr);
    Value* forConditionCompare = builder.CreateICmpSLT(i0, a, "for.condition.compare.result");
    builder.CreateCondBr(forConditionCompare, forBodyBlock, forEndBlock);

    // Fill the "for.body" block:
    //   b = b + i;
    builder.SetInsertPoint(forBodyBlock);
    Value* b0 = builder.CreateLoad(bPtr);
    Value* i1 = builder.CreateLoad(iPtr);
    Value* addResult = builder.CreateAdd(b0, i1, "add.result");
    builder.CreateStore(addResult, bPtr);
    builder.CreateBr(forIncrementBlock);

    // Fill the "for.increment" block:
    //   for (... i++)
    builder.SetInsertPoint(forIncrementBlock);
    Value* i2 = builder.CreateLoad(iPtr);
    Value* incrementedI = builder.CreateAdd(i2, builder.getInt32(1), "i.incremented");
    builder.CreateStore(incrementedI, iPtr);
    builder.CreateBr(forConditionBlock);

    // Fill the "for.end" block:
    //   return b;
    builder.SetInsertPoint(forEndBlock);
    Value* returnValue = builder.CreateLoad(bPtr, "return.value");
    builder.CreateRet(returnValue);

    // Print the IR
    verifyFunction(*function);
    module->print(outs(), nullptr);

    return 0;
}

10.1编译

clang++ -O3 HelloLoop.cpp -o HelloLoop `llvm-config --cflags --ldflags` `llvm-config --libs` `llvm-config --system-libs`

此时在当前路径下生成了一个HelloIfElsePhi的可执行程序

10.2运行
运行HelloLoop(示例):

./HelloLoop

输出结果如下(示例):

; ModuleID = 'Test.c'
source_filename = "Test.c"

define i32 @Test(i32 %a) {
entry:
  %b.address = alloca i32, align 4
  store i32 1, i32* %b.address, align 4
  %i.address = alloca i32, align 4
  store i32 0, i32* %i.address, align 4
  br label %for.condition

for.condition:                                    ; preds = %for.increment, %entry
  %0 = load i32, i32* %i.address, align 4
  %for.condition.compare.result = icmp slt i32 %0, %a
  br i1 %for.condition.compare.result, label %for.body, label %for.end

for.body:                                         ; preds = %for.condition
  %1 = load i32, i32* %b.address, align 4
  %2 = load i32, i32* %i.address, align 4
  %add.result = add i32 %1, %2
  store i32 %add.result, i32* %b.address, align 4
  br label %for.increment

for.increment:                                    ; preds = %for.body
  %3 = load i32, i32* %i.address, align 4
  %i.incremented = add i32 %3, 1
  store i32 %i.incremented, i32* %i.address, align 4
  br label %for.condition

for.end:                                          ; preds = %for.condition
  %return.value = load i32, i32* %b.address, align 4
  ret i32 %return.value
}

11. 控制流语句while

while循环和for循环比较类似,循环的次数取决于判断结果,它们也都用到了比较指令、跳转指令、栈上的变量等。

为了简单起见,我们来以这个简单的C函数生成IR代码(示例):

int main(){
	int n = 10;
	return sum(n);
}

int sum(int n){
	int i = 0;
	int sum = 0;
	while(i <= n){
		sum = sum + i;
		i++;
	}
	return sum;
}

以下是我们调用LLVM C++ API 来生成LLVM IR的代码

//HelloWhile.cpp

#include "llvm/IR/Function.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"

#include <vector>

using namespace llvm;


int main(int argc, char* argv[])
{
    LLVMContext context;

    //create a IRBuilder
    IRBuilder<> builder(context);

    //create a module
    Module* module = new Module("HelloModule", context);

    //create a sum function
    std::vector<Type*> parameters(1, builder.getInt32Ty());
    FunctionType* functionType1 = FunctionType::get(builder.getInt32Ty(), parameters, false);
    Function* function1 = Function::Create(functionType1, GlobalValue::ExternalLinkage, "sum", module); 
    Value* sum_n = function1->getArg(0);
    sum_n->setName("sum_n");


    //create some BasicBlock 
    BasicBlock* entryBlock = BasicBlock::Create(context, "entry.Block", function1);
    BasicBlock* whileConditionBlock = BasicBlock::Create(context, "while.condition", function1);
    BasicBlock* whileBodyBlock = BasicBlock::Create(context, "while.body", function1);

    BasicBlock* whileEndBlock = BasicBlock::Create(context, "while.end", function1);

    //fill the entry block
    //int i = 0;
    //int sum = 0;
    builder.SetInsertPoint(entryBlock);
    Value* iPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "i.address");
    Value* sumPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "sum.address");
    builder.CreateStore(builder.getInt32(0), iPtr);
    builder.CreateStore(builder.getInt32(0), sumPtr);
    builder.CreateBr(whileConditionBlock);

    //fill the while.condition.block
    //while(i<=n)
    builder.SetInsertPoint(whileConditionBlock);
    Value* i0 = builder.CreateLoad(builder.getInt32Ty(),iPtr);
    Value* conditionCmpResult = builder.CreateICmpSLE(i0, sum_n);
    builder.CreateCondBr(conditionCmpResult, whileBodyBlock, whileEndBlock);

    //fill the while.body.block
    //sum = sum + i
    //i++
    builder.SetInsertPoint(whileBodyBlock);
    Value* sum0 = builder.CreateLoad(builder.getInt32Ty(), sumPtr);
    sum0 = builder.CreateAdd(builder.getInt32(1), sum0);
    builder.CreateStore(sum0, sumPtr);

    Value* i1 = builder.CreateLoad(builder.getInt32Ty(), iPtr);
    i1 = builder.CreateAdd(builder.getInt32(1), iPtr);
    builder.CreateStore(i1, iPtr);

    builder.CreateBr(whileConditionBlock);

    //fill the while.end.block
    //return sum
    builder.SetInsertPoint(whileEndBlock);
    Value* sumResult = builder.CreateLoad(builder.getInt32Ty(), sumPtr);
    builder.CreateRet(sumResult);


    //create main function IRbuider

    FunctionType* functionTyp2 = FunctionType::get(builder.getInt32Ty(), false);
    Function* function2 = Function::Create(functionTyp2, GlobalValue::ExternalLinkage, "main", module);

    //create function block
    //  int n = 10
    //  return sum(n)
    BasicBlock* mainBlock = BasicBlock::Create(context, "main.block", function2);

    builder.SetInsertPoint(mainBlock);
    Value* nPtr = builder.CreateAlloca(builder.getInt32Ty(), nullptr, "n.address");
    Value* n0 = builder.CreateStore(builder.getInt32(10), nPtr);

    Value* MainReturn = builder.CreateCall(function1, n0);
    builder.CreateRet(MainReturn);


    //print the IR
    verifyFunction(*function1);
    verifyFunction(*function2);
    module->print(outs(), nullptr);

    return 0;
}

上述代码中出现了函数调用builder.CreateCall(),第一个参数是函数名,第二个参数是传参。

11.1编译

clang++ -O3 HelloWhile.cpp -o HelloWhile `llvm-config --cflags --ldflags` `llvm-config --libs` `llvm-config --system-libs`

此时在当前路径下生成了一个HelloIfElsePhi的可执行程序

11.2运行
运行HelloWhile(示例):

./HelloWhile

输出结果如下(示例):

; ModuleID = 'HelloModule'
source_filename = "HelloModule"

define i32 @sum(i32 %sum_n) {
entry.Block:
  %i.address = alloca i32, align 4
  %sum.address = alloca i32, align 4
  store i32 0, ptr %i.address, align 4
  store i32 0, ptr %sum.address, align 4
  br label %while.condition

while.condition:                                  ; preds = %while.body, %entry.Block
  %0 = load i32, ptr %i.address, align 4
  %1 = icmp sle i32 %0, %sum_n
  br i1 %1, label %while.body, label %while.end

while.body:                                       ; preds = %while.condition
  %2 = load i32, ptr %sum.address, align 4
  %3 = add i32 1, %2
  store i32 %3, ptr %sum.address, align 4
  %4 = load i32, ptr %i.address, align 4
  %5 = add i32 1, ptr %i.address
  store i32 %5, ptr %i.address, align 4
  br label %while.condition

while.end:                                        ; preds = %while.condition
  %6 = load i32, ptr %sum.address, align 4
  ret i32 %6
}

define i32 @main() {
main.block:
  %n.address = alloca i32, align 4
  store i32 10, ptr %n.address, align 4
  %0 = call i32 @sum(void <badref>)
  ret i32 %0
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值