Smali语言基础语法

1 篇文章 0 订阅
1 篇文章 0 订阅

目录

 Smali前期知识

可省略的关键字

 Smali注释

Smali内部寄存器

Smali全类名路径

Smali基础(定义/声明)

Smali数据类型

Smali类声明

Smali函数声明

Smali函数返回关键字

Smali构造函数声明

Smali字段(变量)声明

Smali字段(常量)声明

Smali静态代码块

Smali提高(调用/修改)

Smali函数调用

Smali字段取值与赋值

Smali补充

Smali对象创建

Smali常量数据定义

Smali字符串常量定义

Smali字节码常量定义

Smali数值常量定义

Smali条件跳转

Smali逻辑循环


 Smali前期知识

可省略的关键字

定义:因Smali语言实质为一种便于逆向java的语言,因此其中包含许多便于阅读者理解其对应java源码的关键字,这些关键字即使省略也不影响smali的语法逻辑。

示例1:.line N关键字

.line 3  
const/4 v0, 0x1

iput v0, p0, LTest;->a:I

PS:.line N代表其下代码还原成java代码在源文件第N行 

示例2:.prologue关键字

.method public Test()V
    .registers N
    .prologue

    #do-something

    return-void
.end method

PS:.prologue代表函数的开始位置,其上为类似.registers的关键字,其下为函数逻辑。

示例3:.local关键字

move-result-object v0 # 调用方法后结果储存在v0中
.local v0, "b":Ljava/lang/String;  # 局部变量v0别名为b 是一个String类型 也就是 String b=v0

PS:.local关键字用于标识变量的别名与类型,在java中,一个变量会有一个类似i/j之类的变量名,并且对应的数据类型,而smali中数据都使用寄存器存储16进制数值,不存在这样的区分。因此使用.local关键字注明该变量的别名以及类型,方便理解。

 Smali注释

定义:同其他编程语言,Smali语言中也存在用于标识注释的符号:#

示例:注释示范

# 这是一段注释

Smali内部寄存器

定义:同高级编程语言将数据存入内存时刻调用实现变量存储不同,smali语言类似汇编,将所有的变量和常量都使用寄存器存储。因此,为了在函数的开头便将需要用到的寄存器入栈(其他函数也会用到寄存器,先将寄存器中原本的值入栈,在使用完毕后出栈,以保证当前函数对寄存器的使用不会导致其他函数存储在寄存器中的值被破坏),在一个函数内部的变量个数应当尽量少,并且总数必须在函数的开头声明。

格式:

.registers 寄存器总数(包括参数)    #若函数包含参数,则参数占用的寄存器也计算在内
.locals 寄存器总数(不包括参数)    #不考虑参数占用的寄存器

示例:smali中的函数形式

.method public callMe(II)V
    .registers 5
 	const-string v0,"1"
    const-string v1,"1"
   
   return-void
.end method
#或者
.method public callMe(II)V
    .locals 2
 	const-string v0,"1"
    const-string v1,"1"
   return-void
.end method

PS1:如上代码段所示,使用.registers时需要考虑参数的格式,故其后的寄存器总数更多。

PS2:以上callMe函数为非静态函数,因此对其的调用必须将其对应的类对象作为第一个参数传入,故以上callMe函数的参数实际上为三个,p0为类对象,p1为int,p2为int。

PS3:寄存器名从v0~v15一共16个,不可使用超过16个寄存器!(若参数中同样使用了寄存器则对应减少)

PS4:除非为abstract或final修饰的函数,否则.registers或.locals必须存在

Smali全类名路径

定义:在Smali中,常用到一个称为‘全类名路径’的概念,此概率意味着将一个类的类名以绝对路径的形式给出

示例:在com包下的Test类

Lcom/Test;

PS:如上,当需要使用Test类的‘全类名路径’时,将com.Test改写为com/Test,并在首部加上L,在末尾加上;

JAVA——APK——Smali转换

定义:转换过程:.java文件(java源码)=>.class文件(字节码文件)=>.dex文件(构成apk的文件)=>.smali文件

示例1:smali——java关系图

示例2:java编译为APK

