基于机器学习提升深度学习库API模糊测试效率:从约束学习到工程实践

1. 项目概述与核心挑战

在深度学习(Deep Learning, DL)应用开发中,TensorFlow和PyTorch这类库提供了强大的抽象能力,让开发者能专注于模型设计而非底层实现。然而,这些库本身的复杂性也带来了一个严峻的挑战:它们内部充满了各种API输入约束。简单来说,调用一个API时,你传入的张量(Tensor)的形状、数据类型、数值范围等必须满足一系列特定条件,否则程序就会立刻抛出运行时错误。例如,做批量矩阵乘法的 torch.bmm 函数,要求两个输入张量都必须是三维的,并且它们的批量维度必须相等。这些约束就像API的“使用说明书”,但这份说明书往往不完整、不精确,甚至不存在于官方文档中。

这就给自动化测试,特别是模糊测试(Fuzzing)带来了巨大麻烦。模糊测试的核心思想是自动生成大量随机或半随机的输入数据,去“轰炸”被测程序,以期发现那些开发者未曾预料到的崩溃或错误。但在深度学习库测试中,如果生成的输入数据不满足API的隐藏约束,那么测试执行会在输入验证阶段就立刻失败,根本触及不到API内部可能存在的逻辑错误。这就导致了测试效率的极度低下——大量的计算资源被浪费在执行那些注定会失败的测试用例上。根据相关研究,像ACETest这样的先进模糊测试工具,其生成的测试用例平均通过率(即不触发输入验证错误的比率)也只有29%左右。这意味着超过七成的测试执行都是无效的,严重拖慢了发现真正bug的速度。

那么,有没有办法在测试执行前,就快速、准确地判断一个生成的输入是否有效呢?传统方法试图通过静态分析或符号执行来推断API的约束,但往往因为深度学习库代码的复杂性和动态性而力不从心,推断出的约束要么不精确,要么计算成本极高。这时,机器学习(Machine Learning, ML)提供了一条新思路:我们能不能把判断输入是否有效,看作一个二分类问题?让一个分类器去学习API的“行为模式”,从而在测试生成阶段就过滤掉那些大概率无效的输入。这正是我们接下来要深入探讨的核心: 如何利用机器学习分类器,特别是基于梯度提升树的模型,来学习并预测深度学习库API的输入约束,从而大幅提升模糊测试的效率。

这个思路的价值在于,它绕开了精确推断形式化约束的难题,转而寻求一个高效的、近似但足够准确的预测模型。其核心原理是将高维、复杂的原始输入(如包含具体数值的张量)抽象为低维、结构化的特征(例如仅保留张量的形状信息),然后利用这些特征训练分类器。当这个训练好的分类器集成到测试流程中,作为一个“预过滤器”时,它能在毫秒级别内对成千上万个候选输入进行批量筛选,只放行那些被预测为有效的输入去执行实际的API调用。实测表明,这种方法能将ACETest的有效输入生成率从约29%提升至约61%,几乎翻了一番,这对于需要海量测试用例的模糊测试而言,意味着效率的质的飞跃。

2. 核心思路:从数据中学习约束

2.1 为什么传统方法行不通?

在深入我们的ML方案之前,有必要先理解为什么已有的约束推断方法在深度学习库测试中会遇到瓶颈。传统上,为了生成有效的测试输入,工具会尝试分析API的源代码或执行轨迹,来推导出输入必须满足的条件(即约束),然后将这些约束交给求解器(如Z3)来生成具体值。

这种方法面临几个核心挑战:

  1. 路径爆炸 :深度学习库的输入验证逻辑可能非常复杂,涉及多个条件分支和循环。符号执行这类技术在处理复杂路径时容易陷入状态空间爆炸的困境。
  2. 环境依赖 :一些约束可能依赖于运行时环境或外部状态,难以通过静态分析完全捕获。
  3. 不精确的启发式 :为了应对复杂性,工具可能采用过于宽松或保守的启发式规则来推断约束,导致生成的约束集要么遗漏关键条件(产生无效输入),要么包含过多限制(错过有效输入空间)。
  4. 计算开销 :即使推断出了约束,对于涉及高维张量的复杂约束,约束求解本身也可能非常耗时。

