kernel启动流程-head.S的执行___6.create_page_tables

1.前言

kernel版本:5.10
平台:arm64

本专题主要基于《arm64_linux head.S的执行流程》系列文章,前者是基于3.18,本专题针对的是内核5.10。主要分析head.S的执行过程。本文主要记录head.S的create_page_tables执行过程。
这里假设4级页表,使用section mapping

create_page_tables主要完成如下的工作:

  1. 无效init_pg区域的cacheline;
  2. 清零init_pg内存区域;
  3. 在idmap_pg区域为kernel创建恒等映射,由于只在开启MMU时使用,因此只会为部分代码创建映射;
  4. 在init_pg区域为kernel创建映射
  5. 再次无效init_pg和idmap_pg区域对应的cacheline
    执行完create_page_tables,将得到如下的地址空间布局:
    在这里插入图片描述
    id_map.text为需要创建恒等映射的物理内存区域
    id_map_pg:为恒等区域存放的页表区域
    init_pg为内核镜像存放的页表区域

当kernel image加载到物理内存后,为.idmap.text段创建了idmap映射,其中.idmap.text属于.text段的一部分,idmap_pg页表空间位于.data段与.text段之间;
为整个kernel image创建了init映射,其中init_pg页表位于.data段

为idmap.text创建恒等映射,实际就是创建idmap页表,通过填充pgd, pud, pmd页表项完成,其中pgd,pud为表描述符,指向下级页表,pmd为块描述符,pmd块描述符指向idmap_text区域
一个pgd页表项可以映射512G, 一个pud页表项可以映射1G,一个pmd页表项可以映射2M

2. 主要宏说明

主要宏说明
KIMAGE_VADDRkernel的起始虚拟地址,_text地址, 本例为0xffff800010000000
PAGE_OFFSETLinear Mapping起始虚拟地址,本例为0xffff000000000000
PAGE_ENDLinear Mapping结束虚拟地址,本例为0xffff800000000000
__PHYS_OFFSETkernel的起始虚拟地址, _text地址,本例为0xffff800010000000
KERNEL_STARTkernel的起始虚拟地址,_text地址,本例为0xffff800010000000

3. create_page_tables

/*
 * Setup the initial page tables. We only setup the barest amount which is
 * required to get the kernel running. The following sections are required:
 *   - identity mapping to enable the MMU (low address, TTBR0)
 *   - first few MB of the kernel linear mapping to jump to once the MMU has
 *     been enabled
 */
SYM_FUNC_START_LOCAL(__create_page_tables)

设置初始页表。我们只需要设置内核运行所需要的最基本的页表,包括:

  • 用于启用MMU的恒等映射(低地址,TTBR0)
  • 一旦MMU被启用,内核线性映射的前几MB必须被映射
mov     x28, lr

保存链接地址,通过ret返回

3.1 无效init_pg的dcache区域

/*                                                                                                                                       
 * Invalidate the init page tables to avoid potential dirty cache lines                                                                  
 * being evicted. Other page tables are allocated in rodata as part of                                                                   
 * the kernel image, and thus are clean to the PoC per the boot                                                                          
 * protocol.                                                                                                                             
 */                                                                                                                                      
adrp    x0, init_pg_dir                                                                                                                  
adrp    x1, init_pg_end                                                                                                                  
sub     x1, x1, x0                                                                                                                       
bl      __inval_dcache_area

实际就是将init_pg_dir与init_pg_end之间的区域执行invalid dcache,这需要计算出这段区域的size.

如下是对size大小计算的说明:
根据链接脚本arch/arm64/kernel/vmlinux.lds.S中如下定义:

 . = ALIGN(PAGE_SIZE);                                                 
 init_pg_dir = .;                                                      
 . += INIT_DIR_SIZE;                                                   
 init_pg_end = .;

本例中:
CONFIG_PGTABLE_LEVELS > 3
SWAPPER_PGTABLE_LEVELS=3 ARM64_SWAPPER_USES_SECTION_MAPS:

