【ARM64 ATF 系列 1 -- ATF 中断向量表及SMC 处理流程】


请阅读【嵌入式开发学习必备专栏 Cache | MMU | AMBA BUS | CoreSight | Trace32 | CoreLink | GCC | CSH | Armv8/v9 系统异常分析】


1.1 Trusted Firmware Project

Trusted Firmware Project是一个开源软件项目,最初由ARM管理,后来移交给Linaro管理。Trusted Firmware Project旨在为ARM A-Profile和M-Profile架构处理器提供安全软件的参考实现。

Trusted Firmware Project包括如下项目;TF-A、TF-M、MCUboot、TF-RMM、OP-TEE、
Mbed TLS、Hafnium、Trusted Services、Open CI。

1.1.1 TF-A 介绍

TF-A 是Trusted Firmware-A 的简称,支持的处理器包括 Cortex-A 和 Neoverse系列,为 ARM AProfile 架构(包括 Armv7-A、Armv8-A和Armv9-A)处理器提供了安全软件的参考实现;TF-A支持代码复用,方便移植到不同芯片厂商的模型和硬件平台上,移植工作只需添加平台相关代码;TFA包括了执行在EL3异常级别的Secure Monitor,并支持 AArch32 和 AArch64 执行状态。

TF-A实现了如下ARM 接口标准:

  • The Power State Coordination Interface (PSCI);
  • Trusted Board Boot Requirements CLIENT (TBBR-CLIENT);
  • SMC Calling Convention;
  • System Control and Management Interface (SCMI);
  • Software Delegated Exception Interface (SDEI)
  • PSA FW update specification。

1.1.2 TF-A 启动流程

对于AArch64 状态的 Cold boot,从CPU Reset开始,TF-A按如下启动顺序工作:

  • Boot Loader stage 1 (BL1) AP Trusted ROM;
  • Boot Loader stage 2 (BL2) Trusted Boot Firmware;
  • Boot Loader stage 3-1 (BL31) EL3 Runtime Software;
  • Boot Loader stage 3-2 (BL32) Secure-EL1 Payload (optional);
  • Boot Loader stage 3-3 (BL33) Non-trusted Firmware;

bl1BootromBL1 是启动的第一阶段,该镜像必须要存储在可直接执行的介质中。若芯片支持 XIP 启动方式,其可被存储在片外可直接执行的介质中(如norflash)。若不支持 XIP,则需要存储在芯片的片内 ROM 中,此时在芯片出厂后该部分代码就将被固化,后续再也不能被修改和升级。若芯片要支持安全启动,则需要将 bootrom 作为启动时的信任根

bl2BL2 镜像由 BL1加载,此时 DDR 还没有被初始化,因此它需要被加载到片内的 SRAM 中执行,一般在 BL2 中完成 DDR 的初始化,因此后面的镜像都可以被加载到 DDR 中。BL31、BL32BL33 都是由 BL2 加载的,其中 BL31BL32是可选的,若系统不支持 TRUST OS,则可去掉 BL32,若不支持 EL3 异常等级及 secure monitor,则可去掉 BL31。此外 BL2 还会进行完成 USB 配置,PMIC 的配置 UART 配置,另外,BL2 运行在 EL3 级别。

bl2 有的公司叫做 preboot, 在有的公司称为 SPL

b31:称为 ATF,主要完成双系统切换的工作,有的公司称其为 Sloadersloader 对应的 ATF代码 (也就 arm trust firmware,现在最新版本已经改名) 。

ATF 代码可以从ARM官网 download,再加入一些自己的特有设计。

ATF 的功能主要是负责 tee\ree 切换;运行在EL3级别,由 BL2 (有的公司称其为SPL ) 加载并调用初始化。

