GitHub Bootstrap ARM
Email: BuddyZhang1 buddy.zhang@aliyun.com
Info: ARM 32 – Linux 5.0
目录
简介
实践
vmlinux 入口:第一行运行的代码
内核源码经过编译汇编成目标文件,再通过链接脚本链接成可执行 ELF 文件 vmlinux。vmlinux
位于源码顶层目录,以及使用的链接脚本是 arch/arm/kernel/vmlinux.lds.S。首先开发者应该
找到 vmlinux 的入口地址,然后从入口地址开始调试内核。为了确定 vmlinux 内核的起始地址,
首先通过 vmlinux.lds.S 链接脚本进行分析。
vmlinux.lds.S
vmlinux.lds.S 用于说明内核源码的链接过程,通过链接脚本将所有目标文件链接成一个
ELF 文件,这个文件就是源码顶层目录的 vmlinux。通过分析 vmlinux 对内核的启动
和内存布局都有学习有一定帮助。vmlinux.lds.S 位于 arch/arm/kernel 目录下,经过
预处理,vmlinux.lds.S 会生成 vmlinux.lds。为了了解 ARM linux 的启动过程,需要
通过对链接脚本的分析,从而找到 vmlinux 的入口函数。具体分析如下:
首先 vmlinux.lds.S 起始处的代码如上,根据链接脚本语法,可以知道 OUTPUT_ARCH 关键字
指定了链接之后的输出文件的体系结构是 ARM。ENTRY 关键字指定了输出文件 vmlinux 的入口
地址是 stext, 因此开发者只需找到 stext 的定义就可以知道 vmlinux 的入口函数。接下来
的代码是:
关键字 SECTIONS 定义了 vmlinux 内各个 section 的布局。其中关键 /DISCARD/ 定义
了 vmlinux 在生成过程了,将输入文件指定的段丢弃,这里不做过多讨论。接着链接脚本
指定了一下内存的地址,使当前地址指向了 “PAGE_OFFSET + TEXT_OFFSET”, 这里先看看
两个宏的函数,首先是 TEXT_OFFSET, 其定义在 arch/arm/Makefile, 如下:
因此可以知道 TEXT_OFFSET 的含义就是内核镜像在 DRAM 内的偏移,从上面的定义来看,
默认情况下,内核镜像位于 DRAM 0x00008000 处。接着看一下 PAGE_OFFSET 的定义,
定义在 arch/arm/include/asm/memory.h 里,如下:
从上面的定义可以知道,PAGE_OFFSET 用于指定内核镜像的起始虚拟地址。PAGE_OFFSET 的定
义与 CONFIG_PAGE_OFFSET 有关,该宏定义在 arch/arm/Kconfig 里,如下:
根据 CONFIG_PAGE_OFFSET 宏定义可以知道,其值与 MMU,VMSPLIT_1G, VMSPLIT_2G,
VMSPLIT_3G_OPT 的定义有关,这几个宏的定义如上。开发者可以在
BiscuitOS/output/linux-5.x-arm32/linux/linux/.config 中查看当前内核配置,其
本实践平台基于 vexpress 平台,使用的是 VMSPLIT_2G,所以 PAGE_OFFSET 的值为
0x8000000。
分析完上面两个宏的定义之后,再回到之前的链接脚本源码:
将当前地址设置为 “PAGE_OFFSET + TEXT_OFFSET”, 其含义就是内核镜像的起始虚拟地址
加上内核镜像在 DRAM 中的偏移。接着定义了一个输出段 .head.text, 段内容首先定义了
一个 _text 指向当前地址,然后将 HEAD_TEXT 加入到段内,HEAD_TEXT 的定义如下:
HEAD_TEXT 的定义使用了 KEEP 关键字,其作用就是保留所有输入文件的 .head.text 段。
更多 KEEP 使用方法,请看:
LD scripts 关键字 – KEEP
从上面的定义可以知道,输出文件的 .head.text 段包含了所有输入文件的 .head.text 段,
因此可以知道内核镜像文件中,最前面位置的代码位于 .head.text 段中,vmlinux 的入口
函数 stext 也可能位于 .head.text 段中,因此在 arch/arm/kernel 目录下查找含有
.head.text 段和 stext 的位置。通过查找,在 arch/arm/kernel/head.S 文件中找到
stext 的定义,如下:
从这里虽然找到了 stext 的定义,但还需要确定 stext 是否位于 .head.text 段中,通过
上面的源码,查看 __HEAD 的定义,如下:
因此,__HEAD 宏就是用于指定段属性为 “.head.text”, 因此根据此前的分析,
arch/arcm/kernel/head.S 中的 stext 就是 vmlinux 的入口函数。因此,开发者可以从
stext 处开始调试内核。开发者可以在 stext 处添加断点,然后 GDB 在线调试,调试
情况如下:
从上面的调试可以看出,stext 确实是内核所在的物理起始地址,此时 pc 正好指向内核
解压之后的起始地址 0x60008000, 因此 arch/arm/kernel/head.S 目录下的内容就是
vmlinux 的入口函数 stext 所在的位置。接下来,开发者可以从这个文件开始,一步步
调试内核源码。
首先,开发者可以查看一下这部分代码的注释介绍,如下:
注释中说明了 stext 为内核启动的入口,当 zImage 解压完内核之后,这里就会被最先调用。
为了调用这个入口,MMU 必须关闭,D-cache 必须关闭,I-cache 无所谓,r0 必须为 0,
r1 必须存储体系相关的 ID,以及 r2 必须存储着 ATAG 或者 DTB 的指针。内核镜像的位置
通常都是独立的,如果需要将内核镜像的虚拟地址设置为 0xc0008000, 那么内核镜像的物理
地址可以通过 __pa(0xc0008000) 获得。r1 寄存器中包含了体系相关的 ID 信息,内核在
linux/arch/arm/tools/mach-types 文件中描述了 ID 对应的信息。这里还提示,如果
没有合适的理由,不要试图这段代码。开发者可以使用 stext 作为断点,然后使用 GDB 进行
调试,以此实践上面的内容,实践如下:
首先查看当前地址是 0x60008000, 因此此时内核和用户空间划分采用的 1:1。然后 r1 寄存器的值是
0x8e0,此时查看 arch/arm/tools/mach-types 文件,查看对应的内容如下:
从中可知,目前实践采用的是 vexpress。由于本实践采用了 DTB,因此 r2 寄存器的值指向了 DTB 在
内存中的位置。分析完上面的代码之后,接下来分析源码如下:
由于本实践不支持 THUMB 以及 ARM_BE8, 但支持 CONFIG_ARM_VIRT_EXT 宏,因此上面真正执行的代码
只有 “bl __hyp_stub_install” 行代码,其源码如下:
__hyp_stub_install 也很简单,直接调用 store_primay_cpu_mode 宏,该宏的定义与 ZIMAGE 宏的定义
有关,通过本实践可以知道,ZIMAGE 宏是支持的,因此 store_primay_cpu_mode 的函数定义如下:
从上面的定义来看, store_primay_cpu_mode 不做任何动作,因此 __hyp_stub_install 直接调用
__hyp_stub_install_secondary, 通过上面的源码可以知道,__hyp_stub_install_secondary 首先
执行 “mrs r4, cpsr” 代码,以此获得当前模式的 CPSR 寄存器,然后执行 “and r4, r4, #MODE_MASK”
代码,以此从 CPSR 中独立出模式域。接着调用 compare_cpu_mode_with_primary, 与 store_primary_cpu_mode
一样,其定义与 ZIMAGE 宏有关,因此其定义如下:
因此,__hyp_stub_install_secondary 宏也不做任何实际工作买就是将 CPSR ZF 置位。接下
会判断对比的结果是否为零,如果不为零,则调用 “retne lr” 返回,但是从上面的分析,那么
这里为零不可能返回,那么接下来调用 “cmp r4, #HYP_MODE” 代码,以此检查当前模式是否
是 HYP 模式,如果不是就调用 “retne lr” 命令进行返回。由于在本次实践中,当前模式不是 HYP
模式,因此直接返回。由于剩下的代码不被执行,这里不做分析,开发者可以通过 GDB 断点调试这部分
代码,调试情况如下:
实践结果和预期分析一致,这里对 HYP 模式不做过多解释,接下来执行的代码如下:
这段代码主要是进入 SVC 模式并且屏蔽所有的中断,由于此时已经是 SVC 模式了,因此
不对该代码进行讲解,接下来执行的代码如下:
接下来通过 mrc 指令从 cp15 c0 中获得体系相关的 ID 信息,此时 CP15 C0 的布局如下:
此时选中了 MIDR (Main ID Register) 寄存器,此时 MIDR 的布局如下:
此时 r9 寄存器存储这体系相关 ID 信息,并调用 __lookup_processor_type, 该函数源码如下:
__lookup_processor_type 首先调用 adr 指令获得 __lookup_processor_type_data 的物理地址,
__lookup_processor_type_data 的定义如上,__lookup_processor_type_data 定义为一个 object,
内部定义了三个 long 变量,第一个 long 变量存储了 __lookup_processor_type_data 的虚拟地址;第二个
long 变量存储了 __proc_info_begin 地址;第三个 long 变量存储了 __proc_info_end 地址。
__proc_info_begin 和 __proc_info_end 的定义在 arch/arm/kernel/vmlinux.lds.S 中,如下:
定义在 ARM_TEXT 中,其定义在 arch/arm/kernel/vmlinux/lds.h 里,如下:
其中 ARM_CPU_KEEP(PROC_INFO) 包含了 __proc_info 相关的段,定义如下:
由于本实践中,宏 CONFIG_HOTPLUG_CPU 启用,因此 ARM_CPU_KEEP 宏将 PROC_INFO 段
保留下来了,从定义可以看出,PROC_INFO 链接时将所有输入文件的 .proc.info.init 段都
加在了 ARM_TEXT 段的最后,即输出文件 vmlinux 的 .text 的末尾。并且定义了两个变量
__proc_info_begin 和 __proc_info_end 指向所有 .proc_info_init 段的起始虚拟地址
和终止虚拟地址。.proc.info.init 对应的数据结构定义在 arch/arm/include/asm/procinfo.h
文件中,定义如下:
获得上面的定义之后,开发者继续在 arch/arm/ 目录下查找包含 .proc.info.init 的段,由于本实践
基于 ARMv7 架构,因此找到 arch/arm/mm/proc-v7.S 中包含该段,定义如下:
里面定义了很多 struct proc_info_list 结构,每个只做与本实践有关的分析,其他分析请
参考完成。通过上面的分析之后,可以知道 ARM 内核是如何定义 __proc_info table. 再回到
__lookup_processor_type 函数,代码如下:
在获得 __lookup_processor_type_data 地址之后,调用 “ldmia r3, {r4 - r6}” 代码,将
__lookup_processor_type_data 定义的 3 个 long 分别赋值到 r4,r5,r6 寄存器中,此时
r4 寄存器存储这 __lookup_processor_type_data 的虚拟地址; r5 寄存器存储着
__proc_info_begin 的虚拟地址;r6 寄存器存储着 __proc_info_end 的虚拟地址。
接着调用 “sub r3, r3, r4” 代码,此时 r3 寄存器存储着 __lookup_processor_type_data 物理地址,
r4 寄存器存储着 __lookup_processor_type_data 的虚拟地址,所以这条代码的作用就是计算虚拟地址
转换成物理地址的偏移,接着连续调用几条 add 指令就是将 __proc_info_begin 和 __proc_info_end
的虚拟地址转换成对应的物理地址。此时 r5 寄存器存储着 __proc_info_begin 的物理地址,接着调用
“ldmia r5, {r3, r4}” 代码,ldmia 指令,获得 r5 对应物理地址处两个 long 字节的内容,存储到
r3 和 r4 寄存器中。从之前的分析可以知道 arch/arm/mm/proc-v7.S 的 .proc.info.init 段中
包含了 ARMv7 家族支持的 proc_info 表,因此接下来的代码就是根据体系相关的 ID 找到对应的
proc_info。因此执行完 “ldmia r5, {r3, r4}” 代码之后,r3 寄存器存储着每个 struct proc_info_list
结构的 cpu_val 值,r4 寄存器存储着 cpu_mask 的值,由之前的代码可以知道,此时 r9 寄存器存储着
体系相关的 ID 信息,调用 “and r4, r4, r9” 指令进行掩码操作,获得预期的结果。接着调用
“teq r3, r4” 命令,以此查看是否与 r3 寄存器的值相同,如果相同,那么跳转到 2,并直接返回;
如果不相同,那么将 r5 寄存的值加上 PROC_INFO_SZ,以此遍历到下一个 struct proc_info_list
结构,接着调用 cmp 指令判断此时是不是已经到达 __proc_info_end,如果到达则将 r5 寄存器
的值设置为 0,表示没有找打对应的寄存器;如果没有达到,那么继续查找剩下的
struct proc_info_list。通过上面的代码,CPU 可以找到对应的 proc_info_list 结构。
开发者可以在适当的位置添加断点,然后使用 GDB 调试这个过程,实践情况如下:
从上面的实践可以知道,__lookup_processor_type_data 中的第二个项就是需要查找的对象,
可以通过 arch/arm/mm/proc-v7.S 查看对应的 proc_info 项如下:
因此,从这里实践可以知道,实践所使用的 CPU 是 Cortex A9,以及对应的 struct proc_info_list
信息。这里以后会派上用场。接着代码继续执行,如下:
从 __lookup_processor_type 返回之后,r5 寄存器存储着与体系相关的 proc_info_list 结构
的地址,如果 __lookup_processor_type 没有找到对应的 proc_info_list, 那么 r5 寄存器
的值为 0,根据这个信息,执行 “movs r10, r5” 和 “beq __error_p” 代码,如果 r5 为零,
那么跳转到 __error_p 处执行。由于实践结果可知,不会跳转到 __error_p,所以不对 __error_p
进行分析,继续执行代码:
由于实践中宏 CONFIG_ARM_LPAE 和宏 CONFIG_XIP_KERNEL 都没有启用,因此接下来执行的代码
“adr r3, 2f”, 这段代码是获得 2f 的地址,2f 处定义的内容如下:
2f 处定义了两个 long 变量,第一个 long 存储 2 的虚拟地址;第二个存储 PAGE_OFFSET 的值,
也就是内核镜像起始虚拟地址。上面代码中,使用 “ldmia r3, {r4, r8}” 代码将 2f 的虚拟地址
存储到 r4 寄存器,并将 PAGE_OFFSET 的值存储到 r8 寄存器中。有上面的代码可以知道
r3 存储着 2f 的物理地址,r4 寄存器存储着 2f 的虚拟地址,调用 “sub r4, r3, r4”
就是将 PHYS_OFFSET 减去 PAGE_OFFSET, 然后调用 “add r8, r8, r4” 计算真实的
PHYS_OFFSET。 开发者可以在适当的位置加上断点,然后使用 GDB 进行调试,调试的情况如下:
通过上面的计算,算出了 PHYS_OFFSET 是 0x60000000。接下来执行的代码是:
在执行 __vet_atags 之前,r1 寄存器存储着 Machine 号;r2 寄存器指向 atags 或者 dtb;
r8 存储 PHYS_OFFSET; r9 存储着体系相关的 ID; r10 寄存器存储着 proc_info。接着
执行的代码如下:
从注释可以知道,这段代码的主要任务就是:这里通过代码检测 ATAGS/DTB 的合法性。如果 Uboot
通过 ATAGS 方式传递参数给 kernel,ATAG_CORE 会在 RAM 的第一个 16K 处。但如果
CONFIG_OF_FLATTREE 宏启用,那么也会检查 DTB 的合法性。代码首先检查 r2 寄存器的的对齐
方式,如果没有对齐,则直接跳转到 1 处,并将 0 赋值给 r2 寄存器,然后直接返回。但如果
r2 寄存器已经对齐,那么使用 ldr 指令读取 r2 对应内存处的内容到 r5 寄存器。由于本实践
CONFIG_OF_FLATTREE 宏已经启用,那么接下来将 OF_DT_MAGIC 传递给 r6 寄存器,然后对比
r5 的内容是否与 r6 寄存器的一致。这里 OF_DT_MAGIC 的定义如下:
开发者可以在上面代码适当位置添加断点,然后使用 GDB 进行调试,调试的情况如下:
从上面的实践可以看出,已经检测到 DTB 的存在,并且合法。因此这里就不在继续介绍 ATAGS
相关的代码。继续执行 head.S 里面的代码,如下:
由于本实践启用了 CONFIG_SMP_ON_UP 宏,因此接下来执行的代码是:
代码首先检查 r9 是否支持,这里先看一下 ARMv7 的 MIDR 寄存器,其布局如下:
其中 MDIR[19:16] 对应的是 Architecture 域,其域值定义如下:
此时在使用的位置加上断点,使用 GDB 调试这段代码,调试情况如下:
从实践可以看出,MIDR 的 Architecture 域由 CPUID 决定,因此继续执行如下代码:
代码首先将 r9 寄存器的 16 到 23 bit 都清零了,然后再清零 0 到 3 bit, 以此构造一个
掩码 0xff00fff0, 接下来使用 orr 指令,构造 r4 寄存器的值为 0x4100b020。接着将
0xff00fff0 与 0x4100b020 进行对比,以此确定 CPU 是不是 ARM11MP。如果是,那么就
直接返回。开发者可以在适当的位置加上断点,然后使用 GDB 进行调试,调试的情况如下:
从上面的调试结果看出,该 CPU 不是 ARM11MP。那么接下来执行的代码如下:
首先调用 mrc 指令读取 MPIDR 寄存器的值,存储到 r0 寄存器中,然后将 r0 的值与 0xc0000000
相与,结果存储在 r0 寄存器中,这里 MPIDR 寄存器的布局如下:
其中 ARMv7 中不支持 Multiprocessing Extensions,因此 MPIDR[31:28] 对应的域 Reserved。
因此这里 bne 指令不会跳转。开发者在适当的位置添加断点,然后使用 GDB 进行调试,调试情况
如下:
上面的实践结果符合预期,接下来继续执行代码:
这段代码主要检查 Core 是否是 SMP,检查 Aegis SOC 是否是一个单片的 Cortex-A9 但
SMP 操作是失败的。首先使用 orr 指令是 r4 寄存器的值为 0x4100c090,然后调用 teq 指令,
如果对比结果不为零,那么直接退出,代表 CPU 不是 ARM Cortex-A9。开发者在适当的位置
添加断点,然后使用 GDB 进行调试,调试结果如下:
通过实践可以知道 CPU 是 Cortex-A9, 因此继续执行代码:
接着调用 mrc 指令读取了 p15 c15 寄存器,VMSA CP15 c15 寄存器是
IMPLEMENTATION DEFINED 寄存器。ARMv7 预留 CP15 c15 为 IMPLEMENTATION DEFINED
目的用,请具体描述可以查看手册,这里不做过多解释:
根据 Cortex-A9 手册可以知道 c15 系统控制寄存器布局如下:
通过 “mrc p15, 4, r0, c15, c0” 选中了 Configuration Base Address, 其布局如下:
回到代码,如果 Configuration Base Address 的值为 0,那么这个 CPU 是一个 A9 UP.
如果 r0 寄存器的值不为 0,那么通过 ldr 指令读取 SCU 的配置。更多 SCU 信息请查看
Cortex-A9MP 手册。SCU Configuration 寄存器的布局如下:
接着执行代码 “and r0, r0, #0x3”, 获得 SCU Configuration 寄存器的 LSB 低 2bits,
低 2 位用于指定 CPU 的数量。如果此时 r0 寄存器的值为 0,那么代表系统只有一个 Cortex-A9
CPU,那么 retne 指令不执行。开发者在适当的位置添加断点,然后使用 GDB 进行调试,调试
情况如下:
通过上面的实践可以知道系统只包含一个 CPU,因此继续执行如下代码:
代码首先获得 1f 处的地址,然后使用 ldmia 指令读取 1 的虚拟地址到 r3 寄存器;
读取 __smpalt_begin 虚拟地址到 r4 寄存器;读取 __smpalt_end 的虚拟地址到
r5 寄存器。然后使用 “sub r3, r0, r3” 调整物理地址与虚拟地址之间的偏移,然后
连续调用 add 指令调整 r4,r5 寄存器,以此获得虚拟地址对应的物理地址。在调用
__do_fixup_smp_on_up 之前,这里先分析一下 __smpalt_begin 和 __smpalt_end。
其定义在 arch/arm/kernel/vmlinux.lds.S,如下:
从中可以看出,__smpalt_begin 指向了输出文件 vmlinux .init.smpalt section 的
起始地址,而 __smpalt_end 指向该 section 的终止地址。.init.smpalt section
是由输入文件的 .alt.smp.init section 构成的。.alt.smp.init section 存在于
arch/arm/include/asm/processor.h 和 arch/arm/include/asm/assembler.h 文件中,
如下:
首先查看 __ALT_SMP_ASM 宏,这个宏的作用就是将当前 section 指针指向 .alt.smp.init,
并在进入 .slt.smp.init section 之前就在当前 section 做好了标号 9998 指向参数 smp。
对于 ALT_UP 宏,则将指令都放到了 .alt.smp.init 中,总之上面的宏定义都使用了
.pushsection 和 .popsection 两个伪指令,用意就是让当前 section 指向 .alt.smp.init
section. 更多细节文档请参考:
内核动态补丁(kpatch)及 kpatch pushsection popsection previous的解释
Linux Kernel SMP & nbsp…
开发者可以在 arch/arm/ 目录下查找含有 .alt.smp.init section 的源码。开发者可以在
之前的代码处添加适当的断点,然后使用 GDB 进行调试,调试的情况如下:
在准备好条件之后,函数执行 bl 指令跳转到 __do_fixup_smp_on_up 处执行,代码如下:
此时 r4 寄存器存储 __smpalt_begin, 即 .alt.smp.init section 的起始地址,而 r5
寄存器存储着 __smpalt_end,即 .alt.smp.init section 的结束地址。然后调用 cmp
指令检查 r4 是否已经等于 r5 寄存器,以此检查循环是否结束。如果不相等,那么循环继续,
继续调用 ldmia 指令将 r4 对应地址的内容拷贝到 r0 和 r6 寄存器,由于本次实践不支持
THUMB,那么接下来执行 “str r6, [r0, r3]”, 将 r6 寄存器的值写入到 “r0+r3” 对应的
内存地址上。这样起到了校正 .alt.smp.init section 的作用。开发者可以在适当的位置
添加断点,然后使用 GDB 进行调试,调试情况如下:
实践的结果和预期的一致,r6 寄存器的值已经更新到 “r0+r3” 对应的内存地址上了。经过
多次循环后,函数执行返回。接下来执行的代码是 arch/arm/kernel/head.S
__fixup_pv_table 的主要任务也就是调整 PV table 的地址。函数首先通过 adr 指令获得
1 处的物理地址,然后通过 ldmia 指令,将 1 处的虚拟地址存储在 r3 寄存器;将
__pv_table_begin 的虚拟地址存储在 r4 寄存器;将 __pv_table_end 的虚拟地址存储在
r5 寄存器;将 __pv_phys_pfn_offset 的虚拟地址存储在 r6 寄存器;将 __pv_offset
的虚拟地址存储在 r7 寄存器。接着使用 “mvn” 指令按位取反 0,即将 0xffffffff 的值存储
到 IP 寄存器,然后通过 “subs r3, r0, r3” 代码计算物理地址与虚拟地址之间的偏移,
然后通过 add 指令,分别获得 __pv_table_begin, __pv_table_end, __pv_phys_pfn_offset,
以及 __pv_offset 的物理地址。此时 r8 寄存器存储着修正后的 PHYS_OFFSET, 这里调用
“mov r0, r8, lsr #PAGE_OFFSET” 命令获得内核镜像起始地址对应的页帧号。然后将修正后的
值存储到 __pv_phys_pfn_offset 里,接着将 0xffffffff 存储到 __pv_offset+4 处。
将 r3 寄存器的值右移 24 位并存储到 r6 寄存器,然后将 r6 寄存器载左移 24 位。以此
达到 16M 对齐,通过 teq 检查 r3 寄存器的值是否按 16M 对齐,如果没有,那么就直接跳转
到 __error;如果按 r3 按 16M 对齐,那么将使用 str 指令,将 r3 的值存储到
__pv_offset 地址处。最后跳转到 __fixup_a_pv_table. 开发者可以在适当的位置添加
断点,然后使用 GDB 进行调试,调试的情况如下:
从调试结果来看,和预期分析的一致。这里继续讲解 __pv 相关的表。首先 __pv_table_begin
和 __pv_table_end, 其定义在 arch/arm/kernel/vmlinux.lds.S 里,如下:
因此 __pv_table_begin 指向输出文件 vmlinux 的 .init.pv_table section 的起始地址,
__pv_table_end 指向 .init.pv_table section 的结束地址。__pv_offset 和
__pv_phys_pfn_offset 定义在 arch/arm/kernel/head.S 里面,如下:
这些定义的具体含义以后再做介绍,这里继续分析接下来执行的代码,如下:
首先调用 adr 获得 3f 对应的物理地址存储到 r0 寄存器中,然后将 __pv_offset 的地址
加载到 r6 寄存器,接着再通过 r3 寄存器调整为 __pv_offset 对应的物理地址。
此时内核将 r6 寄存器高 4 字节的内容存储到 r0 寄存器,再将 r6 寄存器低 4
字节的内容存储到 r6 寄存器,并将 r6 寄存器的值向右移动 24 bit,内核继续调用
cmn 和 moveq 两条指令设置 r0 寄存器的值。接下来就是比较重点的内容,内核
首先获得 __pv_table_begin 的地址,然后使用循环遍历表中每一项,表中的每一项
就是一个 long 变量,其指向一个段指令所在的地址,内核通过 “ldr ip, [r7, r3]”
指令获得一条指令所在的地址,这里指的注意的是。__pv_table 里面的内容基本
都是 “ADD/SUB” 指令,因此内核通过修改这两条指令的立即数域,以此修改虚拟地址
和物理地址转换的偏移,如下:
两个指令的机器码格式中,shifter_operand 域都是一致,其定义如下:
上面为 ARMv7 中立即数的表示方式,其包含了 rotate_imm 域和 immed_8 域,
ARMv7 通过将 immed_8 域的值向右循环移动 (2 * rotate_imm 域值),即:
因此 __pv_stub 宏将指令添加到 .init.pv_table section 之后,可以通过
__pv_table_begin 遍历里面的所有指令,并可以修改每条 ADD 或 SUB
指令的指定域,其中包括立即数域。内核首先调用 “bic ip, ip, #0xff”
指令清除 ADD/SUB 指令的 immed_8 域,然后调用 “tst ip, #0xf00”
指令检查 ADD/SUB 指令的 rotate_imm 域是否有效,如果有效,那么
将 r6 寄存器的内容,即物理地址和虚拟地址之间的偏移存储到指令的
立即数域。这样就完成了内核对 .init.pv_table section 内容的修改。
__create_page_table 的主要作用就是初始化内核页表,但这个页表是空的,要等待内核
对页表的内容进行初始化。在指向 __create_page_table 之前,r8 指向 PHYS_OFFSET,
r9 存储着 CPUID 信息,r10 存储着 proc_info 信息。函数首先调用 pgtbl 宏,定义
如下:
pgtal 宏接收两个参数 rd 和 phys。该宏首先将 phys 参数加上 TEXT_OFFSET 获得内核
镜像的物理地址,并存储在 rd 里面。 TEXT_OFFSET 代表内核镜像在 RAM 上的偏移。接着
将内核镜像的物理地址减去 PG_DIR_SIZE, 以该地址作为内核页表的起始地址,并存储
在 rd 中。如之前的代码 “pgtbl r4, r8”, 此时 r8 存储 PHYS_OFFSET, 也就是 RAM
的起始地址,执行完代码之后,r4 指向内核页表的起始物理地址。开发者可以在适当
的位置添加断点,然后使用 GDB 进行调试,调试情况如下:
从上面的实践可以知道,全局页目录的物理地址是 0x60004000。在分析代码之前,这里补充
一个重要的知识点 swapp_pg_dir 变量,其定义如下:
swapper_pg_dir 是一个指向全局页表的指针,swapper_pg_dir 包含的是一个虚拟地址。
内核将页表放在 KERNEL_RAW_VADDR 之前的 16K 虚拟地址上。KERNEL_RAW_VADDR 的定义
是 PAGE_OFFSET + TEXT_OFFSET, PAGE_OFFSET 代表 RAM 起始物理地址对应的虚拟地址,
TEXT_OFFSET 代表内核镜像在 RAM 中的偏移,因此 KERNEL_RAW_VADDR 代表内核镜像起始
物理地址对应的虚拟地址。因此这里定义了一个变量 wapper_pg_dir 的地址就是
KERNEL_RAW_VADDR - PG_DIR_SIZE, 这就是页表起始虚拟地址。因此在
arch/arm/include/asm/pgtable.h 里面定义了 swapper_pg_dir 引用声明:
因此,当 MMU 启用之后,可以通过引用 swapper_pg_dir 来访问全局页表。分析完
swapper_pg_dir 之后,继续分析 __create_page_table,接下来执行的代码是:
在使用 pgtbl 获得全局页目录的物理地址之后,将其存储在 r4 寄存器中,总共有 16K
那么大。接下来执行的任务就是清除全局页目录里面的内容,为写入新内容做准备。首先执行的
两条 mov 指令将 r0 寄存器指向了全局页目录的首地址,并将 0 存储到 r3 寄存器里。
然后调用 add 指向,将 r0 寄存器的值加上 PG_DIR_SIZE,以此获得全局目录的结束地址,
并存储到 r6 中。然后连续使用 4 条 str 指令,将 r3 寄存器的值写入到 r0 开始处的 16 个
字节里,由于 r3 寄存器里存储着 0,那么这样操作就清除了 r0 开始处 16 个字节的内容。
接着调用 teq 指令,判断 r0 是否已经到达全局页目录的结束地址,如果没有,则跳转到 1b
处继续执行,知道 16K 内容全部清理。开发者可以在适当位置添加断点,然后使用 GDB 进行
调试,调试情况如下:
从上面的调试情况可以看出,代码已经将页表清零。接下来执行的代码如下:
上面的代码主要任务就是获得 ARMv7 Cortex-A9 对应的 struct proc_info_list
结构的 __cpu_mm_mmu_flags 成员。接下来用于介绍如何获得 ARMv7 Cortex-A9 对应的
struct proc_info_list 中指定成员。
每个 ARM CPU 对对应一个 struct proc_info_list 结构,该结构维护着 mmu,pgtable
等操作函数和标志,本文档讲解基于 ARMv7 Cortex-A9, 因此接下来的内容讲解如何
获得 ARMv7 Cortex-A9 对应的 struct proc_info_list。首先 struct proc_info_list
定义在 arch/arm/include/asm/procinfo.h 中,如下:
然后 ARMv7 Cortex-A9 的 struct proc_info_list 的内容定义在 arch/arm/mm/proc-v7.S
中,并且通过之前 __proc_info 的分析,可以知道 ARMv7 Cortex-A9 对应的 proc_info
是 __v7_ca9mp_proc_info, 具体实践过程请看:
__proc_info 源码分析实践
接着查看 __v7_ca9mp_proc_info 的定义,如下:
通过上面的内容可以看出 struct proc_info_list 对应的内容,这其中使用了 __v7_proc 宏
来计算 struct proc_info_list 相关的成员,这里重点分析 __v7_proc 宏的实现过程,
源码如下:
通过上面的分析,直接构造 ARMv7 Cortex-A9 的 struct proc_info_list 内容如下:
分析完上面的代码,继续分析 __create_page_tables,代码如下:
通过上面的代码,将从 __v7_ca9mp_proc_info 中读取 __cpu_mm_mmu_flags 的值,开发者
可以在适当位置添加断点,然后使用 GDB 调试,调试情况如下:
从实践结果来看,ARMv7 Cortex-A9 的 proc_info_list 结构中,__cpu_mm_mmu_flags
的值为 0xc0e, 正好等于上面分析的 __v7_ca9mp_proc_info.__cpu_mm_mmu_flags
的值,其由多个宏构成,定义如下:
接着继续分析 __create_page_tables 代码,如下:
这段代码的主要任务就是给 __enable_mmu 函数建立对应的页表。
通过 adr 指令,获得了 __turn_mmu_on_loc 的地址,并调用 ldmia 指令将
__turn_mmu_on_loc 的虚拟地址存储在 r3 寄存器中;将 __turn_mmu_on 的虚拟地址存储
在 r5 寄存器中;最后将 __turn_mmu_on_end 的地址存储在 r6 寄存器。接着按照
常用方法,首先计算出物理偏移,然后校正 r5 和 r6 为 __turn_mmu_on 和
__turn_mmu_on_end 对应的物理地址。将 r5 和 r6 寄存器的值都向右移 SECTION_SHIFT,
接着再将 r5 左移 SECTION_SHIFT,以此达到对齐的作用。将 r5 左移之后的结果与 r7
相或,通过之前的分析,r7 寄存器存储着 proc_info_list.__cpu_mm_mmu_flags 的值。
接着让 r5 寄存器左移 PMD_ORDER 位之后与 r5 相加会得到一个页表地址,此时调用 str
指令将 r3 的值写入到对应的页表里面。然后比较现在有没有到达 __turn_mmu_on,如果到达,
则调过带 lo 的指令继续执行;如果没有到达,则执行带 lo 的指令。开发者可以在适当
的位置添加断点,然后使用 GDB 进行调试,调试的情况如下:
从上面的实践可以看到为 __enable_mmu 函数建立页表的过程。接下来执行的代码是:
这段代码的主要任务就是:从 RAM 起始虚拟地址到内核镜像(包括 .bss 段)结束的虚拟地址
建立页表。首先将 PAGE_OFFSET 向右移 (SECTION_SHIFT - PMD_ORDER) 位,然后加上
r4 寄存器,此时 PAGE_OFFSET 代表 RAM 起始的虚拟地址,r4 存储全局页目录的物理地址,
因此 “add r0, r4, #PAGE_OFFSET » (SECTION_SHIFT - PMD_ORDER)” 的左右就是获得
RAM 起始虚拟地址对应的全局页目录的入口地址。接着调用 “ldr r6, =(_end - 1)”, 通过
这段代码获得内核镜像结束虚拟地址,这个结束地址是包含了 .bss section 的。接下来执行
“orr r3, r8, r7” 代码, 此时 r8 代表 PHYS_OFFSET,这里就是构建第一块物理内存的
的页表内容,即物理地址加上 flags,将结果存储在 r3 寄存器中。接着调用
“add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)”, 通过这段代码就可以获得
内核镜像结束的虚拟地址对应的页目录入口地址,以此作为循环结束的标志。接下来就是
循环将指定的内容填入全局页表里,首先调用 “str r3, [r0], #1 « PMD_ORDER” 代码,
将 r3 寄存器的值也就是页表内容写到 r0 对应的地址,写完之后再将 r0 寄存器的值增加
(1 « PMD_ORDER), 这样就可以指向下一个页表入口。接着执行代码
“add r3, r3, #1 « SECTION_SHIFT”, 通过 add 指令,将 r3 寄存器对应的值增加
(1 « SECTION_SHIFT), 也就是指向下一段内存区块。最后在调用 cmp 指令检查页表项是不是
到达最后一项,如果不是,执行 bls 指令继续循环页表写入操作。开发者可以在适当
的位置加入断点,然后使用 GDB 进行调试,调试的结果如下:
从上面的调试结果可以看出内核为 RAM 到内核镜像结束的虚拟地址创建页表的过程。接下来
执行的代码是:
接下来这段代码是为 uboot 传递给内核的 ATAGS 参数或者 DTB 对应的虚拟地址建立页表。
此时 r2 寄存器存储着 ATAGS 参数物理地址或者 DTB 的物理地址;r8 寄存器存储着
PHYS_OFFSET。首先将 r2 对应的物理地址按 SECTION_SHIFT 对齐,并将对齐之后的值存
储在 r0 寄存器,然后通过 “subne r3, r0, r8” 代码进行地址校正,然后在将 r3 寄存器
的值加上 PAGE_OFFSET,以此获得 ATAGS 或者 DTB 对应的虚拟地址。有了虚拟地址之后,
就通过向右移动 (SECTION_SHIFT - PMD_ORDER) 位,然后加上 r4 页表基地址,以此获得
ATAGS 或 DTB 虚拟地址对应的页表入口地址。通过 “orrne r6, r7, r0” 构建页表内容,
上面动作都完成之后,调用 strne 指令将 r6 页表内容写 r3 页表项入口地址,写完之后
将 r3 增加 (1 « PMD_ORDER) 以此指向下一个页表项入口地址,此时调用
“addne r6, r6, #1 « SECTION_SHIFT” 构建下一个页表项的内容,并调用
“strne r6, [r3]” 代码将页表内容写入到页表项里, 最后执行返回。从上面的分析可以知道,
内核一共为 ATAGS 或者 DTB 创建了 2M 的页表。开发者可以在适当的位置添加断点,然后
使用 GDB 进行调试,调试的情况如下:
上面的实践结果和预期分析的一致,支持内核前期的页表构建完成,接下来执行的代码是:
这段代码主要是对当前各个寄存器的功能做了说明。此时 r1 寄存器存储着机器类型
信息;r2 寄存器存储着 atags/dtb 的指针;r4 寄存器存储着全局页表的基地址; r5 寄
存器存储着页表的高字节 (LPAE 模式);r8 寄存器存储着 PHYS_OFFSET;r9 寄存器
存储着 CPUID;r10 寄存器存储着 proc_info_list 的地址;r13 寄存器存储着
__enable_mmu 的虚拟地址,最终指向 __turn_mmu_on。这里使用 ldr 指令,将
r13 指向 __mmap_switched 的虚拟地址。接下来执行的代码是:
这段代码的主要作用就是调用 ARMv7 Cortex-A9 对应的 proc_info_list 结构的
__cpu_flush。首先调用 badr 指令将 1f 处的地址存储到 lr 寄存器中,以此返回时候
直接跳转到 1f 处,1f 处包含了跳转到 __enable_mmu 的代码。接下来将 r4 寄存器的值
存储到 r8 寄存器,注释上解释这行代码的主要是为了设置 TTBR1 寄存器。接着调用
ldr 寄存器,将 “r10 + PROCINFO_INITFUNC” 指向的地址存储到 r12 寄存器中,通过
查找 PROCINFO_INITFUNC 的定义为 16,此时 r10 寄存器指向 ARMv7 Cortex-A9 对应的
proc_info_list 结构,在通过 PROCINFO_INITFUNC 偏移,此时指向了 proc_info_list
结构的 __cpu_flush。在调用 “add r12, r12, r10” 校正了 r12 的地址,更多关于
r10 寄存器 proc_info_list 内容,请查看:
ARMv7 Cortex-A9 proc_info_list 解析
接着调用 “ret r12” 跳转到指定函数处运行,通过上面的文档可以知道,此时对于的位置
是:__v7_ca9mp_setup。在适当的位置添加断点,然后使用 GDB 进行调试,调试的情况如下:
调试情况和预期一致,接下里执行的代码如下:
__v7_setup 的主要作用就是初始化 TLB,Caches,以及将 MMU 的状态设置为 on。
在执行 __v7_setup 之前,将 r4 用于 TTBR0 寄存器器,r8 寄存器用于 TTBR1 寄存器;
r9 寄存器存储着 ID 信息。经过之前的分析,__v7_setup 首先执行 __v7_ca9mp_setup,
首先将 1 存储到 r10 寄存器中,然后调用 ldr 指令将 __v7_setup_stack_ptr 的物理
地址存储到 r0 寄存器里。这里分析一下 __v7_setup_stack_ptr,其定义如上,
定义一个 word,其内容是 __v7_setup_stack 与当前地址之间的差值。__v7_setup_stack
位于 .bss 段,__v7_setup_stack 只建立了 7 个寄存器大小的空间,通过
__v7_setup_stack_ptr 可以计算出堆栈的位置。接下来执行的代码是:
这段代码的作用就是找到本地堆栈,然后在调用 v7_invalidate_l1 之前将 r1, r2, r3, r4,
r5, r6, 以及 lr 寄存器的值压入堆栈,然后调用 v7_invalidate_l1。代码首先调用
ldr 获得堆栈的偏移,然后通过 add 指令计算出堆栈的物理地址,接着调用 stmia 指令
将 r1, r2, r3, r4, r5, r6, 以及 lr 寄存器的值压入堆栈,最后调用 bl 指令跳转到
v7_invalidate_l1 处执行。开发者可以在适当的位置添加断点进行 GDB 调试,调试情况如下:
通过上面的实践,可以看出堆栈压栈之后的变化,和预期分析一直,继续执行代码如下:
从上面的注释可以知道,在内核第二阶段的初始化过程中,需要在使能 L1 cache 之前调用
v7_flush_dcache_all。由于 cache reset 之后处于位置状态,所以 v7_flush_dcache_all
的执行会引起 clean 和 invalidate 动作,这些动作会导致 cache 大量的未初始化的
cache line 和 cache tag 被同步到主内存,这不是内核想要发生的事。因此在跳转到
kernel 之前修正了动作,仅仅执行 invalidate 动作,而不是 clean+invalidate 动作。
代码首先将 r0 寄存器,然后执行 “mcr p15, 2, r0, c0, c0, 0” 代码,此时对应
的 CP15 C0 布局如下:
此时选中的寄存器是:CSSELR, Cache Size Select Register. 其内存布局如下:
代码先将 0 写入到 CSSELR 寄存器,通过 CSSELR 寄存器的 InD 域写入 0 代表
数据或一致 cache。CSSELR 寄存器的 Level 域写入 0 代表选择 Level 1 Cache。通过
这行代码,内核选中了 Level 1 D-cache。接着调用 “mrc p15, 1, r0, c0, c0, 0”,
此时选中的是 CCSIDR, Cache Size ID Register 寄存器,其布局如下:
通过上面的代码,读取了 CCSIDR 寄存器的值到 r0 寄存器。开发者可以在适当的位置添加
断点,然后使用 GDB 进行调试,调试的情况如下:
在执行后续代码之前,这里补充一下 Cacahe 基础知识:
Cache 的最小数据单元称为 cache line;多个连续的 cache line 称为一个 cache set;
cache 被均分成多个 cache set;每个 cache set 中含有的 cache line 的数量称为
cache way;cache tag 用于管理每个 cache line;cache line + cache tag + flags
称为一个 cache frame。由于不同的 cache 替换策略,所有 cache 的 flush 方法不同,
ARMv7 采用的是 Set/Way 方法 flush cache。更多 cache 内容请看:
ARMv7 Cache Manual
对于 CCSIDR 寄存器,其布局如下:
通过 CSSELR 选中 cache level 之后,CSSIDR 寄存器里描述了对应 cache level 的信息,
根据之前的实践可以知道,此时选中的是 Cache level 0,CSSIDR 寄存器的值 0xe00fe019,
解析寄存器数据的含义,CSSIDR LineSize 域的值是 0x1,代表 cache line 的长度是
8 words;CSSIDR Associativity 域的值是 0x3, 代表 cache way 的值是 4 路;
CSSIDR NumSets 域的值是 0x7f,代表 cache sets 的值为 0x80,;因此 cache 的
大小等于: 0x80 * 0x4 * 8 = 4096 words = 4096 * 4 = 16K, 在 ARMv7 中,word
的长度是 4 字节,因此 Level 1 cache 的大小为 16K。分析完毕之后,接下来执行的代码是:
代码首先将 0x7fff 的值存储在 r1 寄存器中,然后将 r0 寄存器的值右移 13 位,然后与
0x7fff,并将相与的结果存储在 r2 寄存器中,以此获得 CCSIDR 寄存器 NumSets 域的值。
接着用同样的办法,将 0x3ff 存储到 r1 寄存器中,然后将 r0 寄存器的值右移 3 位,
然后与 r1 寄存器的值相与,结果存储在 r3 寄存器中,以此获得 CCSIDR 的 Associativity
域值。将 r2 寄存器的值加 1 获得 Cache Set 的正确值。同理将 r0 寄存器的值与 0x7
相与,以此获得 cache line 的值,然后将 r0 寄存器的值加 4。使用 clz 指令计算
WayShift 的值,最后通过 add 指令,计算真实 Way 的值。开发者可以在适当的位置添加
断点,然后使用 GDB 进行调试,调试结果如下:
通过上面的实践已经知道内核是如何获得 Level 1 cache 的信息,至此,内核为什么要
获得这些信息,以及内核如何 flush 所有的 cache line 呢?在讲其他代码之前,这里
科普一下 ARMv7 flush 的方法,通过之前的介绍,ARMv7 使用 Cache Set/Way 的方式
管理 cache,所以在 flush cache 的时候也是按 Set/Way 方式 flush cache。ARMv7
提供了一个寄存器 DCCISW,只要往 DCCISW 寄存器中写入 Set 和 Way 信息之后,
CPU 就会根据 DCCISW 中的信息去 flush 指定的 cache。DCCISW 的内存布局如下:
通过寄存器的描述,知道上面的代码为什么要这样处理从 CSSIDR 寄存器中值,从 DCCISW
的布局可以知道,只要往 DCCISW 最高位往地位写入 way 的信息,因此有了 “clz r1, r3”
这行代码;同理, DCCISW Set 域指定需要刷新的 set,因此需要将指定的 Set 信息写入
到这个域。因此接下来的代码如下:
通过上面对 DCCISW 的描述,可以知道这段代码的主要任务就是按 Set/Way 的方式 flush
cache,因此只要循环将 cache 按 Set/Way 方式写入,cache 就可以被 flush。
首先 r2 寄存器存储着 cache set 的数量,这里可以将 set 理解为 “行” 的意思,然后
r4 寄存器存储着 cache way 的数量,这里可以将 way 理解为 “列” 的意思。首先
调用 “sub r2, r2, #1” 获得一个新的 set 号,再将 r4 寄存器的值也就是 way 的值
存储到 r3 寄存器。通过 “subs r3, r3, #1” 获得一个新列,通过接下来几条命令,
将 set 和 way 数据构造成 DCCISW 寄存器所需要的格式,然后调用
“mcr p15, 0, r5, c7, c6, 2” 命令将值写入到 DCCISW 寄存器里,此时 CP15 c7 布局如下:
通过命令选中了 DCCISW 寄存器,然后通过 mcr 指令将数据写入到 DCCISW 寄存器里。
以此反复,可以总结为每个 set 一共循环 way 次写 DCCISW 操作,因此总共循环了
set * way - 1 次循环。执行完上面的命令之后,cache 已经被 flush 完毕,那么
函数直接返回。
再次回到 __v7_setup 即 __v7_ca9mp_setup, 继续执行代码如下:
从调用 v7_invalidate_l1 中返回之后,调用 ldmia 指令将之前压入堆栈的寄存器
全部返回给原有的寄存器。由于内核开启了 CONFIG_SMP 宏,那么系统支持 SMP,
但也支持多核中单核心启动,因此上面代码的 ALT_SMP 代码将被丢弃,而使用 ALT_UP
对应的代码。由之前的代码可以知道 r10 此时的值为 1,那么调用 orr 指令之后,r10
寄存器变为了 0x40,再将 r10 寄存器的值存储到 r0 寄存器,接这几条命令之后使用
teq 指令检查 r10 与 r0 是否相等,如果不相等,则执行 mcrne 指。通过上面的分析,
r10 和 r0 在单 CPU 启动的时候是相等的,因此这段代码无实际意义。开发者可以在
适当的位置添加断点,然后使用 GDB 进行调试,调试的结果如下:
实践的结果和预期的一致。接下来执行的代码是:
r9 寄存器存储着 CPUID 信息,通过 and 指令和 teq 指令联合使用,判断当前 CPU
是不是 ARM,如果不是,直接跳转到 __errata_finish; 如果是则需要对特定的 ARM 系列进行
处理。接着从 r9 寄存器中提取 24-27 bits 和 0-3 bits,然后组合成特定的数据,使用
ubfx 继续拼凑数据并存储到 r0 寄存器。接着就是变量 Cortex-A 家族的表,使用 ldr 指令
将 r10 寄存器设置为特定的值,然后检查 r0 与 r10 是否相等,如果相等则跳转到指定的
处理函数。由于实践平台是 ARMv7 Cortex-A9MP,因此这里会跳转到 __ca9_errata,
开发者可以在适当的位置添加断点,然后使用 GDB 进行调试,调试的情况如下:
实践的结果和分析的一致,最后跳转到 __ca9_errata, 接下来的代码如下:
由于 CONFIG_ARM_ERRATA_742230 宏,CONFIG_ARM_ERRATA_742231 宏,
CONFIG_ARM_ERRATA_743622 宏,CONFIG_ARM_ERRATA_751472 宏在本实践中都为打开,
因此 __ca9_errata 只执行了 “b __errata_finish”,那么接下来执行的代码如下:
接下来这段代码就是 MMU 设置的核心代码,在分析代码之前,先讲解涉及的相关寄存器。
“mcr p15, 0, r10, c7, c5, 0” 代码中,对 CP15 C7 中的寄存器操作,此时布局如下:
CP15 C7 是与 Cache 维护和地址转换有关的寄存器,此时选中寄存器:
ICIALLU, Invalidate all instruction caches to PoU. 对该寄存器执行写操作会引起
所有指令 cache 无效(PoU)。下一个涉及的寄存器位于 CP15 C8 里,其包含了很多 TLB 相关
的寄存器,其布局如下:
“mcr p15, 0, r10, c8, c7, 0” 涉及的寄存器是:TLBIALL, invalidate unified TLB.
往这个寄存器里面写操作会导致所有的 TLB 无效。下一个涉及的寄存器位于 CP15 c10 里,
其包含了内存的重映射和 TLB 控制操作,其布局如下:
“mcr p15, 0, r3, c10, c2, 0” 涉及的寄存器是:PRRR, Primary Region Remap Register,
其布局如下:
接下来涉及的寄存器也在 CP15 c10 里,”mcr p15, 0, r6, c10, c2, 1” 涉及的寄存器是:
NMRR, Normal Memory Remap Register, 其内存布局是:
更多寄存器描述请看 ARMv7 Usermanual。接下来分析代码。首先将 r10 寄存器设置为 0,
然后执行 “mcr p15, 0, r10, c7, c5, 0” 代码,向 ICIALLU 寄存器,这样会让
I-BTB cache 无效。由于启用了 MMU,那么 CONFIG_MMU 宏启用,接下来执行
“mcr p15, 0, r10, c8, c7, 0” 代码,向 TLBIALL 寄存器写入 0,导致
I-TLBs 和 D-TLBs 无效。接下来执行代码 “v7_ttb_setup r10, r4, r5, r8, r3”,
其中 v7_ttb_setup 的定义如下:
v7_ttb_setup 宏主要的作用是:
v7_ttb_setup 首先执行的代码是 “mcr p15, 0, \zero, c2, c0, 2”, 此时涉及 CP15 C2,
其布局如下:
代码写入的寄存器是: TTBCR, Translation Table Base Control Register.
TTBCR 寄存器用于控制地址转换,其布局如下:
由于本实践平台支持 SMP 单核心启动,因此 v7_ttb_setup 宏中 ALT_SMP 代码不执行,只
执行 ALT_UP 宏包含的代码,因此 v7_ttb_setup 宏首先向 TTBCR 寄存器中写入 0,初始化
TTBCR 寄存器。接着使用两条 orr 指令对 ttbr0l 和 ttbr1 参数添加了 TTB_FLAGS_UP
标志。然后执行 “mcr p15, 0, \ttbr1, c2, c0, 1”, 其涉及到 TTBR1 寄存器,
该寄存器 Translation Table Base Register 1. 所以 v7_ttb_setup 的作用就是
清零 TTBCR 寄存器,然后向 TTB1 寄存器中写入带 TTB_FLAGS_UP 标志的值。回到
__errata_finish,继续分析代码:
继续上面的分析,调用 v7_ttb_setup 设置了 TTBCR 和 TTBR1 寄存器,然后将 r3 的值
设置为 PRRR,这里可以分析对 PRRR 寄存器的设置,PRRR 宏的定义如下:
首先解析 PRRR 对于 PRRR 寄存器的设置。每个页表项同包含了 TEX[0],C,B 三个位,这
三个位按下表的方式组成一个 n 值:
通过上面的 n 值可以在 PRRR 寄存器中找到对应的 NOSn 域和 TRn 域,这些域都说明了
不同的内存类型的属性,由于这部分比较复杂,更多细节请看:
ARMv7 Memory Manual
NMRR 提供了附加的映射控制域,其也通过页表的 TEX[0],C,B 位决定,由于这部分比较复杂,
更多详细内容,请看上面提供的文档。回到 __errata_finish,继续分析代码:
接着两条 mcr 指令将 PRRR 的值写入了 PRRR 寄存器,同理将 NMRR 的值写入到 NMRR 寄存器,
写入之后,页表映射的属性已经设置好了。通过上面的指令,内核已经设置好了页表转换需要
的基地址寄存器 TTBR 和控制器 TTBCR,以及页表映射属性 PRRR 和控制指令 NMRR,更多
页表内容请看:
ARMv7 分页原理及实践教程
最后执行一条 dsb 内存屏蔽指令让上面的设置有效,接下来执行的代码是:
这段代码的主要任务就是设置 ThumbEE,检查当前系统是否支持 ThumbEE, 如果支持就将
TEEHBR 寄存器设置为 0,然后设置 TEECR 寄存器,使 Unprivileged access disabled。
代码 “mrc p15, 0, r0, c0, c1, 0” 首先读取 ID_PFR0 寄存器,其寄存器布局如下:
其中 State3 域 bits[15:12] 指明系统是否支持 ThumbEE, 该域为 1 代表支持 ThumbEE;
如果该域为 0, 则代表不支持 ThumbEE。代码中获得该域之后,使用 teq 指令对该域进行对比,
如果支持,则继续执行,如果不支持,则跳转到 1 处继续执行。由于 ARMv7 支持 ThumbEE,
那么继续执行下面代码,首先将 r3 寄存器设置为 0, 然后调用 “mcr p14, 6, r3, c1, c0, 0”
代码将 0 写入到 TEEHBR 寄存器,TEEHBR 寄存器的布局如下:
将 TEEHBR 寄存器设置为了 0 之后,初始化了 TEEHBR 寄存器。接着执行代码
“mrc p14, 6, r0, c0, c0, 0” 读取 TEECR 寄存器,寄存器布局如下:
接着调用 orr 指令将 TEECR 的 bit0 置位,这样就会使 Unprivileged access disabled.
最后将 r0 寄存器的写入到 TEECR 寄存器。开发者可以在适当的位置添加断点,然后使用
GDB 进行调试,调试的情况如下:
GDB 实践的结果符合预期,接下来执行的代码如下:
这段代码的主要任务就是构造一段数据,用于设置 SCTLR 寄存器。SCTLR 寄存器的布局如下:
其中,ARMv7 中内核想将 SCTLR 寄存器设置为指定内容,将需要设置的内容存储在 v7_crval,
其源码如下:
从上面的定义可以看出,内核想将 SCTLR 寄存器设置为指定内容,即有些 bit 位必须
设置为已知状态,那么就分析一下这些位的含义:
上面的代码中,就是够着了上面的 SCTLR 寄存器的描述,并存储在 r0 寄存器中,最后返回
arch/arm/kernel/head.S 处继续执行,开发者可以在适当的位置添加断点,然后使用 GDB
进行调试,调试的情况如下:
通过实践可以知道最终 r0 寄存器的值是 0x10c5387d, 这正好与 v7_crval 中的设定是一
致的,处理 bit[10] 与 v7_crval 不一致,是因为内核支持 CONFIG_SWP_EMULATE 宏,
将该 bit 清零了。接着执行返回指令,返回到 arch/arm/kernel/head.S 处继续执行。
enable_mmu 主要任务就是打开 MMU,在这里主要就是加载页表和 domain 访问域。
在执行到 __enable_mmu 的时候,r0 寄存器存储着 SCTLR 寄存器需要写入的值;r1 寄存
器存储着 Mahine ID 信息;r2 寄存器存储着 ATAGS 或者 DTB 的地址;r4 存储着 TTBR0
将要写入的值;r5 存储着 TTBR1 将要写入的值;r9 寄存器存储着处理器相关的 ID;r13 寄
存器存储将要跳转到的位置。首先判断 CONFIG_ALIGNMENT_TRAP 宏和 __LINUX_ARM_ARCH
是否小于 6,由于 ARMv7 LINUX_ARM_ARCH 大于 6,因此执行 “bic r0, r0, #CR_A”,
将 r0 寄存器 CR_A 对应的位清零,当写入 SCTLR 寄存时会影响
“bit[1] Alignment check enable”, 不启用对齐检测。接着由于都没有定义宏
CONFIG_CPU_DCACHE_DISABLE,CONFIG_CPU_BPREDICT_DISABLE,CONFIG_CPU_ICACHE_DISABLE,
以及 CONFIG_ARM_LPAE,因此接下来执行的代码是 “mov r5, #DACR_INIT”, DACR_INIT
定义如下:
由于 CONFIG_CPU_SW_DOMAIN_PAN 宏定义,所以 r5 寄存器存储了 DOMAIN_USER,
DOMAIN_NOACCESS,DOMAIN_KERNEL,DOMAIN_MANAGER,DOMAIN_IO,DOMAIN_CLIENT,
DOMAIN_VECTORS,DOMAIN_CLIENT。接着将 r5 寄存器通过代码
“mcr p15, 0, r5, c3, c0, 0” 写入了到寄存器,这里涉及到 CP15 C3 有关,其布局如下:
通过代码选中了 DACR, Domain Access Control Register. 其寄存器布局如下:
向每个域中写入了访问权限。接着通过代码 “mcr p15, 0, r4, c2, c0, 0” 将 r4 寄存器
的值写入到 TTBR0 寄存器,也就是写入页表的基地址。注意此时写入的是物理地址,而不是
虚拟地址。开发者可以在适当的位置添加断点,然后使用断点调试,调试情况如下:
调试情况和预期一致,接下来跳转到 __turn_mmu_on 处执行,代码如下:
这段代码的主要任务就是向 SCTLR 寄存器写入设置好的值,然后就可以启用 MMU。
代码首先执行了一条伪 nop 代码 “mov r0, r0”, 然后调用 instr_sync 宏,这个宏
起到 isb 的作用,定义如下:
这里添加 isb 的作用就是在向 SCTLR 写入之前,让之前所有指令和流水线都同步完成之后
才执行 isb 之后的指令。接下来执行 “mcr p15, 0, r0, c1, c0, 0”, 将之前配置好
的 SCTLR 内容都写到 SCTLR 寄存器里,然后读取 MIDR 寄存器,最后执行一条 isb 指令,
使刚刚设置的内容都生效。至此,MMU 启动,虚拟地址开始使用。接下来将 r13 寄存器
存储的虚拟地址赋值给 r3,然后直接跳转到 r3 对应的虚拟地址处执行。根据之前的实践
分析可以知道,r3 对应的地址是 __mmap_switched 的虚拟地址。开发者可以在适当的位置
添加断点,然后使用 GDB 进行调试,调试情况如下:
通过上面的实践已经看到内核已经跳转到 __mmap_switched 处继续执行,那么接下来
执行的代码是:
这里开始执行的代码 MMU 已经打开,所有使用的是虚拟地址,__mmap_switched 是为跳转到
start_kernel 做准备。在执行这段代码之前,r0 寄存器存储着 SCTLR 控制器的配置内容;
r1 寄存器存储着 machine ID;r2 寄存器存储着 DTB/ATAGS 的虚拟地址;r9 寄存存储着
处理器 ID。代码首先将 r1,r2,r0 寄存器的值赋值给 r7,r8,r9 寄存器,然后使用 adr 指令
获得 __mmap_switched_data 的虚拟地址,__mmap_switched_data 的定义如下:
在 __mmap_switched_data 数据中,__bss_start 指向 .bss 段的起始地址;__bss_stop
指向 .bss 段的终止地址;init_thread_union 是 init_task 使用其描述的内存区域作为
该进程的堆栈空间,并且和自身的 thread_info 参数共用这部分内存空间。processor_id
存储着处理器 ID;__machine_arch_type 变量用于存储 machine type; __atags_pointer
指向 ATAGS 或者 DTB 的指针, cr_alignment 对齐信息。在回到 __mmap_switched,
继续执行代码 “mov fp, #0” 将 fp 寄存器设置为 0。开发者可以在适当的位置添加断点,
使用 GDB 进行调试,调试情况如下:
调试情况和预期一致,接下来执行的代码如下:
这段的代码主要作用就是清除内核的 .bss 段。调用 “ldmia r4!, {r0, r1, sp}” 从
__mmap_switched_data 中获得数据,并将 __bss_start 存储到 r0 寄存器,将
__bss_stop 存储到 r1 寄存器,将 init_thread_union + THREAD_START_SP
存储到 sp 寄存器,然后将 r1 寄存器减去 r0 寄存器可以获得 .bss 段的长度,
然后将 r1 设置为 0, 然后跳转到 memset 进行清除动作,对于 memset 参数,
r0 对应需要清除的起始地址;r1 代表设置的初始值;r2 代表清除字节数 ,接着调用
arch/arm/lib/memset.S 中的 memset 进行清除动作。开发者可以在适当的位置
加入断点,然后使用 GDB 进行调试,调试的结果如下:
接下来执行的代码是:
这段代码就是跳转到 start_kernel 之前最后一段代码。首先调用 ldmia 指令获得
__mmap_switched_data 数据中对应的内容,然后将处理器 ID,机器类型,atags/DTB 信息
写入到 __mmap_switched_data 指定的位置。最后将确认 r3 寄存器对应的位置是否存在,
如果存在,则将 r10 寄存器的值写入里面,此时 r10 寄存器存储 SCTLR 寄存器的配置。
最后将 lr 寄存器设置为 0 后,跳转到 start_kernel 处继续执行。开发者可以在适当
位置添加断点,然后使用 GDB 进行调试,调试情况如下:
通过上面实践,可以看到最后调用 start_kernel 的情况。至此,kernel 汇编基础部分
初始化已经结束。接下来将进入 C 函数 start_kernel 继续执行代码。