linux运行欧陆风云,Arm linux启动分析(1)

下周准备做linux启动的技术讲座,在这里我慢慢整理下自己的材料,这次我写的是Image的启动过程,也即使zImage解压缩结束后的启动代码,这时候的代码开始地址仍然是0x30008000,下面我结合代码来讲吧:

Image的启动代码是在/arch/arm/kernel/head.S中的:

/*

*  linux/arch/arm/kernel/head.S

*  Kernel startup code for all 32-bit CPUs

*/

/* 内核启动入口点

* Kernel startup entry point.

* 这里通常在解压后直接调用。

* 处理器基本状态要求:

* MMU关闭,D-cach关闭,I-cache不用关系;

* r0 = 0,r1 = 系统号(machine number)

* 这段代码几乎是位置无关的。

* 如果链接内核在0xc0008000,调用的地址为相应的物理地址__pa(0xc0008000)。

* r1的系统号参考arch/arm/tools/mach-types文件的列表。

* 尽量不要在这里添加系统号相关的代码,那应该放在bootloader的代码中。

* 保持这里的代码的整洁。

*/

__INIT

.type stext, %function

/*――――――――――――――――――――――――――――――――――――――――――――――

这个地方就是 kernel 的入口点

―――――――――――――――――――――――――――――――――――――――――――――― */

ENTRY(stext)

msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | MODE_SVC @ ensure svc mode

@ and irqs disabled

/*――――――――――――――――――――――――――――――――――――――――――――――

调用 __lookup_processor_type 检查现在运行的 cpu 的 ID 值和 linux 编译支持的

id 值是否相等。

――――――――――――――――――――――――――――――――――――――――――――――*/

bl __lookup_processor_type  @ r5=procinfo r9=cpuid

/*――――――――――――――――――――――――――――――――――――――――――――――

从该函数返回后,寄存器内容如下:

R9  = cpu ID

R5 = pointer to processor structure

详细的内容请看__lookup_processor_type 的分析 */

.type __lookup_processor_type, %function

__lookup_processor_type:

/*――――――――――――――――――――――――――――――――――――――――――――――

把标号 2 的地址送给 r3, 3f = lable 3 forward

―――――――――――――――――――――――――――――――――――――――――――――― */

adr r3, 3f

/*――――――――――――――――――――――――――――――――――――――――――――――

把 r3 指向内存的地址的内容赋值给 r5,r6,r9

所以,参照标号 3处的声明,我们可以知道:

__proc_info_begin     r5

__proc_info_end       r6

3b                    r9

__proc_info_end 和 __proc_info_begin 这两个标号都是在

/linux/arch/arm/vmlinux.ld 这个脚本中定义的。在连接的时候,ld 会把相应cpu信息

proc_info 放到这两个标号之间。

__proc_info_begin = .;

*(.proc.info)

__proc_info_end = .;

――――――――――――――――――――――――――――――――――――――――――――――*/

ldmda r3, {r5, r6, r9}  @ ldmda弹栈顺序是从右到左,[r3]->r9,[r3-4]->r6,[r3-8]->r5

/*r3 = 标号 3 的加载地址地址,r9 = 标号 3 的连接地址 ,r3是根据pc值确定的,r9是链接阶段就确定的是链接地址*/

sub r3, r3, r9   @ get offset between virt&phys

// r3 = 加载地址和连接地址的差值

//现在,r5 = __proc_info_begin 的加载地址,即在 RAM 中的地址

add r5, r5, r3   @ convert virt addresses to

add r6, r6, r3   @ physical address space

mrc p15, 0, r9, c0, c0 @ get processor id协处理器指令获取cpu id号r9=0x41807202(sep4020)

/*――――――――――――――――――――――――――――――――――――――――――――――

在本例中, r5  =  _arm720_proc_info 这 个 标记定义在

linux/arch/arm/mm/proc-arm720.S

__arm720_proc_info:

.long   0x41807200       r3 = cpu_value

.long   0xffffff00       r4 = cpu_mask

.long   0x00000c1e       mmuflags,一级段描述符

b       __arm720_setup

.

.

―――――――――――――――――――――――――――――――――――――――――――――― */