BL2 执行完成后需要跳转到 BL31,由于 BL31 运行在 EL3 异常等级而 BL2根据需求不同可能运行于secure EL1或EL3。当BL2运行于EL3时可直接通过ERET方式跳转到BL31中,但若其运行在secure EL1时,则只能通过smc异常触发进入EL3异常等级。 显然,此时 BL31 由于尚未设置其自身的SMC 异常处理程序而无法直接处理该异常,因此,为了完成跳转流程,BL1 需要先代理该异常的处理。因此 BL1 在退出之前先设置 SMC 异常处理函数,BL2 触发smc启动 BL31 时,BL1 捕获该异常并根据 BL2 传入的参数设置 BL31 的入口地址和系统初始状态,并通过 ERET 跳转到 BL31 的入口地址处执行

当异常发生时 PE 会根据异常的类型选择一个合适返回地址保存到 ELR_ELn 中,n 的含义和签名 SPSR_ELn 类似。等到异常处理完毕、软件主动调用 ERET 指令,这个指令会将SPSR_ELn 恢复到 PSATE寄存器 中 (严格来说 PSTATE 不是一个寄存器,是一组表示PE状态寄存器的组合) ELR_ELn 的内容更新到 PC 指针,实现异常的返回跳转。

b32:包括 TEE OS(OpenTee), 其中 TE OS 运行在安全模式下的 EL1级别BL31 可根据其镜像加载信息设置入口地址以及其它状态,并完成跳转。BL32 加载完成后将通过 SMC 返回到 BL31,然后由 BL31 跳转到 non secure EL1non secure EL2 以执行 BL33

bl33: 一般是指uboot, 有的会包括(bootloader)uboot->kernel->Anriod流程, uboot 运行在non-secure mode下的EL1级别。

ATF 里面分为BL1,BL2,BL31,BL32,BL33,一般SoC厂商会从BL31添加自己的代码,其他的都是ARM默认的,没有人去改。bl1/bl2 一般都是平台自己实现。

本篇文章主要介绍 ATF。

1.2 ATF 中断向量表

如果触发异常,程序会跳转到异常向量表去执行。整个异常向量表的大小为 0x800字节,每一个entry都是0x80对齐。异常总共有 4种:

  • synchronous;
  • IRQ;
  • FIQ;
  • SError。

根据异常发生的 exception level又分为4类:

  • 1)发生异常的exception level栈指针使用 SP_EL0;
  • 2)发生异常的exception level栈指针使用 SP_ELx;
  • 3)低level触发异常进入高level,而且低 level使用的是64位架构;
  • 4)低level触发异常进入高level,而且低 level使用的是32位架构。

以 linux kernel为例:

  • kernel 中收到中断将进入第2类异常入口;
  • 32位应用程序系统调用进入第3类异常入口;
  • 64位应用程序系统调用进入第4类异常入口。

ATF 中断向量表的定义位于文件:bl31/aarch64/runtime_exceptions.S, 基地址为 runtime_exceptions:

vector_base runtime_exceptions

        /* ---------------------------------------------------------------------
         * Current EL with SP_EL0 : 0x0 - 0x200
         * ---------------------------------------------------------------------
         */
