kernel(二):启动内核

2023-12-13 04:52:41

? ? ? ? 本文主要探讨210内核启动过程。

主Makefile

?? ?定义kernel版本号(2.6.35.7)

?? ?VERSION = 2
?? ?PATCHLEVEL = 6
?? ?SUBLEVEL = 35
?? ?EXTRAVERSION = .7


?? ????指定编译文件生成目录

??make O=/tmp 


??? ?定义交叉编译工具链

CROSS_COMPILE?? ??= /root/arm-2009q3/bin/arm-none-linux-gnueabi-

??? ?指定架构

?ARCH?? ??? ??= arm

链接脚本

?? ?kernel链接脚本需要条件编译,lds格式不支持条件编译
?? ?kernel链接接脚由汇编文件vmlinux.lds.S编译生成vmlinux.lds(arch/arm/kernel/)
?? ?vmlinux.lds中ENTRY(stext)为链接地址入口
?? ?head.S和head-nommu.S都包含ENTRY(stext),head.S使用用MMU,head-nommu.S未使用mmu


head.S

#define KERNEL_RAM_VADDR?? ?(PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR?? ?(PHYS_OFFSET + TEXT_OFFSET)

?????????内核运行的虚拟地址(KERNEL_RAM_VADDR)为0xC0008000,内核运行的物理地址(KERNEL_RAM_PADDR)为0x30008000?? ?

(arch/arm/kernel/.head.o.cmd)
?? ?cmd_arch/arm/kernel/head.o := /root/arm-2009q3/bin/arm-none-linux-gnueabi-gcc -Wp,-MD,arch/arm/kernel/.head.o.d ?-nostdinc -isystem /root/arm-2009q3/bin/../lib/gcc/arm-none-linux-gnueabi/4.4.1/include -I/root/kernel/arch/arm/include -Iinclude ?-include include/generated/autoconf.h -D__KERNEL__ -mlittle-endian -Iarch/arm/mach-s5pv210/include -Iarch/arm/plat-s5p/include -Iarch/arm/plat-samsung/include -D__ASSEMBLY__ -mabi=aapcs-linux -mno-thumb-interwork ?-D__LINUX_ARM_ARCH__=7 -march=armv7-a ?-include asm/unified.h -msoft-float -gdwarf-2 ? ? -DTEXT_OFFSET=0x00008000 ?-c -o arch/arm/kernel/head.o arch/arm/kernel/head.S

(arch/arm/include/asm/memory.h)#define PAGE_OFFSET?? ??? ?UL(CONFIG_PAGE_OFFSET)
(.config)CONFIG_PAGE_OFFSET=0xC0000000


(arch/arm/mach-s5pv210/include/mach/memory.h)
#if defined(CONFIG_MACH_SMDKV210)
#define PHYS_OFFSET?? ??? ?UL(0x30000000)
#else
#define PHYS_OFFSET?? ??? ?UL(0x30000000)
#endif

(include/generated/autoconf.h)
#define CONFIG_MACH_SMDKV210 1

?? ?????????uboot启动内核后,内核运行zImage前解压代码解压zImage,再运行内核入口代码
?? ?????????uboot启动内核(theKernel(0,machid,bd->bi_boot_params))运行时把0写入r0,machid写入r1,bd->bi_boot_params写入r2
?? ?????????kernel启动时MMU是关闭的,硬件需要的物理地址,zImage是整体不能分散加载,需使用位置无关码(物理地址)

?? ??? ?__HEAD
?? ?ENTRY(stext)
?? ?setmode?? ?PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
?? ??? ??? ??? ??? ??? ?@ and irqs disabled
?? ?mrc?? ?p15, 0, r9, c0, c0?? ??? ?@ get processor id
?? ?bl?? ?__lookup_processor_type?? ??? ?@ r5=procinfo r9=cpuid
?? ?movs?? ?r10, r5?? ??? ??? ??? ?@ invalid processor (r5=0)?
?? ?beq?? ?__error_p?? ??? ??? ?@ yes, error 'p'
?? ?bl?? ?__lookup_machine_type?? ??? ?@ r5=machinfo
?? ?movs?? ?r8, r5?? ??? ??? ??? ?@ invalid machine (r5=0)?
?? ?beq?? ?__error_a?? ??? ??? ?@ yes, error 'a'
?? ?bl?? ?__vet_atags
?? ?bl?? ?__create_page_tables

?? ???????? ?????????__HEAD定义后面代码属于.head.text段