//ldmia弹栈顺序是从左到右,[r5]->r3,[r5+4]->r4 ,即低地址的内容放到低编号的寄存器,高地址的内容放到高编号的寄存器,指令结束后r5的指依然为_arm720_proc_info

1: ldmia r5, {r3, r4}   @ value, mask

and r4, r4, r9   @ mask wanted bits

//将r9屏上0xffffff00看sep4020是否是arm720t的内核

teq r3, r4

beq 2f     @若是arm720t内核则直接跳转到标签2

/*――――――――――――――――――――――――――――――――――――――――――――――

proc_info_list 定义在 linux/include/asm-arm/procinfo.h

struct proc_info_list {

unsigned int  cpu_val;

unsigned int  cpu_mask;

unsigned long  __cpu_mmu_flags; // used by head.S

unsigned long  __cpu_flush;  // used by head.S

const char  *arch_name;

const char  *elf_name;

unsigned int  elf_hwcap;

const char  *cpu_name;

struct processor *proc;

struct cpu_tlb_fns *tlb;

struct cpu_user_fns *user;

struct cpu_cache_fns *cache;

};

每一项都是 4 个字节,所以 sizeof(proc_info_list) = 48 byte

――――――――――――――――――――――――――――――――――――――――――――――*/

add r5, r5, #PROC_INFO_SZ  @ sizeof(proc_info_list)=48

cmp r5, r6

blo 1b

mov r5, #0    @ unknown processor

2: mov pc, lr

/*

* This provides a C-API version of the above function.

*/

ENTRY(lookup_processor_type)

stmfd sp!, {r4 - r6, r9, lr}

bl __lookup_processor_type

mov r0, r5

ldmfd sp!, {r4 - r6, r9, pc}

/*

* Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for

* more information about the __proc_info and __arch_info structures.

*/

.long __proc_info_begin

.long __proc_info_end

3: .long .

.long __arch_info_begin

.long __arch_info_end

/*―――――――――――――从__lookup_processor_type返回――――――――――――――――――――――――――――――――― */

movs r10, r5    @ 是有效720t核吗  (r5=0)?

beq __error_p   @ yes, error 'p'

/*――――――――――――――――――――――――――――――――――――――――――――――

__lookup_machine_type 通过 R1 寄存器,判断体系类型,R1 = machine

architecture number

―――――――――――――――――――――――――――――――――――――――――――――― */

bl __lookup_machine_type  @ r5=machinfo

/*―――――――――――――――――――――――――――――――――――――――――――――― */

/*  r1 = machine architecture number

* Returns:

*  r3, r4, r6 corrupted

*  r5 = mach_info pointer in physical address space

*/

.type __lookup_machine_type, %function

__lookup_machine_type:

adr r3, 3b

/*――――――――――――――――――――――――――――――――――――――――――――――

把 r3 指向内存的地址的内容赋值给 r4,r5,r6

所以,参照标号 3处的声明,我们可以知道:

3b                   r4

__arch_info_begin r5

__arch_info_end  r6

__arch_info_end 和 __arch_info_begin 这两个标号都是在

/linux/arch/arm/vmlinux.ld 这个脚本中定义的。在连接的时候,ld 会把相应体系架构

arch_info 放到这两个标号之间。

__arch_info_begin = .;

*(.arch.info.init)

__arch_info_end = .;

――――――――――――――――――――――――――――――――――――――――――――――*/

ldmia r3, {r4, r5, r6}

sub r3, r3, r4   @ get offset between virt&phys

//r5=__arch_info_begin的加载地址

add r5, r5, r3   @ convert virt addresses to

add r6, r6, r3   @ physical address space

/*――――――――――――――――――――――――――――――――――――――――――――――

__arch_info_begin 和__arch_info_end  的类型都是 struct  machine_desc。其实

就是指向一个 machine_desc 结构首尾的两个地址标号。

struct machine_desc 定义在 linux/include/asm-arm/mach/arch.h 中

struct machine_desc {

/*

* Note! The first four elements are used

* by assembler code in head.S

*/

