vmlinux.lds.S的链接信息
.head.text : {
_text = .;
HEAD_TEXT
}
启动代码是在HEAD_TEXT段中的
__HEAD
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
efi_signature_nop // special NOP to identity as PE/COFF executable
b primary_entry // branch to kernel start, magic
.quad 0 // Image load offset from start of RAM, little-endian
le64sym _kernel_size_le // Effective size of kernel image, little-endian
le64sym _kernel_flags_le // Informative flags, little-endian
.quad 0 // reserved
.quad 0 // reserved
.quad 0 // reserved
.ascii ARM64_IMAGE_MAGIC // Magic number
.long .Lpe_header_offset // Offset to the PE header.
__EFI_PE_HEADER
除了b primary_entry之外,后面那些字段主要都是给引导程序用来解析、校验和加载映像的元数据。
primary_entry
SYM_CODE_START(primary_entry)
bl record_mmu_state
bl preserve_boot_args
adrp x1, early_init_stack
mov sp, x1
mov x29, xzr
adrp x0, init_idmap_pg_dir
mov x1, xzr
bl __pi_create_init_idmap
cbnz x19, 0f
dmb sy
mov x1, x0 // end of used region
adrp x0, init_idmap_pg_dir
adr_l x2, dcache_inval_poc
blr x2
b 1f
0: adrp x0, __idmap_text_start
adr_l x1, __idmap_text_end
adr_l x2, dcache_clean_poc
blr x2
1: mov x0, x19
bl init_kernel_el // w0=cpu_boot_mode
mov x20, x0
*/
bl __cpu_setup // initialise processor
b __primary_switch
SYM_CODE_END(primary_entry)
- bl record_mmu_state
将当前 MMU 和缓存状态保存到 record_mmu_state(),以便后面可能恢复。 - bl preserve_boot_args
保存从 boot-loader 传入的启动参数(传给内核命令行、设备树等)。
adrp x1, early_init_stack
mov sp, x1
mov x29, xzr
把栈指针切换到早期初始化栈,清零FP指针(x29
),建立一个干净的调用帧。
adrp x0, init_idmap_pg_dir
mov x1, xzr
bl __pi_create_init_idmap
将 identity-map 页目录的基地址加载到 x0
。
- bl __pi_create_init_idmap
调用create_init_idmap()
,在这块预留的内存区域里搭建好一个“1:1 映射”的页表,用于在打开 MMU 之前访问内核的虚拟地址。
注:
为什么是
__pi_create_init_idmap
而不是create_init_idmap
?在arch\arm64\kernel\pi\Makefile中,有一段给符号添加prefix的代码:
$(obj)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_ \ --remove-section=.note.gnu.property
cbnz x19, 0f
dmb sy
mov x1, x0 // x0 此时指向 idmap 末端地址
adrp x0, init_idmap_pg_dir
adr_l x2, dcache_inval_poc
blr x2
b 1f
0: adrp x0, __idmap_text_start
adr_l x1, __idmap_text_end
adr_l x2, dcache_clean_poc
blr x2
-
cbnz x19, 0f
:如果cpu_boot_mode
(x19
)不为零(通常表示“二级启动”或“热启动”),跳到标签0
;否则走下面的路径。-
路径 A(cold boot,
x19==0
):dmb sy
:全局内存屏障,确保前面的内存映射修改生效。mov x1, x0
:把__pi_create_init_idmap
返回的“映射末端”地址放入x1
。- 重新
adrp x0, init_idmap_pg_dir
加载页目录基址。 adr_l x2, dcache_inval_poc
:取到缓存失效函数入口,blr x2
:调用dcache_inval_poc(pg_dir, end)
,对这段 mapping 区域做缓存失效(invalidate),保证后续执行能看到最新映射。b 1f
:跳过标签 0。
-
路径 B(warm boot,
x19≠0
):adrp x0, __idmap_text_start
/adr_l x1, __idmap_text_end
:定位 idmap 文本区的开始和结束。adr_l x2, dcache_clean_poc
:取到缓存清理函数入口。blr x2
:调用dcache_clean_poc(start, end)
,把这段代码清理到 PoC(Point-of-Coherency),确保二级启动时用的 idmap 代码能被所有核一致地看到。
-
- 跳转到内核主入口
mov x0, x19
bl init_kernel_el // w0 = cpu_boot_mode
mov x20, x0 // 保存 init_kernel_el 的返回值(新 boot mode / status)
bl __cpu_setup // 对当前处理器做更进一步的初始化
b __primary_switch // 跳到 C 语言实现的主启动分支
- init_kernel_el:早的汇编阶段完成了 EL1/EL2 路径的环境准备,确保后续的 C 代码能在正确的 Exception Level 和处理器状态下运行
- __cpu_setup:在 EL1 早期阶段初始化内存管理单元(MMU)相关的寄存器,并屏蔽了用户态对高特权资源的直接访问,为内核后续的页面表创建、缓存配置和系统运行环境打下基础
- __primary_switch:配置寄存器,开启MMU,跳转进入内核。
create_init_idmap
来详细分析一下create_init_idmap都干了什么。
asmlinkage u64 __init create_init_idmap(pgd_t *pg_dir, pteval_t clrmask)
{
u64 ptep = (u64)pg_dir + PAGE_SIZE;
pgprot_t text_prot = PAGE_KERNEL_ROX;
pgprot_t data_prot = PAGE_KERNEL;
pgprot_val(text_prot) &= ~clrmask;
pgprot_val(data_prot) &= ~clrmask;
map_range(&ptep, (u64)_stext, (u64)__initdata_begin, (u64)_stext,
text_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);
map_range(&ptep, (u64)__initdata_begin, (u64)_end, (u64)__initdata_begin,
data_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);
return ptep;
}
ptep
(page table entry pointer)用来指向下一层页表(PUD/PTE)的起始地址。map_range
创建直接映射的页表
调用了两次 map_range
,在同一套页表(同一个 pg_dir
)里,对两个不同的虚拟地址区间分别做映射,目的是给代码段和数据段设置不同的访问属性。
map_range
void __init map_range(u64 *pte, u64 start, u64 end, u64 pa, pgprot_t prot,
int level, pte_t *tbl, bool may_use_cont, u64 va_offset)
{
u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
pteval_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
u64 lmask = (PAGE_SIZE << lshift) - 1;
start &= PAGE_MASK;
pa &= PAGE_MASK;
/* Advance tbl to the entry that covers start */
tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;
/*
* Set the right block/page bits for this level unless we are
* clearing the mapping
*/
if (protval)
protval |= (level == 2) ? PMD_TYPE_SECT : PTE_TYPE_PAGE;
while (start < end) {
u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));
if (level < 2 || (level == 2 && (start | next | pa) & lmask)) {
/*
* This chunk needs a finer grained mapping. Create a
* table mapping if necessary and recurse.
*/
if (pte_none(*tbl)) {
*tbl = __pte(__phys_to_pte_val(*pte) |
PMD_TYPE_TABLE | PMD_TABLE_UXN);
*pte += PTRS_PER_PTE * sizeof(pte_t);
}
map_range(pte, start, next, pa, prot, level + 1,
(pte_t *)(__pte_to_phys(*tbl) + va_offset),
may_use_cont, va_offset);
} else {
/*
* Start a contiguous range if start and pa are
* suitably aligned
*/
if (((start | pa) & cmask) == 0 && may_use_cont)
protval |= PTE_CONT;
/*
* Clear the contiguous attribute if the remaining
* range does not cover a contiguous block
*/
if ((end & ~cmask) <= start)
protval &= ~PTE_CONT;
/* Put down a block or page mapping */
*tbl = __pte(__phys_to_pte_val(pa) | protval);
}
pa += next - start;
start = next;
tbl++;
}
}
1. 函数作用和参数含义
- 功能:在给定的页表层级(
level
)上,为虚拟地址区间[start, end)
建立映射到物理地址pa
,并设置访问属性prot
。 - 主要参数:
u64 *pte
:指向一个“分配器”页(或页表块)的起始指针,用于在需要时分配下一级页表页面。u64 start, end
:要映射的虚拟地址范围(半开区间)。u64 pa
:映射起始的物理地址。pgprot_t prot
:页属性(可读/写/执行等)。int level
:当前处理的页表层级(0 表示顶层,最大到 3)。pte_t *tbl
:当前层级的页表基址,用于写入映射条目。bool may_use_cont
:是否允许使用连续(contiguous)映射以减少表项数量。u64 va_offset
:页表物理地址到内核虚拟地址的偏移,用于把物理地址转换为内核可寻址指针。
2. 计算各种掩码
u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
pteval_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
u64 lmask = (PAGE_SIZE << lshift) - 1;
cmask
用于判断地址和物理地址是否对齐到可以做“连续”映射的粒度。只有在第 3 级(页级映射)才可能做连续页映射。- 去掉原有的类型位(PTE/PMD/etc),留出位置后面 OR 上正确的类型。
lmask
用来对齐当前层级能一次映射的最大连续范围。例如,level=2
(PMD)时,lmask = (4KB << PTDESC_TABLE_SHIFT) -1 = (2MB)-1
。
3. while循环代码的作用
使用 **level < 2 || (level == 2 && (start | next | pa) & lmask)**判断
- 是PGD或者PUD递归调用map_range,填充下一级页表,如果是PMD但是没有按照2Mb对齐,也进入下一级页表
- PMD可以使用2Mb大页映射,或者到了PTE页表,增加start地址,填充当前页表的下一个页表项
注:
这里会把没有按照2M对齐的部分使用PTE映射,对齐的部分会使用2M的PMD大页映射。
__primary_switch
SYM_FUNC_START_LOCAL(__primary_switch)
adrp x1, reserved_pg_dir
adrp x2, init_idmap_pg_dir
bl __enable_mmu
adrp x1, early_init_stack
mov sp, x1
mov x29, xzr
mov x0, x20 // pass the full boot status
mov x1, x21 // pass the FDT
bl __pi_early_map_kernel // Map and relocate the kernel
ldr x8, =__primary_switched
adrp x0, KERNEL_START // __pa(KERNEL_START)
br x8
SYM_FUNC_END(__primary_switch)
- __enable_mmu:主要的作用就是把ttbr0、ttbr1寄存复制,同时开启mmu
phys_to_ttbr x2, x2
msr ttbr0_el1, x2
load_ttbr1 x1, x1, x3
set_sctlr_el1 x0
这里会把init_idmap_pg_dir赋值给ttbr0,ttbr0这个页表寄存器是在虚拟地址高位为0的时候使用的。
启动阶段idmap是直接映射,高位地址都是0,所以需要用的ttbr0
-
__pi_early_map_kernel:
early_map_kernel()
在 MMU 打开后立刻运行,负责清理早期内存、解析启动参数、调整寄存器设置,再把整个内核镜像映射到虚拟地址空间中。idmap_cpu_replace_ttbr1(init_pg_dir);
设置ttbr1,后续切换到0xFFFF…的虚拟地址也可以正常工作。
-
__primary_switched:
- 任务初始化:构造 CPU 对应的
task_struct
(idle task)。 - 异常向量:设置 EL1 的中断/异常处理入口。
- 栈帧设置:构建函数帧,准备局部调用。
- 关键全局变量:保存 FDT 指针、内核镜像偏移。
- 运行时标志:记录启动模式、做 KASAN 和 VHE 收尾。
- 转入 C 端:最终调用
start_kernel()
,完成高层初始化。
- 任务初始化:构造 CPU 对应的