?? ?__HEAD
?? ?ENTRY(stext)
?? ?(include/linux/init.h)
?? ?#define __HEAD?? ??? ?.section?? ?".head.text","ax"

????????????cp15协处理器c0读取出硬件CPU ID号,用于合法性检验,合法则继续启动,不合法则停止启动,转向__error_p启动失败
?? ?????????内核包含CPU ID号码数组,该函数从硬件中读取的cpu id号码和数组中的对比,相同则不合法

__lookup_processor_type
    (System.map)
?? ?c0008168 t __lookup_processor_type
?? ?c00081a4 T lookup_processor_type


?? ?(arch/arm/kernel/setup.c)
?? ?list = lookup_processor_type(read_cpuid_id());
?? ?if (!list) {
?? ??? ?printk("CPU configuration botched (ID %08x), unable "
?? ??? ? ? ? ? "to continue.\n", read_cpuid_id());
?? ??? ?while (1);
?? ?}

?? ?(arch/arm/kernel/include/asm/cputype.h)
?? ?#define CPUID_ID?? ?0

?? ?static inline unsigned int __attribute_const__ read_cpuid_id(void)
?? ?{
?? ??? ?return read_cpuid(CPUID_ID);
?? ?}

???????? ? __lookup_machine_type函数用于校验机器码,类似cpu id校验
?? ????????__vet_atags函数校验uboot给内核传参(ATAGS格式)?

?? ?????????__create_page_tables

?? ?????????????????函数用于建立页表,内核链接在虚拟地址处,kernel需要启动MMU
?? ?????????????????kernel建立页表:kernel先建立段式页表(同uboot建立页相同页,大小为1MB,1MB映射4GB需4096页表项,每个页表项4字节,共需16KB做页表),建立细页表(细页表项大小为4kb)


?? ?????????__switch_data?(arch/arm/kernel/head-common.S)

__switch_data:
    ?.long?? ?__mmap_switched
?? ?.long?? ?__data_loc?? ??? ??? ?@ r4
?? ?.long?? ?_data?? ??? ??? ??? ?@ r5
?? ?.long?? ?__bss_start?? ??? ??? ?@ r6
?? ?.long?? ?_end?? ??? ??? ??? ?@ r7
?? ?.long?? ?processor_id?? ??? ??? ?@ r4
?? ?.long?? ?__machine_arch_type?? ??? ?@ r5
?? ?.long?? ?__atags_pointer?? ??? ??? ?@ r6
?? ?.long?? ?cr_alignment?? ??? ??? ?@ r7
?? ?.long?? ?init_thread_union + THREAD_START_SP @ sp
__mmap_switched:
?? ?adr?? ?r3, __switch_data + 4

?? ?ldmia?? ?r3!, {r4, r5, r6, r7}
?? ?cmp?? ?r4, r5?? ??? ??? ??? ?@ Copy data segment if needed
1:?? ?cmpne?? ?r5, r6
?? ?ldrne?? ?fp, [r4], #4
?? ?strne?? ?fp, [r5], #4
?? ?bne?? ?1b

?? ?mov?? ?fp, #0?? ??? ??? ??? ?@ Clear BSS (and zero fp)
1:?? ?cmp?? ?r6, r7
?? ?strcc?? ?fp, [r6],#4
?? ?bcc?? ?1b