unsigned int  nr;  /* architecture number */

unsigned int __deprecated phys_ram; /* start of physical ram */

unsigned int  phys_io; /* start of physical io */

unsigned int  io_pg_offst; /* byte offset for io

* page tabe entry */

const char  *name;  /* architecture name */

unsigned long  boot_params; /* tagged list  */

unsigned int  video_start; /* start of video RAM */

unsigned int  video_end; /* end of video RAM */

unsigned int  reserve_lp0 :1; /* never has lp0 */

unsigned int  reserve_lp1 :1; /* never has lp1 */

unsigned int  reserve_lp2 :1; /* never has lp2 */

unsigned int  soft_reboot :1; /* soft reboot  */

void   (*fixup)(struct machine_desc *,

struct tag *, char **,

struct meminfo *);

void   (*map_io)(void);/* IO mapping function */

void   (*init_irq)(void);

struct sys_timer *timer;  /* system tick timer */

void   (*init_machine)(void);

};

而对于我们的SEP4020其真正的定义是在/arch/arm/mach-sep4020/4020.c中

MACHINE_START(GFD4020, "4020 board")

.phys_io = 0x10000000,

.io_pg_offst = ((0xe0000000) >> 18) & 0xfffc,

.boot_params = 0x30000100,

.fixup  = fixup_gfd4020,

.map_io  = sep4020_map_io,

.init_irq =  sep4020_init_irq,

.init_machine = sep4020_init,

.timer  = &sep4020_timer,

MACHINE_END

看到这里,我们就不难明白下边这条指令了,struct machine_desc 中第一个就是

nr,即 architecture number

r3 = MACH_TYPE_GFD4020

――――――――――――――――――――――――――――――――――――――――――――――*/

1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type ,MACHINFO_TYPE = 0

//r1是由解压缩程序/arch/arm/boot/compressed/head.S最后传过来的,或者是uboot传过来的体系结构号

teq r3, r1    @ matches loader number?

beq 2f    @ found

add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc

cmp r5, r6

blo 1b

mov r5, #0    @ unknown machine

2: mov pc, lr

/*―――――――――――――――――从__lookup_machine_type返回――――――――――――――――――――――――――――― */

movs r8, r5    @ invalid machine (r5=0)?是不是我们的SEP4020系统结构

beq __error_a   @ yes, error 'a'

/*――――――――――――――――――――――――――――――――――――――――――――――

设置 mmu 之前,设置临时内核页表

―――――――――――――――――――――――――――――――――――――――――――――― */

bl __create_page_tables

/*――――――――――――――――――――――――――――――――――――――――――――――

/*  我们在这里只映射内核启动的临时页表

* Setup the initial page tables.  We only setup the barest

* amount which are required to get the kernel running, which

* generally means mapping in the kernel code.

*

* r8  = machinfo  体系结构信息

* r9  = cpuid     cpu 的ID

* r10 = procinfo  cpu信息

*

* Returns:

*  r0, r3, r6, r7 corrupted

*  r4 = physical page table address

*/

.type __create_page_tables, %function

__create_page_tables:

/*―――――――――――――――――――――――――――――――――――――――――――――― */

// Page offset: 3GB  内核页表的偏移在/inculde/asm/memory.h

#define PAGE_OFFSET  UL(0xc0000000)

#ifndef __virt_to_phys

#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)

#define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET)

#endif

而这其中的PHYS_OFFSET则是我们需要在我们的SEP4020的定义自己的主存ram的基址的物理地址,我们是在/include/asm-arm/arch-sep4020/memory.h中定义的

#define PHYS_OFFSET UL(0x30000000)

/*TEXT_OFFSET 是在在arch/arm/Makefile第140行,有

TEXT_OFFSET := $(textofs-y)

第90行有

textofs-y := 0x00008000

所以TEXT_OFFSET := 0x00008000

在153行有export TEXT_OFFSET将此变量输出。*/

