由dump.c来理解 arm linux的 mmu 映射

本文档详细介绍了Linux内核在不同体系结构下如何遍历和解析页表,包括从pgd、pud、pmd到pte的逐级遍历,以及在特定体系结构(如ARM)中的页表属性处理。通过地址标记和页表级别,展示了内存区域的划分和权限设置。此外,还包含了调试辅助函数以输出当前系统的页表信息。

/*
 * Debug helper to dump the current kernel pagetables of the system
 * so that we can see what the various memory ranges are set to.
 *
 * Derived from x86 implementation:
 * (C) Copyright 2008 Intel Corporation
 *
 * Author: Arjan van de Ven <arjan@linux.intel.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License.
 */
#include <linux/debugfs.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/seq_file.h>

#include <asm/fixmap.h>
#include <asm/pgtable.h>

struct addr_marker {
    unsigned long start_address;
    const char *name;
};

static struct addr_marker address_markers[] = {
    { MODULES_VADDR,    "Modules" },
    { PAGE_OFFSET,        "Kernel Mapping" },
    { 0,            "vmalloc() Area" },
    { VMALLOC_END,        "vmalloc() End" },
    { FIXADDR_START,    "Fixmap Area" },
    { CONFIG_VECTORS_BASE,    "Vectors" },
    { CONFIG_VECTORS_BASE + PAGE_SIZE * 2, "Vectors End" },
    { -1,            NULL },
};


static struct addr_marker address_markersxxxx[] = {
    { MODULES_VADDR,    "Modules" },
    //{ PAGE_OFFSET,        "Kernel Mapping" },
    //{ 0,            "vmalloc() Area" },
    //{ VMALLOC_END,        "vmalloc() End" },
    //{ FIXADDR_START,    "Fixmap Area" },
    //{ CONFIG_VECTORS_BASE,    "Vectors" },
    //{ CONFIG_VECTORS_BASE + PAGE_SIZE * 2, "Vectors End" },
    { -1,            NULL },
};


struct pg_state {
    struct seq_file *seq;
    const struct addr_marker *marker;
    unsigned long start_address;
    unsigned level;
    u64 current_prot;
};

struct prot_bits {
    u64        mask;
    u64        val;
    const char    *set;
    const char    *clear;
};

//[     0.910709] i = 4
//[     0.912889] pg_level[i].mask = 7bc
//                                0111      1011     1100
//                                 10 9 8   7 5 4    2 3 
static const struct prot_bits pte_bits[] = {
    {
        .mask    = L_PTE_USER,  //8
        .val    = L_PTE_USER,
        .set    = "USR",
        .clear    = "   ",
    }, {
        .mask    = L_PTE_RDONLY,  //7
        .val    = L_PTE_RDONLY,
        .set    = "ro",
        .clear    = "RW",
    }, {
        .mask    = L_PTE_XN,    //9
        .val    = L_PTE_XN,
        .set    = "NX",
        .clear    = "x ",
    }, {
        .mask    = L_PTE_SHARED,  //10
        .val    = L_PTE_SHARED,
        .set    = "SHD",
        .clear    = "   ",
    }, {
        .mask    = L_PTE_MT_MASK,  //2 3 4 5
        .val    = L_PTE_MT_UNCACHED,
        .set    = "SO/UNCACHED",
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_BUFFERABLE,
        .set    = "MEM/BUFFERABLE/WC",
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_WRITETHROUGH,
        .set    = "MEM/CACHED/WT",
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_WRITEBACK,
        .set    = "MEM/CACHED/WBRA",
#ifndef CONFIG_ARM_LPAE
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_MINICACHE,
        .set    = "MEM/MINICACHE",
#endif
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_WRITEALLOC,
        .set    = "MEM/CACHED/WBWA",
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_DEV_SHARED,
        .set    = "DEV/SHARED",
#ifndef CONFIG_ARM_LPAE
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_DEV_NONSHARED,
        .set    = "DEV/NONSHARED",
#endif
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_DEV_WC,
        .set    = "DEV/WC",
    }, {
        .mask    = L_PTE_MT_MASK,
        .val    = L_PTE_MT_DEV_CACHED,
        .set    = "DEV/CACHED",
    },
};