引自:http://www.wowotech.net/linux_kenrel/create_page_tables.html
ARM64_SWAPPER_USES_SECTION_MAPS这个宏定义是说明了swapper/idmap的映射是否使用section map。什么是section map呢?我们用一个实际的例子来描述。假设VA是48 bit,page size是4K,那么,在地址映射过程中,地址被分成9(level 0) + 9(level 1) + 9(level 2) + 9(level 3) + 12(page offset),对于kernel image这样的big block memory region,使用4K的page来mapping有点得不偿失,在这种情况下,可以考虑让level 2的Translation table entry指向一个2M 的memory region,而不是下一级的Translation table。所谓的section map就是指使用2M的为单位进行映射。当然,不是什么情况都是可以使用section map,对于kernel image,其起始地址是2M对齐的,因此block size是2M的情况下才OK,对于PAGE SIZE是16K,其Block descriptor指向了一个32M的内存块,PAGE SIZE是64K的时候,Block descriptor指向了一个512M的内存块,因此,只有4K page size的情况下,才可以启用section map。

关于INIT_DIR_SIZE 宏定义如下:

#define INIT_DIR_SIZE (PAGE_SIZE * EARLY_PAGES(KIMAGE_VADDR, _end))
#define EARLY_PAGES(vstart, vend) ( 1                   /* PGDIR page */                                \
                        + EARLY_PGDS((vstart), (vend))  /* each PGDIR needs a next level page table */  \
                        + EARLY_PUDS((vstart), (vend))  /* each PUD needs a next level page table */    \
                        + EARLY_PMDS((vstart), (vend))) /* each PMD needs a next level page table */

#define EARLY_PGDS(vstart, vend) (EARLY_ENTRIES(vstart, vend, PGDIR_SHIFT))                                                                                                                                                                             
#define EARLY_PUDS(vstart, vend) (0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
#define EARLY_PMDS(vstart, vend) (EARLY_ENTRIES(vstart, vend, SWAPPER_TABLE_SHIFT))                                                                                                  

#define EARLY_ENTRIES(vstart, vend, shift) (((vend) >> (shift)) \
                                        - ((vstart) >> (shift)) + 1 + EARLY_KASLR)
                                        - 
#define PAGE_SIZE               (_AC(1, UL) << PAGE_SHIFT)	

kernel启动流程-1.head.S的执行_概述中可知KIMAGE_VADDR就是_text,因此:

  • INIT_DIR_SIZE=(1<<12) * EARLY_PAGES(_text, _end)

  • EARLY_PAGES(_text, _end) =
    1+EARLY_PGDS(_text, _end)+EARLY_PUDS(_text, _end)+EARLY_PMDS(_text, _end)

  • EARLY_PGDS(_text, _end)=
    EARLY_ENTRIES(_text, _end, PGDIR_SHIFT)=
    (_end >> PGDIR_SHIFT)- (_text >> PGDIR_SHIFT) + 1 + EARLY_KASLR

  • EARLY_PUDS(_text, _end)=
    0

  • EARLY_PMDS(_text, _end)=
    EARLY_ENTRIES(_text, _end, SWAPPER_TABLE_SHIFT)=
    (_end >> SWAPPER_TABLE_SHIFT)- (_text >> SWAPPER_TABLE_SHIFT) + 1 + EARLY_KASLR

KASLR, kernel address space layout randomization,内核地址空间布局随机化,是linux内核的一个非常重要的安全机制。KASLR技术可以让kernel image映射的地址相对于链接地址有个偏移,安全性上有一定的提升。

PGDIR_SHIFT 宏定义如下:

#define PGDIR_SHIFT             ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)

#define SWAPPER_PGTABLE_LEVELS  (CONFIG_PGTABLE_LEVELS - 1)
#define IDMAP_PGTABLE_LEVELS    (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)

#define PAGE_SHIFT              CONFIG_ARM64_PAGE_SHIFT
#define CONFIG_ARM64_PAGE_SHIFT 12

