编译原理——小白看这一篇很有帮助

编译原理的一些理解

目录

前言

一、       编译器翻译流程

二、       词法分析以及Flex工具

三、       TINY源码中看词法分析

四、       语法分析和Bison工具

五、       TINY源码中看语义分析和中间代码(目标代码)生成

六、       TM目标平台和中间代码规范

七、       总结

 

 

 

前言

编译,这个词作何解释?可以说是一个字一个字的翻译,何解?“译”解为翻译之意。如同我们与外国人交流时,要将我们的语言转换翻译成他们能够理解意思的语言,常见的就是中译英。可想而知,如果两个人的语言不一致,那么他们将难以交流,也无法确切的表达出自己的想法给他人去执行或者协作,所以我们用翻译官这个工作,更有甚者就是同声翻译这个技能。那么就引出了编译器,编译器就是充当着翻译官的角色。给何人翻译?给人和机器之间做翻译。机器是人造到,但是听不懂人的语言,是不是觉得不可思议,其实不然,一开始人造的计算机,人们使用的语言(01机器码)机器当然是听得懂的,但是那样的语言对人类来说过于困难,学习门槛高,于是就是有了其他的利于人们理解的语言(如汇编),再之后就是高等程序语言百花齐放的时候了。所以,说到底,这个翻译官充当的作用就是,将人说的高等程序语言翻译成机器能听懂的01机器码,这就是编译的“译”,当然再此基础之上还可以考虑翻译的“信达雅”,也就是优化,减少废话的翻译,只正确无误表达核心内容。

那么还有前头一个“编”,放在前头说明编更加重要,这也是不是叫做译编却是编译的原因吧。“编”就是编程实现“译”的过程,将翻译的过程使用程序编程来实现,也就是“编”指的是生成编译器的工具。可以说,我认为,没有这个“编”的过程,也就没有编译的这个学科领域,为什么,因为这实现了一个质的飞跃,可以说是等同于计算机诞生的价值。先说计算机诞生,诞生于的二战时期的计算机是为了破译当时德国的恩尼格玛密码机,它有一亿亿个可能性,然后会每日切换公钥,所以使得当时的英俄盟军要必须在一日之内破解出他们真正的密钥,这样庞大的计算量,是人类所无法完成的,因而图灵前辈就提出了计算机并制造了它,这就是一个质的飞跃。而编译的“译”来说,让一个人写一个高等程序语言到目标代码的编译器或许他能承受,但是如果因为某些原因,语言需要拓展,或者单纯就是出现了很多很多的高级语言,需要这个人去写百十个编译器,这样的工作或许就难以完成了吧,因而这个“编”便体现出了价值所在,拿着编译器生成工具,高级语言要求改一下作为输入,能够很快给出对应的编译器程序,这就是一个质的飞跃,也是一个艺术性的体现

这就是我所学习到的编译之意。编译器构建方法学就是使用模型、算法以及工具去生成编译器。工具毫无疑问指的就是编译器生成工具,即lex和yacc(升级版是FLEX和BISON),模型就是生成工具的输入文件格式(如FLEX的词法文件.l),而算法应该就是在翻译过程中如何能够做到信达雅的翻译。以上,就是我对整个课程学习的理解和感悟的总概。

 

图1.1 编译器翻译流程

上图是根据当前所学和所认知画的一个编译器对代码文件进行翻译的工作流程图。当中融入了FLEX词法分析生成工具生成词法分析部分和BISON语法分析生成工具生成语法分析部分和语义分析部分以及中间代码部分的内容,也是整个编译过程中的最主要的部分。下面细说一下翻译流程中每一个部分的工作(参考资料)。

1.词法分析器

将输入的字符流转换为特定的词素(标识符)。这一步是识别组合字符的过程,识别出数字,标识符,关键字等过程就在于此。若我们这里设计的编译器不包含预处理器,那么,在这里还需要做一些预处理的过程,如识别出注释,并将其忽略掉。使用正则表达式来描述词法单元的模式。这一步的输入是字符,输出是标识符流。

2.语法分析器

