LLVM IR / LLVM指令集入门

本文基于LLVM 12官方文档的LLVM Language Reference Manual。以学习笔记为主。所以本文会摘录一些常见/常用的指令。对于一些更加深层次的指令属性/特性,待我对LLVM有更深的理解再单独写文章记录。

1. LLVM IR简介

LLVM IR可以理解为LLVM平台的汇编语言,所以官方也是以语言参考手册(Language Reference Manual)的形式给出LLVM IR的文档说明。既然是汇编语言,那么就和传统的CUP类似,有特定的汇编指令集。但是它又与传统的特定平台相关的指令集(x86, ARM, RISC-V等)不一样,它定位为平台无关的汇编语言。也就是说,LLVM IR是一种相对于CUP指令集高级,但是又是一种低级的代码中间表示(比抽象语法树等高级表示更加低级)。

1. 1 LLVM IR的基本特点为:

  • SSA。在指令集层面,以SSA的形式表示。指令集中存在Phi指令。
  • 类型安全。它在指令集层面,是有类型的,也就是说指令是需要有特定的类型作为约束的。
  • 低级操作。指令集相对比较底层。
  • 灵活。能够很利索地表示“所有”的高级语言。也叫Universal IR。
  • 它是LLVM编译各个阶段通用的IR

1.2 LLVM IR的表示形式

  • 存在形式
    • 内存。基于LLVM开发需要用到许多的类。include "llvm/IR/XX"
    • 磁盘(二进制)。一般以.bc的形式存在。方便JIT编译器快速加载。
    • 磁盘(文本)。可读的汇编语言表示,一般以.ll的形式存在,本文研究的就是这种形式的LLVM IR
  • 特点
    • 轻量
    • 低级
    • 富有表达性
    • 有类型的。利于指针分析
    • 可扩展性的
  • 良好的格式
    • LLVM提供了验证格式正确性的Verification Pass。它不仅仅是语法上的格式验证,还从语义上进行验证。比如说%x = add i32 1, %x在语法上符合,但是它并不是SSA形式的,所以会验证格式错误

1.3 LLVM IR中的标识符(Identifier)

LLVM IR中有两种标识符类型

  • 全局标识。以@开头的
    • 函数
    • 全局变量
  • 局部标识。以%开头
    • 寄存器名
    • 类型名

LLVM IR中有3种标识符格式

  • 有名字的值(Named Values)

    • 格式:[%@][-azA-Z . ] [ − a − z A − Z ._][-a-zA-Z .][azAZ._0-9]*
    • 例如
      • %foo
      • @DivisionByZero
      • %a.really.long.identifier
  • 无名字的值(Unnamed Values)

    • 格式:用无符号数值作为它们的前缀
    • 例如
      • %12
      • @2
      • %44
  • 常量(后文再讲)

2. LLVM整体结构

官方文档罗列得非常多,这里暂时只列出几个我觉得比较重要的

2.1 Module

LLVM程序是由若干个Module组成。而Module中由如下三个部分组成:

  • 函数
  • 全局变量
  • 符号表项

其中函数/全局变量都可以认为是全局值(即全局函数和全局变量);
而全局值能够用一个指向内存位置的指针标识。其中

  • 全局变量通过指向char数组的指针标识。
  • 函数通过指向函数的指针标识。

全局值都有链接类型。链接,即它们可能会结合LLVM Linker,合并每个Module的函数/全局变量定义。并resolve前向声明,合并符号表项

2.2 链接类型

全局值(全局变量/函数)会有一种如下的链接类型(这里只简单了解2中类型):

  • private
    • 只能在当前module被访问
      链接private全局值到另外一个模块中,可能会导致此private对象被重命名以防止名字冲突
  • internal
    • 类似private,全局值在object file中被当成一个local符号
    • 与C语言中的static关键字相关
  • available_externally
  • linkonce
  • weak
  • common
  • appending
  • extern_weak
  • linkonce_odr,
  • weak_odr
  • external
  • extern_weak

2.2 调用惯例

