start_kernel函数之前的汇编分析

   在分析这段代码之前,我们必须先找到汇编代码的入口位置,也就是Bootloader启动linux时所跳转到的地址。内核一般被压缩后保存到FLASH上的,在Bootloader启动Linux之前必须先解压缩内核,关于内核解压缩部分请参照arch/arm/boot/compressed这里面的代码,这里就不细将了。

   在进入Linux汇编代码之前,Bootloader的执行影响CPU的状态,其状态一般为:

        1. CPU必须处于SVC(supervisor)模式,并且IRQ和FIQ中断都是禁止的;
        2. MMU(内存管理单元)必须是关闭的, 此时虚拟地址对物理地址;
        3. 数据cache(Data cache)必须是关闭的
        4. 指令cache(Instruction cache)可以是打开的,也可以是关闭的,这个没有强制要求;
        5. CPU 通用寄存器0 (r0)必须是 0;
        6. CPU 通用寄存器1 (r1)必须是 ARM Linux machine type(在后面会将到machine type)

        7. CPU 通用寄存器2 (r2) 必须是 kernel parameter list 的物理地址(parameter list 是由boot loader传递给kernel,用来描述设备信息属性的列表)

   Linux汇编的入口地址请看文件arch/arm/kernel/head.s,我们现在来看看文件里面的代码吧。

    __INIT
 .type stext, #function
ENTRY(stext)
 mov r12, r0
 mov r0, #PSR_F_BIT | PSR_I_BIT | MODE_SVC @ make sure svc mode
 msr cpsr_c, r0   @ and all irqs disabled
 bl __lookup_processor_type

我们来分析一下__lookup_processor_type这里面的函数吧,次函数的还是在本文件夹当中。

__lookup_processor_type:
    adr r5, 2f
    ldmia r5, {r7, r9, r10}
    sub r5, r5, r10   @ convert addresses
    add r7, r7, r5   @ to our address space
    add r10, r9, r5
    mrc p15, 0, r9, c0, c0  @ get processor id
1: ldmia r10, {r5, r6, r8}  @ value, mask, mmuflags
    and r6, r6, r9   @ mask wanted bits
    teq r5, r6
    moveq pc, lr
    add r10, r10, #PROC_INFO_SZ  @ sizeof(proc_info_list)
    cmp r10, r7
    blt 1b
    mov r10, #0    @ unknown processor
    mov pc, lr

2: .long __proc_info_end
    .long __proc_info_begin
    .long 2b
    .long __arch_info_begin
    .long __arch_info_end

r5保存了标号2的地址,接着就把__proc_info_end到2b的地址对应的付给r7,r9,r10。后面的连续三行语句的作用就是实现地址的转换,这样r10就存放着__proc_info_start,r7存放着__proc_info_end。其实这两者是一个全局变量的,arch/arm/kernel/vmlinux.lds.s中是如下定义的:

   __proc_info_begin = .;
        *(.proc.info)
   __proc_info_end = .;

其实可以看到这里有个区域的,就是所有文件中的.proc.info段都是属于这个区域的,我们再来看看我刚才说那两个全局变量的结构体。

  在 include/asm-arm/procinfo.h 中:
        00029: struct proc_info_list {
        00030:         unsigned int                cpu_val;
        00031:         unsigned int                cpu_mask;
        00032:         unsigned long                __cpu_mm_mmu_flags;        /* used by head.S */
        00033:         unsigned long                __cpu_io_mmu_flags;        /* used by head.S */
        00034:         unsigned long                __cpu_flush;                /* used by head.S */
        00035:         const char                *arch_name;
        00036:         const char                *elf_name;
        00037:         unsigned int                elf_hwcap;
        00038:         const char                *cpu_name;
        00039:         struct processor        *proc;
        00040:         struct cpu_tlb_fns        *tlb;
        00041:         struct cpu_user_fns        *user;
        00042:         struct cpu_cache_fns        *cache;
        00043: };