将输入的标识符流转换为特定的语句。这里面需要做的就是组合单词,并按照特定的语法规则去匹配出合法的语句。如 if (bool-expression ){ statement } 就是简单的if 语句,而所需要的就有 关键字if ,符号 (,布尔表达式,符号 ) ,符号 { ,合法的语句, 符号 } 。而这里的布尔表达式与合法的语句则又有合法的标识符流组成。而我们把这一步处理完毕后,我们只会留下我们所需要关注的核心内容。如if 语句,我们传递下去后,我们就不需要符号 ( )  { } 等了。而表示这样的核心内容则是抽象语法树(Abstract Syntax Tree ,简称AST)。使用文法(强大于正则表达式)来描述语法单元的模式。这一步的输入是标识符流,输出是抽象语法树。另外在语法分析的过程中实际上是包含了下面两步的——语义分析和中间代码的生成——即后两步并不是独立的环节,而是在语法分析的过程中附带完成的。

3.语义分析

在这一步,我们往往会对AST进行分析。如类型检查,变量先定义后使用等。如简单的int i = "hello"; 那么我们在遍历AST的时候,则会识别出来类型问题。而在这里我们往往需要一个符号表来记录变量以及对应的类型,然后分析AST的时候来进行查询与处理等。这里引入一个重要概念,语法制导翻译方案(Syntax Directed Transaction Scheme,简称SDT),在SDT的动作中插入对应的语义分析动作即可在语法分析的过程中完成语义分析部分这一步的输入是抽象语法树,输出的依然是抽象语法树

4.中间代码生成

这一步处于来编译器前端与后端的交界处。一般来说,我们会设计一个抽象于机器平台的中间语言( Intermediate Representation,简称IR,如三地址码 ) 以便于进行后续机器无关的优化(如死代码消除,函数内联优化,for循环展开等)以及生成多机器平台的底层代码。在这一方面,往往有两个选择,一个是基于栈的IR,一个是基于寄存器的IR。前者易于编程与操作,而后者则性能更好。一般来说,很多编译型语言的IR都是基于寄存器的,如LLVM IR。可以知道,TINY编译器的中间代码生成是基于寄存器的,而我们课程所学的内容是基于栈的IR。LR分析是在规约时完成动作,LL分析是在展开中完成动作。这一步的输入是抽象语法树,输出的是中间代码

5.中间代码优化

这一步则是来让代码更加的高效。中间代码优化也叫做机器无关代码优化。机器无关的优化如寻找公共子表达式,消除死代码,代数恒等式的利用,函数内联优化,for循环展开等。这一步输入是中间代码IR, 输出依然是中间代码IR而若编译器有多套的IR,那么可能输入的IR与输出的IR是不一样格式的IR,但是如LLVM这样的则是同一套IR。

6.目标代码生成

根据《编译原理》教材上的内容,目标代码的生成是根据中间代码(三地址码)语句的内容生成的DAG(有向无环图)去构建目标代码,规定了几种基本的简单的操作,如LD dst,addr 把位置addr上的值加载到位置dst上,对于三地址码 x = 1 则需要一个LD指令去将1这个const整数值加载到寄存器中。这一步输入的是中间代码IR,输入的是目标代码。

这就是整个编译器需要做的工作。即对每一个目标语言的源代码需要进行这些工作,使得高级程序语言变成了一个目标机器能够听得懂然后去执行的语言。

词法分析(lexical analysis)是计算机科学中将字符序列转换为标记(token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner)。词法分析器一般以函数的形式存在,供语法分析器调用。

--摘自维基百科

       可以知道,这里的词法分析就是将输入的代码序列扫描然后划分成一个个的词素或者叫Token,以供其后的语法分析器作为输入。而这个词法分析的过程就是使得字符流输入变成易于处理的token流。词法的输入模式是正则表达式,将需要的规则提炼成正则表达式作为词法分析工具的输入。

Flex(lex)工具能够将输入的正则表达式和一些规则变成C语言的代码。

lex文件格式:

%{

C语言声明,一般声明全局变量和函数,会复制进lex.yy.c

%}

定义正则表达式的名字,可以在规则段中使用

%%