因此,我们需要一种更“务实”的方法:不追求完美地、形式化地推导出所有约束,而是追求高效地、以高概率识别出无效输入。

2.2 机器学习分类器的可行性分析

将API输入有效性判断建模为一个二分类问题,其可行性建立在以下几个关键观察之上:

观察一:输入有效性具有可学习的模式。 虽然API的约束在代码层面可能很复杂,但其表现形式——即什么样的输入会导致 RuntimeError ——在数据层面是明确的。通过收集大量(有效,无效)的输入-标签对,一个足够强大的分类器有可能学习到这些约束背后的决策边界。例如,对于 torch.bmm ,分类器需要学习“两个输入张量的维度都必须为3”以及“它们的第0维(批量大小)必须相等”这两个规则。

观察二:张量形状是核心的抽象特征。 深度学习库API的约束,绝大部分都围绕着张量的 形状 (shape)和 数据类型 (dtype)。例如,“矩阵相乘要求第一个矩阵的列数等于第二个矩阵的行数”、“卷积核大小不能超过输入尺寸”等。张量的具体元素值(如每个像素的RGB值)在大多数输入验证中反而不重要。这为我们提供了巨大的降维机会:我们可以将每个具体的张量输入,抽象为其形状元组(例如 (10, 3, 4) )和数据类型,而忽略其内部存储的百万甚至上亿个具体数值。这瞬间将问题的维度从数百万降低到个位数,使得机器学习模型的训练变得可行且高效。

观察三:存在高效的批量推理机制。 现代ML框架(如ONNX Runtime, TensorRT)对批量推理(Batch Inference)做了大量优化。这意味着我们可以一次性将成千上万个抽象后的输入特征(形状元组)组成一个批次,送入模型进行预测,其耗时远小于逐个执行API调用。这种“批处理”能力是提升整体测试吞吐量的关键。

观察四:训练数据可以自动生成。 我们不需要手动标注数据。对于一个目标API,我们可以利用简单的策略(如随机生成、组合测试)自动产生大量输入,然后通过 实际执行 该API来获得标签:不抛异常的就是有效(正例),抛 RuntimeError 的就是无效(反例)。这个过程虽然需要执行API,但它是离线的、一次性的数据准备阶段。一旦模型训练完成,在线测试阶段就不再需要为每个候选输入都执行一次API来验证其有效性。

基于以上观察,我们的技术路线图变得清晰: 自动生成带标签的(输入,有效性)数据集 -> 将具体输入抽象为形状等关键特征 -> 训练一个二分类ML模型 -> 将模型集成到模糊测试流程中,作为生成器与执行器之间的高效过滤器。

3. 实操要点:从数据生成到模型集成

3.1 训练数据生成的策略与权衡

生成高质量的训练数据是整个流程的基石。我们的目标是覆盖尽可能多的有效和无效输入情况,同时控制生成成本。这里主要探讨两种策略:随机生成和配对组合生成。

3.1.1 随机生成策略

这是最直接的方法。对于目标API的每个参数,根据其类型(int, float, bool, str, Tensor),在一个预设的范围内随机生成值。

  • 张量 ��限制最大维度(例如6维)和每个维度大小的范围(例如[0, 10])。元素值可以随机初始化。
  • 整型/浮点型 :设定一个合理的取值范围(例如[-100, 100])。
  • 布尔型 :随机选择True或False。
  • 字符串型 :从一个从API文档中提取的预定义候选集合中随机选择。

生成大量(如1万个)随机输入后,逐个调用目标API。根据是否抛出异常来打上“有效”或“无效”的标签。

注意 :随机策略的优点是实现简单,能快速生成大量数据。但其缺点是分布可能不均匀,容易遗漏某些边界情况或参数间存在强关联的约束组合,导致模型在某些区域学习不充分。

3.1.2 配对组合生成策略

这是一种更系统的采样方法,源于软件测试中的“成对测试”思想。其核心是:对于多个参数的系统,大多数缺陷是由两个参数间的交互引发的。因此,确保每一对参数的所有可能取值组合至少被覆盖一次,就能以较低的测试用例数发现大部分问题。