//[     0.904633] i = 3
//[     0.906787] pg_level[i].mask = 18c10
static const struct prot_bits section_bits[] = {
#ifdef CONFIG_ARM_LPAE
sssss
    {
        .mask    = PMD_SECT_USER,
        .val    = PMD_SECT_USER,
        .set    = "USR",
    }, {
        .mask    = L_PMD_SECT_RDONLY | PMD_SECT_AP2,
        .val    = L_PMD_SECT_RDONLY | PMD_SECT_AP2,
        .set    = "ro",
        .clear    = "RW",
#elif __LINUX_ARM_ARCH__ >= 6
//xxxxxxxx
    {
        .mask    = PMD_SECT_APX | PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_APX | PMD_SECT_AP_WRITE,
        .set    = "    ro",
    }, {
        .mask    = PMD_SECT_APX | PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_WRITE,
        .set    = "    RW",
    }, {
        .mask    = PMD_SECT_APX | PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_READ,
        .set    = "USR ro",
    }, {
        .mask    = PMD_SECT_APX | PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .set    = "USR RW",
#else /* ARMv4/ARMv5  */
kkkkk
    /* These are approximate */
    {
        .mask   = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = 0,
        .set    = "    ro",
    }, {
        .mask   = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_WRITE,
        .set    = "    RW",
    }, {
        .mask   = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_READ,
        .set    = "USR ro",
    }, {
        .mask   = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .val    = PMD_SECT_AP_READ | PMD_SECT_AP_WRITE,
        .set    = "USR RW",
#endif
    }, {
        .mask    = PMD_SECT_XN,
        .val    = PMD_SECT_XN,
        .set    = "NX",
        .clear    = "x ",
    }, {
        .mask    = PMD_SECT_S,
        .val    = PMD_SECT_S,
        .set    = "SHD",
        .clear    = "   ",
    },
};


int g_debug_en = 0x00;

struct pg_level {
    const struct prot_bits *bits;
    size_t num;
    u64 mask;
};

static struct pg_level pg_level[] = {
    {
    }, 
    { /* pgd */
    }, 
    { /* pud */
    }, 

    { /* pmd */
        .bits    = section_bits,
        .num    = ARRAY_SIZE(section_bits),
    }, 

    { /* pte */
        .bits    = pte_bits,
        .num    = ARRAY_SIZE(pte_bits),
    },
    
};

static void dump_prot(struct pg_state *st, const struct prot_bits *bits, size_t num)
{
    unsigned i;

    for (i = 0; i < num; i++, bits++) {
        const char *s;

        if ((st->current_prot & bits->mask) == bits->val)
            s = bits->set;
        else
            s = bits->clear;

        if (s){
            //seq_printf(st->seq, " %s", s);
            printk(" %s", s);
        }
    }
}

static void note_page(struct pg_state *st, unsigned long addr, unsigned level, u64 val)
{
    static const char units[] = "KMGTPE";
    u64 prot = val & pg_level[level].mask;


    
    if( g_debug_en && val ){
        printk("addr = %x\n", addr );
        printk("val = %llx\n", val);
        printk("st->current_prot = %llx\n", st->current_prot );
        printk("prot = %llx\n", prot );
    }

    if (!st->level) {
        st->level = level;
        st->current_prot = prot;
        
        //seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
        printk("---[ %s ]---\n", st->marker->name);

        
    } else if( prot != st->current_prot || level != st->level || addr >= st->marker[1].start_address ){
        const char *unit = units;
        unsigned long delta;

        

        
        if (st->current_prot) {
            //seq_printf(st->seq, "0x%08lx-0x%08lx   ", st->start_address, addr);
            printk( "0x%08lx-0x%08lx   ", st->start_address, addr);

            delta = (addr - st->start_address) >> 10;
            while (!(delta & 1023) && unit[1]) {
                delta >>= 10;
                unit++;
            }
            //seq_printf(st->seq, "%9lu%c", delta, *unit);
            printk("%9lu%c", delta, *unit);
            if (pg_level[st->level].bits){
                dump_prot(st, pg_level[st->level].bits, pg_level[st->level].num);
            }
            //seq_printf(st->seq, "\n");
            printk("\n");
        }

        if (addr >= st->marker[1].start_address) {
            st->marker++;
            //seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
            printk("---[ %s ]---\n", st->marker->name);
        }
        st->start_address = addr;
        st->current_prot = prot;
        st->level = level;
    }
}

static void walk_pte(struct pg_state *st, pmd_t *pmd, unsigned long start)
{
    pte_t *pte = pte_offset_kernel(pmd, 0);
    unsigned long addr;
    unsigned i;

    //return x - PHYS_OFFSET + PAGE_OFFSET;
    //printk("PTRS_PER_PTE = %x\n", PTRS_PER_PTE );
    //printk("PAGE_SIZE = %x\n", PAGE_SIZE );
    //PHYS_OFFSET = 40000000
    //PAGE_OFFSET = c0000000
    //PTRS_PER_PTE = 0x200
    //PAGE_SIZE = 0x1000

    //walk_pte: start: 0xf19c5000, pte: 0xee1e3714, *pte: 0x405c5653
    //walk_pte: start: 0xf19c6000, pte: 0xee1e3718, *pte: 0x405c6653

    printk( "\n\npmd = %p, *pmd = %x\n", pmd, *pmd );
    printk("walk_pte: start_addr: %lx, pte: %x, *pte: %x\n", start, pte, *pte );

    //如果是两级页表的第一级,bit0是1,bit1是0.
         //最后一个!pmd_present在这里跟pmd_none是一个逻辑             
         //如果是段表,第12行的note_page会被调用,这个处理的是pmd[0],处理完后
         //第17行判断如果pmd[1]如果也是段表的话,再处理pmd[1] 
         //这里的SECTION_SIZE是1MB
         //如果是二级页表的第一级的话,walk_pte会被调用
         //这个函数会找到第二级页表的基地址,然后遍历其中的每一个二级页表项  
         //pmd = c0007ff8, *pmd = 6b7fd861
         //walk_pte: start_addr: ffe00000, pte: eb7fd000, *pte: 0
         //*c0007ff8 = 0x6b7fd 861 *c0007ffc=0x6b7fd c61
         //861 说明是一个页表描述符 一级页表地址为eb7fd000 - eb7fe000
         //取得虚地址 0xeb7fd000 (0x6b7fd861 & 0xfffff000)+ 0x8000 0000
         //eb7fd7c0: df e5 7f 6b df f4 7f 6b 00 00 00 00 00 00 00 00
         // 7c0 /4 = 1f0    
         //从ffe00000 地址开始 每4k占用一个1级页表项
         //1f0*4k=1F0000 
         //ffe00000 + 1F0000 = ffff0000
         //虚拟地址 ffff0000对应的二级页表描述符是 df e5 7f 6b, 0x6b7fe5df
         //0xeb7fe000

         //[ 4657.441225] wsm_buf.data1x: eb7fe000: ff 03 00 ea 65 04 00 ea f0 ff 9f e5 43 04 00 ea  ....e.......C...
         //[ 4657.452660] wsm_buf.data1x: eb7fe010: 22 04 00 ea 81 04 00 ea 00 04 00 ea 87 04 00 ea  "...............
         //[ 4657.463714] wsm_buf.data1x: eb7fe020: f1 de fd e7 f1 de fd e7 f1 de fd e7 f1 de fd e7  ................
         //[ 4657.474955] wsm_buf.data1x: eb7fe030: f1 de fd e7 f1 de fd e7 f1 de fd e7 f1 de fd e7  ................
         //从数据观察 应该是Vectors

        #if 0
[ 2604.230527] addr = ffff0000
[ 2604.230529] val = 6b7fe5df
[ 2604.230530] st->current_prot = 0
[ 2604.230531] prot = 59c
[ 2604.230533] ---[ Vectors ]---
[ 2604.230534] addr = ffff1000
[ 2604.230535] val = 6b7ff4df
[ 2604.230537] st->current_prot = 59c
[ 2604.230538] prot = 49c
[ 2604.230546] 0xffff0000-0xffff1000           4K USR ro x  SHD MEM/CACHED/WBWA
[ 2604.230554] 0xffff1000-0xffff2000           4K     ro x  SHD MEM/CACHED/WBWA
[ 2604.230555] ---[ Vectors End ]---
[ 2604.230600] parse.2 
[ 2604.230601] parse.3 
        #endif


    //    如果采用页表映射的方式,段映射表就变成一级映射表(Linux中称为PGD),其页表项提供的不再是物理地址,而是二级页表的基地址。

//32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。

//以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。

//最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。

//这个过程在ARM32架构中由MMU硬件完成,软件不需要介入。

        


        


    


         
    

    if( start == 0xffe00000 ){    
        g_debug_en = 0x01;
    }else{
        g_debug_en = 0x00;
    }
        
    for (i = 0; i < PTRS_PER_PTE; i++, pte++) {
        addr = start + i * PAGE_SIZE;
        

        if( g_debug_en){
            //printk("walk_pte: start_addr: %lx, pte: %x, *pte: %x\n", addr, pte, *pte );
        }
        note_page(st, addr, 4, pte_val(*pte));
    }
}


  
static void walk_pmd(struct pg_state *st, pud_t *pud, unsigned long start)
{
    pmd_t *pmd = pmd_offset(pud, 0);
    unsigned long addr;
    unsigned i;


    //printk( "PTRS_PER_PMD = 0x%x\n", PTRS_PER_PMD ); 
    //printk( "PMD_SIZE = 0x%x\n", PMD_SIZE ); 
    //printk( "SECTION_SIZE = 0x%x\n", SECTION_SIZE ); 


    //PTRS_PER_PMD = 0x1
    //PMD_SIZE = 0x200000
    //SECTION_SIZE = 0x100000

    if( pmd < 0xc0007000 ){
        //return;
    }

    if( pmd > 0xc0007100 ){
        //printk("debug quit!!!\n");
        //return;
    }

    
    //g_debug_en = 0x01;
    for( i=0; i<PTRS_PER_PMD; i++, pmd++ ){
         addr = start + i * PMD_SIZE;
         //printk( "pmd = 0x%p\n", pmd ); 
         //printk( "*pmd = 0x%x\n", *pmd ); 
         //pmd_large判断段表还是两级页表的第一级,这里实际上是判断表项的[1:0]
         //如果是段表,bit0任意,bit1位1;   1e 14 01 40
         //0x400 1141e  e 1110   1141e 是段描述符条目的属性
         //对于段映射 假设逻辑地址是0xc0000001
         //cpu首先从页表基址寄存器 取得页表基址 0xc0004000
         //从0xc0004000开始 每4字节 为一个条目 每条目寻址1M
         //0x1000 4k字节有1k的条目数 可寻址1G
         //0xc0004000-0xc0005000 映射逻辑地址 0x00000000-0x40000000
         //0xc0005000-0xc0006000 映射逻辑地址 0x40000000-0x80000000
         //0xc0006000-0xc0007000 映射逻辑地址 0x80000000-0xc0000000
         //上面这些表项都是0 用户空间 0-3G

         //由此开始则是内核空间的映射 3G-4G
         //*0xc0007000 = 0x4001141e,由e判断出是一个段映射 这个条目管1M的空间
         //0x4000 0000-0x4010 0000
         //也就是说物理地址 0x4000 0000-0x4010 0000 对应的内核态 虚拟地址是 0xc000 0000 - 0xc010 0000

         
         

         
         
         //如果是两级页表的第一级,bit0是1,bit1是0.
         //最后一个!pmd_present在这里跟pmd_none是一个逻辑             
         //如果是段表,第12行的note_page会被调用,这个处理的是pmd[0],处理完后
         //第17行判断如果pmd[1]如果也是段表的话,再处理pmd[1] 
         //这里的SECTION_SIZE是1MB
         //如果是二级页表的第一级的话,walk_pte会被调用
         //这个函数会找到第二级页表的基地址,然后遍历其中的每一个二级页表项  


        
         //pmd = c0007ff8, *pmd = 6b7fd861
         //walk_pte: start_addr: ffe00000, pte: eb7fd000, *pte: 0
         //*c0007ff8 = 0x6b7fd861 *c0007ffc=0x6b7fdc61
         


         if (pmd_none(*pmd) || pmd_large(*pmd) || !pmd_present(*pmd)){
             //printk( "pmd.q\n" );
             //表项为0 或者是段表项
             //printk( "\n\npmd.seg table0:\n" );
            note_page(st, addr, 3, pmd_val(*pmd));
        }else{
            //printk( "\n\npmd.page table:\n" );
            //非0的页表项  二级页表映射
            walk_pte(st, pmd, addr);
        }
        if( SECTION_SIZE < PMD_SIZE && pmd_large(pmd[1])){
            //printk( "pmd.seg table1:\n" ); 
            note_page(st, addr + SECTION_SIZE, 3, pmd_val(pmd[1]));
            //下一个段表项条目
        }
    }
}

static void walk_pud(struct pg_state *st, pgd_t *pgd, unsigned long start)
{
    pud_t *pud = pud_offset(pgd, 0);
    unsigned long addr;
    unsigned i;
     
    //printk( "PTRS_PER_PUD = 0x%x\n", PTRS_PER_PUD );   
    //printk( "PUD_SIZE     = 0x%x\n",     PUD_SIZE );   


    //PTRS_PER_PUD = 0x1
    //PUD_SIZE     = 0x200000


          
    for (i = 0; i < PTRS_PER_PUD; i++, pud++) {
        addr = start + i * PUD_SIZE;
        if (!pud_none(*pud)) {

            //printk("parse.4 \n");
            walk_pmd(st, pud, addr);
        } else {
            printk("parse.5 \n");
            note_page(st, addr, 2, pud_val(*pud));
        }
    }
}

static void walk_pgd(struct seq_file *m)
{
    pgd_t *pgd = swapper_pg_dir;
    struct pg_state st;
    unsigned long addr;
    unsigned i;

    memset(&st, 0, sizeof(st));
    st.seq = m;
    st.marker = address_markers;


    //printk("PTRS_PER_PGD = 0x%x\n", PTRS_PER_PGD);
    //printk("PGDIR_SIZE = 0x%x\n", PGDIR_SIZE);
    //PTRS_PER_PGD = 0x800
    //PGDIR_SIZE = 0x200000

    //i 2048次循环
    //每次的地址是2M  
    //2k * 2M = 4G
    for( i=0; i<PTRS_PER_PGD; i++,pgd++ ){
         addr = i * PGDIR_SIZE;

         //printk("walk_pgd pgd = 0x%p\n", pgd);
         //printk("walk_pgd addr = 0x%x\n", addr);

         if( !pgd_none(*pgd) ){
             //printk("parse.0 \n");
             walk_pud(&st, pgd, addr);
         } else {
             printk("parse.1 \n");
             note_page(&st, addr, 1, pgd_val(*pgd));
         }
    }
    printk("parse.2 \n");
    note_page(&st, 0, 0, 0);
    printk("parse.3 \n");
}


static int ptdump_show(struct seq_file *m, void *v)
{
    walk_pgd(m);
    return 0;
}

static int ptdump_open(struct inode *inode, struct file *file)
{
    return single_open(file, ptdump_show, NULL);
}

static const struct file_operations ptdump_fops = {
    .open        = ptdump_open,
    .read        = seq_read,
    .llseek        = seq_lseek,
    .release    = single_release,
};


static int pttest_show(struct seq_file *m, void *v)
{

    unsigned int pgd;
    printk("in pttest_show\n");

    pgd = cpu_get_pgd();

     printk("pgd = 0x%lx\n", pgd);
     printk("init_mm.pgd = 0x%lx\n", init_mm.pgd);

     

     

     

    
    return 0;
}

static int pttest_open(struct inode *inode, struct file *file)
{
    return single_open(file, pttest_show, NULL);
}

static unsigned char k_buff[1024];
static ssize_t pttest_write(struct file *file, const char __user *buf,
    size_t count, loff_t *pos)
{
    struct seq_file *sf = (struct seq_file *)file->private_data;
    unsigned long testcase;
    int ret;

    

    


    
    ret = kstrtoul_from_user(buf, count, 16, &testcase);
    if (ret){
        return ret;
    }
    printk("testcase = 0x%lx\n", testcase);


    print_hex_dump_bytes("wsm_buf.data1x: ", DUMP_PREFIX_ADDRESS, (u8 *)(testcase), 0x100 );

    
    return count;
}


static const struct file_operations pttest_fops = {
    .open        = pttest_open,
    .read        = seq_read,
    .write        = pttest_write,
    .llseek        = seq_lseek,
    .release    = single_release,
};

static int ptdump_init( void )     
{              
    struct dentry *pe;
    unsigned i, j;
      
    //#define VMALLOC_START        (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))


    //printk( "in ptdump_init\n" );
    //printk( "ARRAY_SIZE(pg_level) = %d\n", ARRAY_SIZE(pg_level) );


    //printk( "VMALLOC_START = %x\n", VMALLOC_START );

    
    //printk( "high_memory = %x\n", high_memory );


    

    //[     0.893023] ARRAY_SIZE(pg_level) = 5
    //[     0.896949] VMALLOC_START = ec000000
    //[     0.900896] high_memory = eb800000
    //[     0.904633] i = 3
    //[     0.906787] pg_level[i].mask = 18c10
    //[     0.910709] i = 4
    //[     0.912889] pg_level[i].mask = 7bc


    
    
    for( i=0; i<ARRAY_SIZE(pg_level); i++ ){
         if( pg_level[i].bits ){      
             for( j=0; j<pg_level[i].num; j++ ){   
                  pg_level[i].mask |= pg_level[i].bits[j].mask;
             }

             //printk( "i = %x\n", i );
             //printk( "pg_level[i].mask = %llx\n", pg_level[i].mask );
             
         }
    }

    
    address_markers[2].start_address = VMALLOC_START;

    
    debugfs_create_file("pttest", 0400, NULL, NULL, &pttest_fops);
    pe = debugfs_create_file("kernel_page_tables", 0400, NULL, NULL, &ptdump_fops);
    return pe ? 0 : -ENOMEM;
}
__initcall(ptdump_init);
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值