解读Class类文件结构

语言无关系的关键在 于 JVM字节码;Java虚拟机不与任何编译成class字节码的语言绑定,只要能够编译成有效的Class字节码都解析执行。

语言无关性


一、Class文件结构

任何一个Class文件都对应着唯一 一个接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。


问题: 当出现超过8位字节码来表示的数据项怎么办呢?

会按照高位在前的方式分割成若干个8位字节进行存储;补充大端法小端法


二 如何描述Class文件

无符号 组合成类似与C结构的伪结构来描述Class文件。


三、ClassFile Structure

一个class 由单个ClassFile 结构组成。ClassFile由下面结构组成。

ClassFile {
    u4             magic;        //识别Class文件格式,具体值为0xCAFEBABE; 即魔术
    u2             minor_version;//次版本号
    u2             major_version;//主版本号
    u2             constant_pool_count;//常量池容量计数
    cp_info        constant_pool[constant_pool_count-1];

    //它代表各种各样的字符串常量、类和接口名、字段名以及在ClassFile结构及其子结构中引用的其他常量。
    //每个常量池表条目的格式由它的第一个“标记”字节表示。

    u2             access_flags;     //访问标志
    u2             this_class;       //类索引
    u2             super_class;     //父类索引
    u2             interfaces_count;//接口索引数
    u2             interfaces[interfaces_count]; //接口索引集合
 //接口数组中的每个值必须是常量池中有效索引。

    u2             fields_count;     //字段数    
    field_info     fields[fields_count];
    u2             methods_count;   //方法数
    method_info    methods[methods_count]; 

    u2             attributes_count;  //属性数
    attribute_info attributes[attributes_count];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3.1 文件结构解析

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中中间没有添加任何分隔符


特定描述
8位字节为单位
顺序有严格控制
大小有规律,因为表是符合结构数据,大小不确定外,其他都是大小确定的。

类型

描述Class类文件结构的类型

类型描述
无符号u1u2u4u8来分别代表1个字节2个字节4个字节8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据

3.2 字节码文件解析

将下面一点代码使用JDK1.8编译成Class文件进行讲解


package org.fenixsoft.clazz;

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通过命令javac将这个类编译成classs文件。notepad++装一个HEX-Editor 插件后,打开这个class文件。


接下来就是解析这份字节码。


3.2.1 魔术 cafe babe

  u4             magic; 
 
 
  • 1

每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件。使用魔数而不使用文件扩展名是出于安全方面的考虑,因为文件扩展名可以很随意的被改动。


3.2.2 版本号

 u2             minor_version;//次版本号
 u2             major_version;//主版本号
 
 
  • 1
  • 2

minor_version:占2字节,次版本号,0x0000
majro_version:占2字节,主版本号,0x0034, 转换过来就是52


3.2.3 常量池(constant_pool_count 和 constant_pool)

ClassFile {
...
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
...
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

constant_pool_count : 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值 (计数从1开始,其他计数从0开始,因为0有其他作用)


(一) 常量池数目:

常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21


(二)常量池内容:

constant_pool : 主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析翻译到具体的内存地址之中

常量池的都是表结构,如上图展示,但是在常量池中的表的结构又是怎么样的呢?


注意:所有常量池中的条码都具有下面通用结构:

cp_info {
    u1 tag;
    u1 info[];
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

每个条码都有一个tag 去标志它
常量池中的条目


结合Class字节码来讲解常量池中各个类型的结构

下面圈定了常量池中的内容,通过tag 定位到具体哪个类型,根据类型的结构进行逐一分析

常量池中的内容


按照常量池中类型,整理出21个记录

序号字节码常量池类型字符串
10a 00 04 00 12CONSTANT_Methodref_info
209 00 03 00 13CONSTANT_Fieldref
307 00 14CONSTANT_Class
407 00 15CONSTANT_Class
501 00 01 6dCONSTANT_Utf8_infom
601 00 01 49CONSTANT_Utf8_infoi
701 00 06 36 69 6e 69 74 3eCONSTANT_Utf8_info
801 00 03 28 29 56CONSTANT_Utf8_info()
901 00 04 43 6f 64 65CONSTANT_Utf8_infoCode
1001 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 65CONSTANT_Utf8_infoLineNumberTable
1101 00 12 4c 6f 63 61 6c 56 72 69 61 62 6c 65 54 61 62 6c 65CONSTANT_Utf8_infoLocalVariableTable
1201 00 04 74 68 69 73CONSTANT_Utf8_infothis
1301 00 1f 4c 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 74 3bCONSTANT_Utf8_infoLong/fenixsoft/clazz/TestClass;
1401 00 03 69 6eCONSTANT_Utf8_infoinc
1501 00 03 28 29 49CONSTANT_Utf8_info()I
1601 00 0a 53 6f 75 72 63 65 46 69 6c 65CONSTANT_Utf8_infoSourceFile
1701 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61CONSTANT_Utf8_infoTestClass.java
180c 00 07 00 08CONSTANT_NameAndType
190c 00 05 00 06CONSTANT_NameAndType
2001 00 1d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2e 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73CONSTANT_Utf8_infoorg/fenixsoft/clazz/TestClass
2101 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74CONSTANT_Utf8_infojava/lang/Object

constant_pool table 把上图看做常量池表; 可以理解为将所有


同时也可利用javap 命令整理



3.2.3.1 常量池分析

(一) 根据tag值找类型:

在常量池后面的第一个地址(偏移地址:0x0000000A),是 0x0A (十进制为10),tag=10; 找到Constant Pool tag 定位到了 CONSTANT_Methodref 这个类型。

(二) 结构类型:

Fields, methods, and interface methods 具有相似的结构

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

分别对 CONSTANT_Methodref_infotag, class_index,name_and_type_index 进行介绍

items描述
tagCONSTANT_Methodref_info结构的tag值为10
class_indexCONSTANT_Methodref_info 结构的class_index必须是一个类类型,而不是接口类型
name_and_type_indexname_and_type_index 的值必须是constant_pool表 中的一个有效索引;这索引值上的对应的常量池条目(The constant_pool entry) 也一定是CONSTANT_NameAndType_info 结构;这个条目也是具有字段或方法作为成员的类或接口类型

【entry 条目;词条;账目;记录 】

补充:假如CONSTANT_Methodref_info 结构的方法名称以'<' ('\u003c') 开始,那么这个方法一定是特定的 <init>; 代表一个实例初始方法,返回类型也一定是 void。

16进制


CONSTANT_NameAndType_info 结构类型

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • tag: CONSTANT_NameAndType_infotag 值一定是 12

  • name_index : 它的值一定是constant_pool table(常量池表) 有效索引值。那个索引对应的constant_pool entry(常量池条目),一定是CONSTANT_Utf8_info 结构; 代表特定的 方法 名; 也可以表示字段或方法的有效限定名

  • descriptor_index: 它的值是constant_pool table(常量池表)的一个有效索引,这个constant_pool entry 将是一个CONSTANT_Utf8_info 为结构; 代表字段或者方法的描述符。


结构: CONSTANT_Utf8_info

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

这个CONSTANT_Utf8_info 结构别用来表示常量字符串值。

  • tag:CONSTANT_Utf8_info 的tag值一定是 1
  • length: bytes数组的长度值;不是实际字符串的长度值。
  • bytes:字节数组包含了字符串的每个字节
    • 如果没有字节可是值为0
    • 也可能是因为没有字节分布于 [(byte)0xf0 , (byte)0xff] (0-255)

length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:从'\u0001'到'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从'\u0080'到'\u07ff‘之间的所有字符的缩略编码用两个字节表示,从'\u0800'到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示


常量池中14中常量项结构总表

可以通过查阅总表来获取各项信息


没有出现在代码中的其他常量

其中有一些常量似乎从来没有在代码中出现过,如“I”“V”“<init>”“LineNumberTable”
“LocalVariableTable”等,这些看起来在代码任何一处都没有出现过的常量是哪里来的呢

3.2.4 访问标记(access_flags)

这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等

TestClass是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLICACC_SUPER标志应当为,而ACC_FINALACC_INTERFACEACC_ABSTRACTACC_SYNTHETICACC_ANNOTATIONCC_ENUM这6个标志应当为,因此它的access_flags的值应为:0x0001|0x0020=0x0021


3.2.5 类索引、父类索引与接口索引集合

    类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系


  • 类索引用(this_class)于确定这个类的全限定名,
  • 父类索引(super_class)用于确定这个类的父类的全限定名。
  • 由于Java语言不允许多重继承,所以父类索引只有一个; 但是接口可以实现好几个,所以是个集合,
  • 除了java.lang.Object之外, 所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0

结构描述:


itemdesc
this_classthis_class 的值一定是常量池中的有效索引,这个索引对应的常量项是一个CONSTANT_Class_info ;这个表示由这个类文件定义的类或接口
super_class可能是0 ,或者是常量池中有效索引,这个索引对应的常量项是一个CONSTANT_Class_info;代表这个类文件对应类的父类;直接超类和它的任何超类都不能在其类文件结构的access flags项中设置ACC_FINAL 标志;加入这个值为0,那个这个文件一定是Object类
interfaces_count接口数量
interfaces[]数组中的每个值都必须是常量池中的有效索引,每个索引对应的常量项都是CONSTANT_Class_info 这样的结构。 顺序从左到右

因为interfaces_count 数量为 0 ,所以后面的interfaces[] 就不占用地址。


3.2.6 字段表集合

结合案例,字段表的数量为 1个;让后对这个进行分析

每个字段的结构都如下:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

一个类文件中没有两个字段可能具有相同的名称和描述。

(一) 访问标识符( access_flags )如下:

标志描述
ACC_PUBLIC0x0001public 访问修饰符
ACC_PRIVATE0x0002private 访问修饰符
ACC_PROTECTED0x0004protected 访问修饰符
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_VOLATILE0x0040valatile
ACC_TRANSIENT0x0080transient
ACC_SYNTHETIC0x1000synthetic
ACC_ENUM0x4000enum

对访问标识符的解释
ACC_PUBLICACC_PRIVATEACC_PROTECTED三个标志最多只能选择其一
接口中的字段必须有ACC_PUBLICACC_STATICACC_FINAL标志
ACC_ENUM标志指示该字段用于保存枚举类型的元素
ACC_SYNTHETIC标志表明该字段是由编译器生成的,并没有出现在源代码中

(二) name_index:

对常量池的引用, 其值为常量池中的有效索引,代表着字段的简单名称。

(三) descriptor_index:

  • 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
  • 根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值void类型都用一个大写V字符来表示,而对象类型则用字符L加对象的全限定名来表示


  • 对于数组类型,每一维度将使用一个前置的[字符来描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为:[[Ljava/lang/String,一个整型数组int[]将被记录为[I

  • 用描述符来描述方法时按照参数列表,返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()之内。

  • 如方法 void inc()的描述符为()V
  • 方法 java.lang.String toString() 的描述符为()Ljava/lang/String
  • 方法 :
int indexOf(char[]source, 
    int sourceOffset, 
    int sourceCount, 
    char[]target, 
    int targetOffset,  
    int targetCount, 
    int fromIndex)

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

描述符为([CII[CIII)I


字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的



3.2.7 方法集合

结合案例,本次方法的数量为2

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过方法表结构,集合本文案例


(一) access_flags:


与属性进行对比:

  • 因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。
  • 与之相对的,synchronizednativestrictfpabstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZEDACC_NATIVECC_STRICTFPACC_ABSTRACT标志。

(二) name_index:

(三) 描述符索引:

描述符索引

通过分析得出了public void init()


方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目


与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>方法和实例构造器<init>


3.2.8 属性表(attribute_info)集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息;限制相对其他表相对宽松一些。

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。下面每个属性都具备的结构特征。

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

在分析方法表的时候已经讲述了一个Code属性
(一)Code属性描述

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口者抽象类中的方法就不存在Code属性

下面的图中就是Code.


Code的两个属性值:


类型名称数量描述
u2attribute_name_index1attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code,它代表了该属性的属性名称
u4attribute_length1attribute_length指示了属性值的长度
u2max_stack1max_stack代表了操作数栈(Operand Stacks)深度的最大值。虚拟机运行的时候需要根据这个值来分配栈帧(StackFrame)中的操作栈深度
u2max_locals1max_locals代表了局部变量表所需的存储空间
u4code_length1code_length代表字节码长度;理论上一个方法的字节码不超过u4,但实际是u2,如果超过这个限制,javac会拒绝
u1codecode_lengthcode是用于存储字节码指令的一系列字节流
u2exception_table_length1异常表长度
exception_infoexception_tableexception_table_length
u2attributes_count1属性数量
attribute_infoattributesattributes_count

Code属性表中的code(字节码)
2a b7 00 0a b1
 
 
  • 1
  • 读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶
  • 读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
  • 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器<init>方法的符号引用。
  • 读入b1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。

Java虚拟机执行字节码是基于栈的体系结构。但是与一般基于堆栈的零字节指令又不太一样,某些指令(如invokespecial)后面还会带有参数


继续分析Code中的剩余两个属性:LineNumberTableLocalVariableTable

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:lines选项来取消或要求生成这项信息。

如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pcline_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号

归纳为下面的结构

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number; 
    } line_number_table[line_number_table_length];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

.LocalVariableTable属性结构如下

LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,
  • 它也不是运行时必需的属性,但默认会生成到Class文件之中,
  • 可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。

local_variable_info项

itemdesc
start_pclength代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围
name_indexdescriptor_index指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符
index这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为indexindex+1两个


另外一个方法(不做详细介绍了):


使用javap打印信息

args_size 为什么等于 一

<init>()和inc(),都没有参数的,为什么args_size会为1?
而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那Locals又为什么会等于1?

在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果inc()声明为static,那Args_size就不会等于1而是等于0


ClassFile 最后一个属性:


到这个地方整个class文件就大致讲完了


后记

  • 还有很多属性没有讲解;当分析到对应的表时查询官方文档即可
  • code 字节码指令还没有详细讲解;将在新的一篇中详细讲解。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值