• linux-3.2.36内核启动1-启动参数(arm平台 启动参数的获取和处理,分析setup_arch)【转】


    转自:http://blog.csdn.net/tommy_wxie/article/details/17093297

    最近公司要求调试一个内核,启动时有问题,所以就花了一点时间看看内核启动。

    看的过程中总结了一点东西,希望可以帮助大家调试内核。

    当我开始看的时候,第一件事是从网上搜集资料,不看不知道,一看吓一跳!牛人太多了,像这种内核启动的上古代码早就被人分析的彻彻底底。这注定我写的只能是烂微博了。

    为了此微博有存在的必要,我会显示内核启动打印的代码位置(用绿色表示)及出现错误打印的原因(用红色表示),同时我会尽力用添加打印(用蓝色字,同时给出对应于本人平台的打印结果)或实例来说明一些细节

    注意我的是Linux-3.2.36,有的老版本machine的判断位置不一样。

    首先看启动参数

    http://blog.chinaunix.net/uid-20543672-id-3151113.html

    有两种启动参数

    标签列表(taggedlist)或设备树(devicetree)。

    引导程序和内核约定r2寄存器中存放的数据所指向的内存地址。

    说设备树的微博,可以看看下面这个

    http://blog.csdn.net/21cnbao/article/details/8457546

    这两个注意:

    标签列表必须置于内核自解压和initrd'bootp'程序都不会覆盖的内存区。建议放在RAM的头16KiB中。

    设备树必须置于内核自解压不会覆盖的内存区。建议将其放置于RAM的头16KiB中

    我简单介绍标签列表格式

    ·  基地址 -> +-----------+

    ·           | ATAG_CORE | |

    ·          +-----------+  |

    ·           | ATAG_MEM |  | 地址增长方向

    ·          +-----------+  |

    ·           | ATAG_NONE | |

    ·          +-----------+  v

    viarch/arm/include/asm/setup.h

    struct tag_header {

          __u32size; //标签总大小(包括tag_header)

          __u32tag; //标签标识

    };

    上面的图就是要linux获取的第一个tag的头的__u32 tag要是ATAG_CORE

    最后一个是ATAG_NONE。

    struct tag {

            structtag_header hdr;

            union {

                    struct tag_core         core;// 标签列表开始                struct tag_mem32        mem;// 内存信息标签(可以有多个标签,以标识多个内存区块)

                   struct tag_videotext    videotext;// VGA文本显示参数标签

                   struct tag_ramdisk      ramdisk;// ramdisk参数标签(位置、大小等)

                   struct tag_initrd       initrd;// 压缩的ramdisk参数标签

                   struct tag_serialnr     serialnr;// 板子串号标签

                   struct tag_revision     revision;// 板子版本号标签

                   struct tag_videolfb     videolfb;// 帧缓冲初始化参数标签

                   struct tag_cmdline      cmdline;//就是uboot的bootargs

     

                    /*

                     *Acorn specific

                     */

                   struct tag_acorn        acorn;

     

                    /*

                     *DC21285 specific

                     */

                   struct tag_memclk       memclk;

            } u;

    };

    先简单的解释一下cmdline,你可以在用户态下cat /proc/cmdline

    看到,就是uboot的bootargs。

    上面的微博有有对bootm分析,我只说一点

    setup_start_tag (bd);  //设置ATAG_CORE,这里面有params = (struct tag *) bd->bi_boot_params;

    参数地址

     

    kernel_entry(0, machid, bd->bi_boot_params);

    r0 = 0 r1 = machid r2 = bd->bi_boot_params)

    bd->bi_boot_params在board下对应的板子目录下

    我的

    bd->bi_boot_params = 0x30000100 我的ram开始是0x30000000

    内核的建议

    建议放在RAM的头16KiB中,还有一句是:但是不可将其放置于“0”物理地址处,因为内核认为:r2中为0,意味着没有标签列表和dtb传递过来。这在启动是的汇编代码可以看到。

    Bootloader到内核的start_kernel之间还有自解压和一些汇编代码,这个我会在下一篇博客分析,现在主要是从setup_arch开始。这也是自解压之后,我们能看到内核打印开始的地方。

    下面我们进入内核,看看内核如何获取这些启动参数

    init/main.c

    asmlinkage void __init start_kernel(void)

    {

           ……

           printk(KERN_NOTICE"%s", linux_banner);//这个就是我们linux启动时打印的版本信息,我的:Linux version 3.2.0(root@localhost.localdomain) (gcc version 4.4.3 (ctng-1.6.1) ) #70 Thu Jun 2010:50:51 CST 2013

           setup_arch(&command_line);

    ……

    下面是我们的主机

    蓝色的字为我添加的调试打印,将在后面调试时开出来

    arch/arm/kernel/setup.c

    void __init setup_arch(char **cmdline_p)

    {

           struct machine_desc *mdesc;

           setup_processor();

    根据读出的cpuid,进行匹配,在汇编代码中有也有判断,我简单说一下,一个是从cpu读的id(用cp15协处理器),一个是从arch/arm/mm/ proc-armXXX.c读的,例如我的arm920t,

    __arm920_proc_info:

            .long   0x41009200

    就是这个id,如果不匹配,while(1)死循环。

    cpu信息就在这里打印,我的:

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

    CPU: VIVT data cache, VIVT instruction cache

    如果你没有那么是你的cpu类型选的不对,但是cpu类型一般是和machine一起选的,所以还是看下面的

            //wxl 添加

            printk(KERN_NOTICE “__atags_pointer: %lx ”, (unsigned long)__atags_pointer);

            打印为__atags_pointer : 30000100

            还记得uboot的

            bd->bi_boot_params= 0x30000100

           mdesc = setup_machine_fdt(__atags_pointer);这个函数时获取设备树的信息,我的没有,会返回NULL

           if (!mdesc)

                    mdesc =setup_machine_tags(machine_arch_type);

    这个是处理tagged_list的主要函数,在下面,我没有放这里,你先看看它在向下看。

           machine_desc = mdesc;

           machine_name = mdesc->name;这是MINI2440

    #ifdef CONFIG_ZONE_DMA

           if (mdesc->dma_zone_size) {

                    extern unsigned longarm_dma_zone_size;

                    arm_dma_zone_size =mdesc->dma_zone_size;DMA区域大小

            }

    #endif

           if (mdesc->soft_reboot)

                    reboot_setup("s");

           这个是重启方式,”s”为软件,”h”为硬件,最终重启调用arch_reset(mode,cmd);这个函数成功就重启不会返回,如果没有成功,你会看到Reboot failed --System halted打印;这个函数由平台提供,samsung没有什么软件重启模式,mach-ebsa110有,有兴趣可以看看。

           init_mm.start_code = (unsigned long) _text;

           init_mm.end_code   = (unsignedlong) _etext;

           init_mm.end_data   = (unsignedlong) _edata;

           init_mm.brk        = (unsignedlong) _end;

    上面的在平台连接脚本arch/arm/kernel/vmlinux.lds还有个vmlinux.lds.S看到大小

    . = 0xC0000000 + 0x00008000;

     .head.text : {

      _text = .;

      *(.head.text)

     }

    如果你编译内核会在源码目录下生产

    System.map文件

    我贴一点

    000000c A cpu_arm920_suspend_size

    c0004000 A swapper_pg_dir

    c0008000 T _text

    c0008000 T stext

    c02f9b90 A _etext

    c03367c0 D _edata

    c0367db8 A _end

    能看到一些上面的东西,不过我们还是要打印一下

            //wxl 增加

            printk(KERN_NOTICE“_text =%lx _etext=%lx _edata=%lx _end=%lx ”,

                  (unsigned long)_text, (unsigned long) _etext, (unsigned long) _edata, (unsigned long) _end);

    打印结果

    _text = c0008000

    _etext=c02f9b90

    _edata=c03367c0

    _end=c0367db8

    和上面一样吧

           /* populate cmd_line too for later use, preserving boot_command_line */

           strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);

           *cmdline_p = cmd_line; 将boot_command_line复制到cmd_line中

           这个函数是对cmdline的early_param处理,后面还有处理,在这个微博不会看到

           parse_early_param();

    这里需要注意的是内核的cmdline中的参数按照其被需要的先后,分为early和非early的。

    parse_args("early options", cmdline, NULL, 0,do_early_param);

    这个代码在kernerl/params.c下

            while (*args) {

                    int ret;

                    intirq_was_disabled;

                    //wxl 增加

                   printk(KERN_NOTICE “parse_args args: %s ”, args);

    打印结果

    parse_args args: mem=64M console=ttySAC0,115200 no_console_suspendroot=/dev/mtdblock3 rootfstype=jffs2mtdparts=384K(u-boot),128K(u-boot-env),5M(kernel),20M(root)

    和我uboot的bootargs一样

                    args =next_arg(args, &param, &val);

                   irq_was_disabled = irqs_disabled();

    //wxl增加

    printk(KERN_NOTICE “param:%s val =%s ”, param, val);

    打印结果。循环打印

    param: mem val =64M

    param: console val =ttySAC0,115200

    param: no_console_suspend val =(null)

    param: root val =/dev/mtdblock3

    param: rootfstype val =jffs2

    param: mtdparts val=384K(u-boot),128K(u-boot-env),5M(kernel),20M(root)

    就是一个取

    params是NULL,num是0,unknown是do_early_param函数

                    ret =parse_one(param, val, params, num, unknown);

                   

                    由于parse_one的num是0,所以parse_one就是执行do_early_param

    System.map下有c0316740 T__setup_start

                   c0316aa0 T __setup_end

    在链接文件里,__setup_start = .;*(.init.setup) __setup_end = .;

    static int __init do_early_param(char *param, char *val)

    {

            const struct obs_kernel_param *p;

     struct obs_kernel_param {

     const char *str;           //在cmdline中相应参数名。

     int(*setup_func)(char *);  //对于此参数的专用处理函数

     int early;                 //是否为早期需要处理的参数

     };

            for (p = __setup_start; p <__setup_end; p++) {

                    if ((p->early &&parameq(param, p->str)) ||

                        (strcmp(param,"console") == 0 &&

                         strcmp(p->str, "earlycon") ==0)

                    ) {这里看到判断了early是1,是0的在后面处理,在此微博不会看到了,以后我在说。不过console可以用earlycon指定的,就不要early,例如:

    8250_early.c:early_param("earlycon",setup_early_serial8250_console);

                            if(p->setup_func(val) != 0)参数对应就可以执行对应函数

                                    printk(KERN_WARNING

                                          "Malformed early option '%s' ", param);你看到这个打印就知道是你的early处理函数出错

                    }

            }

            /* We accept everything at this stage.*/

            return 0;

    }

    这就是执行.init.setup的东西,也就是执行

    #define early_param(str, fn)

             __setup_param(str,fn, fn, 1)

    early_param("mem", early_mem); mem就是

    我们要看一下early_param->arm_add_memory(start, size);这个start = PHTS_OFFSET就是0x30000000 size就是我们传入的64M即0x4000000

    arm_add_memory会把mem信息存入meminfo(下一篇文章看到这个定义)

          

    下面四个和内存有关,下一篇微博进行分析

            sanity_check_meminfo();

            arm_memblock_init(&meminfo, mdesc);

            paging_init(mdesc);

            request_standard_resources(mdesc);

    下面两个我的平台不涉及,不说了

           unflatten_device_tree();

    #ifdef CONFIG_SMP

           if (is_smp())

                    smp_init_cpus();

    #endif

           

           reserve_crashkernel();

    用于内核崩溃时的保留内核此功能通过内核command line参数中的"crashkernel="保留下内存用于主内核崩溃时获取内核信息的导出。格式crashkernel=size[KMG][@offset[KMG]]

    Kdump会用到,这位同志的linux启动有这个,你们看看

    http://hi.baidu.com/sunboy_2050/item/6404d9ff3a3003e61a111fcb

     

           tcm_init();

    参考

    http://blog.csdn.net/sergeycao/article/details/6030226

    #ifdef CONFIG_MULTI_IRQ_HANDLER

    Kconfig解释

    config MULTI_IRQ_HANDLER

            bool

            help

              Allow eachmachine to specify it's own IRQ handler at run time.

           handle_arch_irq = mdesc->handle_irq; 我的平台没有

    #endif

    选择Console类型

    #ifdef CONFIG_VT

    #if defined(CONFIG_VGA_CONSOLE)

           conswitchp = &vga_con;

    #elif defined(CONFIG_DUMMY_CONSOLE)

           conswitchp = &dummy_con;

    #endif

    #endif

           early_trap_init();

    对中断向量表进行早期初始化

    void __init early_trap_init(void)

    {

    #if defined(CONFIG_CPU_USE_DOMAINS)

            unsigned long vectors = CONFIG_VECTORS_BASE;

    #define CONFIG_VECTORS_BASE 0xffff0000   arm中断向量表位置

    #else

            unsigned long vectors = (unsigned long)vectors_page;

    #endif

    先看看我的平台

    System.map

    c02fbea0 T __kuser_helper_start

    c02fbee0 t __kuser_memory_barrier

    c02fbf00 t __kuser_cmpxchg

    c02fbf20 t __kuser_get_tls

    c02fbf3c t __kuser_helper_version

    c02fbf40 T __kuser_helper_end

    c02fbf40 T __stubs_start

    c02fbf40 t vector_irq

    c02fbfc0 t vector_dabt

    c02fc040 t vector_pabt

    c02fc0c0 t vector_und

    c02fc140 t vector_fiq

    c02fc144 t vector_addrexcptn

    c02fc164 T __stubs_end

    c02fc164 T __vectors_start

    c02fc184 T __vectors_end

            extern char__stubs_start[], __stubs_end[];

            extern char__vectors_start[], __vectors_end[];

            extern char__kuser_helper_start[], __kuser_helper_end[];

    上面定义在arch/arm/kernel/entry-armv.S

            int kuser_sz =__kuser_helper_end - __kuser_helper_start;

            /*

             * Copy thevectors, stubs and kuser helpers (in entry-armv.S)

             * into the vectorpage, mapped at 0xffff0000, and ensure these

             * are visible tothe instruction stream.

             */

            下面就是把向量表和kuser重映射到0xffff0000地址

            memcpy((void*)vectors, __vectors_start, __vectors_end - __vectors_start);

            memcpy((void*)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);

            memcpy((void*)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

            /*

             * Do processorspecific fixups for the kuser helpers

             */

            映射处理特殊的kuser帮助

            地址在vectors + 0xfe0,如果有tls_emu或has_tls_reg

           kuser_get_tls_init(vectors);

            /*

             * Copy signalreturn handlers into the vector page, and

             * set sigreturnto be a pointer to these.

             */

    sigreturn_codes是返回值数组

          memcpy((void *)(vectors + KERN_SIGRETURN_CODE- CONFIG_VECTORS_BASE),

                  sigreturn_codes, sizeof(sigreturn_codes));

    const unsigned long syscall_restart_code[2] = {

           SWI_SYS_RESTART,        /*swi  __NR_restart_syscall */

            0xe49df004,             /* ldr  pc, [sp], #4 */

    };

    直接把2进制代码填入,这个数组的都是命令。SWI,即software interrupt软件中断,__NR_restart_syscall是软件中断号。ldr pc, [sp], #4 可能是lr装入pc返回,我没有具体看

            memcpy((void*)(vectors + KERN_RESTART_CODE - CONFIG_VECTORS_BASE),

                   syscall_restart_code,sizeof(syscall_restart_code));

           flush_icache_range(vectors, vectors + PAGE_SIZE);

    最终就是__glue(_CACHE, vectors + PAGE_SIZE)

    #ifdef __STDC__

    #define ____glue(name,fn)      name##fn

    #else

    #define ____glue(name,fn)      name/**/fn

    #endif

    #define __glue(name,fn)        ____glue(name,fn)

    就是组成一个标记

           modify_domain(DOMAIN_USER, DOMAIN_CLIENT);这个最终会用cp15协处理器去设置domain

    }

           if (mdesc->init_early)需要早期的初始化,我的没有,你只要知道这个在处理early参数之后。

                    mdesc->init_early();

    }

    static struct machine_desc * __initsetup_machine_tags(unsigned int nr)

    {

           struct tag *tags = (struct tag *)&init_tags;同一目录下有定义init_tags

           struct machine_desc *mdesc = NULL, *p;

           char *from = default_command_line;这个就是内核配置的cmdline,在内核配置选项

    通过bootloader传递过来的设备ID来匹配一个 struct machine_desc结构体,搞过移植应该知道,uboot和kernel的设备ID是要一样的,还记得uboot如何传的吧,r1 =machid,

    有个这个结构体表示structmachine_desc {

    在大家的平台是arch/arm/mach-*/mach-*.c下,我的

    MACHINE_START(MINI2440, "MINI2440") //id, name

            /* Maintainer:Michel Pollet <buserror@gmail.com> */

            .atag_offset    = 0x100,//标签列表相对地址,还记得我的是0x30000100吧,所以是0x100

            .map_io         = mini2440_map_io,//io映射函数

            .init_machine   = mini2440_init,//板子初始化函数

            .init_irq       = s3c24xx_init_irq,//中断初始化函数

            .timer          = &s3c24xx_timer,//系统tick定时器

    MACHINE_END

           init_tags.mem.start = PHYS_OFFSET;内存开始的物理地址

           /*

            * locate machine in the list of supported machines.

            */

           for_each_machine_desc(p)

                    if (nr ==p->nr) {

                           printk("Machine: %s ", p->name);

    我的打印:

    Machine: MINI2440

                           mdesc = p;

                           break;

                    }

            if (!mdesc) {

                    early_print(" Error:unrecognized/unsupported machine ID"

                            "(r1 = 0x%08x). ", nr);

    如果你看到这个打印应该知道是设备id不匹配

                   dump_machine_table(); /* does not return */死循环

            }

           if (__atags_pointer)

                    tags =phys_to_virt(__atags_pointer);

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

           else if (mdesc->atag_offset)上面看到是0x100,如果bootloader传的对就不用他了

                    tags = (void *)(PAGE_OFFSET +mdesc->atag_offset);

          //wxl添加

           printk(KERN_NOTICE “tags virt addr = %lxPAGE_OFFSET = %lx PHYS_OFFSET = %lx ”, (unsigned long)tags, (unsigned long)PAGE_OFFSET,(unsigned long)PHYS_OFFSET);

    打印为

    tags virt addr = c0000100 PAGE_OFFSET = c0000000 PHYS_OFFSET =30000000

    #if defined(CONFIG_DEPRECATED_PARAM_STRUCT)//参数检测

           /*

            * If we have the old style parameters, convert them to

            * a tag list.

            */

           if (tags->hdr.tag != ATAG_CORE)还记得第一个要是ATAG_CORE吧

                    convert_to_tag_list(tags);不用看代码,就知道是转换为现在的格式

    #endif

           if (tags->hdr.tag != ATAG_CORE) {如何不可用参数,看处理

    #if defined(CONFIG_OF)

                    /*

                     * If CONFIG_OF is set, thenassume this is a reasonably

                     * modern system that shouldpass boot parameters

                     */

                    early_print("Warning:Neither atags nor dtb found ");

    #endif

                    tags = (struct tag*)&init_tags;用内核默认的,所以从这可以看到,不可用启动参数不会引起无法启动

           }

           if (mdesc->fixup)

                   mdesc->fixup(tags,&from, &meminfo);对应你平台提供的函数,你可以为你的平台处理tagged list、cmdline和meminfo数据,我的没有。

           if (tags->hdr.tag == ATAG_CORE) {

                   //wxl 添加

                   printk(KERN_NOTICE “meminfo.nr_banks = %d ”, meminfo.nr_banks)

                    打印结果

    meminfo.nr_banks = 0

                    if (meminfo.nr_banks != 0)/如果mem已经初始化,就不要tags里的mem了

                            squash_mem_tags(tags);

                    save_atags(tags);保存到atags_copy

                    parse_tags(tags);解析

    static void __init parse_tags(const struct tag *t)

    {

            for (;t->hdr.size; t = tag_next(t))

                    if(!parse_tag(t))

                           printk(KERN_WARNING

                                   "Ignoring unrecognised tag 0x%08x ",

                                   t->hdr.tag);

    }

    static int __init parse_tag(const struct tag *tag)

    {

            extern struct tagtable __tagtable_begin, __tagtable_end;

            struct tagtable*t;

    这个处理和对command_line处理很像,

    System.map里面

    c0312780 T __tagtable_begin

    c0312780 t __tagtable_parse_tag_cmdline

    c0312788 t __tagtable_parse_tag_revision

    c0312790 t __tagtable_parse_tag_serialnr

    c0312798 t __tagtable_parse_tag_ramdisk

    c03127a0 t __tagtable_parse_tag_videotext

    c03127a8 t __tagtable_parse_tag_mem32

    c03127b0 t __tagtable_parse_tag_core

    c03127b8 t __tagtable_parse_tag_initrd2

    c03127c0 t __tagtable_parse_tag_initrd

    c03127c8 T __pv_table_begin

    c03127c8 T __tagtable_end

    例如__tagtable_parse_tag_mem32,你在内核只能找到parse_tag_mem32函数

    __tagtable(ATAG_MEM, parse_tag_mem32);

    #define __tag __used __attribute__((__section__(".taglist.init")))

    #define __tagtable(tag, fn)

    static struct tagtable __tagtable_##fn__tag = { tag, fn }

    __tagtable_parse_tag_mem32就是用##连接得到的。

            for (t =&__tagtable_begin; t < &__tagtable_end; t++)

            {

            //wxl add

            printk(KERN_NOTICE “tag->hdr.tag = %lxt->tag = %lx ”, (unsigned ling)tag->hdr.tag, (unsigned ling)t->tag);

    tag->hdr.tag = 54410001 t->tag = 54410009

    tag->hdr.tag = 54410001 t->tag = 54410007

    tag->hdr.tag = 54410001 t->tag = 54410006

    tag->hdr.tag = 54410001 t->tag = 54410004

    tag->hdr.tag = 54410001 t->tag = 54410003

    tag->hdr.tag = 54410001 t->tag = 54410002

    tag->hdr.tag = 54410001 t->tag = 54410001   ATAG_CORE

    tag->hdr.tag = 54410002 t->tag = 54410009

    tag->hdr.tag = 54410002 t->tag = 54410007

    tag->hdr.tag = 54410002 t->tag = 54410006

    tag->hdr.tag = 54410002 t->tag = 54410004

    tag->hdr.tag = 54410002 t->tag = 54410003

    tag->hdr.tag = 54410002 t->tag = 54410002   ATAG_MEM

    tag->hdr.tag = 54410009 t->tag = 54410009   ATAG_CMDLINE

    从这个打印看会先执行mem在执行cmdline

    __tagtable(ATAG_CMDLINE, parse_tag_cmdline);

    执行cmdline就是把cmdline保存在default_command_line,处理command_line会用到

                    if(tag->hdr.tag == t->tag) {当tag==MEM时,执行parse_tag_mem32

                           t->parse(tag);

    我们只看parse_tag_mem32

    static int __init parse_tag_mem32(const struct tag *tag)

    {

             //wxl add

            printk(KERN_NOTICE"tag->u.mem.start = %lx tag->u.mem.size = %lx ", (unsignedlong)tag->u.mem.start, (unsigned long)tag->u.mem.size);

           打印如下

           tag->u.mem.start =30000000 tag->u.mem.size = 4000000

           我的bootargs mem=64M和这个一样,但是如果我mem=32M,那内存会设为32M,因为在下面cammand_line处理还要重新调用arm_add_memory,还会把meminfo.nr_banks = 0

            return arm_add_memory(tag->u.mem.start, tag->u.mem.size);

    }

    和command_line中的”mem=”处理一样

                           break;

                    }

            }

            return t <&__tagtable_end;

    }

            }

           /* parse_early_param needs a boot_command_line */

           strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

           return mdesc;

    }

  • 相关阅读:
    如何使用websocket实现前后端通信
    影响MySQL的性能(一)磁盘的选择
    springboot结合日志门面SLF4j和日志实现Logback的使用
    分享一个猜数字小游戏的脚本
    关于drop table *** purge (drop后不过回收站)
    关于DateBase link(dbLINK)及同义词
    关于数据更新(update)
    关于insert into(插入值)
    关于wm_concat(把一列的值,通过','进行分隔后,合并成一个值进行显示)
    关于PIVOT(用于行转列)
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/6336550.html
Copyright © 2020-2023  润新知