?ARM(?? ?ldmia?? ?r3, {r4, r5, r6, r7, sp})
?THUMB(?? ?ldmia?? ?r3, {r4, r5, r6, r7}?? ?)
?THUMB(?? ?ldr?? ?sp, [r3, #16]?? ??? ?)
?? ?str?? ?r9, [r4]?? ??? ??? ?@ Save processor ID
?? ?str?? ?r1, [r5]?? ??? ??? ?@ Save machine type
?? ?str?? ?r2, [r6]?? ??? ??? ?@ Save atags pointer
?? ?bic?? ?r4, r0, #CR_A?? ??? ??? ?@ Clear 'A' bit
?? ?stmia?? ?r7, {r0, r4}?? ??? ??? ?@ Save control register values
?? ?b?? ?start_kernel
ENDPROC(__mmap_switched)

? ? ? ? ? ? ?建立段式页表后执行__switch_data,__switch_data中执行__mmap_switched函数
?? ?????????__mmap_switched函数复制数据段、清除bss段(构建C环境),保存cpu id、机器码、tag首地址
? ? ? ? ? ?start_kernel跳转到C阶段

内核启动的C阶段(start_kernel,init/main.c)


????????smp_setup_processor_id()设置对称多处理器(多核CPU)
????????lockdep_init内核调试模块处理内核自旋锁死锁
????????cgroup_init_early处理进程组

????????printk(KERN_NOTICE "%s", linux_banner);

const char linux_banner[] =
?? ?"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
?? ?LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
(include/generated/utsrelease.h)
#define UTS_RELEASE "2.6.35.7"

(include/generated/compile.h)
#define UTS_VERSION "#1 PREEMPT Tue Dec 12 11:46:41 CST 2023"
#define LINUX_COMPILE_BY "root"
#define LINUX_COMPILE_HOST "kax-virtual-machine"
#define LINUX_COMPILER "gcc version 4.4.1 (Sourcery G++ Lite 2009q3-67) "

include/generated/下的文件为编译过程中生成的头文件


????????????????printk定义打印级别(0-7)来过滤显示机制

#define?? ?KERN_EMERG?? ?"<0>"?? ?/* system is unusable?? ??? ??? ?*/
#define?? ?KERN_ALERT?? ?"<1>"?? ?/* action must be taken immediately?? ?*/
#define?? ?KERN_CRIT?? ?"<2>"?? ?/* critical conditions?? ??? ??? ?*/
#define?? ?KERN_ERR?? ?"<3>"?? ?/* error conditions?? ??? ??? ?*/
#define?? ?KERN_WARNING?? ?"<4>"?? ?/* warning conditions?? ??? ??? ?*/
#define?? ?KERN_NOTICE?? ?"<5>"?? ?/* normal but significant condition?? ?*/
#define?? ?KERN_INFO?? ?"<6>"?? ?/* informational?? ??? ??? ?*/
#define?? ?KERN_DEBUG?? ?"<7>"?? ?/* debug-level messages?? ??? ??? ?*/


??????????setup_arch

????????????????用来确定内核机器的arch和machine

void __init setup_arch(char **cmdline_p)
{
?? ?struct tag *tags = (struct tag *)&init_tags;
?? ?struct machine_desc *mdesc;
?? ?char *from = default_command_line;

?? ?unwind_init();

?? ?setup_processor();
?? ?mdesc = setup_machine(machine_arch_type);
?? ?machine_name = mdesc->name;

?? ?if (mdesc->soft_reboot)
?? ??? ?reboot_setup("s");

?? ?if (__atags_pointer)
?? ?{
?? ??? ?tags = phys_to_virt(__atags_pointer);
?? ??? ?printk("@@@@@@@ atags_pointer not null\n");
?? ??? ?
?? ?}
?? ?else if (mdesc->boot_params)
?? ?{
?? ??? ?tags = phys_to_virt(mdesc->boot_params);
?? ??? ?printk("@@@@@@@ boot params not null\n");
?? ?}
?? ?printk("@@@@@@@linter#####boot_params:%p,mdesc->boot_params:%p\n",tags);
?? ?/*
?? ? * If we have the old style parameters, convert them to
?? ? * a tag list.
?? ? */
?? ?if (tags->hdr.tag != ATAG_CORE)
?? ??? ?convert_to_tag_list(tags);
?? ?if (tags->hdr.tag != ATAG_CORE)
?? ??? ?tags = (struct tag *)&init_tags;

?? ?if (mdesc->fixup)
?? ??? ?mdesc->fixup(mdesc, tags, &from, &meminfo);

?? ?if (tags->hdr.tag == ATAG_CORE) {
?? ??? ?if (meminfo.nr_banks != 0)
?? ??? ??? ?squash_mem_tags(tags);
?? ??? ?save_atags(tags);
?? ??? ?parse_tags(tags);
?? ?}

?? ?init_mm.start_code = (unsigned long) _text;
?? ?init_mm.end_code ? = (unsigned long) _etext;
?? ?init_mm.end_data ? = (unsigned long) _edata;
?? ?init_mm.brk?? ? ? = (unsigned long) _end;

?? ?/* parse_early_param needs a boot_command_line */
?? ?strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

?? ?/* populate cmd_line too for later use, preserving boot_command_line */
?? ?strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
?? ?*cmdline_p = cmd_line;

?? ?printk("$$$$$$$$$cmdline:%s\n",cmd_line);
?? ?parse_early_param();

?? ?paging_init(mdesc);
?? ?request_standard_resources(&meminfo, mdesc);

#ifdef CONFIG_SMP
?? ?smp_init_cpus();
#endif

?? ?cpu_init();
?? ?tcm_init();

?? ?/*
?? ? * Set up various architecture-specific pointers
?? ? */
?? ?init_arch_irq = mdesc->init_irq;
?? ?system_timer = mdesc->timer;
?? ?init_machine = mdesc->init_machine;

#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
?? ?conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
?? ?conswitchp = &dummy_con;
#endif
#endif
?? ?early_trap_init();
}
setup_processor();

??????????????????setup_processor用来查找CPU信息并打印信息

static void __init setup_processor(void)
{
?? ?struct proc_info_list *list;

?? ?/*
?? ? * locate processor in the list of supported processor
?? ? * types. ?The linker builds this table for us from the
?? ? * entries in arch/arm/mm/proc-*.S
?? ? */
?? ?list = lookup_processor_type(read_cpuid_id());
?? ?if (!list) {
?? ??? ?printk("CPU configuration botched (ID %08x), unable "
?? ??? ? ? ? ? "to continue.\n", read_cpuid_id());
?? ??? ?while (1);
?? ?}

?? ?cpu_name = list->cpu_name;

#ifdef MULTI_CPU
?? ?processor = *list->proc;
#endif
#ifdef MULTI_TLB
?? ?cpu_tlb = *list->tlb;
#endif
#ifdef MULTI_USER
?? ?cpu_user = *list->user;
#endif
#ifdef MULTI_CACHE
?? ?cpu_cache = *list->cache;
#endif

?? ?printk("CPU: %s [%08x] revision %d (ARMv%s), cr=%08lx\n",
?? ? ? ? ? cpu_name, read_cpuid_id(), read_cpuid_id() & 15,
?? ? ? ? ? proc_arch[cpu_architecture()], cr_alignment);

?? ?sprintf(init_utsname()->machine, "%s%c", list->arch_name, ENDIANNESS);
?? ?sprintf(elf_platform, "%s%c", list->elf_name, ENDIANNESS);
?? ?elf_hwcap = list->elf_hwcap;
#ifndef CONFIG_ARM_THUMB
?? ?elf_hwcap &= ~HWCAP_THUMB;
#endif

?? ?cacheid_init();
?? ?cpu_proc_init();
}

???????????????setup_machine的参数machine_arch_type是机器码编号(2456)

mdesc = setup_machine(machine_arch_type);
machine_name = mdesc->name;
(include/generated/mach-types.h)
#define MACH_TYPE_SMDKV210 ? ? ? ? ? ? 2456

#ifdef CONFIG_MACH_SMDKV210
# ifdef machine_arch_type
# ?undef machine_arch_type
# ?define machine_arch_type?? ?__machine_arch_type
# else
# ?define machine_arch_type?? ?MACH_TYPE_SMDKV210
# endif
# define machine_is_smdkv210()?? ?(machine_arch_type == MACH_TYPE_SMDKV210)
#else
# define machine_is_smdkv210()?? ?(0)
#endif
(arch/arm/kernel/setup.c)
struct machine_desc *mdesc;

(arch/arm/include/asm/mach/arch.h)
struct machine_desc {
?? ?/*
?? ? * Note! The first four elements are used
?? ? * by assembler code in head.S, head-common.S
?? ? */
?? ?unsigned int?? ??? ?nr;?? ??? ?/* architecture number?? ?*/
?? ?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);
};

?

ENTRY(lookup_machine_type)
?? ?stmfd?? ?sp!, {r4 - r6, lr}
?? ?mov?? ?r1, r0
?? ?bl?? ?__lookup_machine_type
?? ?mov?? ?r0, r5
?? ?ldmfd?? ?sp!, {r4 - r6, pc}
ENDPROC(lookup_machine_type)

? ? ? ? ? ? ? ? ?setup_machine调用lookup_machine_type调用__lookup_machine_type
? ? ? ? ? ? ? ? ?内核把各种CPU架构信息组成machine_desc实例并赋予段属性(.arch.info.init)保证链接在一起,__lookup_machine_type遍历各描述符

????????????????setup_arch函数处理cmdline(初步bootargs参数)

char *from = default_command_line;

/* parse_early_param needs a boot_command_line */
?? ?strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

?? ?/* populate cmd_line too for later use, preserving boot_command_line */
?? ?strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
?? ?*cmdline_p = cmd_line;

?? ?printk("$$$$$$$$$cmdline:%s\n",cmd_line);
?? ?parse_early_param();

????????????????default_command_line(CONFIG_CMDLINE)是默认命令行参数
????????????????uboot通过tag给kernel传递cmdline为空则使用kernel默认命令行参数

static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;

(include/generated/autoconf.h)
#define CONFIG_CMDLINE "console=ttySAC2,115200"


????????????????setup_command_line(cmdline解析,kernel/init/init)

cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3


????????????????????????解析为
????????????????????????????????console=ttySAC2,115200 ?
????????????????????????????????root=/dev/mmcblk0p2 rw?
????????????????????????????????init=/linuxrc ?? ??? ??? ?
????????????????????????????????rootfstype=ext3?? ?

????????????????????????cmdline参数

????????????????????????????????root=/dev/xxx指定根文件系统位置
????????????????????????????????rootfstype=xxx指定根文件系统的文件系统类型
????????????????????????????????console=指定控制台信息(串口,波特率)
????????????????????????????????mem=xxx指定内核系统内存大小
????????????????????????????????init=指定init进程?? ??? ?

????????start_kernel其他函数
????????????????trap_init?? ??? ??? ??? ??? ?设置异常向量表
????????????????mm_init?? ??? ??? ??? ??? ??? ?内存管理模块初始化
????????????????scheduler_init?? ??? ??? ??? ??? ?内核调度系统初始化
????????????????early_irq_init&init_IRQ?? ??? ?中断初始化
????????????????console_init?? ??? ??? ? ? ?控制台初始化


????????rest_init(init文件)
????????????????start_kernel调用许多xx_init函数为初始化内核模块函数,rest_init用于串联已初始化的模块

static noinline void __init_refok rest_init(void)
?? ?__releases(kernel_lock)
{
?? ?int pid;

?? ?rcu_scheduler_starting();
?? ?/*
?? ? * We need to spawn init first so that it obtains pid 1, however
?? ? * the init task will end up wanting to create kthreads, which, if
?? ? * we schedule it before we create kthreadd, will OOPS.
?? ? */
?? ?kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
?? ?numa_default_policy();
?? ?pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
?? ?rcu_read_lock();
?? ?kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
?? ?rcu_read_unlock();
?? ?complete(&kthreadd_done);
?? ?unlock_kernel();

?? ?/*
?? ? * The boot idle thread must execute schedule()
?? ? * at least once to get things moving:
?? ? */
?? ?init_idle_bootup_task(current);
?? ?preempt_enable_no_resched();
?? ?schedule();
?? ?preempt_disable();

?? ?/* Call into cpu_idle with preempt disabled */
?? ?cpu_idle();
}

????????????????调用kernel_thread启动2个内核线程:kernel_init和kthreadd
????????????????调用schedule函数开启内核调度系统,linux系统运行
????????????????调用cpu_idle函数结束内核启动进入死循环
????????????????内核调度系统里的进程需要运行,调度系统终止cpu_idle死循环进程(空闲进程),执行该进程
????????????????进程0:cpu_idle()函数,idle进程(死循环)
????????????????进程1:kernel_init函数为init进程
????????????????进程2:kthreadd函数是进程2,是linux内核守护进程,保证linux内核正常工作的
????????????????init进程挂载根文件系统并运行根文件系统下的init程序转变为用户态应用程序,脱离内核态,转为用户态

????????????????init进程

????????????????????????init进程构建交互界面,创建其他进程(login、命令行、shell...),shell进程启动其他用户进程,shell工作后就可运行其他命令和程序

?? ????????????????????打开控制台

? ? ? ? ? ? ? ? ? ? ? ? ? ? ?打开/dev/console文件(210)并复制2次文件描述符,三个文件描述符分别是0(标准输入)、1(标准输出)、2(标准错误)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?进程1打开三个文件描述符,进程1的子进程都有3个三描述符

? ? ? ? ? ? ? ? ? ? ? ?挂载根文件系统
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? prepare_namespace函数中挂载根文件系统(init/do_mounts.c)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? uboot传参中root=/dev/mmcblk0p2 rw定义根文件系统位置和权限
?? ???????????????????????????uboot传参中rootfstype=ext3定义rootfs类型。


? ? ? ? ? ? ? ? ? ? ? ?执行用户态下(init进程)
?? ????????????????????????????uboot传参init=/linuxrc指定rootfs中的init程序
?? ????????????????????????????其他备用init进程:/sbin/init,/etc/init,sbin/init,/bin/sh

????????start_kernel函数打印了内核初始化信息、内核模块初始化(譬如内存管理、调度系统、异常处理···)
????????setup_arch:机器码架构查找,架构相关硬件初始化,uboot传参cmdline给内核
????????rest_init:构建进程0,1,2,挂载根文件系统,进程1转为用户态进程

内核框架图

文章来源:https://blog.csdn.net/tyro_wzp/article/details/134956574
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。