我们来看看一个具体文件中的.proc.info段,代码如下:

 在arch/arm/mm/proc-arm926.S 中:
        00464:         .section ".proc.info", #alloc, #execinstr
        00465:
        00466:         .type        __arm926_proc_info,#object
        00467: __arm926_proc_info:
        00468:         .long        0x41069260                        @ ARM926EJ-S (v5TEJ)
        00469:         .long        0xff0ffff0
        00470:         .long   PMD_TYPE_SECT | /
        00471:                 PMD_SECT_BUFFERABLE | /
        00472:                 PMD_SECT_CACHEABLE | /
        00473:                 PMD_BIT4 | /
        00474:                 PMD_SECT_AP_WRITE | /
        00475:                 PMD_SECT_AP_READ
        00476:         .long   PMD_TYPE_SECT | /
        00477:                 PMD_BIT4 | /
        00478:                 PMD_SECT_AP_WRITE | /
        00479:                 PMD_SECT_AP_READ
        00480:         b        __arm926_setup
        00481:         .long        cpu_arch_name
        00482:         .long        cpu_elf_name
        00483:         .long        HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_VFP|HWCAP_EDSP|HWCAP_JAVA
        00484:         .long        cpu_arm926_name
        00485:         .long        arm926_processor_functions
        00486:         .long        v4wbi_tlb_fns
        00487:         .long        v4wb_user_fns
        00488:         .long        arm926_cache_fns
        00489:         .size        __arm926_proc_info, . - __arm926_proc_info

其实我们可以看出他们之间的联系,简单来说就是在arch/arm/kernel/vmlinux.lds.s文件中其实就是对struct proc_info_list 结构体全局变量 __proc_info_begin 、__proc_info_end 的初始化,或是定义。就是说每个文件可以有一个或是几个不同的定义,这样的文件还有很多个。每个定义代表一个处理器的信息,等下我们要通过这些信息来查看是否是我们系统支持的。

   现在回到我们刚才的代码位置:

   mrc p15, 0, r9, c0, c0  @ get processor id在r9里面存放着从硬件上获得的处理器型号。

   ldmia r10, {r5, r6, r8}  @ value, mask, mmuflags刚才在上面的代码分析中了解到,r10存放着系统支持的处理器信息的开始位置,每个处理器都会对应一个处理器信息,我们已经知道这些信息会存放在这样的struct proc_info_list 结构变量里,这里r10指向第一个。r5,r6,r8存放的内容就是处理器ID号、处理器ID号对应的屏蔽字、内存管理单元的第一级描述符控制字段。这些都是前面那个变量的成员,其中用稍后就可以看到了。

   and r6, r6, r9   @ mask wanted bits
   teq r5, r6

这里就对前两个成员进行了运用,先是和屏蔽字相与,结果与ID号相比较。

  moveq pc, lr
  add r10, r10, #PROC_INFO_SZ  @ sizeof(proc_info_list)
  cmp r10, r7
  blt 1b
  mov r10, #0    @ unknown processor
  mov pc, lr

下面的代码就是循环进行比较,知道找到与系统匹配的处理器,如果不匹配了,就说明这个系统不支持这样的处理器,返回后会直接跳转到错误位置的。一下就是返回后的代码。

  teq r10, #0    @ invalid processor?
  moveq r0, #'p'   @ yes, error 'p'
  beq __error
  bl __lookup_architecture_type

很简单,如果r10为0,就说明没有找到匹配的,如果找到的话,r10存放着与之匹配的处理器信息。接下来又要跳转__lookup_architecture_type,我们来看看这部分代码:

2: .long __proc_info_end
 .long __proc_info_begin
 .long 2b
 .long __arch_info_begin
 .long __arch_info_end

/*
 * Lookup machine architecture in the linker-build list of architectures.
 * Note that we can't use the absolute addresses for the __arch_info
 * lists since we aren't running with the MMU on (and therefore, we are
 * not in the correct address space).  We have to calculate the offset.
 *
 *  r1 = machine architecture number
 * Returns:
 *  r2, r3, r4 corrupted
 *  r5 = physical start address of RAM
 *  r6 = physical address of IO
 *  r7 = byte offset into page tables for IO
 */
__lookup_architecture_type:
    adr r4, 2b
    ldmia r4, {r2, r3, r5, r6, r7} @ throw away r2, r3
    sub r5, r4, r5   @ convert addresses
    add r4, r6, r5   @ to our address space
    add r7, r7, r5