vector_entry sync_exception_sp_el0
#ifdef MONITOR_TRAPS
        stp x29, x30, [sp, #-16]!

        mrs     x30, esr_el3
        ubfx    x30, x30, #ESR_EC_SHIFT, #ESR_EC_LENGTH

        /* Check for BRK */
        cmp     x30, #EC_BRK
        b.eq    brk_handler

        ldp x29, x30, [sp], #16
#endif /* MONITOR_TRAPS */

        /* We don't expect any synchronous exceptions from EL3 */
        b       report_unhandled_exception
end_vector_entry sync_exception_sp_el0

vector_entry irq_sp_el0
        /*
         * EL3 code is non-reentrant. Any asynchronous exception is a serious
         * error. Loop infinitely.
         */
        b       report_unhandled_interrupt
end_vector_entry irq_sp_el0


vector_entry fiq_sp_el0
        b       report_unhandled_interrupt
end_vector_entry fiq_sp_el0
vector_entry serror_sp_el0
        no_ret  plat_handle_el3_ea
end_vector_entry serror_sp_el0

        /* ---------------------------------------------------------------------
         * Current EL with SP_ELx: 0x200 - 0x400
         * ---------------------------------------------------------------------
         */
vector_entry sync_exception_sp_elx
        /*
         * This exception will trigger if anything went wrong during a previous
         * exception entry or exit or while handling an earlier unexpected
         * synchronous exception. There is a high probability that SP_EL3 is
         * corrupted.
         */
        b       report_unhandled_exception
end_vector_entry sync_exception_sp_elx

vector_entry irq_sp_elx
        b       report_unhandled_interrupt
end_vector_entry irq_sp_elx

vector_entry fiq_sp_elx
        b       report_unhandled_interrupt
end_vector_entry fiq_sp_elx

vector_entry serror_sp_elx
#if !RAS_EXTENSION
        check_if_serror_from_EL3
#endif
        no_ret  plat_handle_el3_ea
end_vector_entry serror_sp_elx
        /* ---------------------------------------------------------------------
         * Lower EL using AArch64 : 0x400 - 0x600
         * ---------------------------------------------------------------------
         */
vector_entry sync_exception_aarch64
        /*
         * This exception vector will be the entry point for SMCs and traps
         * that are unhandled at lower ELs most commonly. SP_EL3 should point
         * to a valid cpu context where the general purpose and system register
         * state can be saved.
         */
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_sync_exception
end_vector_entry sync_exception_aarch64

vector_entry irq_aarch64
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_interrupt_exception irq_aarch64
end_vector_entry irq_aarch64

vector_entry fiq_aarch64
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_interrupt_exception fiq_aarch64
end_vector_entry fiq_aarch64

vector_entry serror_aarch64
        apply_at_speculative_wa
#if RAS_EXTENSION
        msr     daifclr, #DAIF_ABT_BIT
        b       enter_lower_el_async_ea
#else
        handle_async_ea
#endif
end_vector_entry serror_aarch64
        /* ---------------------------------------------------------------------
         * Lower EL using AArch32 : 0x600 - 0x800
         * ---------------------------------------------------------------------
         */
vector_entry sync_exception_aarch32
        /*
         * This exception vector will be the entry point for SMCs and traps
         * that are unhandled at lower ELs most commonly. SP_EL3 should point
         * to a valid cpu context where the general purpose and system register
         * state can be saved.
         */
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_sync_exception
end_vector_entry sync_exception_aarch32

vector_entry irq_aarch32
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_interrupt_exception irq_aarch32
end_vector_entry irq_aarch32

vector_entry fiq_aarch32
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_interrupt_exception fiq_aarch32
end_vector_entry fiq_aarch32

vector_entry serror_aarch32
        apply_at_speculative_wa
#if RAS_EXTENSION
        msr     daifclr, #DAIF_ABT_BIT
        b       enter_lower_el_async_ea
#else
        handle_async_ea
#endif
end_vector_entry serror_aarch32

与 32 位架构不同,64 位架构的异常向量表地址不固定,在启动的时候由软件将异常向量表的地址设置到专用寄存器VBAR(Vector Base Address Register)中。

include/arch/aarch64/asm_macros.S 可以看到宏 vector_base 将向量表放到了 .vectors 段中:

       /*
         * Declare the exception vector table, enforcing it is aligned on a
         * 2KB boundary, as required by the ARMv8 architecture.
         * Use zero bytes as the fill value to be stored in the padding bytes
         * so that it inserts illegal AArch64 instructions. This increases
         * security, robustness and potentially facilitates debugging.
         */
        .macro vector_base  label, section_name=.vectors
        .section \section_name, "ax"
        .align 11, 0
        \label:
        .endm

bl31/aarch64/bl31_entrypoint.S 可以看到 向量表的基地址赋值给了 _exception_vectors:

        /* ---------------------------------------------------------------------
         * For RESET_TO_BL31 systems which have a programmable reset address,
         * bl31_entrypoint() is executed only on the cold boot path so we can
         * skip the warm boot mailbox mechanism.
         * ---------------------------------------------------------------------
         */
        el3_entrypoint_common                                   \
                _init_sctlr=1                                   \
                _warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS  \
                _secondary_cold_boot=!COLD_BOOT_SINGLE_CPU      \
                _init_memory=1                                  \
                _init_c_runtime=1                               \
                _exception_vectors=runtime_exceptions           \
                _pie_fixup_size=BL31_LIMIT - BL31_BASE

include/arch/aarch64/el3_common_macros.S 中的 el3_entrypoint_common 函数中将向量表的基地址赋值给了 vbar_el3((Vector Base Address Register, Exception Level 3)) 寄存器, 用于指定异常向量表的基地址。通过修改VBAR_EL3寄存器的值,可以改变异常处理程序的入口地址。

        .macro el3_entrypoint_common                                    \
                _init_sctlr, _warm_boot_mailbox, _secondary_cold_boot,  \
                _init_memory, _init_c_runtime, _exception_vectors,      \
                _pie_fixup_size
		
		......
        /* ---------------------------------------------------------------------
         * Set the exception vectors.
         * ---------------------------------------------------------------------
         */
        cix_postcode_debug_asm _code=0x150
        adr     x0, \_exception_vectors
        msr     vbar_el3, x0
        isb
		......

在ARM Trusted Firmware(ATF)中,el3_entrypoint_common是一个通用的入口点函数,用于处理从非安全世界(例如:EL2, EL1)或安全世界(EL3)进入EL3时的一些初始化工作。

以下是el3_entrypoint_common函数的一些常见作用:

  • 保存当前的执行环境,包括当前异常级别下的系统寄存器。

  • 设置新的执行环境,包括初始化栈,设置新的异常向量表地址,等等。

  • 根据需要,可能会进行一些硬件初始化或配置,例如初始化内存管理单元(MMU)。

  • 最后,el3_entrypoint_common函数通常会跳转到其他函数进行进一步的初始化,这可能包括启动操作系统,进入休眠状态,或者处理更具体的异常。

需要注意的是,el3_entrypoint_common函数的具体实现可能会根据具体的硬件平台和ATF版本有所不同,因此上述的作用可能会有所差异。

1.2.1 ATF 汇编宏 vector_base

对于ARMv7 的定义(include/arch/aarch32/asm_macros.S):

        /*
         * Declare the exception vector table, enforcing it is aligned on a
         * 32 byte boundary.
         */
        .macro vector_base  label
        .section .vectors, "ax"
        .align 5
        \label:
        .endm

对于ARMv8 的定义(include/arch/aarch64/asm_macros.S):

        /*
         * Declare the exception vector table, enforcing it is aligned on a
         * 2KB boundary, as required by the ARMv8 architecture.
         * Use zero bytes as the fill value to be stored in the padding bytes
         * so that it inserts illegal AArch64 instructions. This increases
         * security, robustness and potentially facilitates debugging.
         */
        .macro vector_base  label, section_name=.vectors
        .section \section_name, "ax"
        .align 11, 0
        \label:
        .endm

1.3 ATF SMC 中断处理流程

在上面 1.1 节的内容中我们可以看到处理同步异常的汇编宏: handle_sync_exception(分为AArch32 和 AArch64 两种情况,这里值讨论AArch64),由于 SMC 触发的异常属于同步异常,所以执行 SMC 指令后首先会走到 sync_exception_aarch64 处。看下 sync_exception_aarch64 在ATF 中的实现:

vector_entry sync_exception_aarch64
        /*
         * This exception vector will be the entry point for SMCs and traps
         * that are unhandled at lower ELs most commonly. SP_EL3 should point
         * to a valid cpu context where the general purpose and system register
         * state can be saved.
         */
        apply_at_speculative_wa
        check_and_unmask_ea
        handle_sync_exception
end_vector_entry sync_exception_aarch64

可以看到 SMC 异常的处理包含三部分,接下来分别对这三部做介绍。

  • apply_at_speculative_wa
        .macro  apply_at_speculative_wa
#if ERRATA_SPECULATIVE_AT
        /*
         * Explicitly save x30 so as to free up a register and to enable
         * branching and also, save x29 which will be used in the called
         * function
         */
        stp     x29, x30, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_X29]
        bl      save_and_update_ptw_el1_sys_regs
        ldp     x29, x30, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_X29]
