Linux kernel 启动流程分析6---第一阶段创建临时内核页表
一、MMU和页表简单介绍
1.1、MMU简单介绍
MMU(Memory Management Unit,内存管理单元)是计算机系统中负责管理内存访问的硬件组件。它的作用是将程序中的虚拟地址映射到物理内存地址,并提供内存保护和管理功能。Linux 操作系统在其内存管理中依赖于 MMU 来实现虚拟内存的管理、进程隔离、内存分页等功能。
1、虚拟内存与物理内存
在现代计算机中,内存分为虚拟内存和物理内存:
-
虚拟内存:是操作系统为每个进程分配的一块地址空间,程序可以使用虚拟内存地址进行访问,而不需要直接操作物理内存地址。虚拟内存的引入使得每个进程都有一个独立的地址空间,提高了安全性和稳定性。
-
物理内存:是计算机中实际存在的内存硬件,通常是RAM(随机存取存储器)。操作系统通过 MMU 将虚拟内存地址映射到物理内存地址。
2、MMU的工作原理
MMU 的核心功能是地址映射,即将虚拟地址转换为物理地址。Linux 系统使用分页(paging)和段式管理(segmentation)等技术来实现虚拟内存和物理内存的转换。
(1)虚拟地址到物理地址的转换
在 Linux 中,MMU 将虚拟地址分成多个页面(pages)。每个页面通常是 4KB 或更大(例如 2MB 或 1GB)。虚拟地址到物理地址的转换是通过一个叫做**页表(Page Table)**的数据结构来完成的。
- 页表(Page Table):是内存管理中的关键数据结构。每个进程有自己的页表,它记录了虚拟页面和物理页面的映射关系。MMU 通过页表查找虚拟地址对应的物理地址。
(2)分页(Paging)
分页是 MMU 中最常见的内存管理方式。虚拟内存被划分为多个固定大小的块,称为“页”(page)。物理内存也被划分为同样大小的块,称为“页框”(page frame)。每个虚拟页面被映射到一个物理页框。
- 页表项(Page Table Entry, PTE):页表中的每一项包含了虚拟页面和物理页面之间的映射关系,以及一些状态信息,如访问权限(只读、可写等)。
- TLB(Translation Lookaside Buffer):为了加速虚拟地址到物理地址的转换,MMU 使用一个缓存(TLB)来存储最近访问的页表项,避免频繁访问主内存。
(3)内存保护
MMU 提供内存保护机制,可以防止进程访问不属于自己的内存区域。例如,如果进程尝试访问它没有权限的内存地址,MMU 会触发一个页面错误(Page Fault),操作系统就可以采取适当的措施(如终止进程或进行页面交换)。
(4)页表层次结构
Linux 中的页表通常采用多级结构,以减少内存消耗。例如,x86_64 架构采用了四级页表(PGD、PUD、PMD 和 PTE):
- PGD(Page Global Directory):顶级页表,指向下一级页表。
- PUD(Page Upper Directory):第二级页表。
- PMD(Page Middle Directory):第三级页表。
- PTE(Page Table Entry):页表项,记录了虚拟页和物理页的映射关系。
不同的硬件架构可能有不同的页表层次结构,例如 ARM 体系结构可能使用两级或三级页表。
3、MMU 的功能
-
虚拟地址到物理地址的转换:通过页表和 MMU,虚拟地址被转换为物理地址,从而使得操作系统可以实现进程的虚拟内存空间。
-
内存保护:通过设置页表项中的访问权限,MMU 可以确保进程只能访问分配给它的内存区域。非法访问会导致页面错误(Page Fault)。
-
内存共享:不同进程可以通过共享物理页面来共享内存。例如,多个进程可能共享代码段或库,而每个进程的私有数据则在独立的内存区域。
-
分页交换:当物理内存不足时,Linux 会使用交换空间(swap)将不活跃的页面移动到磁盘上的交换区(swap space)。MMU 通过页面错误机制触发页面交换。
1.2、页表介绍
MMU工作的核心是页表,也就是其根据页表来找到映射关系以及权限。
页表由页表项构成,每一个虚拟地址映射区都会有一个对应的页表项。
在 ARM 架构上,Linux 的页表管理机制包括了 段式管理、段页式管理 和 页表式管理 三种方式。这三种方式是针对不同的 ARM 架构版本以及内存管理需求的设计。每种方式的实现都与虚拟地址到物理地址的映射方式、管理粒度以及内存保护等方面相关。
1. 段式管理(Section Mapping)
段式管理是 ARM 架构中较为简单的一种虚拟内存管理方式,适用于较早的 ARM 架构(如 ARMv4 和 ARMv5)。
- 地址空间划分:段式管理将虚拟地址空间划分为大小为 1MB 或 4MB 的块,每个块(即“段”)直接映射到物理内存。
- 映射机制:每个段对应一个物理地址段,段的映射关系通常是静态的,即将虚拟地址的一部分映射到一个物理地址块。
- 缺点:由于每个段的大小固定且较大,灵活性较差,对于精细的内存管理能力有限。
例如,如果采用 1MB 大小的段,虚拟地址空间的划分将会是:
- 每 1MB 的虚拟地址空间对应一个物理内存段。
- 这种方式的页表结构相对简单,通常只需要一个表项来表示每个段的映射。
2. 段页式管理(Section and Page Table Mapping)
段页式管理是一种混合型的内存管理方式,在 ARMv6 和 ARMv7 以及更高版本的 ARM 架构中得到广泛应用。它结合了段式管理和分页管理的特点,适合需要更细粒度控制的系统。
-
地址空间划分:段页式管理通常将虚拟地址空间分为两部分:
- 大段(通常为 1MB 或 4MB)用于映射大块的物理内存(类似于段式管理),对于静态、常用的内存区域采用大段映射。
- 小页面(通常为 4KB 或 64KB)用于精细的内存分配和管理,对于动态分配的内存区域(如堆栈和堆)使用分页机制进行管理。
-
映射机制:
- 对于 大段映射,虚拟地址的高位部分用来索引大段映射表,映射到一个大的物理内存块。
- 对于 小页面映射,剩余部分用来索引页表,逐级将虚拟地址映射到物理页面。
这种方式可以在不增加过多页表开销的前提下,提供一定程度的灵活性,尤其适合嵌入式系统或需要同时处理大块内存和小块动态内存的系统。
3. 页表式管理(Page Table Mapping)
页表式管理是 ARM 架构中最常见的内存管理方式,广泛应用于 ARMv7 及之后的架构(如 ARMv8)。这种方式为现代操作系统提供了高度的灵活性,并且支持更复杂的虚拟内存机制。
-
地址空间划分:页表式管理将虚拟地址空间分成许多小的页面(通常是 4KB),每个虚拟页面对应一个物理页面。
-
多级页表:ARM 架构(尤其是 ARMv7 和 ARMv8)使用多级页表机制(通常是 2 级或 3 级页表)来管理虚拟地址和物理地址的映射。这种方式使得操作系统可以动态地分配内存,且具有更好的灵活性。
- 例如,ARMv7 使用 2 级页表,一级页表用于索引大块内存(通常是 1GB 或 2GB),二级页表则进一步细分每个大块到更小的 4KB 页面。
- ARMv8 架构使用 3 级页表,可以支持更大的虚拟地址空间。
-
映射机制:在页表式管理下,操作系统可以为每个进程创建独立的页表,确保虚拟地址空间的隔离和保护。每个页表项存储了虚拟地址到物理地址的映射,并且可以设置访问权限(如读、写、执行权限)。
-
优点:
- 高度灵活:可以支持大内存容量、虚拟内存和共享内存。
- 内存保护:可以通过设置页表项的权限位来防止非法访问。
- 动态内存分配:操作系统可以根据需要分配或释放页面。
- 段式管理页表
在arm打开MMU初期,使用的是临时内核页表,其类型就是段式页表。
段式页表将4GB的地址空间(32bit系统)划分成4096个1MB的段,因此段式页表有4096个页表项,每个页表项有32bit(4 byte),故段式页表需要16KB的空间页表项格式如下:
- 位号 功能
- 31:20 bit 段序号
- 20: 0 bit MMU flag
1.3、Arm上MMU的工作原理
在 Linux 中,内存管理单元(MMU)与操作系统的内存管理紧密集成。Linux 使用虚拟内存来为每个进程提供独立的地址空间,并通过 MMU 实现虚拟地址到物理地址的转换。具体来说,Linux 中的 MMU 主要涉及以下几个部分:
- 进程的地址空间:每个进程有自己的独立虚拟地址空间。MMU 通过进程的页表来管理这些虚拟地址与物理地址的映射。
- 页表管理:Linux 内核会管理每个进程的页表,并根据需要进行分页和换页操作。内核也提供了对页表项的控制,以实现内存保护和共享。
- 内存映射文件:Linux 支持将文件映射到进程的虚拟内存中,这通过 MMU 完成。文件的内容被映射到虚拟内存地址空间的某个区域,并在访问时通过 MMU 转换为物理地址。
- 内存分配:Linux 内核使用 伙伴系统(Buddy System)来进行内存分配,并使用 MMU 将分配的内存区域映射到虚拟地址。
Arm将页表基地址存放在协处理器cp15的c2寄存器上,具体参考《ARM的CP15协处理器的寄存器》。
如下说明:
CP15 中的寄存器 C2 保存的是页表的基地址,即一级映射描述符表的基地址。
Arm的MMU会根据虚拟地址计算出其相应页表项的偏移,从cp15的c2寄存器中获取页表基址之后,加上偏移得到对应的页表项地址。后续操作就是根据页表结构来做的。
如果是段式页表的话,再根据段内偏移以及页表项中的物理段基址最终得到对应的物理地址。
段式管理页表工作举例(先不关心MMU flag):
- 假设:
- (1) 页表基地址为0x0(存放在CP15的c2寄存器上)。
- (2) 0xc0000000所在段(也就是段序号为0xc00)的页表项地址0x3000。
- (3) 页表项地址0x3000的值为0x20000000(也就是段序号为0x300)。
- 当虚拟地址为0xc0001000,计算方式如下:
- (1) 左移20位的得到虚拟地址所在段序号为0xc00,获取低20位得到段内偏移为0x1000。
- (2) 计算对应页表项地址=页表基地址0x0+段序号0xc00*页表项长度4=0x3000。
- (3) 0x3000地址上的值为0x20000000,提取高12位得到0x200,所以对应物理段基址为0x20000000。
- (4) 物理段基址加上段内偏移得到实际的物理地址0x20001000。
1.4、临时内核页表及其内容
为了打开MMU,内核需要创建一个临时内核页表,用于kenrel启动过程中的打开MMU的过渡阶段。
并且,使用的是段式管理的方法。
需要建立如下区域的映射
打开MMU的函数的代码区域的恒等映射。
恒等映射是指虚拟地址和物理地址一致的映射。
在打开MMU的过程中,CPU还是按照地址顺序一条接着一条去获取指令,也就是说此时PC指针还是指向这段代码区域的物理地址。当MMU打开之后,如果没有恒等映射的话,PC指针此时还是指向这段代码区域的物理地址,但实际上CPU会把PC指针的地址当作虚拟地址进行操作,而造成找不到对应的物理地址。因此,如果做了恒等映射,虚拟地址和物理地址一致,及时CPU会把PC指针的地址当作虚拟地址进行操作,最终仍会映射到相同的物理地址上。
kernel镜像的映射
例如:kernel的链接地址是0xc0008000。而我们把kernel加载到0x20008000的位置上。kernel镜像的连接区域映射到实际的物理地址的区域。
dtb区域的映射
在kernel启动初期需要使用到dtb的东西,因此,在临时内核页表中需要做这些区域的映射。
二、s5pv210映射说明
内存地址和对应段页表项的地址如下:
- (s5p210的物理RAM地址偏移是0x20000000,所以段页表项的基地址是0x20004000)
- 虚拟段 物理段 对应页表项地址 计算方式 临时页表映射的值
- 0x00000000-0x000FFFFF - 0x20004000 (0x20004000+0x000*4) -
- 0x00100000-0x002FFFFF - 0x20004004 (0x20004000+0x001*4) -
- 0x00200000-0x003FFFFF - 0x20004008 (0x20004000+0x002*4) -
- …
- 0x20100000-0x201FFFFF 0x20100000-0x201FFFFF 0x20004804 (0x20004000+0x201*4) (0x201<<20) | mmuflags
- …
- 0xC0000000-0xC00FFFFF 0x20000000-0x200FFFFF 0x20007000 (0x20004000+0xC00*4) (0x200<<20) | mmuflags
- 0xC0100000-0xC01FFFFF 0x20100000-0x201FFFFF 0X20007004 (0x20004000+0xC01*4) (0x201<<20) | mmuflags
- …
- 0xC0500000-0xC05FFFFF 0x20500000-0x205FFFFF 0X20007014 (0x20004000+0xC05*4) (0x205<<20) | mmuflags
- …
- 0xDFC00000-0xDFCFFFFF 0x3FC00000-3FCFFFFF 0x200077F0 (0x20004000+0xDFC*4) (0x205<<20) | mmuflags
- 0xDFD00000-0xDFDFFFFF 0x3FD00000-3FDFFFFF 0x200077F4 (0x20004000+0xDFC*4) (0x205<<20) | mmuflags
- …
- 0xFFF00000-0xFFFFFFFF - 0x20007FFC (0x20004000+0xFFF*4) -
- 其中(markdown搞个图片不方便,只能用表格和文字描述了。。。。)
[0x20100000-0x201FFFFF]段是打开MMU的函数的代码区域的恒等映射。
[0xC0000000-0xC00FFFFF]段到[0xC0500000-0xC05FFFFF]段是kernel镜像的映射
[0xDFC00000-0xDFCFFFFF]段到[0xDFD00000-0xDFDFFFFF]段是dtb内存区域的映射
三、代码分析
3.1、代码入口
(1)分配物理地址给r8
ENTRY(stext)
...
#define PLAT_PHYS_OFFSET UL(CONFIG_PHYS_OFFSET)
ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
物理地址PHYS_OFFSET的定义如下:
arch/arm/Kconfig
config PHYS_OFFSET
hex "Physical address of main memory" if MMU
depends on !ARM_PATCH_PHYS_VIRT
default 0x20000000 if ARCH_S5PV210
所以config是0x20000000,和s5pv210的ddr起始ram一致。
(2)调用 bl __create_page_tables
ENTRY(stext)
...
bl __create_page_tables
__create_page_tables主要用于创建临时内核页表。
__create_page_tables代码总览,后续小节会详细分析:
arch/arm/kernel/head.S
移除掉CONFIG_ARM_LPAE和CONFIG_DEBUG_LL的无关部分
/*
* Setup the initial page tables. We only setup the barest
* amount which are required to get the kernel running, which
* generally means mapping in the kernel code.
*
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*
* Returns:
* r0, r3, r5-r7 corrupted
* r4 = physical page table address
*/
__create_page_tables:
pgtbl r4, r8 @ page table address
/*
* Clear the swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
#ifdef CONFIG_ARM_LPAE
/*
* Build the PGD table (first level) to point to the PMD table. A PGD
* entry is 64-bit wide.
*/
mov r0, r4
add r3, r4, #0x1000 @ first PMD table address
orr r3, r3, #3 @ PGD block type
mov r6, #4 @ PTRS_PER_PGD
mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER
1:
#ifdef CONFIG_CPU_ENDIAN_BE8
str r7, [r0], #4 @ set top PGD entry bits
str r3, [r0], #4 @ set bottom PGD entry bits
#else
str r3, [r0], #4 @ set bottom PGD entry bits
str r7, [r0], #4 @ set top PGD entry bits
#endif
add r3, r3, #0x1000 @ next PMD table
subs r6, r6, #1
bne 1b
add r4, r4, #0x1000 @ point to the PMD tables
#ifdef CONFIG_CPU_ENDIAN_BE8
add r4, r4, #4 @ we only write the bottom word
#endif
#endif
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr_l r5, __turn_mmu_on @ _pa(__turn_mmu_on)
adr_l r6, __turn_mmu_on_end @ _pa(__turn_mmu_on_end)
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
str r3, [r4, r5, lsl #PMD_ENTRY_ORDER] @ identity mapping
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
/*
* The main matter: map in the kernel using section mappings, and
* set two variables to indicate the physical start and end of the
* kernel.
*/
add r0, r4, #KERNEL_OFFSET >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
ldr r6, =(_end - 1)
adr_l r5, kernel_sec_start @ _pa(kernel_sec_start)
#if defined CONFIG_CPU_ENDIAN_BE8 || defined CONFIG_CPU_ENDIAN_BE32
str r8, [r5, #4] @ Save physical start of kernel (BE)
#else
str r8, [r5] @ Save physical start of kernel (LE)
#endif
orr r3, r8, r7 @ Add the MMU flags
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ENTRY_ORDER)
1: str r3, [r0], #1 << PMD_ENTRY_ORDER
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
bls 1b
eor r3, r3, r7 @ Remove the MMU flags
adr_l r5, kernel_sec_end @ _pa(kernel_sec_end)
#if defined CONFIG_CPU_ENDIAN_BE8 || defined CONFIG_CPU_ENDIAN_BE32
str r3, [r5, #4] @ Save physical end of kernel (BE)
#else
str r3, [r5] @ Save physical end of kernel (LE)
#endif
#ifdef CONFIG_XIP_KERNEL
/*
* Map the kernel image separately as it is not located in RAM.
*/
#define XIP_START XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR)
mov r3, pc
mov r3, r3, lsr #SECTION_SHIFT
orr r3, r7, r3, lsl #SECTION_SHIFT
add r0, r4, #(XIP_START & 0xff000000) >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
str r3, [r0, #((XIP_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ENTRY_ORDER]!
ldr r6, =(_edata_loc - 1)
add r0, r0, #1 << PMD_ENTRY_ORDER
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ENTRY_ORDER)
1: cmp r0, r6
add r3, r3, #1 << SECTION_SHIFT
strls r3, [r0], #1 << PMD_ENTRY_ORDER
bls 1b
#endif
/*
* Then map boot params address in r2 if specified.
* We map 2 sections in case the ATAGs/DTB crosses a section boundary.
*/
mov r0, r2, lsr #SECTION_SHIFT
cmp r2, #0
ldrne r3, =FDT_FIXED_BASE >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
addne r3, r3, r4
orrne r6, r7, r0, lsl #SECTION_SHIFT
strne r6, [r3], #1 << PMD_ENTRY_ORDER
addne r6, r6, #1 << SECTION_SHIFT
strne r6, [r3]
#if defined(CONFIG_ARM_LPAE) && defined(CONFIG_CPU_ENDIAN_BE8)
sub r4, r4, #4 @ Fixup page table pointer
@ for 64-bit descriptors
#endif
3.2 、解析
1. 函数定义
函数的名称是 __create_page_tables
,它的作用是设置最基础的页表,以支持内核的启动。该函数使用 ARM 汇编语法,涉及到分页机制,尤其是针对 32 位和 64 位 ARM 处理器的不同配置(如 LPAE)。
2. 参数传递
r8 = phys_offset
:物理地址偏移。r9 = cpuid
:CPU ID,虽然这在代码中没有直接用到,但在一些配置中可能用于选择特定的处理器配置。r10 = procinfo
:处理器信息,存储有关处理器的配置信息。
3. 清除交换区页表
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
这部分代码用于清空页目录,初始化页表为空值。PG_DIR_SIZE
表示页表的大小(通常是 4KB 或更大的内存段)。代码会将每个页表项(通常是 4 字节)设置为 0。
4. 设置 ARM LPAE 页表(大页支持)
#ifdef CONFIG_ARM_LPAE
mov r0, r4
add r3, r4, #0x1000 @ first PMD table address
orr r3, r3, #3 @ PGD block type
mov r6, #4 @ PTRS_PER_PGD
mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER
1:
#ifdef CONFIG_CPU_ENDIAN_BE8
str r7, [r0], #4
str r3, [r0], #4
#else
str r3, [r0], #4
str r7, [r0], #4
#endif
add r3, r3, #0x1000 @ next PMD table
subs r6, r6, #1
bne 1b
在 LPAE(Large Physical Address Extension)配置下,ARM 页表会使用 64 位的 PGD(Page Global Directory)条目,并将每个 PGD 条目指向一个 PMD(Page Middle Directory)。代码为每个 PGD 条目设置一个指向 PMD 的地址,并按顺序填充页目录表。
PGD
和PMD
是页表的不同级别,PGD
表示页全局目录,PMD
表示页中间目录。根据 LPAE,PGD 是 64 位条目,而普通 ARM 架构通常是 32 位条目。orr r3, r3, #3
是设置 PGD 条目的类型为“块映射”类型。
5. 建立内核的身份映射
adr_l r5, __turn_mmu_on
adr_l r6, __turn_mmu_on_end
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
1: orr r3, r7, r5, lsl #SECTION_SHIFT
str r3, [r4, r5, lsl #PMD_ENTRY_ORDER]
cmp r5, r6
addlo r5, r5, #1
blo 1b
这段代码的目的是为内核代码提供一个身份映射(identity mapping)。身份映射的意思是物理地址与虚拟地址相同,通常在内核初始化时使用。
__turn_mmu_on
是一个内核函数的地址,用于启用 MMU(内存管理单元)。SECTION_SHIFT
定义了内存分页的大小(通常是 1MB)。
6. 映射内核代码区域
add r0, r4, #KERNEL_OFFSET >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
ldr r6, =(_end - 1)
adr_l r5, kernel_sec_start
#if defined CONFIG_CPU_ENDIAN_BE8 || defined CONFIG_CPU_ENDIAN_BE32
str r8, [r5, #4]
#else
str r8, [r5]
#endif
orr r3, r8, r7
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ENTRY_ORDER)
1: str r3, [r0], #1 << PMD_ENTRY_ORDER
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
bls 1b
这部分代码将内核代码区域映射到页表中。内核的起始和结束地址会被保存到变量 kernel_sec_start
和 kernel_sec_end
中。
7. XIP 内核映射(如果配置了)
#ifdef CONFIG_XIP_KERNEL
#define XIP_START XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR)
mov r3, pc
mov r3, r3, lsr #SECTION_SHIFT
orr r3, r7, r3, lsl #SECTION_SHIFT
add r0, r4, #(XIP_START & 0xff000000) >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
str r3, [r0, #((XIP_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ENTRY_ORDER]!
ldr r6, =(_edata_loc - 1)
add r0, r0, #1 << PMD_ENTRY_ORDER
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ENTRY_ORDER)
1: cmp r0, r6
add r3, r3, #1 << SECTION_SHIFT
strls r3, [r0], #1 << PMD_ENTRY_ORDER
bls 1b
#endif
如果内核配置了 XIP(Execute In Place),内核映像将直接从闪存中执行,而不是从 RAM 中加载。此代码将闪存地址映射到虚拟地址空间。
8. 映射启动参数
mov r0, r2, lsr #SECTION_SHIFT
cmp r2, #0
ldrne r3, =FDT_FIXED_BASE >> (SECTION_SHIFT - PMD_ENTRY_ORDER)
addne r3, r3, r4
orrne r6, r7, r0, lsl #SECTION_SHIFT
strne r6, [r3], #1 << PMD_ENTRY_ORDER
addne r6, r6, #1 << SECTION_SHIFT
strne r6, [r3]
这部分代码为启动参数(如设备树或 ATAGs)创建映射。如果指定了启动参数地址(r2
),则会将其映射到虚拟地址空间。
9. 结尾的修正
#if defined(CONFIG_ARM_LPAE) && defined(CONFIG_CPU_ENDIAN_BE8)
sub r4, r4, #4 @ Fixup page table pointer
#endif
如果启用了 LPAE 并且 CPU 是 BE8(大端模式),则需要修正页表指针,确保页表的地址正确。