1: ldr r5, [r4]   @ get machine type
    teq r5, r1    @ matches loader number?
    beq 2f    @ found
    add r4, r4, #SIZEOF_MACHINE_DESC @ next machine_desc
    cmp r4, r7
    blt 1b
    mov r7, #0    @ unknown architecture
    mov pc, lr
2: ldmib r4, {r5, r6, r7}  @ found, get results
    mov pc, lr
其实仔细看看,会发现和上述的代码差不多,同样r4保存着上面标号2的地址,把r6,r7附上了 __arch_info_begin、__arch_info_end变量的地址,后面三句又是地址转换,重新确认后的地址保存在r7,r4上面。我们来看看这两个变量具体是什么。

 __arch_info_begin 和 __arch_info_end是在 arch/arm/kernel/vmlinux.lds.S中:

        00034:                __arch_info_begin = .;
        00035:                        *(.arch.info.init)
        00036:                __arch_info_end = .;
这三行的意思是: __arch_info_begin 的位置上,放置所有文件中的 ".arch.info.init" 段的内容,然后紧接着是 __arch_info_end 的位置.我们来看看这两个变量的类型。(struct machine_desc)
        kernel 使用struct machine_desc 来描述 machine type.
        在 include/asm-arm/mach/arch.h 中:

        00017: struct machine_desc {
        00018:         /*
        00019:          * Note! The first four elements are used
        00020:          * by assembler code in head-armv.S
        00021:          */
        00022:         unsigned int                nr;                /* architecture number        */
        00023:         unsigned int                phys_io;        /* start of physical io        */
        00024:         unsigned int                io_pg_offst;        /* byte offset for io
        00025:                                                  * page tabe entry        */
        00026:
        00027:         const char                *name;                /* architecture name        */
        00028:         unsigned long                boot_params;        /* tagged list                */
        00029:
        00030:         unsigned int                video_start;        /* start of video RAM        */
        00031:         unsigned int                video_end;        /* end of video RAM        */
        00032:
        00033:         unsigned int                reserve_lp0 :1;        /* never has lp0        */
        00034:         unsigned int                reserve_lp1 :1;        /* never has lp1        */
        00035:         unsigned int                reserve_lp2 :1;        /* never has lp2        */
        00036:         unsigned int                soft_reboot :1;        /* soft reboot                */
        00037:         void                        (*fixup)(struct machine_desc *,
        00038:                                          struct tag *, char **,
        00039:                                          struct meminfo *);
        00040:         void                        (*map_io)(void);/* IO mapping function        */
        00041:         void                        (*init_irq)(void);
        00042:         struct sys_timer        *timer;                /* system tick timer        */
        00043:         void                        (*init_machine)(void);
        00044: };
        00050: #define MACHINE_START(_type,_name)                        /
        00051: static const struct machine_desc __mach_desc_##_type        /
        00052:  __attribute_used__                                        /
        00053:  __attribute__((__section__(".arch.info.init")) = {        /
        00054:         .nr                = MACH_TYPE_##_type,                /
        00055:         .name                = _name,
        00056:
        00057: #define MACHINE_END                                /
        00058: };        
        
        内核中,一般使用宏MACHINE_START来定义machine type.
        对于at91, 在 arch/arm/mach-at91rm9200/board-ek.c 中:
        00137: MACHINE_START(AT91RM9200EK, "Atmel AT91RM9200-EK"
        00138:         /* Maintainer: SAN People/Atmel */
        00139:         .phys_io        = AT91_BASE_SYS,
        00140:         .io_pg_offst        = (AT91_VA_BASE_SYS >> 1 & 0xfffc,
        00141:         .boot_params        = AT91_SDRAM_BASE + 0x100,
        00142:         .timer                = &at91rm9200_timer,
        00143:         .map_io                = ek_map_io,
        00144:         .init_irq        = ek_init_irq,
        00145:         .init_machine        = ek_board_init,
        00146: MACHINE_END

其实和上述是差不多的,后面我就不再分析了,就是看看系统对这个arm板子架构是否支持。但是要注意这条语句ldmib r4, {r5, r6, r7}  @ found, get results这里很明显是说,如果找到的匹配的话就把某些信息进行保存,r5、r6、r7分别保存了系统物理内存基址、系统外设物理基址、I/O页偏移量。

    好了,现在来到的难点,也是重点。我们先返回到主程序当中往下接着看:

   teq r7, #0    @ invalid architecture?
   moveq r0, #'a'   @ yes, error 'a'
   beq __error
   bl __create_page_tables

会发现这里又要跳转到__create_page_tables这样的函数里面,这个函数主要做的工作就是创建页表。创建页表是通过函数 __create_page_tables 来实现的. 这里,我们使用的是arm的L1主页表,L1主页表也称为段页表(section page table),L1 主页表将4 GB 的地址空间分成若干个1 MB的段(section),因此L1页表包含4096个页表项(section entry). 每个页表项是32 bits(4 bytes),因而L1主页表占用 4096 *4 = 16k的内存空间.
下面我们来分析 __create_page_tables 函数:

         在 arch/arm/kernel/head.S 中:

00206:         .type        __create_page_tables, %function
00207: __create_page_tables:
00208:         pgtbl        r4                                @ page table address
00209:
00210:         /*
00211:          * Clear the 16K level 1 swapper page table
00212:          */

上面注释明确说明了,建立页表后再清零。
00213:         mov        r0, r4
00214:         mov        r3, #0
00215:         add        r6, r0, #0x4000
00216: 1:     str        r3, [r0], #4
00217:         str        r3, [r0], #4
00218:         str        r3, [r0], #4
00219:         str        r3, [r0], #4
00220:         teq        r0, r6
00221:         bne        1b

这里要注意的是,r4保存着页表的物理基地址,从0x4000开始往后16KB都被清零了,这里将被用来存放一级页表的。
00222:
00223:         ldr        r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
00224:
00225:         /*
00226:          * Create identity mapping for first MB of kernel to
00227:          * cater for the MMU enable.  This identity mapping
00228:          * will be removed by paging_init().  We use our current program
00229:          * counter to determine corresponding section base address.
00230:          */

上面注释说了,我们先建立1MB的映射,为了可以使能MMU,其实映射的建立就是往刚才的上述的16KB的空间里的某个单元添东西。231这个代码取出了当前指令所在的内存段,232这个代码给R3附上了这个段将来被映射所对应物理地址,最后把这个地址存放在16KB这个空间中对应的单元(每个1MB的段都会对应一个固定的单元)。这里的虚拟地址和物理地址的映射关系是一一对应的。
00231:         mov        r6, pc, lsr #20                        @ start of kernel section
00232:         orr        r3, r7, r6, lsl #20                @ flags + kernel base
00233:         str        r3, [r4, r6, lsl #2]                @ identity mapping
00234:
/*
  * Now setup the pagetables for our kernel direct
  * mapped region.  We round TEXTADDR down to the
  * nearest megabyte boundary.  It is assumed that
  * the kernel fits within 4 contigous 1MB sections.
  */
 add r0, r4,  #(TEXTADDR & 0xff000000) >> 18 @ start of kernel
 str r3, [r0, #(TEXTADDR & 0x00f00000) >> 18]!
 add r3, r3, #1 << 20
 str r3, [r0, #4]!   @ KERNEL + 1MB
 add r3, r3, #1 << 20
 str r3, [r0, #4]!   @ KERNEL + 2MB
 add r3, r3, #1 << 20
 str r3, [r0, #4]   @ KERNEL + 3MB
由注释可以看出这段程序的功能就是建立内核开始连续4MB的物理地址到虚拟地址的映射关系,TEXTADDR=0xc0008000这个是内核存放的虚拟地址,前两句的功能就是求出内核虚拟地址所对应的段,把对应的段保存在r0里面。通过r0可以求得页表里面的哪个单元。这样就可以把当前代码所在段的后续段的物理地址都一次保存在这个单元和这个单元后的连续单元里面。

 add r0, r4, #VIRT_OFFSET >> 18
 add r2, r5, r8
 str r2, [r0]

VIRT_OFFSET=PAGE_OFFSET=0xc0000000是系统虚拟地址的起始地址。这三条语句的功能就是建立这个虚拟地址到物理地址的映射。先找到这个虚拟地址所对应的段,接着就可以获得这个段在一级页表中对应的单元,r5就是系统物理内存基地址,这样这个单元里保存的就是这个基地址。简单来说就是建立了系统内存的起始地址的映射。

  到目前为止,一共映射了建立了6段的映射,起始这里的六段虚拟地址只是映射了4段物理地址,也就是说有3段虚拟地址映射1段物理地址,是重映射。这里是为什么,我就不多说了。为什么这样映射,后面会提到。关于#ifdef的内容我们忽略,这个和系统配置和调试有关系。

   当 __create_page_tables 返回之后此时,一些特定寄存器的值如下所示:
    r4 = pgtbl              (page table 的物理基地址)

    r10 = procinfo          (struct proc_info_list的基地址)

adr lr, __turn_mmu_on  @ return (PIC) address
add pc, r10, #12

可以看出这里为pc赋值,加上#12后就可以访问到__cpu_flush这个变量了,之前我们在__lookup_processor_type里面就给这个变量附上了值了,就是每个处理器的__xxx_setup()。这样我们就会跳转到这个函数来了。在我们需要在开启mmu之前,做一些必须的工作:清除ICache, 清除 DCache, 清除 Writebuffer, 清除TLB等.这些一般是通过cp15协处理器来实现的,并且是平台相关的. 这就是,__cpu_flush 需要做的工作.

下面我们来分析函数 __arm926_setup
        
        在 arch/arm/mm/proc-arm926.S 中:
00391:         .type        __arm926_setup, #function
00392: __arm926_setup:
00393:         mov        r0, #0
00394:         mcr        p15, 0, r0, c7, c7                @  清除(invalidate)Instruction Cache 和 Data Cache.
00395:         mcr        p15, 0, r0, c7, c10, 4                @ 清除(drain) Write Buffer.

如果有配置了MMU,则需要清除(invalidate)Instruction TLB 和Data TLB
接下来,是对控制寄存器c1进行配置,请参考 ARM926 TRM.
00396: #ifdef CONFIG_MMU
00397:         mcr        p15, 0, r0, c8, c7                @ invalidate I,D TLBs on v4
00398: #endif
00399:
00400:

如果配置了Data Cache使用writethrough方式, 需要关掉write-back.
00401: #ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
00402:         mov        r0, #4                                @ disable write-back on caches explicitly
00403:         mcr        p15, 7, r0, c15, c0, 0
00404: #endif
00405:
00406:         adr        r5, arm926_crval                @取arm926_crval的地址到r5中, arm926_crval 在第424行

这里我们需要看一下424和425行,其中用到了宏crval,crval是在 arch/arm/mm/proc-macro.S 中:

        00053:         .macro        crval, clear, mmuset, ucset
        00054: #ifdef CONFIG_MMU
        00055:         .word        /clear
        00056:         .word        /mmuset
        00057: #else
        00058:         .word        /clear
        00059:         .word        /ucset
        00060: #endif
        00061:         .endm

配合425行,我们可以看出,首先在arm926_crval的地址处存放了clear的值,然后接下来的地址存放了mmuset的值(对于配置了MMU的情况) 。所以,在407行中,我们将clear和mmuset的值分别存到了r5, r6中
00407:         ldmia        r5, {r5, r6}
00408:         mrc        p15, 0, r0, c1, c0                @ 获得控制寄存器c1的值
00409:         bic        r0, r0, r5                              @将r0中的 clear (r5) 对应的位都清除掉
00410:         orr        r0, r0, r6                              @设置r0中 mmuset (r6) 对应的位
00411: #ifdef CONFIG_CPU_CACHE_ROUND_ROBIN
00412:         orr        r0, r0, #0x4000                        @ .1.. .... .... ....
00413: #endif
00414:         mov        pc, lr                                     @取lr的值到pc中.
00415:         .size        __arm926_setup, . - __arm926_setup
00416:
00417:         /*
00418:          *  R
00419:          * .RVI ZFRS BLDP WCAM
00420:          * .011 0001 ..11 0101
00421:          *
00422:          */
00423:         .type        arm926_crval, #object
00424: arm926_crval:
00425:         crval        clear=0x00007f3f, mmuset=0x00003135, ucset=0x00001134

由于在跳转到setup函数之前,我们就把__turn_mmu_on标号保存到lr当中了,注意到414行代码。接下来就是跳转到__turn_mmu_on这个函数,我们来看看这个函数具体干些什么哦。

 .align 5
 .type __turn_mmu_on, %function
__turn_mmu_on:
 ldr lr, __switch_data
#ifdef CONFIG_ALIGNMENT_TRAP(查看系统是否要求地址对齐检测)
 orr r0, r0, #2   @ ...........A. @如果开启了这项功能的话,就使r0得bit1置1.
#endif
 mcr p15, 0, r0, c1, c0, 0  @ 将r0写入cp15的c1寄存器当中,使能mmu和配置上述功能。
 mrc p15, 0, r3, c0, c0, 0  @ 然后将cp15得c0寄存器,即主标识符寄存器的值读到r3里。与r9里的值相同。
 mov r3, r3
 mov r3, r3
 mov pc, lr

接下来又跳转到__switch_data处,我们来看看它的代码:

00014:         .type        __switch_data, %object
00015: __switch_data:
00016:         .long        __mmap_switched
00017:         .long        __data_loc                        @ r4
00018:         .long        __data_start                        @ r5
00019:         .long        __bss_start                        @ r6
00020:         .long        _end                                @ r7
00021:         .long        processor_id                        @ r4
00022:         .long        __machine_arch_type                @ r5
00023:         .long        cr_alignment                        @ r6
00024:         .long        init_thread_union + THREAD_START_SP @ sp
第16 - 24行: 定义了一些地址,例如第16行存储的是 __mmap_switched 的地址, 第17行存储的是 __data_loc 的地址 ......可以看出它直接跳转到__mmap_switched,我们来看看它的代码:

 .align 5
__mmap_switched:
 adr r2, __switch_data + 4    @ 取 __switch_data + 4的地址到r2. 从上文可以看到这个地址就是第17行的地址.
 ldmia r2, {r2, r3, r4, r5, r6, r7, r8, sp}  @依次取出从第17行到第20行的地址,存储到r2,r3,r4, r5, r6, r7,r8,sp 中.
        对照上文,我们可以得知:
                r2 - __data_loc    @数据存放的位置
                r3 - __data_start  @数据存放的开始位置
                r4 - __bss_start    @bss的开始位置
                r5 - _end                @bss的结束位置,也就是内核结束位置

                r6 - processor_id

                r7 - __machine_arch_type

                r8 - cr_alignment

                sp - init_thread_union+THREAD_START_SP(init_thread_union+8912)也就是在系统创建第一个任务之前,将init_thread_union.init_thread_info作为系统启动过程的线程信息结构变量。

    cmp r2, r3    @比较 __data_loc 和 __data_start,如果不等,将__data_loc得数据复制到__data_start处。

1: cmpne r3, r4
    ldrne fp, [r2], #4
    strne fp, [r3], #4
    bne 1b

    mov fp, #0    @ 将 BSS之间的内存清零。
1: cmp r4, r5
    strcc fp, [r4],#4
    bcc 1b

    str r9, [r6]   @ 赋给全局变量processor_id

    str r1, [r7]   @ 赋给全局变量__machine_arch_type

    bic r2, r0, #2   @ 将保存在r0的cp15德c1寄存器的值的bit1清零,再赋给r2。
    stmia r8, {r0, r2}   @ 很明显cr_alignment全局变量里面存放着r0,cr_no_alignment全局变量里面存放着r2的值。

    b start_kernel
好了,这样就全部讲完了,总结为以下:


        1. 确定 processor type                        (75 - 78行)
        2. 确定 machine type                        (79 - 81行)
        3. 创建页表                                (82行)     
        4. 调用平台特定的__cpu_flush函数        (在struct proc_info_list中)        (94 行)                           
        5. 开启mmu                                (93行)
        6. 切换数据                                 (91行)
       
        最终跳转到start_kernel                        (在__switch_data的结束的时候,调用了 b        start_kernel)

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值