对于用Java编写的Android程序,其转换过程为.java文件(java源码)=>.class文件(字节码文件)=>.dex文件(构成apk的文件)。

PS:.dex文件类似.class文件,是一种底层的,方便机器理解与运行的文件,要逆向修改一个APK可以通过直接修改dex文件实现,但是其难度较高。

示例3:APK反编译为smali文件包

对一个apk进行反编译即对构成apk的.dex文件包进行反编译,使用baksmali.jar可实现:.dex文件=>.smali文件。每个.smali文件实质上对应一个.java文件,对.smali文件进行修改后使用smali.jar可实现:.smali文件=>.dex文件。

PS:.smali文件是由smali语言书写的代码文件,而smali语言和java语言几乎具有一一对应的关系。相对来说易于理解、修改、书写。通过smali语言作为中转,可以实现反编译并修改一个APK。

Smali基础(定义/声明)

Smali数据类型

定义:与Java相同,Smali中也有一套数据类型体系,且该体系实质上是同Java一一对照的。

示例1:Smali——Java数据类型对照表

PS:如上图所示,Smali语言中的数据类型的关键词基本上为Java中同名数据类型的首字母大写,需要着重记忆的例外为:1.long类型的关键词为大写J;2.boolean类型的关键词为大写Z;3.数组的关键词为左半边中括号[;4.object类型(各种类)的关键词为大写L加上该类的全类名路径,路径中的层级用/分割。

示例2:smali——java变量定义对照

byte _byte=127;
short _short=888;
int _int=1234;
long _long=786565455;
float _float=347823.99f;
double _double=6875654564.121;
char _char='易';
boolean _bool=true;
const/16 v1, 0x7f
.local v1, "_byte":B

const/16 v3, 0x378
.local v3, "_short":S

const/16 v6, 0x4d2
.local v6, "_int":I

const-wide/32 v4, 0x2ee2094f
.local v4, "_long":J

const v7, 0x48df8c00
.local v7, "_float":F

const-wide v8, 0x41f99d229a41ef9eL
.local v8, "_double":D

const/16 v2, 0x6613
.local v2, "_char":C

const/4 v0, 0x1
.local v0, "_bool":Z

PS1:如上代码段2,其中const/16 v1, 0x7f    .local v1, "_byte":B这两句对应代码段1中的byte _byte=127;第一句const/16 v1, 0x7f意为将常量0x7f存入v1寄存器,在其中占用16位字节;第二句.local v1, "_byte":B意为将v1寄存器的别名定为_byte,并确定其数据类型为B(在smali中数据类型B对应java中的数据类型byte)

PS2:根据PS1中的逻辑递推,每两句smali中的变量定义代码对应一句java中的变量定义代码。

PS3:对于以上代码段2中以v0~v7命名的各寄存器,可见到其中缺失了v5寄存器,其原因为代码段:const-wide/32 v4, 0x2ee2094f    .local v4, "_long":J中J类型(long类型)的数据占用64位字节,因此需要两个寄存器才可完整存放,由此实质上v4、v5寄存器在此处都被占用了,只不过此时保存在其中的数据较小用不到v5寄存器,此处才未写出,若数值增大,则此处的代码中会出现对v5寄存器的使用,同理v9寄存器也在使用v8寄存器的同时被使用了。

Smali类声明

定义:在Smali中声明一个类的意义同Java,但Smali会将Java中省略的一些步骤显现出来,因此其更为繁琐、底层。

基础格式:

.class+权限修饰符+类名;

示例:Smali——Java类声明对照

public class Test implements CharSequence
{
}
.class public LTest;    #声明类(不可省略)
.super Ljava/lang/Object;    #声明该类所继承的父类,同Java,若没有指定其他父类,则所有类的父类都是Object(不可省略)
.implements Ljava/lang/CharSequence;    #若该类实现了接口,则添加该代码(视情况可省略)
.source "Test.java"    #反编译的过程中自动生成的标识该smali类对应的java源码类的标识,无实际作用(可省略)

PS:如上代码块,在Smali中定义一个类时必须将该类所有的信息完整表现,且该类的类名、继承的类名、实现的类名都要用L+全类名路径+分号的格式给出,以上Test类若非定义与根目录而是定义于com.temp包内,则全类名要用Lcom/temp/Test;的格式。

Smali函数声明

定义:在Smali中声明一个函数的内核通Java,但表现方式略有不同。

格式:

.method 权限修饰符+静态修饰符+方法名(参数类型)返回值类型
    #方法体
.end method

示例1:Smali——Java函数(无参无返回值)声明对照

public class Test
{
    public static void getName(){}
}
.class public LTest;
.super Ljava/lang/Object;
.source "Test.java"

.method public static getName()V
    return-void
.end method

PS:如上代码段所示,在Smali中的函数不以大括号结束而以.end method结束。且即使返回值为void也必须使用return-void进行返回

示例2:Smali——Java函数(带参带返回值)声明对照

public class Test
{
    public static String getName(String a,int b){
        return "hello"
    }
}
.class public LTest;
.super Ljava/lang/Object;
.source "Test.java"

.method public static getName(Ljava/lang/String;I)Ljava/lang/String;
    const-string v0, "hello"
    return-object v0
.end method

PS:如上代码段所示,当函数带参数时将参数在函数声明头中列出,各参数间不需要分隔符

PS:如上代码段所示,当函数带返回值时,将对应类型的值通过寄存器返回。

Smali函数返回关键字

定义:不同于Java,在Smali中,不同的函数返回类型需要使用不同的返回关键字

示例:函数返回关键字类别及数据类型对照表

PS:如上图1所示,smali中总共分为四种函数返回关键字。如上图2所示,对于不同的数据类型,使用不同的函数返回关键字

PS2:如上图2,long、double为64位数据类型,因此需要使用return-wide关键字,而其他基本数据类型为32位及以下,只需要使用return。对于空类型使用return-void。而对于大小未知的数组及object,则使用return-object。

Smali构造函数声明

定义:构造函数的声明类同函数的声明,但在几个地方有其独特性。

格式:

.method+权限修饰符+constructor <init>(参数类型)返回值类型
    #方法体
.end method

示例:Smali——Java构造函数对照

public class Test
{
    public Test(String a){
    }
}
.class public LTest;
.super Ljava/lang/Object;
.source "Test.java"

.method public constructor <init>(Ljava/lang/String;)V
    invoke-direct {p0},Ljava/lang/Object;-><init>()V #调用父类(Object)的构造函数
    return-void
.end method

PS:如上代码段,构造函数的声明与普通函数声明的区别在于:1.必须加constructor修饰符;2.函数名必须为<init>;3.函数中必须调用父类的构造函数(java的构造函数中默认会调用父类的构造函数,但在代码中可以省略,而在Smali中不能省略)。

Smali字段(变量)声明

定义:在Smali中声明一个字段(变量)的方式类似Java。

格式:

.field 权限修饰符+静态修饰符+变量名:变量全类名路径;

示例:Smali——Java字段声明对照

public class Test
{
    private static String a;
}
.class public LTest;
.super Ljava/lang/Object;
.source "Test.java"

.field private static a:Ljava/lang/String;    #声明一个String类的对象,命名为a

PS:如上代码段所示,在一个类中声明变量

Smali字段(常量)声明

定义:在Smali中声明一个字段(常量)的方式几乎同声明一个字段(变量)。

格式:

.field 权限修饰符+静态修饰符+final+常量名:常量全类名路径;=常量值

示例:Smali——Java常量声明对照

public class Test
{
    private static final String a="hello";
}
.class public LTest;
.super Ljava/lang/Object;
.source "Test.java"

.field private static final a:Ljava/lang/String;="hello"    #声明一个String类的常量对象,命名为a,赋值为"hello"

PS:如上代码段所示,在一个类中声明常量

Smali静态代码块

定义:在Smali中同样可声明静态代码块,其声明方式类同构造函数的声明。

格式:

.method+static+constructor <clinit>()V
    #方法体
.end method

示例1:Smali——Java静态代码块对照

public class Test
{
    static{}
}
.class public LTest;
.super Ljava/lang/Object;

.method public static constructor<clinit>()V
    return-void
.end method

PS:如上代码块所示,静态代码块的声明同构造函数的声明的区别在于:1.增加了static修饰符;2.函数名改为了<clinit>;3.无参数;4.返回类型固定为void

示例2:Smali静态字段声明位置

public class Test
{
    public static String a="a";
    static{}
}
.class public LTest;
.super Ljava/lang/Object;

.method public static constructor<clinit>()V
    .field public static a:Ljava/lang/String;="a"
    return-void
.end method

PS:如上代码块所示,当类中声明了静态的字段时,该字段的声明必须于静态代码块中进行,因该字段属于类而非对象,且静态代码块在构造函数之前被执行。

Smali提高(调用/修改)

Smali函数调用

定义:在Smali中调用方法的函数与Java中略有区别,对于不同的函数使用了不同的调用关键字

格式:

invoke-virtual    #非私有实例函数的调用
invoke-direct    #构造函数以及私有函数的调用
invoke-static    #静态函数的调用
invoke-super    #父类函数的调用
invoke-interface    #接口函数的调用

示例1:非私有实例函数的调用

invoke-virtual {参数},函数所属类名;->函数名(参数类型)返回值类型;
public class Test
{
    public Test(String a){
        getName();
    }
    public String getName(){
        return "hello";
    }
}
.class public LTest;
.super Ljava/lang/Object;

.method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V    #调用父类的构造函数
    invoke-virtual{p0},LTest;->getName()Ljava/lang/String;    #调用普通成员getName函数
    return-void
.end method

.method public getName()Ljava/lang/String;
    const-string v0,"hello"    #定义局部字符串常量
    return-object v0    #返回常量
.end method

PS:以上代码段中定义的getName函数是没有参数的,但是在调用该函数时却传入了一个参数p0,该p0参数实质上为类的对象,即java中的this。因被调用的getName函数非静态函数,因此在使用该函数时必须传入一个this作为参数,而这一步在java中被默认执行,故书写代码时常被省略。

示例2:构造函数以及私有实例函数的调用

invoke-direct {参数},函数所属类名;->函数名(参数类型)返回值类型;
public class Test
{
    public Test(String a){
        getName();
    }
    private String getName(){
        return "hello";
    }
}
.class public LTest;
.super Ljava/lang/Object;

.method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V    #调用父类的构造函数
    invoke-direct{p0},LTest;->getName()Ljava/lang/String;    #调用私有成员getName函数
    return-void
.end method

.method private getName()Ljava/lang/String;
    const-string v0,"hello"    #定义局部字符串常量
    return-object v0    #返回常量
.end method

PS:如以上代码段所示,对于私有函数以及构造函数,只需更改调用关键词。

示例3:静态函数的调用

invoke-static {参数},函数所属类名;->函数名(参数类型)返回值类型;
public class Test
{
    public Test(String a){
        String b=getName();
    }
    private static String getName(){
        return "hello";
    }
}
.class public LTest;
.super Ljava/lang/Object;

.method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V    #调用父类的构造函数
    invoke-static{},LTest;->getName()Ljava/lang/String;    #调用私有成员getName函数
    move-result-object v0    #将返回值赋给v0
    return-void
.end method

.method private getName()Ljava/lang/String;
    const-string v0,"hello"    #定义局部字符串常量
    return-object v0    #返回常量
.end method

PS:如以上代码段所示,对于静态函数的调用,不需要传入this,因此不需要p0参数。

示例4:父类成员函数的调用

invoke-super {参数},函数所属类名;->函数名(参数类型)返回值类型;
@Override
protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
}
.method protected onCreate(Landroid/os/Bundle;)V
    .registers 2
    
    invoke-super{p0,p1},Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
    return-void