#endif
        .endm
  • 首先,编译器会检查是否启用了规避异步异常的修补方案(ERRATA_SPECULATIVE_AT)。如果启用了,就会执行一系列的指令。
    首先使用STP指令将寄存器x29x30的值存储到堆栈中,以便释放这两个寄存器供后续的指令使用。

  • 然后,它调用 save_and_update_ptw_el1_sys_regs函数。这个函数的作用根据函数名,应该是保存并更新EL1系统寄存器的页表遍历(Page Table Walk, PTW)相关的值。这可能是由于部分ARM处理器存在的一些硬件异常,需要通过软件来进行规避。

  • 最后,它使用LDP指令从堆栈中恢复寄存器x29x30的值。

这段代码的主要作用是,在处理异常时,如果启用了异步异常的规避措施,就保存当前的状态,调用特定的函数进行处理,然后恢复之前的状态。

  • check_and_unmask_ea
        /*
         * For SoCs which do not implement RAS, use DSB as a barrier to
         * synchronize pending external aborts.
         */
        dsb     sy

        /* Unmask the SError interrupt */
        msr     daifclr, #DAIF_ABT_BIT

        /* Use ISB for the above unmask operation to take effect immediately */
        isb

        /*
         * Refer Note 1. No need to restore X30 as both handle_sync_exception
         * and handle_interrupt_exception macro which follow this macro modify
         * X30 anyway.
         */
        str     x30, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_LR]
        mov     x30, #1
        str     x30, [sp, #CTX_EL3STATE_OFFSET + CTX_IS_IN_EL3]
        dmb     sy
  • msr daifclr, #DAIF_ABT_BIT 打开Abort中断,Abort中断分为两种类型:Prefetch Abort和Data Abort。对于这两种Abort中断,处理器都会进入异常处理模式,并跳转到预设的异常处理程序进行处理。
    • Prefetch Abort:这种中断是由于指令预取阶段出现的错误触发的,例如尝试执行一个非法地址中的指令或者违反内存保护策略等。
    • Data Abort:这种中断是由于数据访问阶段出现的错误触发的,例如尝试读取或写入一个非法地址或者违反内存保护策略等。
  • isb:Instruction Synchronization Barrier(指令同步屏障)这条指令确保了在它之前的所有指令都完成后,才开始执行它之后的指令。在这里,它确保了打开Abort中断的设置立即生效。。
  • str x30, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_LR] 将寄存器x30的值存储到堆栈的指定位置。这里的位置是基于sp(Stack Pointer,栈指针)寄存器和偏移量CTX_GPREGS_OFFSET + CTX_GPREG_LR计算得到的。
  • mov x30, #1:将1写入到寄存器x30。
  • str x30, [sp, #CTX_EL3STATE_OFFSET +CTX_IS_IN_EL3]:将寄存器x30的值存储到堆栈的指定位置。这里的位置是基于sp寄存器和偏移量CTX_EL3STATE_OFFSET + CTX_IS_IN_EL3计算得到的。这条指令标志着异常处理程序现在正在 EL3 执行级别中运行。
  • dmb sy:Data Memory Barrier(数据内存屏障)。这条指令确保了在它之前的内存访问指令都完成后,才会执行它之后的指令。

这段代码的主要作用是打开Abort 中断,并将一些寄存器值存储到堆栈中以备后续的异常处理程序使用,同时也标志着异常处理程序正在 EL3执行级别中运行。

  • SMC 中断处理(bl31/aarch64/runtime_exceptions.S):
        /* ---------------------------------------------------------------------
         * This macro handles Synchronous exceptions.
         * Only SMC exceptions are supported.
         * ---------------------------------------------------------------------
         */
        .macro  handle_sync_exception
#if ENABLE_RUNTIME_INSTRUMENTATION
        /*
         * Read the timestamp value and store it in per-cpu data. The value
         * will be extracted from per-cpu data by the C level SMC handler and
         * saved to the PMF timestamp region.
         */
        mrs     x30, cntpct_el0
        str     x29, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_X29]
        mrs     x29, tpidr_el3
        str     x30, [x29, #CPU_DATA_PMF_TS0_OFFSET]
        ldr     x29, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_X29]
