PS:要转载请注明出处,本人版权所有。PS: 这个只是基于《我自己》的理解,如果和你的原则及想法相冲突,请谅解,勿喷。
无
最开始,我仅仅是对linux比较感兴趣,觉得其很神奇的,能够做到很多事情。后面了解到其源码也是开源的,于是抱着学习的态度,简要的看了看相关的代码,在那个时候,我还看的比较粗略,仅仅是简单的会点编译,执行linux命令等等。这期间还有一个有印象的有趣的事儿就是那个pdf《Linux 那些事儿之USB》,大概就是讲述了作者因为要看pian儿,但是U盘识别不到,所以去细读USB相关linux 内核内容的资料。这精神,虽然我看不懂,但是我大为震撼!!!
在我工作的近几年来,逐渐的和linux打上了交道,从最开始的hisi 3520a1系列的流媒体处理开始,为其搭建了文件系统,编译内核,同时为其适配EC20 4G模块,这期间,我基本都是照着别人的教程或者说文档,渐渐的熟悉了一些linux内核的一些事务。同时这期间,我做过一些简单的字符驱动玩耍模块,只能说玩玩可以的。
在前几年中的某段时间,我接到一个任务,要在android进程之间大量传输数据2。这个时候我调研到了一个叫做android 匿名共享内存的东西,我发现了一个binder的驱动程序和linux unix socket的功能可以在android 和 linux 里面实现进程间的文件描述符的共享,注意这个方法是通用的,不像某些功能在linux里面能够使用,在android里面不能够使用。在这个时候,我天马行空实现了一个类似 binder的驱动demo3。这可以说是我第一个为了自己写的内核及的相关代码,而且具有实际应用意义。
在这些工作过程中,我逐渐的觉得自己学习的《操作系统原理》与现实的差别,特别想把书中知识和实际系统结合起来,经过查询,如果想要大概了解linux 内核,最好从其远古的版本读起来,因为大概的脉络没有变,新内核只是更加的结构化,多了很多现代的功能。于是乎,有了《Linux Kernel 0.12 启动简介,调试记录(Ubuntu1804, Bochs, gdb)》一文4。经过了《Linux Kernel 0.12 启动简介,调试记录(Ubuntu1804, Bochs, gdb)》一文的学习之后,我基本了解了linux kernel 0.12版本内核的基本工作原理,例如其调度,内存管理等。其次是对于x86架构下,linux kernel 0.12的启动流程有了一个简要的认知。
在最近这段时间,我的工作有部分和ai相关,有部分和android和linux的差异相关,需要我对linux内核有更深的印象和见解。于是在以前的基础上,这次,我要实际分析我们工作中所用的最新版本的内核,再一次的去验证一个内核从上电开始,到系统完整起来的过程。由于现代linux内核非常的巨大,所以我只关注我喜欢的部分。
本文主要是分析aarch64架构的arch/arm64/kernel/head.S 到 init/main.c 中的start_kernel的过程。这可能也是我短时间内最后一次分析这种启动的过程,因为其实道理都是相同的,大部分都是cpu初始化,虚拟内存启用,由实地址切换为虚拟地址,创建init_task,设置sp,进入start_kernel。其实这里很多都是和特定的CPU有关系,内容是固定的。但是虚拟内存启用,初始task创建,初始sp指针初始化这些和《操作系统原理》有关联,可以印证我们所学。
本文分为两大部分,一部分是head.S到main.c的调试环境建立, 二是从上电开始到进入start_kernel的代码注释分析和部分解释。
本文的测试环境为qemu-system-aarch64 raspi3b 模拟板卡。linux内核为树莓派内核 rpi-5.15.y, 下载地址为:https://github.com/raspberrypi/linux.git
在make menu的时候勾选: Kernel hacking > Compile-time checks and compiler options > Compile the kernel with debug info通过如下命令生成镜像:
cd rpi-linux-kernel-dircp arch/arm64/configs/bcm2711_defconfig .configmake ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfigmake ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs下载ubuntu-base-18.04.5-base-arm64.tar.gz的基础文件系统。
# 解压文件系统到指定目录tar -xzf ubuntu-base-18.04.5-base-arm64.tar.gz -C temp/*# 制作裸文件镜像dd if=/dev/zero of=linuxroot.img bs=1M count=2048sudo mkfs.ext4 linuxroot.imgmkdir rootfssudo mount linuxroot.img rootfs/sudo cp -rfp temp/* rootfs/sudo umount rootfs/e2fsck -p -f linuxroot.imgresize2fs -M linuxroot.img只有新版的qemu才支持raspi3b模拟板卡
# 下载qemu代码git clone https://github.com/qemu/qemu.gitcd qemumkdir buildcd build../configure --prefix=/home/sky/LinuxKernel/qemu_install --target-list=arm-softmmu,arm-linux-user,armeb-linux-user,aarch64-softmmu,aarch64-linux-user,aarch64_be-linux-user make这里的linux目录是内核目录,qemu_install是qemu生成的最新可执行文件目录,当前目录有rootfs镜像linuxroot.img。
# -S freeze CPU at startup (use 'c' to start execution)# -s shorthand for -gdb tcp::1234# 注意下面命令如果要直接运行,而不是等待gdb调试,请去掉最后的-s 和 -S。./qemu_install/bin/qemu-system-aarch64 \ -M raspi3b \ -kernel ./linux/arch/arm64/boot/Image \ -dtb ./linux/arch/arm64/boot/dts/broadcom/bcm2710-rpi-3-b.dtb \ -drive id=hd-root,format=raw,file=./linuxroot.img \ -m 1024M \ -serial stdio \ -smp 4 \ -device usb-kbd \ -device usb-tablet \ -device usb-net,netdev=net0 \ -netdev user,id=net0,hostfwd=tcp::5555-:22 \ -append "rw earlycon=pl011,0x3f201000 console=ttyAMA0 loglevel=8 root=/dev/mmcblk0 rootwait" -S -s注意请在qemu启动后面加上-s 和 -S。当连接成功时,这个时候cpu还未执行,输入ni执行到第一条指令。
# 没有gdb-multiarch自行安装# 这里的vmlinux就是编译生成的最终镜像gdb-multiarch linux/vmlinux# 在gdb cli中执行target remote localhost:1234我这里把整个head.S里面重要的部分都dump下来了。跟着这个部分然后参考head.S去阅读,会有奇效。长文注释警告。
首先是上电部分,当板卡上电后,会执行bootloader,bootloader会将内核和dtb放到特定的位置,然后按照Linux arm64 boot protocal去初始化对应的寄存器,最后进入head.S的第一条指令。
@ boot start ... ...//Linux arm64 boot protocal@ https://www.kernel.org/doc/Documentation/arm64/booting.txt@ 0x08000000 FDT/*- 主 CPU 通用寄存器设置 x0 = 系统 RAM 中设备树 blob (dtb) 的物理地址。 x1 = 0(留作将来使用) x2 = 0(留作将来使用) x3 = 0(留作将来使用)*///注意这里的0x18地址存放的是fdt的地址,地址为0x080000000x0000000000000000: ldr x0, 0x180x0000000000000004: mov x1, xzr0x0000000000000008: mov x2, xzr0x000000000000000c: mov x3, xzr//注意这里的0x20是存放的kernel地址,地址为0x002000000x0000000000000010: ldr x4, 0x200x0000000000000014: br x4@ 0x00200000 head.S start ... ...@ =======> now, go to 0x00200000注意,当我们进入head.S的最开始的地方的时候,有一个标准得到头如下。其中code1的部分将会跳转到真正执行的地方。
@ The decompressed kernel image contains a 64-byte header as follows:@ u32 code0; /* Executable code */ @ u32 code1; /* Executable code */@ u64 text_offset; /* Image load offset, little endian */@ u64 image_size; /* Effective Image size, little endian */@ u64 flags; /* kernel flags, little endian */@ u64 res2 = 0; /* reserved */@ u64 res3 = 0; /* reserved */@ u64 res4 = 0; /* reserved */@ u32 magic = 0x644d5241; /* Magic number, little endian, "ARM\x64" */@ u32 res5; /* reserved (used for PE COFF offset) */@ 0x200000 code0@ 0x200004 code1 @ 0x200008 text_offset @ 0x20000c @ 0x200010 image_size @ 0x200014 @ 0x200018 flags @ 0x20001c @ 0x200020 res2 @ 0x200024 @ 0x200028 res3 @ 0x20002c @ 0x200030 res4 @ 0x200034 @ 0x200038 magic @ 0x20003c res5//注意,这里的code0,code1就是地址0x200000和0x200004的指令。整个0x200000到0x200040就是内核镜像的64字节头。0x0000000000200000: ccmp x18, #0x0, #0xd, pl // special NOP to identity as PE/COFF executable0x0000000000200004: b 0x1190000 @ =======> now, go to 0x1190000(primary_entry)这是整个内核启动部分最重要的函数,所有的东西都在这里做完,然后跳转到start_kernel。下面我们会来重点分析这个部分的内容。详细请看注释。
//注意这里的几个bl指令,覆盖了进入kernel_start前的所有操作@ SYM_CODE_START(primary_entry)//跳转过去保存boot参数//保存x0(fdt),x1,x2,x3到符号boot_args的变量中,靠dcache_inval_poc中的ret返回到下一行指令0x0000000001190000: bl 0x1190020 //preserve_boot_args//跳转过去执行不同异常等级的初始化,这里比较复杂,从开始的el2异常级别跳转到el1级别。0x0000000001190004: bl 0xd5d000 //init_kernel_el//将内核镜像开始地址给x23,也就是0x2000000x0000000001190008: adrp x23, 0x200000// KASLR offset, defaults to 00x000000000119000c: and x23, x23, #0x1fffff//跳转过去根据w0的值,保存相关的cpu boot mode,注意当前我们的cpu已经处于el1等级,w0 存的是 el2 的标识符0x0000000001190010: bl 0xd5d1f8//创建页表,这里面的创建只填充了相关的页表项,并没有开启mmu//idmap = 0x117e00, 0x117e0 = 0x117f03, 0x117f30 = 0x00c00701, // 注意,这时页表项表示2MB的区域。idmap区域包含了__cpu_setup和__primary_switch//这里执行完,有两个重要的数据结构:idmap_pg_dir 和 init_pg_dir//我们将物理地址__idmap_text_start映射到虚拟地址[__idmap_text_start, __idmap_text_end],注意观察,物理地址和虚拟地址基本是一致的。//还将物理地址_text映射到虚拟地址[KIMAGE_VADDR + KASLR, _end]0x0000000001190014: bl 0x1190040//The following calls CPU setup code, see arch/arm64/mm/proc.S//注意,这里已经准备好了SCTLR在x0中,下面就是相关的初始化,然后准备打开mmu的参数0x0000000001190018: bl 0xd5d6f4//最终的初始化,开启mmu,并跳转到kernel_start0x000000000119001c: b 0xd5d3d8@ SYM_CODE_END(primary_entry)此部分对应保存启动参数,主要还是保存启动时,x0~x3。
@ SYM_CODE_START_LOCAL(preserve_boot_args)//x21保存dtb物理地址0x0000000001190020: mov x21, x0//将dtb,x1,x2,x3物理地址存放到变量arch/arm64/kernel/setup.c:u64 __cacheline_aligned boot_args[4];0x0000000001190024: adrp x0, 0x15450000x0000000001190028: add x0, x0, #0x0//存放dtb,x10x000000000119002c: stp x21, x1, [x0]//存放x2,x30x0000000001190030: stp x2, x3, [x0, #16]@ 刷新cache0x0000000001190034: dmb sy0x0000000001190038: add x1, x0, #0x200x000000000119003c: b 0x2346a8@ SYM_CODE_END(preserve_boot_args)此部分很长,其实主要是汇编代码稍微复杂,其基本的作用就是创建页表,这里面的创建只填充了相关的页表项。分别创建了这里执行完,有两个重要的数据结构:idmap_pg_dir和init_pg_dir的数据结构。这里用的是两级映射,第一级是全局映射,第二级是每个项2MB的映射。这里的idmap_pg_dir映射的是__cpu_setup和__primary_switch部分的内容,这部分主要涉及到mmu开启的过程,需要将物理地址和虚拟地址对应起来。init_pg_dir主要是映射的是kernel虚拟地址和kernel镜像地址。
@ SYM_FUNC_START_LOCAL(__create_page_tables)//保存返回值到x280x0000000001190040: mov x28, x30//加载init_pg_dir 到x00x0000000001190044: adrp x0, 0x181d000//加载init_pg_end 到x10x0000000001190048: adrp x1, 0x1820000@ 刷新cache0x000000000119004c: bl 0x2346a8//加载init_pg_dir 到x00x0000000001190050: adrp x0, 0x181d000//加载init_pg_end 到x10x0000000001190054: adrp x1, 0x1820000//求出init_pg的大小放入x1中count0x0000000001190058: sub x1, x1, x0//向x0中写入0,然后x1 -= 64,当x1等于0时候,所有pg清理完毕。0x000000000119005c: stp xzr, xzr, [x0], #160x0000000001190060: stp xzr, xzr, [x0], #160x0000000001190064: stp xzr, xzr, [x0], #160x0000000001190068: stp xzr, xzr, [x0], #160x000000000119006c: subs x1, x1, #0x400x0000000001190070: b.ne 0x119005c // b.any//根据配置加载不同的flag, SWAPPER_MM_MMUFLAGS0x0000000001190074: mov x7, #0x701 // #1793// 获取idmap的页表基地址,idmap_pg_dir0x0000000001190078: adrp x0, 0x117e000@ 获取idmap的代码段虚地址,__idmap_text_start0x000000000119007c: adrp x3, 0xd5d000@ 将系统地址线位数给x5, VA_BITS_MIN0x0000000001190080: mov x5, #0x27 // #39//获取变量地址到x6, vabits_actual0x0000000001190084: adrp x6, 0x17280000x0000000001190088: add x6, x6, #0x10//将39写入变量vabits_actual0x000000000119008c: str x5, [x6]0x0000000001190090: dmb sy0x0000000001190094: dc ivac, x6@ 判断虚拟地址空间是否够IDmap来映射0x0000000001190098: adrp x5, 0xd5d0000x000000000119009c: clz x5, x50x00000000011900a0: cmp x5, #0x19@ 这里要跳转,不需要扩展虚拟地址0x00000000011900a4: b.ge 0x11900e0 // b.tcont@ 这部分是虚拟地址扩展的相关操作,这里不做详解0x00000000011900a8: adrp x6, 0x15550000x00000000011900ac: add x6, x6, #0xcc80x00000000011900b0: str x5, [x6]0x00000000011900b4: dmb sy0x00000000011900b8: dc ivac, x60x00000000011900bc: mov x4, #0x200 // #5120x00000000011900c0: add x5, x0, #0x1, lsl #120x00000000011900c4: mov x6, x50x00000000011900c8: orr x6, x6, #0x30x00000000011900cc: lsr x5, x3, #390x00000000011900d0: sub x4, x4, #0x10x00000000011900d4: and x5, x5, x40x00000000011900d8: str x6, [x0, x5, lsl #3]0x00000000011900dc: add x0, x0, #0x1, lsl #12@ 从上面不需要扩展虚拟地址跳转而来,0x00000000011900a4@ 将512 个 pgd entry存入x40x00000000011900e0: adrp x4, 0x15550000x00000000011900e4: ldr x4, [x4, #3280]@ 将__idmap_text_end放入x60x00000000011900e8: adrp x6, 0xd5d0000x00000000011900ec: add x6, x6, #0x7e0@ 这里开始映射[__idmap_text_start, __idmap_text_end] 到 idmap_pg_dir中,@ 且,这部分内容就是cpu_setup部分的内容,恰好对应开启mmu的代码。//tbl: x0 = idmap_pg_dir = 0x117e000//rtbl: x1 = 0 //vstart: x3 = __idmap_text_start = 0xd5d000//vend: x6 = __idmap_text_end = 0xd5d7e0//flags: x7 = SWAPPER_MM_MMUFLAGS//phys: x3 = __idmap_text_start = 0xd5d000//pgds: x4 = idmap_ptrs_per_pgd = 512//tmp regs: x10, x11, x12, x13, x14@ macro map_memory start ...@ __idmap_text_end - 1 是idmap映射结束的地方0x00000000011900f0: sub x6, x6, #0x1@ x1 = idmap的页表基地址(idmap_pg_dir) + 2^12 ,并指向了下一个page entry0x00000000011900f4: add x1, x0, #0x1, lsl #12@ 将x1 保存到 x140x00000000011900f8: mov x14, x1@ 将count赋值为00x00000000011900fc: mov x13, #0x0 // #0// vstart: x3 = __idmap_text_start = 0xd5d000// vend: x6 = __idmap_text_end = 0xd5d7e0// shift: 30// ptrs: x4 = idmap_ptrs_per_pgd = 512// istart: x10// iend: // count: x13@ compute_indices start ...//将vend右逻辑偏移shift(30)位0x0000000001190100: lsr x11, x6, #30//将ptrs(number of entries in page table)存放到istart, 每个表512项0x0000000001190104: mov x10, x4//pte 数量减一0x0000000001190108: sub x10, x10, #0x1//此时算出来vend的pgd index,根据虚拟地址右移30位,还剩9位,恰好表示512个项的id。// iend = (vend >> shift) & (ptrs - 1)0x000000000119010c: and x11, x11, x100x0000000001190110: mov x10, x40x0000000001190114: mul x10, x10, x13// iend += count * ptrs0x0000000001190118: add x11, x11, x10//将vstart右逻辑偏移shift位0x000000000119011c: lsr x10, x3, #300x0000000001190120: mov x13, x40x0000000001190124: sub x13, x13, #0x1// istart = (vstart >> shift) & (ptrs - 1), 此时算出来vstart的pgd index0x0000000001190128: and x10, x10, x13//计算出多少项page entry0x000000000119012c: sub x13, x11, x10@ compute_indices end ...@ populate_entries start ... 0x0000000001190130: mov x12, x1//给当前entry设置内存属性0x0000000001190134: orr x12, x12, #0x3@ 向一级页表idmap_pg_dir(0x117e000)存入二级页表地址(0x117f000)0x0000000001190138: str x12, [x0, x10, lsl #3]0x000000000119013c: add x1, x1, #0x1, lsl #120x0000000001190140: add x10, x10, #0x10x0000000001190144: cmp x10, x110x0000000001190148: b.ls 0x1190130 // b.plast@ populate_entries end ... //注意,这里相当于tbl=tbl+PAGE_SIZE,主要还是指向了二级页表//相当于现在一级页表为:idmap_pg_dir(0x117e000),里面存放的是0x0117f003//这里的x14是二级页表的地址,为0x0117f0000x000000000119014c: mov x0, x14//sv = rtbl = tbl+PAGE_SIZE+PAGE_SIZE,相当于指向了三级页表0x0000000001190150: mov x14, x1@ compute_indices start ...//将vend右逻辑偏移shift位0x0000000001190154: lsr x11, x6, #21//将ptrs(number of entries in page table)存放到istart, 每个表512项0x0000000001190158: mov x10, #0x200 // #512//pte 数量减一0x000000000119015c: sub x10, x10, #0x1//此时算出来vend的pgd index// iend = (vend >> shift) & (ptrs - 1)0x0000000001190160: and x11, x11, x100x0000000001190164: mov x10, #0x200 // #5120x0000000001190168: mul x10, x10, x13// iend += count * ptrs0x000000000119016c: add x11, x11, x10//将vstart右逻辑偏移shift位0x0000000001190170: lsr x10, x3, #210x0000000001190174: mov x13, #0x200 // #5120x0000000001190178: sub x13, x13, #0x1// istart = (vstart >> shift) & (ptrs - 1), 此时算出来vstart的pgd index0x000000000119017c: and x10, x10, x13//计算出多少项page entry0x0000000001190180: sub x13, x11, x10@ compute_indices end ...@ x13 = 0xc00000, x3 = 0xd5d0000x0000000001190184: and x13, x3, #0xffffffffffe00000@ populate_entries start ... @ x12 = 0xc000000x0000000001190188: mov x12, x13//给当前entry设置内存属性0x000000000119018c: orr x12, x12, x7@ 向二级页表(0x117f000 + 6*8 = 0x117f030)存入地址0xc007010x0000000001190190: str x12, [x0, x10, lsl #3]0x0000000001190194: add x13, x13, #0x200, lsl #120x0000000001190198: add x10, x10, #0x10x000000000119019c: cmp x10, x110x00000000011901a0: b.ls 0x1190188 // b.plast@ populate_entries end ... @ init_pg_dir 给x00x00000000011901a4: adrp x0, 0x181d000@ KIMAGE_VADDR 给x50x00000000011901a8: mov x5, #0xffffffc0ffffffff // #-2705829396490x00000000011901ac: movk x5, #0x800, lsl #160x00000000011901b0: movk x5, #0x0// add KASLR displacement0x00000000011901b4: add x5, x5, x23@ 将页表项数目给x40x00000000011901b8: mov x4, #0x200 // #512@ _end 给x60x00000000011901bc: adrp x6, 0x1820000@ _start 给x30x00000000011901c0: adrp x3, 0x200000@ 求出_end-_start0x00000000011901c4: sub x6, x6, x3@ 算出基于KIMAGE_VADDR和KASLR的偏移0x00000000011901c8: add x6, x6, x5//tbl: x0 = init_pg_dir//rtbl: x1 = 0//vstart: x5 = KIMAGE_VADDR + KASLR//vend: x6 = _end//flags: x7 = SWAPPER_MM_MMUFLAGS//phys: x3 = _text//pgds: x4 = PTRS_PER_PGD//tmp regs: x10, x11, x12, x13, x14@ macro map_memory start ...@ 开始填充页表init_pg_dir0x00000000011901cc: sub x6, x6, #0x10x00000000011901d0: add x1, x0, #0x1, lsl #120x00000000011901d4: mov x14, x10x00000000011901d8: mov x13, #0x0 // #00x00000000011901dc: lsr x11, x6, #300x00000000011901e0: mov x10, x40x00000000011901e4: sub x10, x10, #0x10x00000000011901e8: and x11, x11, x100x00000000011901ec: mov x10, x40x00000000011901f0: mul x10, x10, x130x00000000011901f4: add x11, x11, x100x00000000011901f8: lsr x10, x5, #300x00000000011901fc: mov x13, x40x0000000001190200: sub x13, x13, #0x10x0000000001190204: and x10, x10, x130x0000000001190208: sub x13, x11, x100x000000000119020c: mov x12, x10x0000000001190210: orr x12, x12, #0x30x0000000001190214: str x12, [x0, x10, lsl #3]0x0000000001190218: add x1, x1, #0x1, lsl #120x000000000119021c: add x10, x10, #0x10x0000000001190220: cmp x10, x110x0000000001190224: b.ls 0x119020c // b.plast0x0000000001190228: mov x0, x140x000000000119022c: mov x14, x10x0000000001190230: lsr x11, x6, #210x0000000001190234: mov x10, #0x200 // #5120x0000000001190238: sub x10, x10, #0x10x000000000119023c: and x11, x11, x100x0000000001190240: mov x10, #0x200 // #5120x0000000001190244: mul x10, x10, x130x0000000001190248: add x11, x11, x100x000000000119024c: lsr x10, x5, #210x0000000001190250: mov x13, #0x200 // #5120x0000000001190254: sub x13, x13, #0x10x0000000001190258: and x10, x10, x130x000000000119025c: sub x13, x11, x100x0000000001190260: and x13, x3, #0xffffffffffe000000x0000000001190264: mov x12, x130x0000000001190268: orr x12, x12, x70x000000000119026c: str x12, [x0, x10, lsl #3]0x0000000001190270: add x13, x13, #0x200, lsl #120x0000000001190274: add x10, x10, #0x10x0000000001190278: cmp x10, x110x000000000119027c: b.ls 0x1190264 // b.plast@ macro map_memory end ...//内存屏障0x0000000001190280: dmb sy@ 刷新cache0x0000000001190284: adrp x0, 0x117e0000x0000000001190288: adrp x1, 0x11810000x000000000119028c: bl 0x2346a8@ 刷新cache0x0000000001190290: adrp x0, 0x181d0000x0000000001190294: adrp x1, 0x18200000x0000000001190298: bl 0x2346a8@ 返回到bl __cpu_setup0x000000000119029c: ret x28@ SYM_FUNC_END(__create_page_tables)这部分是刷新i/d cache
@ dcache_inval_poc start ...0x00000000002346a8: mrs x3, ctr_el00x00000000002346ac: nop0x00000000002346b0: ubfx x3, x3, #16, #40x00000000002346b4: mov x2, #0x4 // #40x00000000002346b8: lsl x2, x2, x30x00000000002346bc: sub x3, x2, #0x10x00000000002346c0: tst x1, x30x00000000002346c4: bic x1, x1, x30x00000000002346c8: b.eq 0x2346d0 // b.none0x00000000002346cc: dc civac, x10x00000000002346d0: tst x0, x30x00000000002346d4: bic x0, x0, x30x00000000002346d8: b.eq 0x2346e4 // b.none0x00000000002346dc: dc civac, x00x00000000002346e0: b 0x2346e80x00000000002346e4: dc ivac, x00x00000000002346e8: add x0, x0, x20x00000000002346ec: cmp x0, x10x00000000002346f0: b.cc 0x2346e4 // b.lo, b.ul, b.last0x00000000002346f4: dsb sy0x00000000002346f8: ret@ dcache_inval_poc end ...这部分就是对应的是开始的在el2模式下初始化,并返回到el1,并保存启动参数。
@ SYM_FUNC_START(init_kernel_el)//读取当前的异常等级0x0000000000d5d000: mrs x0, currentel//判断是否为异常等级20x0000000000d5d004: cmp x0, #0x8//跳转到el2(qemu 模拟机器执行路径), init_el20x0000000000d5d008: b.eq 0xd5d034 // b.none0x0000000000d5d00c: mov x0, #0x30500000 // #8105492480x0000000000d5d010: movk x0, #0x8000x0000000000d5d014: msr sctlr_el1, x00x0000000000d5d018: isb0x0000000000d5d01c: movz x0, #0x0, lsl #160x0000000000d5d020: movk x0, #0x3c50x0000000000d5d024: msr spsr_el1, x00x0000000000d5d028: msr elr_el1, x300x0000000000d5d02c: mov w0, #0xe11 // #36010x0000000000d5d030: eret@ init_el2 start ... ...//配置hcr_el2寄存器,HCR(Hypervisor Configuration Register)0x0000000000d5d034: mov x0, #0x100000000000000 // #720575940379279360x0000000000d5d038: movk x0, #0x300, lsl #320x0000000000d5d03c: movk x0, #0x8000, lsl #160x0000000000d5d040: movk x0, #0x00x0000000000d5d044: msr hcr_el2, x0//ISB. 指令同步屏障0x0000000000d5d048: isb//初始化el2下的各种状态/*.macro init_el2_state __init_el2_sctlr __init_el2_timers __init_el2_debug __init_el2_lor __init_el2_stage2 __init_el2_gicv3 __init_el2_hstr __init_el2_nvhe_idregs __init_el2_nvhe_cptr __init_el2_nvhe_sve __init_el2_fgt __init_el2_nvhe_prepare_eret.endm*///__init_el2_sctlr0x0000000000d5d04c: mov x0, #0x30c50000 // #8182169600x0000000000d5d050: movk x0, #0x8300x0000000000d5d054: msr sctlr_el2, x00x0000000000d5d058: isb//__init_el2_timers0x0000000000d5d05c: mov x0, #0x3 // #30x0000000000d5d060: msr cnthctl_el2, x00x0000000000d5d064: msr cntvoff_el2, xzr//__init_el2_debug0x0000000000d5d068: mrs x1, id_aa64dfr0_el10x0000000000d5d06c: sbfx x0, x1, #8, #40x0000000000d5d070: cmp x0, #0x10x0000000000d5d074: b.lt 0xd5d080 // b.tstop0x0000000000d5d078: mrs x0, pmcr_el00x0000000000d5d07c: ubfx x0, x0, #11, #50x0000000000d5d080: csel x2, xzr, x0, lt // lt = tstop0x0000000000d5d084: ubfx x0, x1, #32, #40x0000000000d5d088: cbz x0, 0xd5d0a80x0000000000d5d08c: mrs x0, pmbidr_el10x0000000000d5d090: and x0, x0, #0x100x0000000000d5d094: cbnz x0, 0xd5d0a00x0000000000d5d098: mov x0, #0x50 // #800x0000000000d5d09c: msr pmscr_el2, x00x0000000000d5d0a0: mov x0, #0x3000 // #122880x0000000000d5d0a4: orr x2, x2, x00x0000000000d5d0a8: ubfx x0, x1, #44, #40x0000000000d5d0ac: cbz x0, 0xd5d0c40x0000000000d5d0b0: mrs x0, s3_0_c9_c11_70x0000000000d5d0b4: and x0, x0, #0x100x0000000000d5d0b8: cbnz x0, 0xd5d0c40x0000000000d5d0bc: mov x0, #0x3000000 // #503316480x0000000000d5d0c0: orr x2, x2, x00x0000000000d5d0c4: msr mdcr_el2, x2//__init_el2_lor0x0000000000d5d0c8: mrs x1, id_aa64mmfr1_el10x0000000000d5d0cc: ubfx x0, x1, #16, #40x0000000000d5d0d0: cbz x0, 0xd5d0d80x0000000000d5d0d4: msr s3_0_c10_c4_3, xzr//__init_el2_stage20x0000000000d5d0d8: msr vttbr_el2, xzr//__init_el2_gicv30x0000000000d5d0dc: mrs x0, id_aa64pfr0_el10x0000000000d5d0e0: ubfx x0, x0, #24, #40x0000000000d5d0e4: cbz x0, 0xd5d1080x0000000000d5d0e8: mrs x0, s3_4_c12_c9_50x0000000000d5d0ec: orr x0, x0, #0x10x0000000000d5d0f0: orr x0, x0, #0x80x0000000000d5d0f4: msr s3_4_c12_c9_5, x00x0000000000d5d0f8: isb0x0000000000d5d0fc: mrs x0, s3_4_c12_c9_50x0000000000d5d100: tbz w0, #0, 0xd5d1080x0000000000d5d104: msr s3_4_c12_c11_0, xzr//__init_el2_hstr0x0000000000d5d108: msr hstr_el2, xzr//__init_el2_nvhe_idregs0x0000000000d5d10c: mrs x0, midr_el10x0000000000d5d110: mrs x1, mpidr_el10x0000000000d5d114: msr vpidr_el2, x00x0000000000d5d118: msr vmpidr_el2, x1//__init_el2_nvhe_cptr0x0000000000d5d11c: mov x0, #0x33ff // #133110x0000000000d5d120: msr cptr_el2, x0//__init_el2_nvhe_sve0x0000000000d5d124: mrs x1, id_aa64pfr0_el10x0000000000d5d128: ubfx x1, x1, #32, #40x0000000000d5d12c: cbz x1, 0xd5d1440x0000000000d5d130: and x0, x0, #0xfffffffffffffeff0x0000000000d5d134: msr cptr_el2, x00x0000000000d5d138: isb0x0000000000d5d13c: mov x1, #0x1ff // #5110x0000000000d5d140: msr zcr_el2, x1//__init_el2_fgt0x0000000000d5d144: mrs x1, id_aa64mmfr0_el10x0000000000d5d148: ubfx x1, x1, #56, #40x0000000000d5d14c: cbz x1, 0xd5d18c0x0000000000d5d150: mov x0, xzr0x0000000000d5d154: mrs x1, id_aa64dfr0_el10x0000000000d5d158: ubfx x1, x1, #32, #40x0000000000d5d15c: cmp x1, #0x30x0000000000d5d160: b.lt 0xd5d168 // b.tstop0x0000000000d5d164: orr x0, x0, #0x40000000000000000x0000000000d5d168: msr s3_4_c3_c1_4, x00x0000000000d5d16c: msr s3_4_c3_c1_5, x00x0000000000d5d170: msr s3_4_c1_c1_4, xzr0x0000000000d5d174: msr s3_4_c1_c1_5, xzr0x0000000000d5d178: msr s3_4_c1_c1_6, xzr0x0000000000d5d17c: mrs x1, id_aa64pfr0_el10x0000000000d5d180: ubfx x1, x1, #44, #40x0000000000d5d184: cbz x1, 0xd5d18c0x0000000000d5d188: msr s3_4_c3_c1_6, xzr// __init_el2_nvhe_prepare_eret0x0000000000d5d18c: mov x0, #0x3c5 // #9650x0000000000d5d190: msr spsr_el2, x0//加载el2的中断向量表0x0000000000d5d194: adrp x0, 0xd4f0000x0000000000d5d198: add x0, x0, #0x00x0000000000d5d19c: msr vbar_el2, x00x0000000000d5d1a0: isb/* * Fruity CPUs seem to have HCR_EL2.E2H set to RES1, * making it impossible to start in nVHE mode. Is that * compliant with the architecture? Absolutely not!*/0x0000000000d5d1a4: mrs x0, hcr_el20x0000000000d5d1a8: and x0, x0, #0x400000000//跳转到1f(0xd5d1d0)位置继续执行0x0000000000d5d1ac: cbz x0, 0xd5d1d00x0000000000d5d1b0: mov x0, #0x30500000 // #8105492480x0000000000d5d1b4: movk x0, #0x8000x0000000000d5d1b8: msr sctlr_el12, x00x0000000000d5d1bc: mov x0, #0x3c9 // #9690x0000000000d5d1c0: msr spsr_el1, x00x0000000000d5d1c4: adr x0, 0xd5d1e80x0000000000d5d1c8: msr elr_el1, x00x0000000000d5d1cc: eret//将x0写入sctlr_el10x0000000000d5d1d0: mov x0, #0x30500000 // #8105492480x0000000000d5d1d4: movk x0, #0x8000x0000000000d5d1d8: msr sctlr_el1, x0//将当前的lr(x30)地址放到elr_el2中,后续eret到el1时,跳转到此地址执行0x0000000000d5d1dc: msr elr_el2, x30//将flag写入w0中0x0000000000d5d1e0: mov w0, #0xe12 // #3602//注意这里的返回值,这里返回到elr_el2指向的地方,//也就是adrp x23, __PHYS_OFFSET, 0x00000000011900080x0000000000d5d1e4: eret0x0000000000d5d1e8: mov x0, #0x3 // #30x0000000000d5d1ec: hvc #0x00x0000000000d5d1f0: mov x0, #0xe12 // #36020x0000000000d5d1f4: ret@ SYM_FUNC_END(init_kernel_el)@ SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)//加载__boot_cpu_mode符号地址,此地址存放0x0000000000d5d1f8: adrp x1, 0x17280000x0000000000d5d1fc: add x1, x1, #0x00x0000000000d5d200: cmp w0, #0xe120x0000000000d5d204: b.ne 0xd5d20c // b.any0x0000000000d5d208: add x1, x1, #0x40x0000000000d5d20c: str w0, [x1]0x0000000000d5d210: dmb sy0x0000000000d5d214: dc ivac, x1@ 返回到0x00000000011900140x0000000000d5d218: ret@ SYM_FUNC_END(set_cpu_boot_mode_flag)这里包含3个步骤:
@ SYM_FUNC_START(__enable_mmu)/*符合ARMv8的PE最大支持的物理地址宽度也是48个bit,当然,具体的实现可以自己定义(不能超过48个bit),具体的配置可以通过ID_AA64MMFR0_EL1 (AArch64 Memory Model Feature Register 0)这个RO寄存器获取*/0xd5d308 mrs x2, id_aa64mmfr0_el1 @ 判断物理地址宽度是否满足最小和最大的要求。0xd5d30c ubfx x2, x2, #28, #4 0xd5d310 cmp x2, #0x0 0xd5d314 b.lt 0xd5d36c // b.tstop 0xd5d318 cmp x2, #0x7 0xd5d31c b.gt 0xd5d36c //update_early_cpu_boot_status0xd5d320 mov x3, #0x0 // #0 0xd5d324 adrp x2, 0x1728000 0xd5d328 add x2, x2, #0x8 0xd5d32c str x3, [x2] 0xd5d330 dmb sy 0xd5d334 dc ivac, x2 //加载idmap_pg_dir到ttbr0//注意,跳转过来时,x1是init_gdir0xd5d338 adrp x2, 0x117e000 0xd5d33c mov x1, x1 0xd5d340 mov x2, x2 //加载两个映射表到tbbr0和1,在el1模式下面0xd5d344 msr ttbr0_el1, x2 0xd5d348 msr ttbr1_el1, x1 0xd5d34c isb //打开mmu0xd5d350 msr sctlr_el1, x0 0xd5d354 isb 0xd5d358 ic iallu 0xd5d35c dsb nsh 0xd5d360 isb 0xd5d364 ret @ SYM_FUNC_END(__enable_mmu)//此部分作用为解决符号重定位问题//readelf -r vmlinux 读取重定位表//readelf -S vmlinux 读取所有section头//hexdump -s 0x24000 -n 16 vmlinux 读取重定位表中的第一项的目的地址内容,// 可以发现为8个0,需要我们来手动填写符号重定位地址@ SYM_FUNC_START_LOCAL(__relocate_kernel)0xd5d390 ldr w9, 0xd5d438 // offset to reloc table │0xd5d394 ldr w10, 0xd5d43c // size of reloc table // default virtual offset │0xd5d398 mov x11, #0xffffffc0ffffffff // #-270582939649 │0xd5d39c movk x11, #0x800, lsl #16 │0xd5d3a0 movk x11, #0x0 // actual virtual offset │0xd5d3a4 add x11, x11, x23 // x9保存了.rela.dyn区域的链接地址// x10保存了.rela.dyn区域的结束地址 │0xd5d3a8 add x9, x9, x11 │0xd5d3ac add x10, x9, x10 │0xd5d3b0 cmp x9, x10 │0xd5d3b4 b.cs 0xd5d3d4 // b.hs, b.nlast //获取一项offset和info+flag │0xd5d3b8 ldp x12, x13, [x9], #24 //获取Addend值 │0xd5d3bc ldur x14, [x9, #-8] │0xd5d3c0 cmp w13, #0x403 │0xd5d3c4 b.ne 0xd5d3b0 // b.any //// relocate,这里主要是修正[offset + KASLR offset] = KASLR offset + append的值,也就是符号重定位了 │0xd5d3c8 add x14, x14, x23 │0xd5d3cc str x14, [x12, x23] │0xd5d3d0 b 0xd5d3b0 │0xd5d3d4 ret @ SYM_FUNC_END(__relocate_kernel)@ SYM_FUNC_START_LOCAL(__primary_switch)0xd5d3d8 mov x19, x0 // preserve new SCTLR_EL1 value 0xd5d3dc mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value 0xd5d3e0 adrp x1, 0x181d000 // 将init_pg_dir 给x1 //跳转过去初始化mmu 0xd5d3e4 bl 0xd5d308 //__enable_mmu //将kernel image的.rela.dyn段实现重定位 0xd5d3e8 bl 0xd5d390 //__relocate_kernel //注意ldr指令加载的是__primary_switched的链接地址,// 注意这里的链接地址已经是虚拟地址了(例子值为:0xffffffc008f902a0) 0xd5d3ec ldr x8, 0xd5d448 //__primary_switched给x8 0xd5d3f0 adrp x0, 0x200000 //__PHYS_OFFSET给x0 //注意这里的跳转指令,这个时候mmu已经生效了,由于目标地址为0xffffffc008f902a0,// 所以这个时候查询的页表为init_pg_dir 0xd5d3f4 blr x8 //跳转到__primary_switched 0xd5d3f8 isb 0xd5d3fc msr sctlr_el1, x20 0xd5d400 isb 0xd5d404 bl 0x1190040 0xd5d408 tlbi vmalle1 0xd5d40c dsb nsh 0xd5d410 isb 0xd5d414 msr sctlr_el1, x19 0xd5d418 isb 0xd5d41c ic iallu 0xd5d420 dsb nsh 0xd5d424 isb 0xd5d428 bl 0xd5d390 0xd5d42c ldr x8, 0xd5d448 0xd5d430 adrp x0, 0x200000 0xd5d434 br x8 @ SYM_FUNC_END(__primary_switch)这部分主要是在页表初始化好后,进行el1的一些cpu设置,并准备好mmu开启参数。
@ .pushsection ".idmap.text", "awx"@ SYM_FUNC_START(__cpu_setup)// Invalidate local TLB0x0000000000d5d6f4: tlbi vmalle10x0000000000d5d6f8: dsb nsh0x0000000000d5d6fc: mov x1, #0x300000 // #3145728// Enable FP/ASIMD0x0000000000d5d700: msr cpacr_el1, x1// Reset mdscr_el1 and disable0x0000000000d5d704: mov x1, #0x1000 // #4096// access to the DCC from EL00x0000000000d5d708: msr mdscr_el1, x1// Unmask debug exceptions now,0x0000000000d5d70c: isb0x0000000000d5d710: msr daifclr, #0x80x0000000000d5d714: mrs x1, id_aa64dfr0_el10x0000000000d5d718: sbfx x1, x1, #8, #40x0000000000d5d71c: cmp x1, #0x10x0000000000d5d720: b.lt 0xd5d728 // b.tstop0x0000000000d5d724: msr pmuserenr_el0, xzr0x0000000000d5d728: mrs x1, id_aa64pfr0_el10x0000000000d5d72c: ubfx x1, x1, #44, #40x0000000000d5d730: cbz x1, 0xd5d7380x0000000000d5d734: msr s3_3_c13_c2_3, xzr0x0000000000d5d738: mov x17, #0x400000000 // #171798691840x0000000000d5d73c: movk x17, #0x44, lsl #160x0000000000d5d740: movk x17, #0xffff0x0000000000d5d744: mov x16, #0x40000000000000 // #180143985094819840x0000000000d5d748: movk x16, #0x30, lsl #320x0000000000d5d74c: movk x16, #0xb559, lsl #160x0000000000d5d750: movk x16, #0x35190x0000000000d5d754: mrs x9, midr_el10x0000000000d5d758: mov x5, #0xffffffffffefffff // #-10485770x0000000000d5d75c: movk x5, #0xffff0x0000000000d5d760: and x9, x9, x50x0000000000d5d764: mov x5, #0x460f0000 // #11753881600x0000000000d5d768: movk x5, #0x100x0000000000d5d76c: cmp x9, x50x0000000000d5d770: b.ne 0xd5d788 // b.any0x0000000000d5d774: mov x5, #0x60000000000000 // #270215977642229760x0000000000d5d778: movk x5, #0x0, lsl #320x0000000000d5d77c: movk x5, #0x0, lsl #160x0000000000d5d780: movk x5, #0x00x0000000000d5d784: bic x16, x16, x50x0000000000d5d788: adrp x9, 0x15550000x0000000000d5d78c: ldr x9, [x9, #3272]0x0000000000d5d790: bfxil x16, x9, #0, #60x0000000000d5d794: mrs x5, id_aa64mmfr0_el10x0000000000d5d798: ubfx x5, x5, #0, #30x0000000000d5d79c: mov x6, #0x5 // #50x0000000000d5d7a0: cmp x5, x60x0000000000d5d7a4: csel x5, x6, x5, hi // hi = pmore0x0000000000d5d7a8: bfi x16, x5, #32, #30x0000000000d5d7ac: mrs x9, id_aa64mmfr1_el10x0000000000d5d7b0: and x9, x9, #0xf0x0000000000d5d7b4: cbz x9, 0xd5d7bc0x0000000000d5d7b8: orr x16, x16, #0x80000000000x0000000000d5d7bc: msr mair_el1, x170x0000000000d5d7c0: msr tcr_el1, x16/* * Prepare SCTLR, INIT_SCTLR_EL1_MMU_ON 给 x0 */0x0000000000d5d7c4: mov x0, #0x200000000000000 // #1441151880758558720x0000000000d5d7c8: movk x0, #0x20, lsl #320x0000000000d5d7cc: movk x0, #0x34f4, lsl #160x0000000000d5d7d0: movk x0, #0xd91d// return to head.S0x0000000000d5d7d4: ret0x0000000000d5d7d8: msr tpidr_el2, x130x0000000000d5d7dc: msr disr_el1, xzr@ SYM_FUNC_END(__cpu_setup)当我们进入__primary_switched的时候,这个时候用的是内核地址。同时地址翻译是由init_pg_dir来完成。此过程初始化init_task,将
@ SYM_FUNC_START_LOCAL(__primary_switched)//将init_task给x40xffffffc008f902a0 adrp x4, 0xffffffc00934f000 <inet6_offloads+1376> 0xffffffc008f902a4 add x4, x4, #0xb80 //init_cpu_task 0xffffffc008f902a8 msr sp_el0, x4 0xffffffc008f902ac ldr x5, [x4, #24] 0xffffffc008f902b0 add sp, x5, #0x4, lsl #12 0xffffffc008f902b4 sub sp, sp, #0x150 0xffffffc008f902b8 stp xzr, xzr, [sp, #304] 0xffffffc008f902bc add x29, sp, #0x130 0xffffffc008f902c0 adrp x5, 0xffffffc009349000 <event_hash+616> 0xffffffc008f902c4 add x5, x5, #0x7f8 0xffffffc008f902c8 ldr w6, [x4, #64] 0xffffffc008f902cc ldr x5, [x5, x6, lsl #3] 0xffffffc008f902d0 msr tpidr_el1, x5 //加载异常向量表地址 0xffffffc008f902d4 adrp x8, 0xffffffc008010000 <bcm2835_handle_irq> 0xffffffc008f902d8 add x8, x8, #0x800 0xffffffc008f902dc msr vbar_el1, x8 0xffffffc008f902e0 isb //将x29,x30放入到sp0xffffffc008f902e4 stp x29, x30, [sp, #-16]! 0xffffffc008f902e8 mov x29, sp // Save FDT pointer 0xffffffc008f902ec adrp x5, 0xffffffc009001000 <tmp_cmdline.73085+2040> 0xffffffc008f902f0 str x21, [x5, #920] // Save the offset between 0xffffffc008f902f4 adrp x4, 0xffffffc008b70000 <kimage_vaddr> 0xffffffc008f902f8 ldr x4, [x4] // the kernel virtual and 0xffffffc008f902fc sub x4, x4, x0 // physical mappings 0xffffffc008f90300 adrp x5, 0xffffffc008ebb000 <rt_sched_class+192> 0xffffffc008f90304 str x4, [x5, #3392] //Clear BSS 0xffffffc008f90308 adrp x0, 0xffffffc009529000 <__kvm_nvhe_cur> 0xffffffc008f9030c add x0, x0, #0x0 0xffffffc008f90310 mov x1, xzr 0xffffffc008f90314 adrp x2, 0xffffffc00961c000 <gssp_stats+24> 0xffffffc008f90318 add x2, x2, #0xf8 0xffffffc008f9031c sub x2, x2, x0 0xffffffc008f90320 bl 0xffffffc008664200 <memset> 0xffffffc008f90324 dsb ishst // pass FDT address in x0 0xffffffc008f90328 mov x0, x21 // Try mapping the FDT early 0xffffffc008f9032c bl 0xffffffc008f946c8 <early_fdt_map> // Parse cpu feature overrides 0xffffffc008f90330 bl 0xffffffc008f96354 <init_feature_override> //already running randomized? 0xffffffc008f90334 tst x23, #0xffffffffffe00000 0xffffffc008f90338 b.ne 0xffffffc008f90350 <__primary_switched+176> // b.any// parse FDT for KASLR options 0xffffffc008f9033c bl 0xffffffc008f96b80 <kaslr_early_init> // KASLR disabled? just proceed 这里直接跳转到 0xffffffc008f90350 0xffffffc008f90340 cbz x0, 0xffffffc008f90350 <__primary_switched+176> // record KASLR offset 0xffffffc008f90344 orr x23, x23, x0 // we must enable KASLR, return 0xffffffc008f90348 ldp x29, x30, [sp], #16 // to __primary_switch() 0xffffffc008f9034c ret // Prefer VHE if possible0xffffffc008f90350 bl 0xffffffc008021e6c <switch_to_vhe> 0xffffffc008f90354 ldp x29, x30, [sp], #16 //跳转到kernel_start 0xffffffc008f90358 bl 0xffffffc008f90c40 <start_kernel> 0xffffffc008f9035c brk #0x800 0xffffffc008f90360 msr tpidr_el2, x5 @ SYM_FUNC_END(__primary_switched)注意,阅读本文这部分时,需要对照着head.S来看,这样才有一个基本的认识。
本文得到的基本流程为,上电,保存上电后的基础寄存器,在el2的模式下初始化cpu,保存启动标志,初始化两个页表(注意,这部分是精华,我这部分注释也最多),然后初始化el1模式下的cpu,并准备好开启mmu,最后在__primary_switch里面开启mmu,当mmu开启后,这个时候的地址还是物理地址,所以我们需要一个映射物理地址和虚拟地址相等的页表(idmap_pg_dir),重定位符号表,最后加载__primary_switched的虚拟地址(其虚拟地址在kernel logical memory map中),跳转到执行(此时查表init_pg_dir),然后初始化init_task,最后进入start_kernel。
本文也没有尝试去完全注释每句代码,太繁杂了,对于我来说也没有意义,特别是一些特别的初始化,只有以后遇到了才去查询。Linux现代内核太大了,后面可能就要去看个人喜欢的模块,这样才有收获,不要尝试去阅读全部内容,那样太难了。
每个人对于这部分的关注可能都是不一样的,如果本文没有写的地方,可以尝试搜索对应的关键字,可以得到更多的信息。

PS: 请尊重原创,不喜勿喷。PS: 要转载请注明出处,本人版权所有。PS: 有问题请留言,看到后我会第一时间回复。