.end method

PS:如以上代码段1所示为安卓开发中常见的onCreate函数,该函数在smali中的表现形式如上代码段2。

示例5:接口函数的调用

invoke-interface {参数},函数所属类名;->函数名(参数类型)返回值类型;
public class Test
{
    private InterTest a=new Test2();
    public Test(String a){}
    public void setAa(){
        InterTest aa=a;
        //调用接口方法
        aa.est2();
    }
    public class Test2 implements InterTest
    {
        public Test2(){}
        public void est2(){}
    }
    interface InterTest
    {
        public void est2();
    }
}
.method public setAa()V
    .registers 2
    iget-object v0,p0,LTest;->a:LTest$InterTest;
    #调用接口方法
    invoke-interface{v0},LTest$InterTest;->est2()V
    return-void
.end method

PS:如以上代码段1所示为java中调用一个接口内的函数,如上代码段2为调用接口函数的核心代码,接口定义等代码暂略。

Smali字段取值与赋值

定义:在Smali中为此前声明的字段进行取值与赋值操作,其中若该字段非静态字段,则赋值时需额外传入该字段对应的对象作为参数。

格式:

iput 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径    #静态字段
iput 存储值的寄存器,存储字段的对象,对象全类名路径->字段名:字段类型全类名路径    #字段
iget 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径    #静态字段
iget 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径    #字段