在我们的场景中,对于每个参数,我们定义一组离散的取值(例如,张量形状列表、整数值列表、字符串枚举值)。然后,使用算法生成一组测试用例,覆盖所有参数两两之间的所有取值组合。

  • 优势 :相比纯随机,它能更均匀、更系统地探索输入空间,尤其擅长捕捉参数之间的交互约束。例如,对于 torch.cat(tensors, dim) 这个API, dim 参数的值必须小于所有输入张量的维度。随机生成可能很难恰好产生大量 dim 值合法且不越界的用例,而成对组合生成能确保 dim 的每个取值都与各种张量形状进行组合。
  • 实施细节 :如果覆盖所有两两组合后仍未达到预设的样本数(如1万),则循环从头开始,直到数量达标。对于连续值或巨大枚举空间,需要先进行离散化(如将浮点数分段,将张量形状分类)。

我们的实验表明, 配对组合策略在多数情况下能训练出召回率更高的模型 。这意味着模型漏报(将有效输入误判为无效)更少,这对于测试至关重要,因为我们不希望错过任何可能触发bug的有效输入。

3.2 特征工程:如何编码输入

原始输入(尤其是张量)不能直接喂给大多数ML模型。我们需要进行特征编码,将其转换为模型可以处理的数值型特征向量。

  1. 张量编码 丢弃具体元素值,只保留形状信息 。这是最关键的一步。一个形状为 (batch, channel, height, width) 的四维张量,被编码为长度为4的整数列表 [batch, channel, height, width] 。如果API有多个张量参数,就将它们的形状列表拼接起来。
  2. 标量参数编码
    • int / float :直接使用其数值。
    • bool :映射为0(False)或1(True)。
    • str :使用标签编码(Label Encoding)或独热编码(One-Hot Encoding),将其映射为整数。例如,对于 padding 模式 [‘valid’, ‘same’] ,可以分别映射为0和1。
  3. 处理可变参数 :有些API接受可变数量的参数(如 *tensors )。我们需要将其固定为最大预期数量,不足的用特定占位符(如 None 或-1)填充,并在特征向量中增加一个“参数数量”的特征。

通过这种编码,我们将一个可能包含高维数组的复杂输入,转换成了一个固定长度的、纯数值的特征向量。例如,一个调用 torch.bmm(mat1, mat2) 的输入,其中 mat1.shape=(b, n, m) , mat2.shape=(b, m, p) ,最终可能被编码为特征向量 [b, n, m, b, m, p] 。模型的任务就是学习这个六维空间中的一个决策边界。

3.3 模型选择与自动化训练

我们不需要从头设计模型结构。在这个结构化数据(表格数据)的分类问题上,梯度提升决策树(Gradient Boosting Decision Trees, GBDT)系列模型通常表现卓越。它们能自动处理特征交互,对数值特征的尺度不敏感,且通常不需要复杂的调参就能获得不错的效果。

为了找到每个API最适合的模型,我们采用 自动化机器学习(AutoML) 框架,这里以AutoGluon为例。其工作流程如下:

  1. 数据准备 :将生成的1万个样本按8:2划分为训练集和验证集。
  2. 模型池 :AutoGluon会准备一个包含多种模型的池子,例如:
    • CatBoost :擅长处理类别特征,且能有效防止过拟合。
    • LightGBM :采用直方图算法,训练速度快,内存消耗低。
    • XGBoost :经典的梯度提升库,稳定且强大。
    • 神经网络 (如FastAI的NN):对于某些复杂模式可能有更好的表示能力。
    • 极端随机树(ExtraTrees) :另一种集成树模型。
  3. 自动训练与调优 :AutoGluon会使用交叉验证等方式,在训练集上对模型池中的所有模型进行训练和超参数优化。
  4. 模型选择 :在验证集上评估所有模型,并生成一个性能排行榜。我们选择综合性能(通常是精度和召回率的平衡)最好的模型作为该API的最终分类器。

使用AutoML的优势在于,它将我们从繁琐的模型选择和调参中解放出来,并能确保为每个API找到一个相对最优的模型。实验结果显示, CatBoost、LightGBM和XGBoost这类梯度提升模型在超过75%的API上表现最佳 ,其中CatBoost独占鳌头,在近一半的案例中都是最佳选择。

3.4 集成到测试流程:ACETest+ML

