关于C++ code coverage tool 的研究

因为内容太多,开始之前先给个目录:
(1)覆盖测试工具的简要对比
(2)LINUX下工具GCOV的实现原理
(3)LINUX下工具GCOV的使用说明
  (4 )  WINDOWS下工具coverage validator原理与使用说明
(5)修改GCOV适用于分布式测试覆盖率统计原理与方法
下面是部分C++ code coverage tool 的一个粗略的对比表格。这里重点研究了GCOV,COVTOOL,coverage validator,后续有时间的话,会针对旺旺重点研究下XCOVER. 有些工具没有具体研究,有兴趣的话,可以查看相应链接。
 
 
 Free orcommercialPlatform/complierCoverage levelShow Execute countersEasy to useoutputUseable in largeimplementothers
gcov FreeLinux ,only gccDecision coverageYesyesHTML reportNoInstrument as compilingnonsupport shared library
COVTOOL FreelinuxLinebooleannoMerge.db, ASCII reportyesInstrument as compilingNonsupport thread
xcover Freeplatform-independent library, gcc4.3+Line coverageYesnoHTML report?Source file only,denpend on stlsoftWritten in c
GCTFreeUnix,linux,Only for cBranch/conditionnoyesNo HTMLyesInstrument as compilingOnly for c,work well with if,case,for
Coverage validatorCommercialWindowsdecisionyesyesHTML,XML,yesNo recompile,With pdb filePowerful filters,do not affect performance
BullseyeCoverage CommercialWindows,unix,Branch/conditionnoyesCsv report,Easy to change to perlyeshookWork well with vc,cppunit
Pure coverageCommercialUnix,windowsdecisionyesyesHTML,XML,yesObject Code InsertionWork well with Purify

其他可以参考:Testwell CTC++, CoverageMeter, Dynamic Code Coverage, TCAT C/C++,

 

 

1、背景介绍

GCOV是一个GNU的本地覆盖测试工具, 伴随GCC发布,配合GCC共同实现对C或者C++文件的语句覆盖和分支覆盖测试。是一个命令行方式的控制台程序。需要工具链的支持。
LCOV由 IBM 开发,由 Linux Test Project 维护的开放源代码工具。是GCOV结果展现的一个前端。这个工具由一组构建于基于文本的GCOV 输出之上的 PERL 脚本构成,以实现基于 HTML 的输出, 并生成一棵完整的 HTML 树。输出包括覆盖率百分比、图表以及概述页,可以快速浏览覆盖率数据。支持大项目,提供三种等级视图,分别为目录视图,文件视图,源代码视图。

2、GCOV分析

2.1 基本概念

1. 基本块BB
如果一段程序的第一条语句被执行过一次,这段程序中的每一个都要执行一次,称为基本块。一个BB中的所有语句的执行次数一定是相同的。一般由多个顺序执行语句后边跟一个跳转语句组成。所以一般情况下BB的最后一条语句一定是一个跳转语句,跳转的目的地是另外一个BB的第一条语句,如果跳转时有条件的,就产生了分支,该BB就有两个BB作为目的地。
下图是个典型的基本块:
2.跳转ARC
从一个BB到另外一个BB的跳转叫做一个arc,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数
3.  程序流图
如果把BB作为一个节点,这样一个函数中的所有BB就构成了一个有向图。,要想知道程序中的每个语句和分支的执行次数,就必须知道每个BB和ARC的执行次数。根据图论可以知道有向图中BB的入度和出度是相同的,所以只要知道了部分的BB或者arc大小,就可以推断所有的大小。
这里选择由arc的执行次数来推断BB的执行次数。
所以对部分 ARC插桩,只要满足可以统计出来所有的BB和ARC的执行次数即可。
以下是针对某个函数的程序流图:

2.2 GCOV原理与实现

2.2.1原理简介