示例1:Smali基本数据类型取值赋值关键字表

PS:对于不同的数据类型,对其进行取值/赋值时需要使用不同的关键字

示例2:Smali实例变量取值赋值关键字表

PS:示例对象基本使用object后缀进行取值/赋值

示例3:smali——java取值赋值对照

public class Test
{
    private  String a="hello";
    public Test(String a){
    }
    public String getA(){
       String aa=a;
   }
}
.class public LTest;#声明类 (必须)
.super Ljava/lang/Object;#声明父类 默认继承Object (必须)
.source "Test.java" # 源码文件 (非必须)

# 声明静态字段
.field private static a:Ljava/lang/String;

#构造方法
.method public constructor <init>(Ljava/lang/String;)V
    .registers 3

    .prologue

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    const-string v0, "hello"
	# 初始化成员变量
    iput-object v0, LTest;->a:Ljava/lang/String;

    return-void
.end method


# 取值方法
.method public getA()Ljava/lang/String;
    .registers 2

	# 类非静态字段取值
    iget-object v0, LTest;->a:Ljava/lang/String;
    return-object v0
.end method

PS:如上,为静态方法的取值与赋值。

Smali数组的取值与赋值

定义:在Smali语法中,对数组元素进行操作有其专有方法

格式:

aput 存储值的寄存器,存储数组的寄存器,存储数组下标的寄存器
aget 存储值的寄存器,存储数组的寄存器,存储数组下标的寄存器