#endif

        mrs     x30, esr_el3
        ubfx    x30, x30, #ESR_EC_SHIFT, #ESR_EC_LENGTH

        /* Handle SMC exceptions separately from other synchronous exceptions */
        cmp     x30, #EC_AARCH32_SMC
        b.eq    smc_handler32

        cmp     x30, #EC_AARCH64_SMC
        b.eq    smc_handler64

        /* Synchronous exceptions other than the above are assumed to be EA */
        ldr     x30, [sp, #CTX_GPREGS_OFFSET + CTX_GPREG_LR]
        b       enter_lower_el_sync_ea
        .endm
  • 这个宏首先检查是否启用了运行时检测(ENABLE_RUNTIME_INSTRUMENTATION),如果启用了,就会读取物理计数器(CNTPCT_EL0)的值,并将其保存在每个CPU的数据中。这个值会被C级别的SMC处理程序提取并保存到PMF时间戳区域。
  • 然后,读取当前异常状态寄存器(ESR_EL3)的值,使用UBFX指令从中提取异常类(EC)字段。
  • 接着,它会检查异常类字段的值,如果值等于EC_AARCH32_SMC(0x13),表示这是一个32位的安全监视器调用(SMC),就跳转到 smc_handler32 标签处处理;如果值等于EC_AARCH64_SMC,表示这是一个64位的SMC,就跳转到 smc_handler64标签处处理。如果异常类字段的值既不是EC_AARCH32_SMC,也不是EC_AARCH64_SMC,则假定这是一个异步异常(EA),并跳转到enter_lower_el_sync_ea 标签处处理。

关于 ESR_EL3 EC 的字段:
在这里插入图片描述

ATF 代码中对于每个异常类型都做了对应的宏定义(include/arch/aarch64/arch.h)

/* Exception Syndrome register bits and bobs */
#define ESR_EC_SHIFT                    U(26)
#define ESR_EC_MASK                     U(0x3f)
#define ESR_EC_LENGTH                   U(6)
#define ESR_ISS_SHIFT                   U(0)
#define ESR_ISS_LENGTH                  U(25)
#define EC_UNKNOWN                      U(0x0)
#define EC_WFE_WFI                      U(0x1)
#define EC_AARCH32_CP15_MRC_MCR         U(0x3)
#define EC_AARCH32_CP15_MRRC_MCRR       U(0x4)
#define EC_AARCH32_CP14_MRC_MCR         U(0x5)
#define EC_AARCH32_CP14_LDC_STC         U(0x6)
#define EC_FP_SIMD                      U(0x7)
#define EC_AARCH32_CP10_MRC             U(0x8)
#define EC_AARCH32_CP14_MRRC_MCRR       U(0xc)
#define EC_ILLEGAL                      U(0xe)
#define EC_AARCH32_SVC                  U(0x11)
#define EC_AARCH32_HVC                  U(0x12)
#define EC_AARCH32_SMC                  U(0x13)
#define EC_AARCH64_SVC                  U(0x15)
#define EC_AARCH64_HVC                  U(0x16)
#define EC_AARCH64_SMC                  U(0x17)
#define EC_AARCH64_SYS                  U(0x18)
#define EC_IABORT_LOWER_EL              U(0x20)
#define EC_IABORT_CUR_EL                U(0x21)
#define EC_PC_ALIGN                     U(0x22)
#define EC_DABORT_LOWER_EL              U(0x24)
#define EC_DABORT_CUR_EL                U(0x25)
#define EC_SP_ALIGN                     U(0x26)
#define EC_AARCH32_FP                   U(0x28)
#define EC_AARCH64_FP                   U(0x2c)
#define EC_SERROR                       U(0x2f)
#define EC_BRK                          U(0x3c)
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

主公CodingCos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值