LLVM支持如下几种调用模式。(这里只简单了解,后续深入再添加)

  • ccc
    The C calling convention
    支持变长参数的函数调用,容忍一些函数声明和实现的不匹配(C里面也是这样)
  • fastcc
  • coldcc
  • cc 10
  • cc 11
  • webkit_jscc
  • anyregcc
  • preserve_mostcc
  • preserve_allcc
  • cxx_fast_tlscc
  • swiftcc
  • tailcc
  • cfguard_checkcc
  • cc <n>
  • 更多可能会在未来添加

3.类型系统

类型系统是LLVM IR中最重要的特性之一。它带来好处:

  • 有类型让许多优化能够直接在IR上进行(一般的三地址码不行,它没类型)
  • 强类型使得LLVM IR更加可读

3.1 Void类型

void就是通常编程语言中的void,空。

3.2 Function类型

函数类型,它的表示成:

<return type> (<parameter list>)

例如:

  • i32 (i32): 它表示的函数为:i32类型作为函数入参,i32类型作为函数返回值
  • float (i16, i32 *) *: 它表示指向函数的指针,此函数以i16,i32指针类型作为入参类型,float作为返回值类型。
  • i32 (i8*, ...): 变长参数的函数,固定长度的入参为i8类型,返回值为i32类型
  • {i32, i32} (i32): i32,i32为结构体的前两个字段类型,此结构体类型作为函数的返回值类型,函数入参为i32类型。

3.3 First Class类型

First Class表示一大类的类型。包括:

  • Single Value
  • Label
  • Token
  • Metadata
  • Aggregate