GCOV是一个纯软件的覆盖测试工具,被测程序的预处理,插桩和编译成目标文件三个步骤由GCC一次完成。GCOV本身只负责数据处理和结果显示,下图是GCOV的工作原理。
gcov工作原理
从左图可以看出,GCOV统计覆盖率主要包括三个阶段:
l  编译阶段:
加入编译选项gcc –o hello –fprofile-arcs –ftest-coverage hello
除了为每个C文件生成*.o目标文件以外还要生成数据文件*.bb和*.bbg(在早期的GCC版本中是包含这两个文件,后期变成*.gcno文件,但是内部仍然包含这两个文件的结构),分别记录行信息和程序流图信息,供GCOV计算覆盖率时用。
l  数据收集与提取阶段:
./hello 执行时收集数据。
被测程序运行后为每个源文件生成一个*.da数据文件,后期编译器成为.gcda文件,分别记录了*.c文件中程序的执行情况。
l  报告形成阶段:
gcov –a hello.c 收集某个源文件的覆盖率情况
执行后会生成输出文件覆盖率情况,可以重定向保存到某个文件中,同时生成hello.c.gcov形式的文件,文件格式是带有标示信息的源文件。
从右边图可以看出,GCOV的插桩时段,是在编译阶段完成:
被测程序文件首先经过编译预处理,然后编译成汇编文件,在生成汇编文件的同时完成插桩。插桩是在生成汇编文件的阶段完成的,因此插桩是汇编时候的插桩,每个桩点插入3~4条汇编语句,直接插入生成的*.s文件中,最后汇编文件汇编生成目标文件。在程序运行过程中桩点负责收集程序的执行信息。

2.2.2 覆盖率收集过程

gcov的实现源文件包括:coverge.c, gcov.c, gcov-io.c, libgcov.c, profile.c ,libgcc2.c以及他们的头文件,以下从具体的三个阶段来讨论覆盖率收集的过程。
1、编译阶段
在编译阶段,当加入相应的编译选项后,由toplev.c中的函数调用coverage.c与profile.c中的函数,这些函数又调用gcov-io.c中的函数。其中coverage.c中的build_gcov_info 产生一些数据结构,并调用gcov_init 。 同时profile.c会创建程序流图,由profile.c中的函数创建的程序流图,同时gcno中的每个arc会调用insert_insn_on_edge函数来增加counter.
2、数据收集阶段
在数据收集阶段,即在程序运行时。会调用libgcov.c中的函数来增加struct gcov_info的count字段的信息,当程序退出时,gcov_exit会被调用,这个函数将收集到的数据信息写入到.gcda文件中。
3、生成报告阶段
运行($gcov 源文件)后就产生这个后缀文件。gcov要分析的话需要依赖于.gcno,.gcda,.c三个文件,记住这个分析一定要保证:.gcda产生的时候依赖的.gcno要一致,就是说我.gcda和.gcno一定要是配套的。
gcov的分析过程:(用.bbg与.bb代替.gcno来讲)
  1. gcov读取.bbg中的程序流图信息,建立被测源文件中每个函数的程序流图
  2. 读取.gcda信息,将已知的弧执行次数填入到程序流图中
  3. 根据节点入度等于出度的原理推算出其他的弧与基本块的执行次数
  4. 读取.bb文件,根本对应关系计算出每行代码的执行次数
  5. 对应分支的话还需要计算分支的起始位置
  6. 输出计算结果

2.2.3 插桩方法