规则段,每一行都是一条规则,每一条规则由匹配模式和事件组成。每当一个模式被匹配到,后面的事件被执行!

%%

用户自定义过程,直接复制到lex.yy.c末尾

图2.1 TINY语言的词法(lex)文件

       Flex 是词法分析工具,它读取输入源文件,然后生成 C 语言源程序,通常默认的是 lex.yy.c, 该文件中包含 yylex( ) 函数,并且可以被 C 编译器编译链接为可执行文件,在该词法分析器运行时,它会根据已定义的规则,在遇到一定的匹配模式时执行相应的C代码,从而完成词法分析动作。当调用yylex的时候,程序会从yyin指针所指向的输入流中逐个读入字符,程序发现了最长的与某个模式Pi 匹配的字符串后,会将该字符串存入到yytext变量中,并设置yyleng变量为该字符串的长度,该字符串也就是词法分析程序分析出来的一个词法单元。然后,词法分析程序会执行模式Pi 对应的动作Ai  (如上面若匹配了”if”就会执行后面的语句{return IF;} ),并使用yylex函数返回Ai 的返回值(通常是词法单元的类型)。

而在TINY词法分析中,使用flex工具之后输出生成的c语言代码是可以得到对应的getToken函数去替换TINY源码中的词法分析部分,编译器是依旧可以运行的。

  • TINY源码中看词法分析

图3.1 部分TINY源码

由TINY源码中NO_PARSE设为true时,程序变成词法分析就可以知道getToken即是tiny程序的词法分析函数,即其所属的scan.c文件为词法分析文件。

图3.2 getToken函数代码

可以看到,getToken函数中有个state是用来表示当前输入缓存区的字符流是什么状态的,初始是START表示刚开始,当前字符串为空。于是在case START中,它需要根据当前拿到的字符char是什么而进入不同的状态,如isdigit()返回true的话就表示这个是个数字,那么进入INNUM状态,当该字符不在满足是isdigit()时,程序会执行ungetNextChar()函数,将当前审阅的字符返还会去,下次再识别(实际上就是指针linepos自减)完成一个回溯的流程。同样,当前符号是一个’=’时,状态为START,则会匹配进入case ‘=’中,是的当前的Token被设置成EQ,表示判等。这就是TINY词法分析做的工作。

语法分析(英语:syntactic analysis,也叫 parsing)是根据某种给定的形式文法对由单词序列(如英语单词序列)构成的输入文本进行分析并确定其语法结构的一种过程。

语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成

--摘自维基百科

       语法分析是编译器工作的第二步,而这也是至关重要的一步,因为其后的语义分析,中间代码生成,都在夹杂在语法分析的过程中完成的。语法分析就是确定上一步得到的词法分析标识符是如何彼此关联的,将Token流识别并做语义分析,IR生成,还有错误检查,然后留下真正核心的部分,就是直白的告诉机器,它需要做什么的部分,也就是抽象语法树AST。

自底向上分析(LR):语法分析器从现有的输入符号串开始,根据相应的文法,决定是该进行移入还是规约,从而改变输入串的形态,如果逻辑匹配成功,最后整个输入Token流会被规约成一个文法的起始符号。

图4.1 BISON语法分析工具

       对于语法分析的生成工具Bison来说,只需要给它一个目标语言的上下文无关文法和相应的语法制导翻译方案SDT,它就能生成该目标语言的语法分析部分到IR生成部分的程序。

Bison(yacc)工具能够将输入的上下文无关文法或者SDT变成C语言的代码。

yacc文件格式:

%{

C语言声明,一般声明全局变量和函数

%}

%token 声明标识符流有哪些,需要和词法分析保持一致,可以在规则段中使用

%%

规则段,每一行都是一条CFG(上下问无关文法)

%%

用户自定义过程,直接复制到tiny.tab.c末尾

图4.2 TINY文法文件tiny.y

Bison为lalr语法分析,属于LR自底向上从左到右分析方法,规约动作是一段C代码,它的作用是每当分析器识别出一个语法符号时,调用该代码,完成一定的动作。通常,我们使用这段代码,来建立当前语法节点与子节点勾连动作。规约动作应该紧接在语法规则的后面。因而就可以在语法分析的过程中产生语法分析树,也就是上面的tiny.y文件所做的。

 