将训练好的分类器集成到现有模糊测试工具(如ACETest)中,是体现其工程价值的关键。下图展示了集成后的工作流程:

原始ACETest流程:
1. 约束推断 -> 2. 约束求解(Z3)-> 3. 生成具体输入 -> 4. 执行API -> 5. 观察结果(崩溃/通过)

改进后的ACETest+ML流程:
1. 约束推断 -> 2. 约束求解(Z3)-> 3. 生成具体输入 -> 3.5 ML模型预过滤 -> 4. 执行API -> 5. 观察结果

关键步骤3.5 ML模型预过滤详解:

  1. 抽象化 :将约束求解器生成的具体输入(包含具体数值的张量等),实时转换为特征向量(即进行特征编码)。
  2. 批量预测 :不是一个个地预测,而是将当前批次生成的所有输入(比如5000个)的特征向量堆叠成一个矩阵,一次性送入ML模型进行批量推理。
  3. 过滤 :模型为每个输入输出一个“有效”概率。我们设定一个阈值(如0.5),将概率高于阈值的输入标记为“预测有效”,低于阈值的标记为“预测无效”。
  4. 分流 :只有被预测为有效的输入,才会被送入真正的API执行步骤。被预测为无效的输入则被直接丢弃,同时触发测试生成器尝试生成新的输入来填补空缺,直到达到预定的测试执行数量。