现在为止,我们知道,
Gcc编译运行产生了什么数据以及gcov分析覆盖率的过程。
还有两个问题:
a.  gcc加入编译参数后,是怎么插桩的
b.  在什么位置,插入了什么数据呢?
1. 插桩过程所进行的修改
1) 每个源文件对应桩点数组:
GCC在插桩的过程中会像源文件的末尾插入一个静态数组,BX2.,数组的大小就是这个源文件中桩点的个数。
BX2+0代表第0个桩点的位置,BX2+n代表第n个桩点的位置。
数组的值就是桩点的执行次数。
2) 每个桩点插入汇编语句:
按照我的理解插入的汇编语句是inc$(BX2+n).
3) BX2数组链表:
为了便于统计,gcc还将各个源文件中的BX2数组链接成一个链表,这个链表结构是在测试main函数之前就产生了,在调用main之前会有一个类似构造函数的函数,进行构建链表。这个函数会在退出时调用exit函数计算执行次数生成.gcda文件。
2. 一些数据结构与函数功能
1) BX2数组:
每个源文件对应一个,记录每个桩点的执行次数。
2) bb结构:
因为要将各个源文件的BX2组织起来,便于统一输出,为每个源文件定义一个bb结构如下:
struct bb
{
long zero_word;  //是否被插入到链表中
const char *file_name; //当前被测试文件名
long *count;//指向bx2的指针
long ncounts;//桩点个数
struct bb *next;//下一个文件的BX2信息
};
3) BX链表:
将BX2组织起来,头指针bb_head,链表元素结构为bb结构。调用void _bb_init_func(struct bb * block), 传入头指针bb_head创建。
4) 创建链表过程:void _bb_init_func(struct bb * block)
GCC为被测源文件插入了一个构造函数_GLOBAL_$I$XXXGCOV()的定义,其中XXX指当前被测文件中的第一个全局函数的函数名的生成,此函数在main函数调用之前会同构造函数一起被调用,这个全局函数的功能就是调用_bb_init_func函数,以该文件的bb结构的起始地址为参数进行调用。
该函数定义在GCC自带的库文件Libgcc.a中,源码位于gcc/libgcc2.c中,定义如下:
void _bb_init_func(struct bb * blocks)
{
if(block->zero_word)//已经连接不管
Return;
if(!bb_head)
ON_EXIT(_bb_exit_func,0);//程序退出时候,写.gcda数据
blocks->zero_word=0;
blocks->next=bb_head;//插入到链表中
bb_head=blocks;
}
该函数首先检查bb结构是否被插入到链表中,如果是则返回,接着检查bb结构的链头是否被初始化,否则注册退出时执行函数_bb_exit_func,该函数负责返回bb链中的每个结构,并生成.gcda数据文件。
这样在main函数之前,所有的bb结构都被连接成一个链表。
5)写入数据文件的过程:_bb_exit_func()
在被测程序运行完成之后,注册退出时会执行函数_bb_exit_func(),将从这个链表的头开始为每一个bb结构开始为源文件创建.gcda文件。写入的文件格式就是BX2数组内容,可以从bb结构中的BX2结构指针找到。
至此,整个插桩过程就讲完了。

 

 

 

 

 

 

 

 

 

 

 

1.         使用说明 
使用GCOV进行代码覆盖率统计,需要注意:
1)            在编译时不要加优化选项,因为加编译选项后,代码会发生变化,这样就找不到哪些是自己写的热点代码。
2)            如果代码中使用复杂的宏,比如说这个宏展开后是循环或者 其他控制结构, gcov只在宏调用出现的那一行报告 ,如果复杂的宏看起来像函数,可以用内联函数来代替。
3)            代码在编写时要注意,每一行最好只有一条语句。
4)            可以用gcov,lcov测试linux内核覆盖率,参考
http://ltp.sourceforge.net/coverage/gcov.php
这里只讨论应用程序的覆盖率。
2.         使用实例
使用主要有三个步骤:
1)            编译阶段:加入编译选项gcc –o hello –fprofile-arcs –ftest-coverage hello,生成记录程序流程图等信息
2)            数据收集与提取阶段:./hello.,生成具体的运行信息
这一阶段生成的数据信息自动保存到。o文件所在的目录,也就是说如果存在/usr/build/hello.o,则生成/usr/build/hello.gcda,但是如果前边目录不存在就会出错。
3)            生成数据报告: gcov  hello.c
以下给出gcov针对c++项目nmap的应用过程
Nmap是一个强大的端口扫描程序,同时Nmap也是著名安全工具Nessus所依赖工具。代码行数在3万行以上。
执行:
  1. CXXFLAGS="-fprofile-arcs -ftest-coverage" LIBS=-lgcov ./configure  èmakefile
  2. Make     è 每个源文件产生一个.gcno文件
  3. ./nmap   è每个源文件生成一个.gcda文件
  4. Gcov *cc  è每个源文件生成一个.gcov文件
    
步骤一:检测代码
按照nmap项目readme文档编译运行一遍,保证代码正确性
步骤二:增加使用gcov的编译选项:-fprofile-arcs -ftest-coverage,链接选项-lgcov或者-coverage
对于手动写的Makefile代码,直接增加编译选项即可
对一些自动生成的Makefile文件,运行./configure –h 查看增加Makefile编译连接选项的方法,增加编译选项:-fprofile-arcs -ftest-coverage ,通过一些环境变量设置即可,比如本程序设置为 CXXFLAGS="-fprofile-arcs -ftest-coverage" LIBS=-lgcov ./configure
 