Bison源码分析

Bison是能够解析文法规则并生成为对应的C语言代码的工具。满足一定限定条件的前提下,使用自底向上的移进-规约算法来完成。对于真实的使用场景,显然非递归的运行效率要远远高于递归下降分析算法(LL1),因此也是绝大多数语法分析器的实现方式。对于Bison语法分析器生成器而言,这里所谓的“限定条件”,即为输入的形式化语法只要满足LALR(1)文法,即可自动化地生成语法分析器。

图4.3  bison源码main函数部分

查看main.c文件,然后对main函数进行分析,发现我们关注的上下文无关文法到DFA就是上图中的1 2 两点。

图4.4  generate_states函数

generate_states()产生的是LR(0)状态, 一个状态包含进入这个状态的符号、它包含的核心项以及它的归约信息和转换信息等。第一个状态是无中生有的。状态是在已有状态基础上产生的。根据每个状态的核心项,生成它的闭包,这个闭包的内容就是在这个状态下可以看见的产生式,生成项集,由项集生成新的状态所需的符号和核心项, 从而生成新状态。产生状态过程是个正向过程,在什么状态和产生式下,看到的下一个输入符。这正是语法分析过程所需的,语法输入看到的只有输入符, 根据状态确认它一下怎么走,是移入、还是归约, 还是语法分析完成。状态有了,要做优化,到了下一步lalr()

图4.5  lalr函数

lalr()目的是要获取向前看符号即FOLLOW函数的结果加入到DFA中

initialize_LA()函数生成向前看符号所需的空间,计算每个状态FOLLOW函数中的符号数量,和空间的起始地址。

initialize_F()函数是计算某个状态后继的符号中的两种情况。1种情况是终结符,1种情况是非终结符。这些符号是在它转换表中的。如果是终结符,那它就是这个状态的后继,如果是非终结符,要判断它是否可以推导出空串,如果可以,那么它的后继也是某个状态的后继。注意这里的后继是终结符,即向前看符号,因输入的总是终结符。即是求FOLLOW函数。

build_relations()函数处理获取向前看符号的第三种情况,某个非终结符在产生式最右边或它在产生右边时,它右边都是非终结符,这些非终结符又可以推导出空串。这时这个非终结符(状态)向前看符号是这个产生式左边的非终结符的向前看符号。

 

到此,文法->LR0(NFA)->LALR(DFA)的过程,其实关键是在产生LR0,之后加上follow向前看符合就可以变成LALR了。generate_states()产生lr(0)状态,generate_states()是个正向推导过程,根据语法文件,确认所有的可能。

  • TINY源码中看语义分析和中间代码(目标代码)生成

图5.1  TINY中间代码格式

显而易见的,tiny的源码中,中间代码格式为 LD xx,ST XX即main函数的codeGen函数中的cGen函数对语法树生成对应的中间代码(也是目标代码,目标就是TM机)。中间代码的指令集和相关的规范会在后面第六节目标平台时进行分析讲解。

TINY编译器整体逻辑可言看下面链接或者图片。
http://naotu.baidu.com/file/1466a91173622a4d7eb9849f546101f5?token=ecdb5a66da7d7e85

图5.2  TINY编译器脉络图

图5.3  TINY编译器源码main函数部分

       可以看出,main1函数中,1为词法分析,2为语法分析,是根据tiny.y(图4.2)生成的,也就是仅仅具有生成抽象语法树的动作的SDT生成的,即在此处得到的是一棵语法分析树,而之后的3是输出符号表等信息,而4就是代码生成,确切的说应该是目标代码生成,TINY语言的目标代码生成,当然,也可以认为该目标代码就是中间代码,因为其还需要在TM虚拟机上翻译运行。

图5.4  codeGen函数中的主要函数cGen

图5.5 一个简单的抽象语法分析树的例子

