zoukankan      html  css  js  c++  java
  • Linux内存管理学习1 —— head.S中的段页表的建立

    作者

    彭东林

    pengdonglin137@163.com

    平台

    TQ2440

    Qemu+vexpress-ca9

    Linux-4.10.17

    概述

       在Linux自解压完毕后,开始执行arch/arm/kernel/head.S,然后跳转到init/main.c中的start_kernel开始执行。在head.S中为了便利Linux内核启动,会建立临时的段页表。这里以TQ2440和vexpress-ca9为例,其中TQ2440使用的SoC是S3C2440,ARM核心是ARM920T,指令集是ARMv4T,而vexpress-ca9是ARM核心是Cortex-A9,指令集是ARMv7。为了便于理解,在分析的时候主要以2440为主,只是顺便说一下ARMv7,因为这两个大同小异。

    下面是代码分析时的一些条件

    1、以设备树的方式启动Linux内核

    2、下面是一些宏和变量的说明:

    说明 TQ2440(ARM920T) vxpress(Cortex-A9)
    CONFIG_ARM_LPAE   No No
    TEXT_OFFSET 内核代码段相对于内核地址空间的偏移量 0x8000 0x8000
    PAGE_OFFSET 内核地址空间的偏移量 0xC000_0000 0xC000_0000
     KERNEL_RAM_VADDR =PAGE_OFFSET+TEXT_OFFSET 0xC000_8000
    0xC000_8000
    PG_DIR_SIZE 一级页表的大小 0x4000 (16KB) 0x4000 (16KB)
    PMD_ORDER 一级页表的每个页表项占用的字节(2^(PMD_ORDER)) 2^2 = 4 2^2 = 4
    swapper_pg_dir

    一级页表的虚拟起始地址

    KERNEL_RAM_VADDR - PG_DIR_SIZE

    0xC000_4000 0xC000_4000
    CONFIG_ARM_VIRT_EXT   No Yes
    CONFIG_XIP_KERNEL   No No
    CONFIG_SMP   No Yes
    CONFIG_SMP_ON_UP   No Yes
    CONFIG_ARM_PATCH_PHYS_VIRT   Yes Yes
    CONFIG_CPU_32v4T  ARM指令集 Yes No
    CONFIG_CPU_32v7  ARM指令集 No Yes
    CONFIG_CPU_V7M  ARM指令集 No No
    __LINUX_ARM_ARCH__ ARM指令集 4 7
    CONFIG_CPU_DCACHE_WRITETHROUGH   No No

     3、地址空间:

    对于TQ2440,板子上面有64MB的物理内存,所以物理内存地址范围是: 0x3000_0000 ~ 0x3400_0000

    对于express板子,分配了1GB的物理内存,所以物理内存地址范围是: 0x6000_0000 ~ 0xA000_0000

    正文

    在进入head.S是,MMU和D-Cache是关闭的,r0是0,r1的值任意,r2的值是dtb镜像在内存中的物理起始地址。

    下面是对head.S精简后的代码:

     1 ENTRY(stext)
     2 
     3 #ifdef CONFIG_ARM_VIRT_EXT
     4     bl    __hyp_stub_install
     5 #endif
     6     @ ensure svc mode and all interrupts masked
     7     safe_svcmode_maskall r9
     8 
     9     mrc    p15, 0, r9, c0, c0        @ get processor id
    10     bl    __lookup_processor_type        @ r5=procinfo r9=cpuid
    11     movs    r10, r5                @ invalid processor (r5=0)?
    12     beq    __error_p            @ yes, error 'p'
    13 
    14     adr    r3, 2f
    15     ldmia    r3, {r4, r8}
    16     sub    r4, r3, r4            @ (PHYS_OFFSET - PAGE_OFFSET)
    17     add    r8, r8, r4            @ PHYS_OFFSET
    18 
    19     /*
    20      * r1 = machine no, r2 = atags or dtb,
    21      * r8 = phys_offset, r9 = cpuid, r10 = procinfo
    22      */
    23     bl    __vet_atags
    24 #ifdef CONFIG_SMP_ON_UP
    25     bl    __fixup_smp
    26 #endif
    27 
    28     bl    __fixup_pv_table
    29 
    30     bl    __create_page_tables
    31 
    32     /*
    33      * The following calls CPU specific code in a position independent
    34      * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
    35      * xxx_proc_info structure selected by __lookup_processor_type
    36      * above.
    37      *
    38      * The processor init function will be called with:
    39      *  r1 - machine type
    40      *  r2 - boot data (atags/dt) pointer
    41      *  r4 - translation table base (low word)
    42      *  r5 - translation table base (high word, if LPAE)
    43      *  r8 - translation table base 1 (pfn if LPAE)
    44      *  r9 - cpuid
    45      *  r13 - virtual address for __enable_mmu -> __turn_mmu_on
    46      *
    47      * On return, the CPU will be ready for the MMU to be turned on,
    48      * r0 will hold the CPU control register value, r1, r2, r4, and
    49      * r9 will be preserved.  r5 will also be preserved if LPAE.
    50      */
    51     ldr    r13, =__mmap_switched        @ address to jump to after
    52                         @ mmu has been enabled
    53     badr    lr, 1f                @ return (PIC) address
    54 
    55     mov    r8, r4                @ set TTBR1 to swapper_pg_dir
    56 
    57     ldr    r12, [r10, #PROCINFO_INITFUNC]
    58     add    r12, r12, r10
    59     ret    r12
    60 1:    b    __enable_mmu
    61 ENDPROC(stext)
    62     .ltorg
    63 2:    .long    .
    64     .long    PAGE_OFFSET

    下面开始分析上面的代码:

    1、第4行的__hyp_stub_install在vexpress上会执行,而在2440上不执行,这里暂时忽略

    2、第7行的 safe_svcmode_maskall r9 确保处理器进入SVC模式,同时关闭IRQ和FIQ中断。对于2440,做了如下操作:

    msr  cpsr_c, #(PSR_F_BIT | PSR_I_BIT | SVC_MODE)

    3、第9行 mrc p15, 0, r9, c0, c0 用于获得processor id。

    对于2440, CP15的C0的值是0x4112920x,参考手册 ARM920T Technical Reference Manual 的2.3节 CP15 register map summary

    对于vexpress,CP15的C0的值是0x414FC091,参考手册 ARM® Cortex®‑A9 Technical Reference Manual 的 4. System Control

     

    比如对于2440,执行完第3行代码后,r9的值就是0x4112920x,而对于vexpress,r9的值是0x414FC091。

    4、第10到12行,遍历kernel的".proc.info.init"段,找到与该处理器ID匹配的proc_info_list结构体,如果找到的话,r5寄存器存放的是该proc_info_list的物理地址,第11行将该地址存放到r10中,如果没有找到的话,

    寄存器r5值是0,执行完第11行的movs代码后,第12行的beq就会成立,跳转到__error_p处,如果配置了CONFIG_DEBUG_LL,就会打印相应的错误信息:

    Error: unrecognized/unsupported processor variant (0xXXXXXXX)

    上面括号中是实际从CP15的C0里读到的值。

    下面我们看看对于2440和vexpress这两个板子,与之匹配的proc.info.init字段都分别是什么?

    对于2440,该部分定义在arch/arm/mm/proc-arm920.S中:

     1     define_processor_functions arm920, dabort=v4t_early_abort, pabort=legacy_pabort, suspend=1
     2 
     3     .section ".rodata"
     4 
     5     string    cpu_arch_name, "armv4t"
     6     string    cpu_elf_name, "v4"
     7     string    cpu_arm920_name, "ARM920T"
     8 
     9     .align
    10 
    11     .section ".proc.info.init", #alloc
    12 
    13     .type    __arm920_proc_info,#object
    14 __arm920_proc_info:
    15     .long    0x41009200
    16     .long    0xff00fff0
    17     .long   PMD_TYPE_SECT | 
    18         PMD_SECT_BUFFERABLE | 
    19         PMD_SECT_CACHEABLE | 
    20         PMD_BIT4 | 
    21         PMD_SECT_AP_WRITE | 
    22         PMD_SECT_AP_READ
    23     .long   PMD_TYPE_SECT | 
    24         PMD_BIT4 | 
    25         PMD_SECT_AP_WRITE | 
    26         PMD_SECT_AP_READ
    27     initfn    __arm920_setup, __arm920_proc_info
    28     .long    cpu_arch_name
    29     .long    cpu_elf_name
    30     .long    HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
    31     .long    cpu_arm920_name
    32     .long    arm920_processor_functions
    33     .long    v4wbi_tlb_fns
    34     .long    v4wb_user_fns
    35     .long    arm920_cache_fns
    36     .size    __arm920_proc_info, . - __arm920_proc_info

    第1行的define_processor_functions是一个宏,定义在arch/arm/mm/proc-macros.S中,根据传入的参数展开后如下:

        .type    arm920_processor_functions, #object
        .align 2
    ENTRY(arm920_processor_functions)
        .word    dabort
        .word    pabort
        .word    cpu_arm920_proc_init
        .word    cpu_arm920_proc_fin
        .word    cpu_arm920_reset
        .word    cpu_arm920_do_idle
        .word    cpu_arm920_dcache_clean_area
        .word    cpu_arm920_switch_mm
        .word    cpu_arm920_set_pte_ext
        .word    cpu_arm920_suspend_size
        .word    cpu_arm920_do_suspend
        .word    cpu_arm920_do_resume
        .size    arm920_processor_functions, . - arm920_processor_functions

    第4到7行只读,存放了一下字符串,将来在启动阶段(start_kernel --> setup_arch --> setup_processor)会被打印出来

        pr_info("CPU: %s [%08x] revision %d (ARMv%s), cr=%08lx
    ",
            cpu_name, read_cpuid_id(), read_cpuid_id() & 15,
            proc_arch[cpu_architecture()], get_cr());

    如:

    [    0.000000] CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c000717f

    第15到35行的数据将来可以通过一个struct proc_info_list进行访问:

    struct proc_info_list {
        unsigned int        cpu_val;
        unsigned int        cpu_mask;
        unsigned long        __cpu_mm_mmu_flags;    /* used by head.S */
        unsigned long        __cpu_io_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;
    };

    第27行 initfn __arm920_setup, __arm920_proc_info 展开后是: __arm920_setup -  __arm920_proc_info,也就是这里存放了一个这两个符号的地址偏差,将来就可以根据__arm920_proc_info轻松地找到__arm920_setup

    第33和35行的分析类似第1行,都是宏展开后生成的,直接在代码里搜索不到。

    对于v4wbi_tlb_fns 定义在arch/arm/mm/tlb-v4wbi.S中:  define_tlb_functions v4wbi, v4wbi_tlb_flags ,展开如下:

        .type    v4wbi_tlb_fns, #object
    ENTRY(v4wbi_tlb_fns)
        .long    v4wbi_flush_user_tlb_range
        .long    v4wbi_flush_kern_tlb_range
        .long    v4wbi_tlb_flags
        .size    v4wbi_tlb_fns, . - v4wbi_tlb_fns

    对于arm920_cache_fns, 定义在arch/arm/mm/proc-arm920.S中 define_cache_functions arm920 展开后:

        .align 2
        .type    arm920_cache_fns, #object
    ENTRY(arm920_cache_fns)
        .long    arm920_flush_icache_all
        .long    arm920_flush_kern_cache_all
        .long    arm920_flush_kern_cache_louis
        .long    arm920_flush_user_cache_all
        .long    arm920_flush_user_cache_range
        .long    arm920_coherent_kern_range
        .long    arm920_coherent_user_range
        .long    arm920_flush_kern_dcache_area
        .long    arm920_dma_map_area
        .long    arm920_dma_unmap_area
        .long    arm920_dma_flush_range
        .size    arm920_cache_fns, . - arm920_cache_fns

    第34行,对于v4wb_user_fns 定义在arch/arm/mm/copypage-v4wb.c中:

    struct cpu_user_fns v4wb_user_fns __initdata = {
        .cpu_clear_user_highpage = v4wb_clear_user_highpage,
        .cpu_copy_user_highpage    = v4wb_copy_user_highpage,
    };

    如果将vmlinux反汇编,可以看到__arm920_proc_info这段的内容如下:

    c06adf80 <__proc_info_begin>:
    c06adf80:       41009200    #cpu_val
    c06adf84:       ff00fff0    #cpu_mask    
    c06adf88:       00000c1e    #__cpu_mm_mmu_flags    
    c06adf8c:       00000c12    #__cpu_io_mmu_flags     
    c06adf90:       ff968a3c    #__cpu_flush  
    c06adf94:       c04ed874    #arch_name
    c06adf98:       c04ed87b    #elf_name
    c06adf9c:       00000007    #elf_hwcap
    c06adfa0:       c04ed87e    #cpu_name
    c06adfa4:       c06b4040    #proc
    c06adfa8:       c06b4034    #tlb
    c06adfac:       c06b402c    #user
    c06adfb0:       c00168c0    #cache

    对于vexpress,对应的是proc.info.init定义在arch/arm/mm/proc-v7.S中,只留下需要关注的部分:

        define_processor_functions ca9mp, dabort=v7_early_abort, pabort=v7_pabort, suspend=1
    
        .section ".rodata"
    
        string    cpu_arch_name, "armv7"
        string    cpu_elf_name, "v7"
        .align
    
        .section ".proc.info.init", #alloc
    
        /*
         * Standard v7 proc info content
         */
    .macro __v7_proc name, initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0, proc_fns = v7_processor_functions
        ALT_SMP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | 
                PMD_SECT_AF | PMD_FLAGS_SMP | mm_mmuflags)
        ALT_UP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | 
                PMD_SECT_AF | PMD_FLAGS_UP | mm_mmuflags)
        .long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | 
            PMD_SECT_AP_READ | PMD_SECT_AF | io_mmuflags
        initfn    initfunc, 
    ame
        .long    cpu_arch_name
        .long    cpu_elf_name
        .long    HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | 
            HWCAP_EDSP | HWCAP_TLS | hwcaps
        .long    cpu_v7_name
        .long    proc_fns
        .long    v7wbi_tlb_fns
        .long    v6_user_fns
        .long    v7_cache_fns
    .endm
    
        /*
         * ARM Ltd. Cortex A9 processor.
         */
        .type   __v7_ca9mp_proc_info, #object
    __v7_ca9mp_proc_info:
        .long    0x410fc090
        .long    0xff0ffff0
        __v7_proc __v7_ca9mp_proc_info, __v7_ca9mp_setup, proc_fns = ca9mp_processor_functions
        .size    __v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info

    进一步展开后是:

     1     string  cpu_v7_name, "ARMv7 Processor"    
     2     define_processor_functions ca9mp, dabort=v7_early_abort, pabort=v7_pabort, suspend=1
     3 
     4     .section ".rodata"
     5 
     6     string    cpu_arch_name, "armv7"
     7     string    cpu_elf_name, "v7"
     8     .align
     9 
    10     .section ".proc.info.init", #alloc
    11 
    12     /*
    13      * ARM Ltd. Cortex A9 processor.
    14      */
    15     .type   __v7_ca9mp_proc_info, #object
    16 __v7_ca9mp_proc_info:
    17     .long    0x410fc090
    18     .long    0xff0ffff0
    19     ALT_SMP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | 
    20             PMD_SECT_AF | PMD_FLAGS_SMP)
    21     ALT_UP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | 
    22             PMD_SECT_AF | PMD_FLAGS_UP)
    23     .long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | 
    24         PMD_SECT_AP_READ | PMD_SECT_AF
    25     initfn    __v7_ca9mp_setup, __v7_ca9mp_proc_info
    26     .long    cpu_arch_name
    27     .long    cpu_elf_name
    28     .long    HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | 
    29         HWCAP_EDSP | HWCAP_TLS
    30     .long    cpu_v7_name
    31     .long    ca9mp_processor_functions
    32     .long    v7wbi_tlb_fns
    33     .long    v6_user_fns
    34     .long    v7_cache_fns
    35     .size    __v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info

    跟2440一样,其中的部分标号的定义如下:

    ca9mp_processor_functions:  定义在arch/arm/mm/proc-v7.S中 define_processor_functions ca9mp, dabort=v7_early_abort, pabort=v7_pabort, suspend=1

        .type    ca9mp_processor_functions, #object
        .align 2
    ENTRY(ca9mp_processor_functions)
        .word    v7_early_abort
        .word    v7_pabort
        .word    cpu_ca9mp_proc_init
        .word    cpu_ca9mp_proc_fin
        .word    cpu_ca9mp_reset
        .word    cpu_ca9mp_do_idle
        .word    cpu_ca9mp_dcache_clean_area
        .word    cpu_ca9mp_switch_mm
        .word    cpu_ca9mp_set_pte_ext
        .word    cpu_ca9mp_suspend_size
        .word    cpu_ca9mp_do_suspend
        .word    cpu_ca9mp_do_resume
        .size    ca9mp_processor_functions, . - ca9mp_processor_functions

    v7wbi_tlb_fns:定义在arch/arm/mm/tlb-v7.S中 define_tlb_functions v7wbi, v7wbi_tlb_flags_up, flags_smp=v7wbi_tlb_flags_smp ,展开如下:

    ENTRY(v7wbi_tlb_fns)
        .long    v7wbi_flush_user_tlb_range
        .long    v7wbi_flush_kern_tlb_range
        ALT_SMP(.long    flags_smp=v7wbi_tlb_flags_smp )
        ALT_UP(.long    v7wbi_tlb_flags_up )
        .size    v7wbi_tlb_fns, . - v7wbi_tlb_fns

    v6_user_fns:定义在arch/arm/mm/copypage-v6.c中:

    struct cpu_user_fns v6_user_fns __initdata = {
        .cpu_clear_user_highpage = v6_clear_user_highpage_nonaliasing,
        .cpu_copy_user_highpage    = v6_copy_user_highpage_nonaliasing,
    };

    v7_cache_fns:定义在arch/arm/mm/cache-v7.S中 define_cache_functions v7 ,展开如下:

        .align 2
        .type    v7_cache_fns, #object
    ENTRY(v7_cache_fns)
        .long    v7_flush_icache_all
        .long    v7_flush_kern_cache_all
        .long    v7_flush_kern_cache_louis
        .long    v7_flush_user_cache_all
        .long    v7_flush_user_cache_range
        .long    v7_coherent_kern_range
        .long    v7_coherent_user_range
        .long    v7_flush_kern_dcache_area
        .long    v7_dma_map_area
        .long    v7_dma_unmap_area
        .long    v7_dma_flush_range
        .size    v7_cache_fns, . - v7_cache_fns

    对vmlinux反汇编后,可以看到__v7_ca9mp_proc_info部分的数据:

    c06ee5fc <__v7_ca9mp_proc_info>:
    c06ee5fc:       410fc090        #cpu_val
    c06ee600:       ff0ffff0        #cpu_mask
    c06ee604:       00011c0e        #__cpu_mm_mmu_flags
    c06ee608:       00000c02        #__cpu_io_mmu_flags
    c06ee60c:       ffa2e260        #__cpu_flush
    c06ee610:       c0701b64        #arch_name
    c06ee614:       c0701b6a        #elf_name
    c06ee618:       00008097        #elf_hwcap
    c06ee61c:       c011c780        #cpu_name
    c06ee620:       c0958094        #proc
    c06ee624:       c09081dc        #tlb
    c06ee628:       c095802c        #user
    c06ee62c:       c0958000        #cache

    回到head.S继续分析,上面说完proc.info.init段的内容后,下面分析__lookup_processor_type:

     1 __lookup_processor_type:
     2     adr    r3, __lookup_processor_type_data
     3     ldmia    r3, {r4 - r6}
     4     sub    r3, r3, r4            @ get offset between virt&phys
     5     add    r5, r5, r3            @ convert virt addresses to
     6     add    r6, r6, r3            @ physical address space
     7 1:    ldmia    r5, {r3, r4}            @ value, mask
     8     and    r4, r4, r9            @ mask wanted bits
     9     teq    r3, r4
    10     beq    2f
    11     add    r5, r5, #PROC_INFO_SZ        @ sizeof(proc_info_list)
    12     cmp    r5, r6
    13     blo    1b
    14     mov    r5, #0                @ unknown processor
    15 2:    ret    lr
    16 ENDPROC(__lookup_processor_type)
    17 
    18 /*
    19  * Look in <asm/procinfo.h> for information about the __proc_info structure.
    20  */
    21     .align    2
    22     .type    __lookup_processor_type_data, %object
    23 __lookup_processor_type_data:
    24     .long    .
    25     .long    __proc_info_begin
    26     .long    __proc_info_end
    27     .size    __lookup_processor_type_data, . - __lookup_processor_type_data

    由于还没有开启MMU,所以虚拟地址就是物理地址,但是由于kernel代码段的链接地址是从0xC0008000开始,而对于2440来说,物理内容的范围是0x3000_0000到0x3400_0000,所以如果直接用虚拟地址访问的话,程序一定会跑飞了。

    所以在第2到第6行的代码首先会对第25行__proc_info_begin和第26行的__proc_info_end的虚拟地址转换,转换成物理地址,分别存放在r5和r6中,转换方法很简单

    第7到第14行开始从r5(也就是"proc.info.init"段的起始物理地址)开始,以#PROC_INFO_SZ为步长进行遍历,寻找跟r9中的cpu id匹配的proc_info_list。匹配的方法很简单:从之前的分析知道,proc_info_list的前两个成员分别是cpu_val (r3)和cpu_mask (r4),将这两个值读出来,然后进行如下判断:(r9 & cpu_mask) 是否等于 cpu_val,如果相等,意味着找到匹配项,然后返回,此时r5中存放的是找到的proc_info_list的物理地址。否则的话,继续遍历下一个proc_info_list,直到遍历到最后一个proc_info_list,如果没有找到,r5被赋值为0,然后返回。

    回到head.S继续分析。

    5、第14到第17行代码完成的任务是计算物理内存的起始地址,方法如下:

        adr    r3, 2f
        ldmia    r3, {r4, r8}
        sub    r4, r3, r4            @ (PHYS_OFFSET - PAGE_OFFSET)
        add    r8, r8, r4            @ PHYS_OFFSET
        .ltorg
    2:    .long    .
        .long    PAGE_OFFSET

    首先获得2f标号的物理地址,在哪里存放的是2f标号的虚拟地址以及0xC000_0000。然后计算2f的物理地址跟虚拟地址之间的差值,再该差值加上0xC000_0000,就可以得到物理内存的起始地址。当然这里的前提是kernel被加载到(物理内存的起始地址 + 0x8000)处开始执行。

    比如对于2440,执行完上面的操作后,r8的值是0x3000_0000,对于vexpress来说是,r8是0x6000_0000.

    6、第23行,检查r2中传递的设备树镜像是否合法,如果不合法的话,r2会被清0。检查方法是:判断r2指向的地址的前4个字节是否等于OF_DT_MAGIC,是的话,表示合法,否则不合法

    7、第25和第28行暂时忽略

    未完待续

    8、第30行调用__create_page_tables建立段式页表。

  • 相关阅读:
    How to get the IIS root path in other application.
    Web.UI.Controls与页面事件的冲突问题。
    分析在服务器上设置计时器的问题。
    首次感觉我的电脑过时了。。。。。。。。郁闷。
    Google Logos
    2005年的最后一天
    TreeView的几个使用小技
    浅淡反射问题
    The restricted headers are:
    在服务器上用Timer遇到的小问题。。。。
  • 原文地址:https://www.cnblogs.com/pengdonglin137/p/7818386.html
Copyright © 2011-2022 走看看