步骤三:编译连接
修改Makefile文件后,执行make, 针对每个源文件会生成.gcno文件
步骤四:测试
运行单个测试用例或测试用例组,生成.gcda文件,如下运行./nmap 127.0.0.1后,结果如下
步骤五:运行gcov生成覆盖测试信息
如下所示,分析其中的一个源文件及其相关联文件的测试覆盖率情况,默认情况下会生成sourefilename.c.gcov文件,可以添加-l,-p选项生成具体的目录及长文件名。如下所示,分别对main.cc与output.cc的进行覆盖率统计。这里查看的是行覆盖率,也可以添加-b,-f给出分支覆盖率信息,具体可以通过man gcov查看
下边是给出的生成行覆盖率的信息:
main.cc
Output.cc
步骤六:查看.gcov文件
显示源代码的执行情况,如下所示,查看output.cc的执行情况,以下分别是output.cc.gcov文件的头与部分信息,其中红框部分标注该行代码的执行次数,-表示没有被插桩到的代码行。
下图是分支覆盖率信息:
函数开始前给出整体信息:
一些分支情况:
步骤七: 用lcov查看整体代码覆盖情况
使用lcov前对覆盖率数据清空,lcov –z –d ./
在源码路径下运行lcov –b ./  -d ./  -c -o output.info,-b指示以当前目录作为相对路径,-d表示统计目录中的覆盖数据文件而不是内核数据,并将生成的信息存于-o所示文件,具体参数参考:lcov –h 查看
      最后,可以合并多个覆盖率信息,用-a 选项
      Lcov  --add-tracefile .out/a.info –a ./out.info –a ./out/b.info
步骤八:用genhtml查看总体视图与网页视图
如下,可以看出本次覆盖测试成功instrument的行数有近两万行,执行的行数却只有三千多行,可以反复的增加测试用例,提高覆盖测试率。下图分别给出了整体覆盖率和各个源文件的覆盖情况。
3.         常见错误
1.        .gcda文件目录出错,找不到要创建的目录,这种主要用于跨平台情况。
这个是由于.gcda文件的生成默认保存到.o所在的目录,但是如果.o所在目录不存在,就会出现错误。
设置环境变量可以解决这个问题。
设置GCOV_PREFIX=/target/run'同GCOV_PREFIX_STRIP=1
则生成的.gcda文件 将会保存到 /target/run/build/foo.gcda。
2.        the gcov message "Merge mismatch for summaries”
可以将.gcda全部删除或者对整个文件全部编译,而不是单个改变的文件,这个是由于gcda与gcno不相配导致的,因为两者之间都有个时间戳用来记录是不是相同的。

 

 

 

 

 

 

Coverage validator
1. 简介
2. 原理简介
3. 使用
4. 优缺点
 1.         Coverage validator简介