cGen函数就是主要的代码生成函数,生成的是类似于LD dst,addr , JEQ x,0(x) 的IR代码。根据语法分析树,是StmtKey 基本语句类型,还是ExpKey 表达式类型的结点,从而执行相应的代码生成代码。调用genStmt()函数会对基本语句进行生成并且会在里面递归调用cGen函数,记录基本语句如if、repeat的起始点和终止点,依照tiny文法来严格生成中间代码。

       可以猜想到,该中间代码生成的代码程序cGen也是可以由bison工具针对特定的SDT作为输入而产生的,只是这个SDT并不是容易写出来的,但是可以猜想,如果熟悉这个TM的目标平台的代码格式的和对SDT的书写语法有所了解的话,是可以写出这个中间代码生成的SDT的,类似于下面的SDT。

图5.6 手写SDT

至于语义分析部分,语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。比如语义分析的一个工作是进行类型审查,审查每个算符是否具有语言规范允许的运算对象,当不符合语言规范时,编译程序应报告错误。如有的编译程序要对实数用作数组下标的情况报告错误。又比如某些程序规定运算对象可被强制,那么当二目运算施于一整型和一实型对象时,编译程序应将整型转换为实型而不能认为是源程序的错误。语义分析是编译程序最实质性的工作;第一次对源程序的语义作出解释,引起源程序质的变化。

  • TM目标平台和中间代码规范

TM是运行tiny语言的运行程序xx.tm的虚拟机平台,是TINY语言编译器编译之后的汇编代码的运行环境。TM机的主要功能是将TM的汇编代码读入和执行,它具有一般计算机类似的精简指令级RISC。TM汇编语言和一般的Intel汇编语言差不多,包括寄存器寻址、操作符等。一条典型的代码如:LD 0,10(1),这里面10(1)就是寄存器1中地址为基址,10为偏移地址,寻址结果放入寄存器0。三目操作符:SUB 0,1,0表示将寄存器1和寄存器0的差值结果放入寄存器0。

图6.1  TM机的指令集

TM机的指令集并不复杂,RR instruction是表示两个寄存器之间的指令,包括加减乘除和输入输出,RM instruction则是指的是寄存器与内存之间进行操作的指令,包括加载和存储,而RA instruction则是跳转指令,涵盖了大于、小于、等于、大于等于、小于等于和不等于这些逻辑跳转指令,值得一提的是LDA a,b(c)指令是只将寄存器c的值作为地址偏移b位置后的地址赋值给a,而LDC a,b(c) 就是单纯的把b的值赋值给a,c是什么都没有关系。这也就是所谓的中间代码的规范了。tiny语言中间代码的规范准则。

 

       TM机源码分析

       tm机其实就是类似一个小型的控制台程序,运行前必须读取一个tm文件然后再接收一些命令然后执行相应命令的操作。

图6.2  TM机的控制台命令

图6.3  TM及源码的main函数

       可以看到main中前面的框中的代码是读取文件,后面的是输出结束提示,主要核心程序就是中间这个框中的do-while代码,可以看到他就是在循环运行doCommand(),直到收到q命令,返回false使得程序循环结束。而我们经常使用的运行指令就是g,它会运行tm文件

图6.4  g指令的主要代码

       看了源码之后知道,TM机设置了八个寄存器分别由0-7进行编号,而命令r可以输出当前寄存器的使用情况,PC指令寄存器被设置为7号寄存器。图6.4中第一个while循环中的stepTM()是执行tm文件的核心代码,而writeInstruction()函数则是输出执行过程的每一条指令,将traceflag标记位设置为true则可在运行时看到运行时的执行信息。

图6.5  stepTM()核心代码

可以看到,在tm文件中的中间代码都在这里被得以执行,比如read指令翻译成的IN指令,会使得stepTM函数执行case opIN 然后再对控制台输入进行读取且对输入是否是数字进行判断。在比如,ADD 0,1,2指令,将会被执行case opADD  ,将寄存器0与寄存器2的和放到寄存器1中进行存储。而对于跳转指令 JEQ 0,-10(7) 跳转指令的寄存器必须是7指令寄存器pc,因为跳转寻址是根据当前指令位置进行寻址的,如该指令这表示若寄存器0的值等于=则跳转到当前指令往上10条的指令位置。图6.6很好的说明了跳转指令的工作流程