#define KERNEL_RAM_ADDR (PAGE_OFFSET + TEXT_OFFSET)  @其中TEXT_OFFSET = 0x8000

//swapper_pg_dir是放启动时的临时页表的页表基址(虚地址)

.globl swapper_pg_dir

.equ swapper_pg_dir, KERNEL_RAM_ADDR - 0x4000

//这个宏就是根据内核ram首址(虚拟地址)计算出我们内核页表的页表基址(物理地址)

.macro pgtbl, rd

ldr \rd, =(__virt_to_phys(KERNEL_RAM_ADDR - 0x4000))

.endm

―――――――――――――――――――――――――――――――――――――――――――――― */

pgtbl r4    @ page table address

//这样r4 = 内核页表的页表基址(物理地址)

/*

* Clear the 16K level 1 swapper page table

*/

mov r0, r4

mov r3, #0

//r6 = 内核的KERNEL_RAM_ADDR

add r6, r0, #0x4000

//首先对16k的一级页表内容清0

1: str r3, [r0], #4

str r3, [r0], #4

str r3, [r0], #4

str r3, [r0], #4

teq r0, r6

bne 1b

//PROCINFO_MMUFLAGS = 8;这样 r7 = 0x00000c1e       mmuflags,一级段描述符 ,在proc-arm720.S 中定义