Coverage validator(Software Verification Limited公司的产品)是一个代码覆盖测试工具。可供软件开发者和软件质量测试人员使用。Coverage validator可以帮助你确定工程的代码覆盖率,识别出单元测试中未测试的功能,以交互,实时的方式显示出代码覆盖情况来提高软件测试质量,你也可以合并所有单元测试的覆盖测试数据。可以在创建单元测试报告的同时生成测试报告。
常见的各种覆盖测试工具像CoverageMeter,gcov主要原理是替换了原有的编译器,在代码中进行插桩。因此, 这些覆盖测试工具的特点是需要重编被测试代码。这也是大部分覆盖率工具常用的方法。而Coverage Validator,不需要重编被测代码,只需要提供被测二进制程序的pdb文件,就能统计其代码覆盖率,所以对于每个应用的每个DLL/EXE模块,只是简单的需要PDB或者MAP文件即可。它能同时统计行覆盖,分支覆盖,函数覆盖等。 Coverage validator的insrumention是很快的,只需要几秒,而不是几分钟。跟non-instrumented应用速度是差不多的,不像其他的工具要慢上2到10倍。
Coverage Validator有个很大的好处是可以设置过滤条件,可以设置只统计部分模块的覆盖率数据。可以设置只统计某个DLL,某个类的覆盖率数据,而且返回结果也可以以文件或者函数返回。返回结果也非常直观,可以导出HTML报告或者XML报告。
目前,它只支持Windows平台。它能支持的调试信息格式参见下面描述:
Coverage Validator can understand debug in information in the following formats:
· Microsoft Program Database (PDB) 
· Borland Turbo Debugger System (TDS) 
· CodeView NB10 
· COFF
2.         Coverage validator使用方法
2.1 下载安装
在其官网上下载30的适用版本:
https://www.softwareverify.com/cpp/coverage/index.html
2.2 使用
Converage validator的使用是非常简单的,以下以一个五子棋程序的测试过程来展现coverage validator的功能特点:
整体图:
先看一下它的运行主页,可以发现coverage validator的功能是相当强大的,提供整体测试覆盖情况,每个文件的覆盖测试,分支覆盖测试,函数覆盖测试,单元覆盖测试,行覆盖测试和诊断分析等。
步骤一:选择要测试的程序或模块
 通过菜单"File"-"Start Application" ,选择要进行覆盖测试的工程。如下图所示选择exe文件或者某个DLL模块即可,同时可以设置相应的环境变量,参数,输入输出文件等。
 
一直next,直到看到如下界面,点击开始测试即可。
 
步骤三:开始测试过程
点击开始应用后,即会跳出客户端界面,开始测试。
步骤四:结果显示
在测试的同时,coverage validator会即时的显示测试的结果信息,你可以变测试摆弄查看相应的测试覆盖率信息,下图是总的测试情况,分别显示已经待测试的文件,函数,分支,代码行,及单元测试组的覆盖率情况。如下,显示的总文件个数,被访问的文件个数,未被访问的文件个数与完全覆盖文件的个数。
 
同时还显示一些comment来给出一些提示,下边是我之前跑的一个程序,会给出一些信息,提示有些内联函数和模块库中的文件没有被覆盖。这个覆盖率是随着功能测试的过程动态变化的,可以变测试边显示覆盖测试结果。
  也可以查看单个源文件的测试情况,如下图:
左边显示了各个源文件覆盖情况,其中浅蓝色的文件表示100%覆盖,红色的表示0覆盖,黄色的表示部分覆盖。对于每个文件分别显示了文件的总行数,被访问的行数,hook的行数,和测试覆盖率情况。右边显示了选择出了的单个文件的具体覆盖信息。黄色表示被覆盖的行,并在行的前边表示了该行代码的执行次数,红色为未访问的行,没有颜色的表示是没有HOOK的行。在文件的上方有具体的信息。同样可以点击左下方的refresh按钮来动态即时的显示代码覆盖情况。
 
利用覆盖测试工具,增加测试用例的方法,从上图的左边可以看出更改用户名模块的代码覆盖情况为0,查看此文件的覆盖测试结果,如下图所示,红色的行表示都没有覆盖,这个时候需要添加用户名更改的测试。
 
下图是增加了测试用例后的用户名更改模块的覆盖情况:
 
同样也可以按照函数的名字,类名字,目录等查看function coverage,如下图左下角的refresh用于更新,Type可以选择相应的显示方式。
 
下图为选择的类图显示方式。
 
如果检查的是CppUnit工程的代码覆盖率,需将Testrunnerd.dll文件复制到可执行文件所在目录。
1.        可以设置过滤条件,只统计加载的某个模块的覆盖率数据。比如,你要测试的是一个DLL,你就可以设置过滤条件,只统计该DLL的代码覆盖率。你还可以设置过滤只统计某个类,某个函数的覆盖率数据。设置方法:菜单:"Configure" - "Settings" - "Filters"。
 