图6.6  JEQ指令跳转详解

       TM机源码分析到此也结束了。给我最大的感受就是,TM虚拟机还是只是叫虚拟机而已,所谓的寄存器也不是真实的物理机寄存器的调用,而是只是他声明的一个“int reg [NO_REGS];”这样的数组而已,本质上是在模拟寄存器的过程,这也是为什么叫虚拟机的原因吧,只是一个运行在物理机上的程序。但是从TM源码中也能学习到计算机底层操作系统是如何对程序代码进行翻译运行的了。TM的源码也很直观的将中间代码该如何书写,如何是符合规则的,如何调用寄存器等内容暴露出来,也就对编译器前端的中间代码生成的SDT的书写具有指导性意义。整个流程下来还是很震撼的。

 

图7.1 编译器执行过程

编译原理的学习,从一开始稍有难度的正则表达式的词法分析开始,步步引人深入,如何将一个字符串分割,有最长匹配原则,有就近匹配原则,有并或闭包运算,这样进入到词法分析的学习中,之后再引出NFA和DFA,将正则表达式更加进一步的化成易懂(指的机器)的形式,是编码实现词法分析变成了可能。

之后到语法分析的各种复杂的语法内容是更加high level的难度,包括LL和LR两大类语法分析,如LL分析中需要将文法消除左递归,提取左公因子,引入新的非终结符,而后使用FIRST函数和FOLLOW函数构造LL1预测分析表,在LR分析中,文法不存在空符号,需要建立DFA,在解决冲突是使用了FOLLOW函数使得文法变成了SLR1文法,之后对每个项集细化FOLLOW向前看符号变成LR1分析以及合并同心项变成LALR1分析。除此之外也分析了基于栈的两种语法分析过程。

语法分析难也是有原因的,因为其后的语法制导翻译方案SDT中可以书写语义分析的动作,书写中间代码生成的动作,而这两项步骤都不是能够独立完成的,他们必须在语法分析的同时进行相应的动作,也就是说,使用bison工具生成对应的语法分析程序,语义分析程序,中间代码生成程序是同时完成的,即包含了语义分析和IR的SDT做为bison工具的输入,就可以得到包含对应内容的程序了。这前端的四步是一个爬坡的过程,也是前端或者说编译器最核心的部分,他们等于是教会了编译器如何做好一个翻译官的工作,如何将一门人使用语言翻译成一个公共易懂的中间语言。

再回到语义分析,语义分析是第一次教编译器去认识语句token流中的意思,而不在是单纯的字符或者token,而是能够去判断当中s的意思,如变量应该先定义后使用、运算分量类型匹配和函数调用定义一致等,说白了就是以人的思维去检查语句是否符合逻辑,符合逻辑意思,这是编译器最实质性的工作,对源程序的语义作出解释,引起源程序质的变化。

而中间代码生成则是产生中间代码的过程。所谓“中间代码”是一种结构简单、含义明确的记号系统,这种记号系统复杂性介于源程序语言和机器语言之间,容易将它翻译成目标代码。比如在对控制流语句if-else进行翻译时需要做回填操作,需要生成goto语句,这些也是中间代码的SDT该做的事情。

紧随其后的就是中间代码的优化工作,消除死代码,循环展开等操作将中间代码提炼精简,就好比把一个人说的话中进行缩写,将废话提取掉,在保证正确性的同时讲语句提炼。再之后就是目标平台代码的生成了,等于是再次翻译,而这次翻译则简单很多了,原因有二,一是因为中间代码是已经比较接近底层的代码了,等于是说美国人要和中国人交流,而中间代码就是将美国人说的英语翻译成了离中国的汉语比较相近的日语了,故而再将日语翻译成汉语就变得简单了。其二是因为在中间代码生成之后,往往都进行了优化,那么再翻译的时候就不会像第一次翻译那样,有这么多的废话,而是已经精简提炼了,需要翻译的工作量也少了,因而更加简单了。

以上,就是对整个编译原理的学习和理解。

That’ All

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值