ldr r7, [r10, #PROCINFO_MMUFLAGS] @ mmuflags

/*

* Create identity mapping for first MB of kernel to

* cater for the MMU enable.  This identity mapping

* will be removed by paging_init().  We use our current program

* counter to determine corresponding section base address.

这次一致映射主要是映射kernel前1MB的地址,这个映射最终会被后面paging_init()更新页表

*/

//获取当前程序的段地址 = r6

mov r6, pc, lsr #20   @ start of kernel section

//将段地址 或上mmuflags,然后赋值给r3

orr r3, r7, r6, lsl #20  @ flags + kernel base

//开始放内核代码的段映射描述符,段表基址为r4,段表内的索引为r6<<2;

str r3, [r4, r6, lsl #2]  @ identity mapping

//这样做是为了解决后面刚开MMU是防止pc值飞掉了,在__turn_mmu_on函数中

/*

* 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.

将kernel往后的4MB的地址建立虚实映射

*/

//#define TEXTADDR  KERNEL_RAM_ADDR即等于0xc0008000

//这里的TEXTADDR是0xc0008000段对应的代码和前面的pc对应的段地址是同一代码,这样做是为了解决后面刚开MMU是防止pc值飞掉了

add r0, r4,  #(TEXTADDR & 0xff000000) >> 18 @ start of kernel

//把start of kernel(0xc0008000)的虚拟地址映射起来,即将它的段描述符保存到段表的相应索引处位置

str r3, [r0, #(TEXTADDR & 0x00f00000) >> 18]!

//建立start of kernel+1MB的虚拟地址映射

add r3, r3, #1 << 20

str r3, [r0, #4]!   @ KERNEL + 1MB

//建立start of kernel+2MB的虚拟地址映射

add r3, r3, #1 << 20

str r3, [r0, #4]!   @ KERNEL + 2MB

//建立start of kernel+3MB的虚拟地址映射

add r3, r3, #1 << 20

str r3, [r0, #4]   @ KERNEL + 3MB

/*

* Then map first 1MB of ram in case it contains our boot params.

建立PAGE_OFFSET=0XC0000000地址的映射,因为这个地址附近包含我们的uboot传给linux的启动参数

*/

add r0, r4, #PAGE_OFFSET >> 18

orr r6, r7, #PHYS_OFFSET

str r6, [r0]

mov pc, lr

―――――――――――――从__create_page_tables返回――――――――――――――――――――――――――――――――― */

/*

* The following calls CPU specific code in a position independent

* manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of

* xxx_proc_info structure selected by __lookup_machine_type

* above.  On return, the CPU will be ready for the MMU to be

* turned on, and r0 will hold the CPU control register value.

*/

//__switch_data是一个标签(即是一个地址),这个即是r13 = __mmap_switched(函数指针)

ldr r13, __switch_data  @ address to jump to after

@ mmu has been enabled

//将__enable_mmu(这是个与地址无关代码)赋值给lr,等会返回执行,模拟一个函数栈

adr lr, __enable_mmu  @ return (PIC) address

/*********************************************************************************************************

//#define PROCINFO_INITFUNC 12,在这里跳转执行arm720t架构的相应初始化代码

__arm720_proc_info:

.long 0x41807200    @ cpu_val

.long 0xffffff00    @ cpu_mask

.long   PMD_TYPE_SECT | \

PMD_SECT_BUFFERABLE | \

PMD_SECT_CACHEABLE | \

PMD_BIT4 | \

PMD_SECT_AP_WRITE | \

PMD_SECT_AP_READ

b __arm720_setup    @ cpu_flush

****************************************************************************************************/

add pc, r10, #PROCINFO_INITFUNC

/*********************************************************************************************************/

//转到__arm720_setup函数来执行

.type __arm720_setup, #function

__arm720_setup:

mov r0, #0

//写CP15的c7寄存器使cache的数据无效

mcr p15, 0, r0, c7, c7, 0  @ invalidate caches

//使整个TLB内部的地址变换条目无效

mcr p15, 0, r0, c8, c7, 0  @ flush TLB (v4)

//把CP15的寄存器c1传给r0

mrc p15, 0, r0, c1, c0  @ get control register

/*********************************************************************************************************

arm720_cr1_clear,arm720_cr1_set宏的定义

.type arm710_cr1_clear, #object

.type arm710_cr1_set, #object

arm710_cr1_clear:

.word 0x0f3f

arm710_cr1_set:

.word 0x013d(mmu使能,禁用地址对齐,cache使能,写缓冲使能,小印地安序,系统保护,rom不保护)

****************************************************************************************************/

ldr r5, arm720_cr1_clear

bic r0, r0, r5

ldr r5, arm720_cr1_set

//r0 = 0x013d(mmu使能,禁用地址对齐,cache使能,写缓冲使能,小印地安序,系统保护,rom不保护)

orr r0, r0, r5

//跳转到__enable_mmu,相当于执行函数__enable_mmu

mov pc, lr    @ __ret (head.S)

.size __arm720_setup, . - __arm720_setup

/****************************************从__arm720_setup返回************************************************************/

/*********************************************************************************************************

/*

* Setup common bits before finally enabling the MMU.  Essentially

* this is just loading the page table pointer and domain access

* registers.

*/

.type __enable_mmu, %function

__enable_mmu:

//#define CONFIG_ALIGNMENT_TRAP 1是在/include/linux/Autoconfig.h定义

//Autoconfig.h是在make时根据Kconfig文件产生的

#ifdef CONFIG_ALIGNMENT_TRAP

orr r0, r0, #CR_A    @CR_A=1 地址对齐

#else

bic r0, r0, #CR_A

#endif

#ifdef CONFIG_CPU_DCACHE_DISABLE

bic r0, r0, #CR_C  @CR_C= 2 D cache使能

#endif

#ifdef CONFIG_CPU_BPREDICT_DISABLE

bic r0, r0, #CR_Z  @CR_Z = 11 分支预测

#endif

#ifdef CONFIG_CPU_ICACHE_DISABLE

bic r0, r0, #CR_I   @CR_I = 12 I cache使能

#endif

//#define domain_val(dom,type) ((type) << (2*(dom)))在/include/asm-arm/Domain.h定义

//MMU中的域是一些段,大页或小页的集合,而在MMU的页表描述符中有些位表示该描述符的域,指明该存储空间所属的域号0~15

mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \

domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \

domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \

domain_val(DOMAIN_IO, DOMAIN_CLIENT))

//配置cp15的域访问控制寄存器

mcr p15, 0, r5, c3, c0, 0  @ load domain access register

//配置cp15的页表基址寄存器c2,r4就是页表的首地址

mcr p15, 0, r4, c2, c0, 0  @ load page table pointer

b __turn_mmu_on

/*

* Enable the MMU.  This completely changes the structure of the visible

* memory space.  You will not be able to trace execution through this.

* If you have an enquiry about this, *please* check the linux-arm-kernel

* mailing list archives BEFORE sending another post to the list.

*

*  r0  = cp #15 control register

*  r13 = *virtual* address to jump to upon completion

*  r13是开MMU后将执行的第一个函数

* other registers depend on the function called upon completion

开启MMU,这时你看到的世界将彻底是虚拟世界了,

*/

.align 5

.type __turn_mmu_on, %function

__turn_mmu_on:

mov r0, r0

//配置cp15的c1,开启MMU

mcr p15, 0, r0, c1, c0, 0  @ write control reg

//因为 ARM 720T 是三级流水线,所以运行三条指令,让流水线充满指令

//读取id寄存器

mrc p15, 0, r3, c0, c0, 0  @ read id reg

//这时候刚开MMU的虚拟地址和实地址是一一映射,pc就不需要跳转了

mov r3, r3

mov r3, r3

//跳转到__mmap_switched函数执行

mov pc, r13

/****************************************从__enable_mmu返回************************************************************/

/******************************************__mmap_switched***************************************************************

/*

* The following fragment of code is executed with the MMU on, and uses

* absolute addresses; this is not position independent.

*这段代码是在开启MMU后执行的,用的是绝对地址,不是地址无关的代码

*  r0  = cp#15 control register

*  r1  = machine ID  是Uboot传过来的体系架构号

*  r9  = processor ID  CPU的id号

*/

.type __mmap_switched, %function

__mmap_switched:

//这是把__data_loc对应的地址赋值给r3

adr r3, __switch_data + 4

/*********************************************************************************************************

__switch_data这个数据块就在其后定义的

.type __switch_data, %object

__switch_data:

.long __mmap_switched

.long __data_loc   @ r4,__data_loc ,__data_start,__bss_start都是在vmlinux.lds中定义的

.long __data_start   @ r5

.long __bss_start   @ r6

.long _end    @ r7

.long processor_id   @ r4

.long __machine_arch_type  @ r5

.long cr_alignment   @ r6

.long init_thread_union + THREAD_START_SP @ sp

*********************************************************************************************************/

ldmia r3!, {r4, r5, r6, r7}

cmp r4, r5    @ Copy data segment if needed

//把动态数据拷贝到全局数据区

1: cmpne r5, r6

ldrne fp, [r4], #4  @fp是帧指针,即r11

strne fp, [r5], #4

bne 1b

//把BSS区清零

mov fp, #0    @ Clear BSS (and zero fp)

1: cmp r6, r7

strcc fp, [r6],#4  @cc是无符号小于

bcc 1b

ldmia r3, {r4, r5, r6, sp}

//将cpu id保存到r4中

str r9, [r4]   @ Save processor ID

//将机器号保存到r5当中

str r1, [r5]   @ Save machine type

/*********************************************************************************************************

cr_alignment是为中断向量表是放在高地址还是放在低地址服务的,在中断分析中会见到的

.globl cr_alignment

.globl cr_no_alignment

cr_alignment:

.space 4

cr_no_alignment:

.space 4

//将MMU control寄存器分别保存到cr_alignment,和cr_no_alignment中

*********************************************************************************************************/

bic r4, r0, #CR_A   @ Clear 'A' bit

stmia r6, {r0, r4}   @ Save control register values

//进入伟大的start_kernel函数

b start_kernel

/****************************************从__enable_mmu返回************************************************************/

.type __switch_data, %object

__switch_data:

.long __mmap_switched

.long __data_loc   @ r4

.long __data_start   @ r5

.long __bss_start   @ r6

.long _end    @ r7

.long processor_id   @ r4

.long __machine_arch_type  @ r5

.long cr_alignment   @ r6

.long init_thread_union + THREAD_START_SP @ sp

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值