3.         Coverage validator优缺点
优点:
1.         不需要重编被测代码,只需要提供被测二进制程序的pdb文件,可以单独的测试DLL/EXE模块
2.         结果数据输出直观,查看方便,代码窗口有颜色标记,详细显示各个函数,分支,文件覆盖情况,并标记每一行代码执行次数。有HTML报告和XML报告
3.         可配合cppunit使用
4.         插桩很快,应用程序的速度也很快
5.         可以设置过滤条件,只统计加载的某个模块的覆盖率数据,某个类,某个函数的覆盖率数据,也可以设置排除条件,排除统计某部分的覆盖率数据。可以一个文件一个文件的返回,也可以一个函数一个函数的返回。
6.         可以即时的查看代码覆盖测试结果信息,在执行的各个阶段查看。
7.         可以用于Native-mode与mixed-mode.net模式
缺点:
1).        结果的自动合成功能不太好,只是在一个SESSION的末期将结果合成。
2).        提供的覆盖测试功能最高达到分支覆盖。
3).        不能够覆盖所有的行,会有数据丢失
4.         参考
https://www.softwareverify.com/cpp/coverage/index.html

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: websocket_codetool是一种用于实现websocket服务的编程工具。websocket是一种全双工通信协议,它可以在浏览器和服务器之间进行双向通信,实现实时数据传输和交互。使用websocket_codetool可以方便地编写websocket服务端代码,实现与客户端的数据传输和处理。 websocket_codetool提供了一些基本的功能,在创建websocket服务时可以进行自定义设置。例如,可以指定websocket服务使用的端口号、协议类型和子协议等。同时,websocket_codetool也支持一些事件处理机制,可以在各种事件发生时触发相应的回调函数,例如连接建立事件、数据收发事件等。 使用websocket_codetool实现websocket服务的过程比较简单,只需要引入相应的库文件,并使用提供的API进行代码编写即可。例如,可以通过websocket_codetool提供的on函数来监听客户端的连接请求,并在连接建立时发送欢迎消息。在接收到客户端发送的数据时,可以使用websocket_codetool提供的send函数进行数据处理或者转发。 总之,使用websocket_codetool可以更加方便地编写实时数据交互的应用程序,复杂的网络编程变得简单易懂,提高了开发效率和代码质量。 ### 回答2: WebSocket是一种在Web浏览器和服务器之间进行实时双向通信的协议。WebSocket_codetool是一个用于实现WebSocket服务的软件工具包,它提供了一组用于处理WebSocket通信的接口和类。 使用WebSocket_codetool可以快速搭建一个WebSocket服务器,让客户端和服务器之间实现实时通信,比如基于WebSocket的聊天室、在线游戏、消息推送等应用。 WebSocket_codetool提供的接口包括实现WebSocket握手、连接管理、数据传输等,并且支持多协议,如HTTP、HTTPS、WS、WSS等。其实现基于Java语言和Netty框架,具有高并发、易扩展、高性能等优点。 WebSocket_codetool还支持自定义消息解码和编码方式,并且提供了多种编码格式,如二进制、JSON、文本等。同时,它还具有可靠性,可以自动进行心跳检测和断线重连等功能,确保数据传输的稳定性和可靠性。 WebSocket_codetool是一个非常优秀的WebSocket服务框架,它的出现简化了WebSocket服务的开发难度,让开发者可以更加专注于业务逻辑的实现。 ### 回答3: c语言是一种非常底层、高效的编程语言,常常用于嵌入式系统和网络编程。WebSocket是一种基于TCP协议的双向通信协议,可以在Web浏览器和服务器之间创建实时通信的连接,用于实现在线游戏、聊天室、股票行情等。 websocket_codetool是一种c语言开发的websocket服务库,可以方便地实现websocket服务。它提供了一套完整的API,包括创建websocket服务、处理连接请求、发送和接收消息等。同时,该库还提供了一些示例代码和文档,方便开发者快速上手。 使用websocket_codetool实现websocket服务,需要先安装该库,并按照文档说明进行开发。开发者需要自己编写处理逻辑,比如游戏逻辑、聊天室交互等,并在代码中调用websocket_codetool提供的API进行通信。 websocket_codetool的优势在于它是一种开源、免费的服务库,可以在自由软件协议下进行修改和使用。而且,由于使用了c语言的特性,它的运行效率非常高,可以满足对性能要求非常高的应用场景。最后,它还支持多平台,包括Windows、Linux等操作系统,可以方便地实现跨平台的websocket服务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值