性能收益分析 : 以文中提到的TensorFlow SigmoidGrad API为例,ACETest原生生成5000个输入,其中只有235个是有效的,通过率仅4.7%。执行5000次API调用耗时31秒。 集成了ML过滤器后,流程变为:生成5000个输入 -> 特征抽象(1.5秒)-> 批量预测(0.1秒)-> 模型筛选出332个“预测有效”的输入 -> 执行这332次API调用(7.4秒)。最终,在这332个输入中,有143个是真正有效的。 虽然最终找到的有效输入绝对数量(143 vs 235)变少了,但 单位时间生成的有效输入数 (#/t)从7.3个/秒提升到了14.3个/秒,效率几乎翻倍。这是因为我们避免了大量注定失败的、耗时的API执行,将时间集中在更有可能成功的候选输入上。

实操心得 :批量推理是效率提升的灵魂。如果对每个输入单独调用模型预测,5000次预测的延迟将远大于批量预测,这会完全抵消过滤带来的收益。务必确保你的模型服务框架支持高效的批量推理。

4. 效果评估与深度解析

4.1 模型性能:精度与召回率的博弈

评估一个二分类模型,我们最关心两个指标: 精度(Precision) 召回率(Recall)

  • 精度 = TP / (TP + FP) :在所有被模型 预测为有效 的输入中,真正有效的比例。高精度意味着模型很少“误放”无效输入。
  • 召回率 = TP / (TP + FN) :在所有 真正有效 的输入中,被模型成功预测出来的比例。高召回率意味着模型很少“误杀”有效输入。

在测试过滤的场景下,这两个指标有着不同的意义:

  • 高精度优先 :如果我们更看重测试执行的“纯净度”,希望尽可能确保每个被执行的输入都是有效的,以减少资源浪费,那么应该追求高精度。但这可能导致一些有效的、能发现bug的输入被过滤掉(低召回率)。
  • 高召回率优先 :如果我们更看重“覆盖率”,不希望错过任何可能触发bug的有效输入,那么应该追求高召回率。但这会导致更多无效输入通过过滤,被送去执行,降低了效率。

我们的实验在183个TensorFlow和PyTorch API上进行了评估。平均来看,使用配对组合策略训练出的模型,在PyTorch API上达到了 88%的精度和82%的召回率 ,在TensorFlow API上达到了 91%的精度和80%的召回率 。这是一个非常理想的平衡点,意味着模型在过滤掉大部分无效输入的同时,只漏掉了约20%的有效输入。

哪些API上模型表现好? 模型在约束相对规整、且训练数据中正负例较平衡的API上表现极佳。例如:

  • torch.broadcast_to(input, shape) :约束是 len(shape) >= input.dim() 。模型能轻松学习到这种基于维度的比较规则。
  • tf.math.top_k(input, k) :约束是输入张量最后一个维度的尺寸必须 >= k 。这也是一个清晰的数值比较。
  • tf.split(value, num_splits, axis) :约束是 value.shape[axis] 必须能被 num_splits 整除。模型也能很好地捕捉这种取模关系。

哪些API上模型表现差? 主要问题出在 训练数据极度不平衡 上。例如 torch.bmm ,其约束要求两个输入都是3D张量,且第一维相等。在随机或成对生成的巨大形状空间里,恰好满足这两个条件的样本(正例)凤毛麟角(实验平均只生成4个)。当正例样本数量过少(比如少于50个)时,模型无法学习到有效的模式,性能会接近随机猜测(精度/召回率接近0%)。

避坑技巧 :在启动针对某个API的模型训练前,先分析其生成数据的正例比例。如果正例比例极低(如<1%),需要考虑采用更智能的生成策略,例如基于约束的引导式生成,或者使用主动学习(Active Learning)方法,有针对性地生成靠近决策边界的困难样本,以丰富正例数据。

4.2 泛化能力:在未知数据上是否依然可靠?

一个常见的担忧是:模型会不会只是在记忆训练集,而对新的、没见过的输入数据失效?为了验证泛化能力,我们在每个API上,用全新的随机种子生成了包含5万个样本的独立测试集进行评估。

结果令人鼓舞:在超过80%的案例中,模型在新测试集上的精度和召回率与在原始验证集上的表现高度一致,差异很小。只有在约20%的案例中,新数据上的精度略有下降,但大部分仍保持在80%的阈值以上。召回率的表现更稳定,仅12%的案例有下降。

这表明, 基于张量形状等抽象特征训练的模型,确实捕捉到了API输入约束的本质规律,而非训练数据的特定噪声,因此具备了良好的泛化能力 。这对于实际应用至关重要,因为测试生成器产生的输入是无穷无尽的,模型必须能应对未曾出现在训练集中的形状组合。

4.3 对Bug发现能力的影响:过滤会漏掉Bug吗?

这是测试工程师最关心的问题:用了ML过滤器,会不会把那些能触发深层Bug的“特殊”有效输入也给过滤掉了?从而导致测试能力下降?

为了评估这一点,我们对比了原始ACETest和集成ML模型的ACETest在相同时间内发现的独特Bug数量。实验设置是:让两个工具运行相同的时间,看谁能触发更多的独特崩溃(Unique Crash)。

结论是令人放心的 :当使用包含至少10%正例样本的数据训练出的模型时,ACETest+ML能够发现原始ACETest所能发现的 90%以上的Bug 。也就是说,过滤掉大部分无效输入所提升的效率, 并没有以显著牺牲Bug发现能力为代价

这背后的逻辑是:大多数能触发深层逻辑错误的有效输入,其输入特征(形状等)通常也符合模型学习到的一般有效模式。模型误杀(False Negative)的,更多是那些虽然有效但可能比较“平凡”的输入。而那些形状奇特、处于约束边界、更容易触发边界条件Bug的输入,只要它们符合约束,模型也有很高的概率将其识别为有效。

重要提示 :这个结论高度依赖于模型的质量,特别是召回率。如果模型的召回率很低(比如低于50%),那么它漏掉有效输入的概率就很大,进而严重影响Bug发现能力。因此,在工程实践中, 必须监控并确保模型在验证集和留出的测试集上保持较高的召回率 (建议>80%)。可以设定一个召回率阈值,低于该阈值的模型不予部署,并触发告警,需要重新检查训练数据或调整模型。

5. 工程实践指南与扩展思考

5.1 实施步骤 checklist

如果你想在自己的深度学习库测试项目中引入这项技术,可以遵循以下步骤:

  1. 目标API选取 :从你的测试范围中,挑选出那些输入约束复杂、通过率低、执行成本高的API作为首要目标。
  2. 数据生成脚本开发
    • 编写能自动调用目标API的脚本。
    • 实现参数生成器(随机/配对组合)。
    • 集成异常捕获逻辑,根据执行结果自动打标签。
    • 关键 :记录下每个输入的具体值及其抽象特征(形状等),并存储为结构化的数据集(如CSV、Parquet)。
  3. 特征编码管道 :编写可复用的代码,能将原始输入(字典或元组形式)转换为模型所需的特征向量。确保训练和推理阶段使用完全相同的编码逻辑。
  4. 模型训练与评估
    • 使用AutoML框架(如AutoGluon, H2O.ai)或手动训练一组候选模型(CatBoost, XGBoost, LightGBM)。
    • 严格划分训练集、验证集和测试集。
    • 在测试集上评估最终模型的精度和召回率。 务必同时关注两个指标 ,根据你的测试策略(重效率还是重覆盖)决定侧重哪一个。
  5. 模型部署与集成
    • 将训练好的模型导出为通用格式(如ONNX, PMML)或使用框架原生格式(如CatBoost的 .cbm )。
    • 在测试框架中,于“输入生成”和“API执行”两个模块之间插入“过滤服务”。
    • 过滤服务加载模型,并对生成器产生的输入进行批量特征编码和预测。
    • 将被预测为有效的输入列表返回给测试执行器。
  6. 监控与迭代
    • 记录在线过滤的统计数据:预测有效率 vs 实际执行通过率。如果两者差距持续过大,说明模型可能漂移或失效。
    • 定期(如每季度)用新生成的数据重新评估模型���能,必要时进行重新训练。

5.2 潜在挑战与应对策略

  • 挑战一:API变更 。深度学习库版本更新可能导致API约束发生变化,使旧模型失效。
    • 策略 :将模型训练和数据生成作为CI/CD流水线的一部分。当库版本升级时,自动触发针对变更API的重新训练流程。
  • 挑战二:极度不平衡数据 。对于某些API,有效输入空间极小,导致正例样本极少。
    • 策略
      • 过采样 :复制或微扰少数类样本。
      • SMOTE :合成少数类样本。
      • 调整类别权重 :在训练时给正例样本更高的损失权重。
      • 改进生成器 :使用符号执行或模糊约束求解的初步结果,来引导生成更多可能有效的输入。
  • 挑战三:非形状约束 。有些API的约束涉及张量元素的具体值(如要求值域为正数)、或字符串参数的复杂语义。
    • 策略 :扩展特征工程。对于值域约束,可以加入张量的统计特征(如最小值、最大值、均值)。对于字符串,可以使用更高级的编码(如嵌入)。但这会增加特征维度和模型复杂度。
  • 挑战四:延迟与吞吐量 。虽然批量推理很快,但特征编码和模型加载也可能成为瓶颈。
    • 策略
      • 特征编码优化 :使用向量化操作,避免在Python循环中进行逐样本编码。
      • 模型服务化 :将模型部署为独立的gRPC/HTTP服务,测试客户端通过RPC调用,可以利用服务端的计算资源和模型缓存。
      • 异步处理 :让生成器、过滤器和执行器以流水线方式异步工作,最大化资源利用率。

5.3 扩展应用场景

这项技术的核心思想——“用学习到的模式替代复杂的规则推断”——可以扩展到更广泛的测试场景:

  1. 其他复杂库的API测试 :不仅限于深度学习库,任何具有复杂、隐式输入约束的库(如图形库、数值计算库、数据库驱动)都可以尝试这种方法。
  2. 组合API测试 :测试深度学习模型训练管道时,往往涉及多个API的连续调用。可以训练一个模型来预测 一组API调用序列 的输入是否整体有效,这能捕捉跨API的约束。
  3. 测试输入优先级排序 :不仅做二分类(有效/无效),还可以让模型预测输入“触发潜在Bug”的 概率分数 。测试执行器可以优先执行分数高的输入,从而更快地发现缺陷。
  4. 辅助约束推断 :模型本身可以作为一种约束的“模糊”描述。通过分析模型的决策边界(例如,对于树模型,可以查看特征重要性),可以反过来为人类工程师或符号执行工具提供关于API可能约束的线索。

将机器学习融入传统的软件测试流程,不是要取代经典方法,而是作为一种强大的补充。它用数据驱动的方式,解决了那些用规则难以清晰表述的问题。在深度学习库测试这个具体战场上,它已经证明了自己能显著提升测试效率。随着AutoML技术的成熟和硬件算力的提升,这类“学习型测试”组件,有望成为未来智能化测试框架中的标准模块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值