3.3.1 Single Value Type
  • Integer(整数)
    • 表示为: iN, 其中N正整数,如i8,i32
  • Float-Point(浮点)
    • half
    • bfloat
    • float
    • double
    • fp128
    • x86_fp80
    • ppc_fp128
  • x86_mmx
  • Pointer(指针类型)
    • 表示形式为: <type> *
  • Vector(向量)
    • Fiexed-length vector(定长向量): < <# elements> x <elementtype> >
    • Scalable vector(变长向量): < vscale x <# elements> x <elementtype> >
      • <vscale x 4 x i32>
  • Label
  • Token
  • Metadata
  • Aggregate(聚合类型)
    • Array(数组):
      • 表示: [<# elements> x ]
    • Structure(结构体)
      • Identified normal struct type
        • %T1 = type { \<type list\> }
          • { i32, i32, i32}: 4字节三元组
          • { float, i32 (i32) * }: 第一个元素float,第二个元素函数指针,函数为4字节入参,返回4字节类型
      • Identified packed struct type
        • %T2 = type <{ \<type list\> }>
          • <{ i8, i32 }>: 只知道5字节大小
    • Opaque Structure
      • 没有结构体,类似C中的结构体类型前向声明
        • %X = type opaque
        • %52 = type opaque

4. 常量

  • 简单常量
    • 布尔常量:i1类型,’true’ 或者 ’false’
    • 整数常量: 4
    • 浮点常量: 123.421, 1.23421e+2,
    • Null指针常量: ‘null’
    • Token常量: ‘none’等
  • 复杂常量
    • 结构体常量:{ i32 4, float 17.0, i32* @G }
    • 数组常量:[ i32 42, i32 11, i32 74 ]
    • 向量常量:< i32 42, i32 11, i32 74, i32 100 >
    • Zero初始化:'zeroinitializer’能够用来初始化任何类型为零值
    • Metadata node:!{!0, !{!2, !0}, !“test”}, !{!0, i32 0, i8* @global, i64 (i64)* @function, !“str”}
  • 全局变量和函数的地址
    • 全局变量和函数的地址隐式地为链接时常量
    • 这些常量当它们的标识被使用时,会被显示地引用,并且是指针类型
  • Undefined值
  • Poison值
  • Well-Defined值
  • 基本块的地址
  • DSO Local Equivalent
  • 常量表达式

5. 指令集

指令集分为如下几大类,这里会挑一些指令列举,随着后续对指令更加深入,再对指令的具体内容进行补充。

  • 终止指令
  • 单目运算
  • 二元运算
  • 二元位运算
  • 向量操作
  • 聚合操作
  • 访问/操作内存
  • 强转操作
  • 其它操作

5.1 终止指令

  • ret

    • ret <type> <value>
    • ret void
    • value必须是first-class类型
    • 例子
      • ret i32 5 ; Return an integer value of 5
      • ret void ; Return from a void function
      • ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2
  • br

    • br i1 , label , label
    • br label ; Unconditional branch
    • 例子
      Test:
      %cond = icmp eq i32 %a, %b
      br i1 %cond, label %IfEqual, label %IfUnequal
      IfEqual:
        ret i32 1
      IfUnequal:
        ret i32 0
      
  • switch

    • switch , label [ , label … ]
    • 例子
      ; Emulate a conditional br instruction
      %Val = zext i1 %value to i32
      switch i32 %Val, label %truedest [ i32 0, label %falsedest ]
      
      ; Emulate an unconditional br instruction
      switch i32 0, label %dest [ ]
      
      ; Implement a jump table:
      switch i32 %val, label %otherwise [ i32 0, label %onzero
                                         i32 1, label %onone
                                         i32 2, label %ontwo ]
      
  • indirectbr

    • indirectbr <somety>* <address>, [ label <dest1>, label <dest2>, … ]
  • invoke

    • <result> = invoke [cconv] [ret attrs] [addrspace(<num>)] | (<function args>) [fn attrs]
      [operand bundles] to label <normal label> unwind label <exception label>
    • 说明
      • normal label对应于ret
      • exception label对应于resume或者其它异常处理机制
      • 这个指令对高级语言的catch实现很重要
    • 例子
      %retval = invoke i32 @Test(i32 15) to label %Continue
              unwind label %TestCleanup              ; i32:retval set
      %retval = invoke coldcc i32 %Testfnptr(i32 15) to label %Continue
              unwind label %TestCleanup              ; i32:retval set`
      
  • callbr

    • <result> = callbr [cconv] [ret attrs] [addrspace(<num>)] <ty>|<fnty> <fnptrval>(<function args>) [fn attrs] [operand bundles] to label <fallthrough label> [indirect labels]
  • resume

    • resume <type> <value>
  • catchswitch

    • <resultval> = catchswitch within <parent> [ label <handler1>, label <handler2>, … ] unwind to caller
    • <resultval> = catchswitch within <parent> [ label <handler1>, label <handler2>, … ] unwind label <default>
  • catchret

    • catchret from <token> to label <normal>
  • cleanupret

    • cleanupret from <value> unwind label <continue>
    • cleanupret from <value> unwind to caller
  • unreachable

5.2 单目运算

  • fneg
    • <result> = fneg [fast-math flags]* <ty> <op1> ; yields ty:result
    • 浮点数或者浮点向量的负

5.3 二元运算

  • add

    • <result> = add <ty> <op1>, <op2> ; yields ty:result
    • <result> = add nuw <ty> <op1>, <op2> ; yields ty:result
    • <result> = add nsw <ty> <op1>, <op2> ; yields ty:result
    • <result> = add nuw nsw <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • nuw
        • No Unsigned Wrap
      • nsw
        • No Signed Wrap
      • 如果无符号/有符号溢出,nuw/nsw设置,得到poison value
  • fadd

    • <result> = fadd [fast-math flags]* <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用相等类型的浮点/浮点向量上
  • sub

    • <result> = sub <ty> <op1>, <op2> ; yields ty:result
    • <result> = sub nuw <ty> <op1>, <op2> ; yields ty:result
    • <result> = sub nsw <ty> <op1>, <op2> ; yields ty:result
    • <result> = sub nuw nsw <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • nuw
        • No Unsigned Wrap
      • nsw
        • No Signed Wrap
      • 如果无符号/有符号溢出,nuw/nsw设置,得到poison value
  • fsub

    • <result> = fsub [fast-math flags]* <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用相等类型的浮点/浮点向量上
  • mul

    • <result> = mul <ty> <op1>, <op2> ; yields ty:result
    • <result> = mul nuw <ty> , <op2> ; yields ty:result
    • <result> = mul nsw <ty> <op1>, <op2> ; yields ty:result
    • <result> = mul nuw nsw <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • nuw
        • No Unsigned Wrap
      • nsw
        • No Signed Wrap
      • 如果无符号/有符号溢出,nuw/nsw设置,得到poison value
  • fmul

    • <result> = fmul [fast-math flags]* <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用相等类型的浮点/浮点向量上
  • udiv

    • <result> = udiv <ty> <op1>, <op2> ; yields ty:result
    • <result> = udiv exact <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • 无符号整数相除
      • 除0行为未知
      • 使用exact时,如果((%op1 udiv exact %op2) mul %op2) != %op1, 则udiv exact得到的结果为poison value
  • sdiv

    • <result> = sdiv <ty> <op1>, <op2> ; yields ty:result
    • <result> = sdiv exact <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • 有符号整数相除
      • 除0行为未知
      • 溢出行为未知
      • 如果exact使用,结果被舍入,则结果为poison value
  • fdiv

    • <result> = fdiv [fast-math flags]* <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用相等类型的浮点/浮点向量上
  • urem

    • <result> = urem <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • 无符号整数除法的余数
      • op2为0,行为未知
    • 例如
      = urem i32 4, %var ; yields i32:result = 4 % %var
  • srem

    • <result> = srem <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用到整数或者整数向量,两个操作数都必须是相同类型
      • 有符号整数除法的余数
      • 结果要么为0,要么与op1有相同的符号
      • op2为0,行为未知
      • 溢出行为未知
  • frem

    • <result> = frem [fast-math flags]* <ty> <op1>, <op2> ; yields ty:result
    • 约束
      • 作用相等类型的浮点/浮点向量上
      • 与被除数相同符号

5.4 二元位运算

  • shl

    • <result> = shl <ty> <op1>, <op2> ; yields ty:result
    • <result> = shl nuw <ty> <op1>, <op2> ; yields ty:result
    • <result> = shl nsw <ty> <op1>, <op2> ; yields ty:result
    • <result> = shl nuw nsw <ty> <op1>, <op2> ; yields ty:result
    • 说明
      • op1左移特定数量的位(op2为无符号)
      • 结果为(op1 * 2^(op2) ) % 2^n
  • lshr

    • <result> = lshr <ty> <op1>, <op2> ; yields ty:result
    • <result> = lshr exact <ty> <op1>, <op2> ; yields ty:result
    • 说明
      • 逻辑右移
  • ashr

    • <result> = ashr <ty> <op1>, <op2> ; yields ty:result
    • <result> = ashr exact <ty> <op1>, <op2> ; yields ty:result
    • 说明
      • 算数右移
  • and

    • <result> = and <ty> <op1>, <op2> ; yields ty:result
  • or

    • <result> = or <ty> <op1>, <op2> ; yields ty:result
  • xor

    • <result> = xor <ty> <op1>, <op2> ; yields ty:result

5.5 向量操作

  • extractelement

    • <result> = extractelement <n x <ty>> <val>, <ty2> <idx> ; yields <ty>
    • <result> = extractelement <vscale x n x <ty>> <val>, <ty2> <idx> ; yields <ty>
    • 例子
      • <result> = extractelement <4 x i32> %vec, i32 0 ; yields i32
  • insertelement

    • <result> = insertelement <n x <ty>> <val>, <ty> <elt>, <ty2> <idx> ; yields <n x <ty>>
    • <result> = insertelement <vscale x n x <ty>> <val>, <ty> <elt>, <ty2> <idx> ; yields <vscale x n x <ty>>
    • 例子
      • <result> = insertelement <4 x i32> %vec, i32 1, i32 0 ; yields <4 x i32>
  • shufflevector

    • <result> = shufflevector <n x <ty>> <v1>, <n x <ty>> <v2>, <m x i32> <mask> ; yields <m x <ty>>
    • <result> = shufflevector <vscale x n x <ty>> <v1>, <vscale x n x <ty>> v2, <vscale x m x i32> <mask> ; yields <vscale x m x <ty>>
    • 例子
      • <result> = shufflevector <4 x i32> %v1, <4 x i32> %v2, <4 x i32> <i32 0, i32 4, i32 1, i32 5> ; yields <4 x i32>
      • <result> = shufflevector <4 x i32> %v1, <4 x i32> undef, <4 x i32> <i32 0, i32 1, i32 2, i32 3> ; yields <4 x i32> - Identity shuffle.
      • <result> = shufflevector <8 x i32> %v1, <8 x i32> undef, <4 x i32> <i32 0, i32 1, i32 2, i32 3> ; yields <4 x i32>
      • <result> = shufflevector <4 x i32> %v1, <4 x i32> %v2, <8 x i32> <i32 0, i32 1, i32 2, i32 3, i32 4, i32 5, i32 6, i32 7 > ; yields <8 x i32>

5.6 访问/操作内存

  • alloca

    • <result> = alloca [inalloca] <type> [, <ty> <NumElements>] [, align <alignment>] [, addrspace(<num>)] ; yields type addrspace(num)*:result
    • 说明
      • 在当前正执行函数的栈帧分配内存,当函数返回到调用点时自动释放此内存。对象分配内存空间会根据datalayout的指示来
      • 例子
        • %ptr = alloca i32 ; yields i32*:ptr
        • %ptr = alloca i32, i32 4 ; yields i32*:ptr
        • %ptr = alloca i32, i32 4, align 1024 ; yields i32*:ptr
        • %ptr = alloca i32, align 1024 ; yields i32*:ptr
  • load

    • <result> = load [volatile] <ty>, <ty>* <pointer>[, align <alignment>][, !nontemporal !<nontemp_node>][, !invariant.load !<empty_node>][, !invariant.group !<empty_node>][, !nonnull !<empty_node>][, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !align !<align_node>][, !noundef !<empty_node>]
    • <result> = load atomic [volatile] <ty>, <ty>* <pointer> [syncscope("<target-scope>")] <ordering>, align <alignment> [, !invariant.group !<empty_node>]
      !<nontemp_node> = !{ i32 1 }
      !<empty_node> = !{}
      !<deref_bytes_node> = !{ i64 <dereferenceable_bytes> }
      !<align_node> = !{ i64 <value_alignment> }
    • 说明
      • 指定从哪个内存地址加载;指定的类型必须是已知first class类型(不包含opaque structural type)
    • 例子
      • %ptr = alloca i32 ; yields i32*:ptr
      • store i32 3, i32* %ptr ; yields void
      • %val = load i32, i32* %ptr ; yields i32:val = i32 3
  • store

    • store [volatile] <ty> <value>, <ty>* <pointer>[, align <alignment>][, !nontemporal !<nontemp_node>][, !invariant.group !<empty_node>] ; yields void
    • store atomic [volatile] <ty> <value>, <ty>* <pointer> [syncscope("<target-scope>")] <ordering>, align <alignment> [, !invariant.group !<empty_node>] ; yields void
      !<nontemp_node> = !{ i32 1 }
      !<empty_node> = !{}
    • 说明
      • 被用来写内存
      • 类型必须是的first-class类型
    • 例子
      • %ptr = alloca i32 ; yields i32*:ptr
      • store i32 3, i32* %ptr ; yields void
      • %val = load i32, i32* %ptr ; yields i32:val = i32 3
  • fence

    • 用于实现happens-before
  • cmpxchg

    • 用于自动修改内存(比较,然后修改)。
  • atomicrmw

    • 用于自动修改内存
  • getelementptr

    • <result> = getelementptr <ty>, * {, [inrange] <ty> <idx>}*
    • <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    • <result> = getelementptr <ty>, <ptr vector> <ptrval>, [inrange] <vector index type> <idx>
    • 说明
      • 获取aggregate数据结构的子元素的地址;它只执行地址计算不访问内存
    • 例子
      struct RT {
        char A;
        int B[10][20];
        char C;
      };
      struct ST {
        int X;
        double Y;
        struct RT Z;
      };
      int *foo(struct ST *s) {
        return &s[1].Z.B[5][13];
      }
      
       %struct.RT = type { i8, [10 x [20 x i32]], i8 }
       %struct.ST = type { i32, double, %struct.RT }
       define i32* @foo(%struct.ST* %s) nounwind uwtable readnone optsize ssp {
         entry:
         %arrayidx = getelementptr inbounds %struct.ST, %struct.ST* %s, i64 1, i32 2, i32 1, i64 5, i64 13
         ret i32* %arrayidx
       }
      

5.6 强转操作

  • trunc ... to
  • zext ... to
    • <result> = zext <ty> <value> to <ty2> ; yields ty2
    • 约束
      • 两个ty必须是整数,或者相同数量的整数向量
      • value的位数要比ty2类型的位数小
    • 例子
      • %X = zext i32 257 to i64 ; yields i64:257
      • %Y = zext i1 true to i32 ; yields i32:1
      • %Z = zext <2 x i16> <i16 8, i16 7> to <2 x i32> ; yields <i32 8, i32 7>
  • sext ... to
  • fptrunc ... to
  • fpext ... to
  • fptoui ... to
  • fptosi ... to
  • uitofp ... to
  • sitofp ... to
  • ptrtoint ... to
  • inttoptr ... to
  • bitcast ... to
  • addrspacecast ... to

5.7 其它操作

  • icmp

    • <result> = icmp <cond> <ty> <op1>, <op2> ; yields i1 or <N x i1>:result
    • <cond>
      • eq: equal
      • ne: not equal
      • ugt: unsigned greater than
      • uge: unsigned greater or equal
      • ult: unsigned less than
      • ule: unsigned less or equal
      • sgt: signed greater than
      • sge: signed greater or equal
      • slt: signed less than
      • sle: signed less or equal
    • 说明
      • 返回布尔值或者布尔值的向量
      • op1和op2必须是integer/pointer或者interger vector
    • 例子
      • <result> = icmp eq i32 4, 5 ; yields: result=false
      • <result> = icmp ne float* %X, %X ; yields: result=false
      • <result> = icmp ult i16 4, 5 ; yields: result=true
      • <result> = icmp sgt i16 4, 5 ; yields: result=false
      • <result> = icmp ule i16 -4, 5 ; yields: result=false
      • <result> = icmp sge i16 4, 5 ; yields: result=false
  • fcmp

    • <result> = fcmp [fast-math flags]* <cond> <ty> <op1>, <op2> ; yields i1 or <N x i1>:result
    • <cond>
      • false: no comparison, always returns false
      • oeq: ordered and equal
      • ogt: ordered and greater than
      • oge: ordered and greater than or equal
      • olt: ordered and less than
      • ole: ordered and less than or equal
      • one: ordered and not equal
      • ord: ordered (no nans)
      • ueq: unordered or equal
      • ugt: unordered or greater than
      • uge: unordered or greater than or equal
      • ult: unordered or less than
      • ule: unordered or less than or equal
      • une: unordered or not equal
      • uno: unordered (either nans)
      • true: no comparison, always returns true
    • 例子
      • <result> = fcmp oeq float 4.0, 5.0 ; yields: result=false
      • <result> = fcmp one float 4.0, 5.0 ; yields: result=true
      • <result> = fcmp olt float 4.0, 5.0 ; yields: result=true
      • <result> = fcmp ueq double 1.0, 2.0 ; yields: result=false
  • phi

    • <result> = phi [fast-math-flags] <ty> [ <val0>, <label0>], …
    • 说明
      • 每个val对应一个label;
    • 每个label对应一个basic block,此bb到phi节点的incoming value就是val
  • select

  • freeze

  • call

  • va_arg

  • landingpad

  • catchpad

  • cleanuppad

  • 10
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值