示例:

invoke-virtual {v1, v0}, Ljava/security/MessageDigest;->digest([B)[B
move-result-object v0
const/16 v4,47
const/16 v5,0
aput-byte v4,v0,v5
aget-byte v3,v0,v5

PS:如上,先将一个数字45从v4中存入字节数组的第0位,后从中将之取出放入v3。

Smali补充

Smali对象创建

定义:Smali语言的内核同Java,同样拥有对象的创建方式

格式:

new-instance+对象名,对象全包名路径;    #声明实例
invoke-direct{变量名},对象全包名路径;-><init>(参数)返回类型    #调用构造函数(若构造函数内还定义了成员变量,则在调用之前需要提前声明该变量并在invoke时作为参数一并传入)

示例:Smali——Java对象创建对照

Test _test=new Test();
new-instance v0,LTest;
invoke-direct {v0},LTest;-><init>()V

PS1:如上代码段所示为在Smali中建立对象的方式,但注意此处只是建立了对象并将之存入寄存器,并未给对象命名

PS2:如上代码段2所示,在创建对象并调用其构造函数时,需要将该对象作为参数传入。

Smali常量数据定义

定义:在Smali语言中,函数返回或函数调用等处若需使用常量,应当如同使用变量一般先行将之定义并存入寄存器中,后方可使用。

Smali字符串常量定义

定义:字符串常量的定义方式

格式:

const-string 常量名,"字符串内容"

示例:Smali中定义字符串常量并将之作为函数返回值

.class public LTest;
.super Ljava/lang/Object;

.method public static getHello()Ljava/lang/String;
    .registers 1    #该函数总共使用了1个寄存器
    const-string v0,"hello"    #定义字符串常量
    return-object v0    #将字符串常量作为返回值
.end method

PS:如上代码段所示,定义一个"hello"字符串存入寄存器,命名为v0,并将之作为返回值返回(因字符串为Object类型的数据,因此使用return-object)

Smali字节码常量定义

定义:字节码常量的定义方式

格式:

const-class 常量名,类全包名路径;

示例:Smali——Java定义字节码常量对照

Class a=TestClass.class
const-class v0,LTestClass;

PS:如上代码段所示,class类型的常量定义

Smali数值常量定义

定义:Smali中的数值常量定义相对复杂,根据数值的类别、大小需使用多种定义关键字。

格式:

const 寄存器,数值 #占用一个寄存器(32位)
    const/4    #占用一个寄存器中的低4位(最高位为符号位)
    const/16    #占用一个寄存器中的低16位(最高位为符号位)
    const    #占用一个寄存器中全部32位(最高位为符号位)
    const/high16    #占用一个寄存器中的16位,且只将数据的高16位存入(最高位为符号位)
const-wide 寄存器,数值 #占用两个寄存器(64位)
    const-wide/16    #占用两个寄存器的同时只使用第一个寄存器的低16位
    const-wide/32    #占用两个寄存器的同时只使用第一个寄存器的32位
    const-wide    #占用两个寄存器的同时只使用两个寄存器的全部64位
    const-wide/high16    #占用两个寄存器的同时只使用第一个寄存器的16位,且只将数据的高16位存入

示例:Smali——Java定义数值常量对照

int i=100;
long j=10000;
const/16 v0,64   
const-wide v1,2710

PS:smali语言中数值默认为16进制

Smali条件跳转

定义:Smali中同样有类似if-else结构的条件跳转语句,其逻辑类似汇编中的条件跳转。

格式:

if-eq vA, vB, :cond_**  #如果vA等于vB则跳转到:cond_** equal

if-ne vA, vB, :cond_**  #如果vA不等于vB则跳转到:cond_**   not  equal

if-lt vA, vB, :cond_**  #如果vA小于vB则跳转到:cond_**    less than

if-ge vA, vB, :cond_**  #如果vA大于等于vB则跳转到:cond_**   greater equal

if-gt vA, vB, :cond_**  #如果vA大于vB则跳转到:cond_**   greater than

if-le vA, vB, :cond_**  #如果vA小于等于vB则跳转到:cond_**  less equal

if-eqz vA, :cond_**  #如果vA等于0则跳转到:cond_** zero
if-nez vA, :cond_**  #如果vA不等于0则跳转到:cond_**
if-ltz vA, :cond_**  #如果vA小于0则跳转到:cond_**
if-gez vA, :cond_**  #如果vA大于等于0则跳转到:cond_**
if-gtz vA, :cond_**  #如果vA大于0则跳转到:cond_**
if-lez vA, :cond_**  #如果vA小于等于0则跳转到:cond_**

示例:Smali——Java条件跳转对照

public class Test {
    public static void main(String[] args) {
        int a=2;
        if(a>1){
            //do-something
        }
    }
}
.method public static main([Ljava/lang/String;)V

    const/16 v0,0x2
    const/4 v1, 0x1
    if-le v0,v1,:cond_0
        #do-something
    :cond_0
    return-void
.end method

PS:如上所示,可以看到因smali中的条件跳转同汇编逻辑相似,因此实现同样的效果时其if判定条件实际上同java中是相反的。

Smali逻辑循环

定义:Smali中同样有类似for结构的逻辑循环语句,其逻辑类似汇编中的jmp。

格式:

goto :cond_**  #跳转到:cond_**

示例:Smali——Java逻辑循环对照

public class Test {
    public static void main(String[] args) {
   
        for(int i=0; i<3;i++){
        }
    }
}
.method public static main([Ljava/lang/String;)V

    const/4 v0, 0x0

    :goto_1
    const/4 v1, 0x3

    if-ge v0, v1, :cond_7

    add-int/lit8 v0, v0, 0x1 # 加法运算符 v0=v0+0x1

    goto :goto_1

    :cond_7
    return-void
.end method

PS:如上所示,通过if-ge配合goto实现条件循环。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值