计算有多少个PGD页表项:

  • PGDIR_SHIFT=(12-3)*(4-0)+3=39
  • EARLY_PGDS(_text, _end)=EARLY_ENTRIES(_text, _end, PGDIR_SHIFT)=
    (_end >> PGDIR_SHIFT)- (_text >> PGDIR_SHIFT) + 1 + EARLY_KASLR =
    (_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR

SWAPPER_TABLE_SHIFT宏定义如下:

#define SWAPPER_TABLE_SHIFT     PUD_SHIFT
#define PUD_SHIFT               ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)

计算有多少个PMD页表项:

  • PUD_SHIFT= (12 - 3) * (4 - 1) + 3 = 30
  • EARLY_PMDS(_text, _end)=EARLY_ENTRIES(_text, _end, SWAPPER_TABLE_SHIFT)=
    (_end >> SWAPPER_TABLE_SHIFT)- (_text >> SWAPPER_TABLE_SHIFT) + 1 + EARLY_KASLR=
    (_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR

通过上面的换算,因此可得INIT_DIR_SIZE:

  • EARLY_PAGES(_text, _end) =1+EARLY_PGDS(_text,_end)+EARLY_PMDS(_text, _end)
    =1 +
    (_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR +
    0 +
    (_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR
  • INIT_DIR_SIZE=(1<<12) * EARLY_PAGES(_text, _end)
    = (1<<12) * EARLY_PAGES(_text, _end)
    = (1<<12) *
    (1 +
    (_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR +
    0 +
    (_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR)

通过编译后生成的arch/arm64/kernel/vmlinux.lds可以看到INIT_DIR_SIZE被编译为:

init_pg_dir = .;
 . += ((1 << 12) * 
 ( 1 + (((((_end)) >> (((12 - 3) * (4 - (4 - 4)) + 3))) - ((((((((((-(((1)) << ((((48))) - 1))))) + (0x08000000))) + (0x08000000))))) >> (((12 - 3) * (4 - (4 - 4)) + 3))) + 1 + (1))) + (0)
+ (((((_end)) >> (((12 - 3) * (4 - (1)) + 3))) - ((((((((((-(((1)) << ((((48))) - 1))))) + (0x08000000))) + (0x08000000))))) >> (((12 - 3) * (4 - (1)) + 3))) + 1 + (1))))); 
 init_pg_end = .;

即:

. += ((1 << 12) *1 + (((_end >> 39) - (((-(1 << (48 - 1)) + (0x08000000)) + 0x08000000) >> 39) + 1 + (1))) + 
 (0) + 
 (((_end >> 30) - (((-(1 << (48 - 1)) + 0x08000000) + 0x08000000) >> 30) + 1 + (1))); 

到此处我们可以知道需要invalide的区域大小包含了PGD table和PMD table,由于采用了SECTION MAPPING, 因此没有使用PUD table(大小为0).

3.2 Clear the init page tables

        /*
         * Clear the init page tables.                                                                                                                                               
         */
        adrp    x0, init_pg_dir                                                                                                                                                      
        adrp    x1, init_pg_end                                                                                                                                                      
        sub     x1, x1, x0
1:      stp     xzr, xzr, [x0], #16                                                                                                                                                  
        stp     xzr, xzr, [x0], #16                                                                                                                                                  
        stp     xzr, xzr, [x0], #16                                                                                                                                                  
        stp     xzr, xzr, [x0], #16                                                                                                                                                  
        subs    x1, x1, #64                                                                                                                                                          
        b.ne    1b                                                                                                                                                                   
        
        mov     x7, SWAPPER_MM_MMUFLAGS 

将init_pg page table区间清零
其中x7保存了SWAPPER_MM_MMUFLAGS,关于SWAPPER_MM_MMUFLAGS 的宏定义如下:

#define SWAPPER_MM_MMUFLAGS     (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)

/*                                                                                                                                                                                   
 * AttrIndx[2:0] encoding (mapping attributes defined in the MAIR* registers).                                                                                                       
 */                                                                                                                                                                                  
#define PMD_ATTRINDX(t)         (_AT(pmdval_t, (t)) << 2)                                                                                                                            

/*                                                                                                                                                                                   
 * Initial memory map attributes.                                                                                                                                                    
 */                                                                                                                                                                                                                                                                                                
#define SWAPPER_PMD_FLAGS       (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S) 
/*最低位为01,根据页表描述符为块描述符*/
#define PMD_TYPE_SECT           (_AT(pmdval_t, 1) << 0)

3.3 Create the identity mapping

adrp    x0, idmap_pg_dir
adrp    x3, __idmap_text_start          // __pa(__idmap_text_start)

x0保存了id_map区域页表存放的起始地址idmap_pg_dir
x3保存了__idmap_text_start的物理地址,它就是需要创建恒等映射的区域起始地址

idmap_pg_dir定义在arch/arm64/kernel/vmlinux.lds.S中:

#arch/arm64/kernel/vmlinux.lds.S
idmap_pg_dir = .;                                                                                                                                                            
 . += IDMAP_DIR_SIZE;                                                                                                                                                         
idmap_pg_end = .; 

IDMAP_DIR_SIZE宏定义如下:

#define IDMAP_DIR_SIZE          (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_PGTABLE_LEVELS    (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
mov     x5, #VA_BITS_MIN

x5保存了虚拟地址位数VA_BITS_MIN,本例VA_BITS_MIN为48

adr_l   x6, vabits_actual                                                                                                                                                    
str     x5, [x6]                                                                                                                                                             
dmb     sy
dc      ivac, x6                // Invalidate potentially stale cache line

将虚拟地址位数保存到vabits_actual中,vabits_actual定义如下:

u64 __section(".mmuoff.data.write") vabits_actual;                    
EXPORT_SYMBOL(vabits_actual);
adrp    x5, __idmap_text_end                                                                                                                                                 
clz     x5, x5
cmp     x5, TCR_T0SZ(VA_BITS)   // default T0SZ small enough?
b.ge    1f 

x5粗放囊

#arch/arm64/include/asm/pgtable-hwdef.h
#define TCR_T0SZ(x)             ((UL(64) - (x)) << TCR_T0SZ_OFFSET) 

#define IDMAP_TEXT                                      \                                                                                                                            
        . = ALIGN(SZ_4K);                               \                                                                                                                            
        __idmap_text_start = .;                         \                                                                                                                            
        *(.idmap.text)                                  \                                                                                                                            
        __idmap_text_end = .;

x3保存了__idmap_text_end的物理地址,它就是需要创建恒等映射的区域结束地址
虚拟地址最大值前导0的个数 和最高物理地址比较,看是否够用,如果物理地址的前导0多,则说明地址够用,不用扩展,直接跳到1

adr_l   x6, idmap_t0sz                                                                                                                                                       
str     x5, [x6]                                                                                                                                                             
dmb     sy                                                                                                                                                                   
dc      ivac, x6                // Invalidate potentially stale cache line

如果物理地址的前导0少,则需要扩展,将物理地址的前导0个数存入idmap_t0sz

/*
 * If VA_BITS == 48, we don't have to configure an additional
 * translation level, but the top-level table has more entries.                                                                                                              
 */     
mov     x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)                                                                                                                            
str_l   x4, idmap_ptrs_per_pgd, x5
#define PHYS_MASK_SHIFT         (CONFIG_ARM64_PA_BITS)
#define CONFIG_ARM64_PA_BITS 48

前面分析可知PGDIR_SHIFT=39,x4保存了(1<<512)??

1:                                                                                                                                                                                   
        ldr_l   x4, idmap_ptrs_per_pgd                                                                                                                                               
        mov     x5, x3                          // __pa(__idmap_text_start)                                                                                                          
        adr_l   x6, __idmap_text_end            // __pa(__idmap_text_end)  

x3保存了需要恒等映射的区域__idmap_text_start的物理地址,赋值给x5;
x6保存了需要恒等映射的区域__idmap_text_end的物理地址(可以参考adr_l的实现)

map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
/*
 * Map memory for specified virtual address range. Each level of page table needed supports
 * multiple entries. If a level requires n entries the next page table level is assumed to be                                                                                        
 * formed from n pages.                                                                                                                                                              
 *
 *      tbl:    location of page table
 *      rtbl:   address to be used for first level page table entry (typically tbl + PAGE_SIZE)                                                                                      
 *      vstart: start address to map
 *      vend:   end address to map - we map [vstart, vend]                                                                                                                           
 *      flags:  flags to use to map last level entries
 *      phys:   physical address corresponding to vstart - physical memory is contiguous                                                                                             
 *      pgds:   the number of pgd entries                                                                                                                                            
 *
 * Temporaries: istart, iend, tmp, count, sv - these need to be different registers                                                                                                  
 * Preserves:   vstart, vend, flags
 * Corrupts:    tbl, rtbl, istart, iend, tmp, count, sv                                                                                                                              
 */     
        .macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv

x0(tbl):存放页表的起始地址,此处为idmap_pg_dir,也就是恒等映射区域页表pgd页表的基地址
x1(rtbl):下一级页表项地址,典型为tbl+PAGE_SIZE
x3(vsatart):开始映射的虚拟地址,此处为__idmap_text_start的物理地址
x6(vend):结束映射的虚拟地址,此处为__idmap_text_end的物理地址
x7(flags): 映射最后一级页表项的标志,此处为SWAPPER_MM_MMUFLAGS
x3(phys):和vstart对应的物理地址,此处为__idmap_text_start
x4(pgds):pgd页表项的个数,此处为idmap_ptrs_per_pgd

此处map_memory主要的功能就是为idmap text创建恒等映射(物理地址等于虚拟地址),.idmap.text段在head.S中声明,idmap text段只包含了head.S的部分代码

引自:http://www.wowotech.net/memory_management/436.html
identity mapping主要是打开MMU的过度阶段,因此对于identity mapping不需要映射整个kernel,只需要映射操作MMU代码相关的部分。如何区分这部分代码呢?当然是利用linux中常用手段自定义代码段。自定义的代码段的名称是".idmap.text"。除此之外,肯定还需要在链接脚本中声明两个标量,用来标记代码段的开始和结束

通过查看System,map可知idmap_text段包含:

44852 ffff800010aca000 T __idmap_text_start
 44853 ffff800010aca000 T el2_setup
 44854 ffff800010aca05c t set_hcr
 44855 ffff800010aca12c t install_el2_stub
 44856 ffff800010aca180 t set_cpu_boot_mode_flag
 44857 ffff800010aca1a4 T secondary_holding_pen
 44858 ffff800010aca1c8 t pen
 44859 ffff800010aca1dc T secondary_entry
 44860 ffff800010aca1e8 t secondary_startup
 44861 ffff800010aca200 t __secondary_switched
 44862 ffff800010aca23c t __secondary_too_slow
 44863 ffff800010aca248 T __enable_mmu
 44864 ffff800010aca2a0 T __cpu_secondary_check52bitva
 44865 ffff800010aca2a4 t __no_granule_support
 44866 ffff800010aca2c8 t __relocate_kernel
 44867 ffff800010aca310 t __primary_switch
 44868 ffff800010aca388 T cpu_resume
 44869 ffff800010aca3a8 T __cpu_soft_restart
 44870 ffff800010aca3e4 T cpu_do_resume
 44871 ffff800010aca474 T idmap_cpu_replace_ttbr1
 44872 ffff800010aca4a8 t __idmap_kpti_flag
 44873 ffff800010aca4ac T idmap_kpti_install_ng_mappings
 44874 ffff800010aca4e8 t do_pgd
 44875 ffff800010aca500 t next_pgd
 44876 ffff800010aca510 t skip_pgd
 44877 ffff800010aca550 t walk_puds
 44878 ffff800010aca558 t do_pud
 44879 ffff800010aca570 t next_pud
 44880 ffff800010aca580 t skip_pud
 44881 ffff800010aca590 t walk_pmds
 44882 ffff800010aca598 t do_pmd
 44883 ffff800010aca5b0 t next_pmd
 44884 ffff800010aca5c0 t skip_pmd
 44885 ffff800010aca5d0 t walk_ptes
 44886 ffff800010aca5d8 t do_pte
 44887 ffff800010aca5fc t skip_pte
 44888 ffff800010aca60c t __idmap_kpti_secondary
 44889 ffff800010aca654 T __cpu_setup
 44890 ffff800010aca740 T __idmap_text_end

3.3 Map the kernel image

/*   
 * Map the kernel image (starting with PHYS_OFFSET).
 */
adrp    x0, init_pg_dir
mov_q   x5, KIMAGE_VADDR                // compile time __va(_text)
add     x5, x5, x23                     // add KASLR displacement
mov     x4, PTRS_PER_PGD
adrp    x6, _end                        // runtime __pa(_end)
adrp    x3, _text                       // runtime __pa(_text)
sub     x6, x6, x3                      // _end - _text
add     x6, x6, x5                      // runtime __va(_end)

map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14

x0(tbl):存放页表的起始地址,此处为init_pg_dir
x1(rtbl):下一级页表项地址,典型为tbl+PAGE_SIZE,此处为init_pg_end
x5(vsatart):开始映射的虚拟地址,此处为KIMAGE_VADDR,即_text
x6(vend):结束映射的虚拟地址,此处为KIMAGE_VADDR+(_end-_text)
x7(flags): 映射最后一级页表项的标志,此处为SWAPPER_MM_MMUFLAGS
x3(phys):和vstart对应的物理地址,此处为_text的物理地址。
x4(pgds):pgd页表项的个数,此处为idmap_ptrs_per_pgd

如上代码的作用主要是对整个kernel image创建页表,此处是一个较粗粒度的映射,主要做2M大小的块映射,且对于内核镜像的代码段,数据段权限没有做区分,只是为了此时能访问内核的一些函数,区别于后续所做的内核细粒度映射(页表基址为swapper_pg_dir)。

关于adrp指令:由于当前MMU关闭状态,因此pc为物理地址,而adrp是相对pc的寻址,因此通过adrp获取的地址为物理地址,如:上面的adrp x6, _end

3.4 无效init_pg和idmap_pg的dcache区域

/*                                                                                                                                                                           
 * Since the page tables have been populated with non-cacheable                                                                                                              
 * accesses (MMU disabled), invalidate those tables again to                                                                                                                 
 * remove any speculatively loaded cache lines.                                                                                                                              
 */                                                                                                                                                                          
dmb     sy                                                                                                                                                                   
                                                                                                                                                                             
adrp    x0, idmap_pg_dir                                                                                                                                                     
adrp    x1, idmap_pg_end                                                                                                                                                     
sub     x1, x1, x0                                                                                                                                                           
bl      __inval_dcache_area                                                                                                                                                  
                                                                                                                                                                             
adrp    x0, init_pg_dir                                                                                                                                                      
adrp    x1, init_pg_end                                                                                                                                                      
sub     x1, x1, x0                                                                                                                                                           
bl      __inval_dcache_area                                                                                                                                                  
                                                                                                                                                                             
ret     x28 

无效idmap区域和init page区域的dcache, 然后返回x28中保存的链接地址lr

参考文档

  1. http://www.wowotech.net/linux_kenrel/create_page_tables.html
    ARM64的启动过程之(二):创建启动阶段的页表
  2. http://www.wowotech.net/memory_management/436.html
    ARM64 Kernel Image Mapping的变化
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值