专栏名称: 嵌入式微处理器
关注这个时代最火的嵌入式微处理器,你想知道的都在这里。
目录
相关文章推荐
纪念币发行信息  ·  重磅!2025新版100元纸钞发行!开始预约! ·  昨天  
纪念币发行信息  ·  重磅!2025新版100元纸钞发行!开始预约! ·  昨天  
海西晨报  ·  涨幅超过黄金!很多人都在买 ·  昨天  
海西晨报  ·  涨幅超过黄金!很多人都在买 ·  昨天  
国际金融报  ·  隐私不合规!银行APP被公开通报,如何整改? ·  2 天前  
金融早实习  ·  西门子艾闻达咨询实习生招聘 ·  2 天前  
Wind万得  ·  刚刚!马斯克的大模型宣布免费 ·  3 天前  
51好读  ›  专栏  ›  嵌入式微处理器

RISC-V Linux汇编启动过程分析

嵌入式微处理器  · 公众号  ·  · 2024-11-28 12:00

正文

RISC-V Linux的汇编启动部分比较简单,不算复杂。有两个部分比较核心:页表创建和重定向。页表创建是用C语言写的,今天先分析汇编部分,先带大家分析整体汇编启动流程,然后再分析重定向。

注意:本文基于linux5.10.111内核

汇编启动流程

先从整体分析汇编做的事情,有个大体框架。

路径: arch/riscv/kernel/head.S ,入口是 ENTRY(_start_kernel)

ENTRY(_start_kernel) 开始进行启动前的一些初始化,建立页表前的主要工作:

  • 关闭所有中断
/* 关闭所有中断 */
csrw CSR_IE, zero
csrw CSR_IP, zero
  • 加载全局指针gp
/* 加载全局指针gp */
.option push
.option norelax
la gp, __global_pointer$
.option pop
  • disable FPU
/* 禁用 FPU 以检测内核空间中浮点的非法使用*/
li t0, SR_FS
csrc CSR_STATUS, t0
  • 选择一个核启动
/* 选择一个核启动 */
la a3, hart_lottery
li a2, 1
amoadd.w a3, a2, (a3)
bnez a3, .Lsecondary_start
  • 清楚bss段
/* 清除bss */
la a3, __bss_start
la a4, __bss_stop
ble a4, a3, clear_bss_done
  • 保存hart id和dtb地址
/* 保存hatr id和dtb地址,hart id保存到a0,dtb地址保存到a1 */
mv s0, a0
mv s1, a1
la a2, boot_cpu_hartid
  • 设置sp指针
    la sp, init_thread_union + THREAD_SIZE
  • 上述工作完成,会开始临时页表的创建,跳转到C函数setup_vm建立临时页表
    mv a0, s1
call setup_vm // 跳转到C函数setup_vm,setup_vm会创建临时页表
  • 重定向
#ifdef CONFIG_MMU
la a0, early_pg_dir
call relocate //重定向,实际就是开启MMU
#endif
  • 设置异常向量地址,重载C环境
    call setup_trap_vector
/* 重载C环境 */
la tp, init_task
sw zero, TASK_TI_CPU(tp)
la sp, init_thread_union + THREAD_SIZE
  • 最后跳转到C函数start_kernel,开始C语言部分初始化,汇编部分执行完毕
tail start_kernel

完整_start_kernel汇编代码:

ENTRY(_start_kernel)
/* 关闭所有中断 */
csrw CSR_IE, zero
csrw CSR_IP, zero

/* 在源码中,这里有一个M模式处理的宏,这里没有用到,直接跳过*/

/* 加载全局指针gp */
.option push
.option norelax
la gp, __global_pointer$
.option pop

/* 禁用 FPU 以检测内核空间中浮点的非法使用*/
li t0, SR_FS
csrc CSR_STATUS, t0

#ifdef CONFIG_SMP
li t0, CONFIG_NR_CPUS
blt a0, t0, .Lgood_cores
tail .Lsecondary_park
.Lgood_cores:
#endif

/* 选择一个核启动 */
la a3, hart_lottery
li a2, 1
amoadd.w a3, a2, (a3)
bnez a3, .Lsecondary_start

/* 清除bss */
la a3, __bss_start
la a4, __bss_stop
ble a4, a3, clear_bss_done
clear_bss:
REG_S zero, (a3)
add a3, a3, RISCV_SZPTR
blt a3, a4, clear_bss
clear_bss_done:

/* 保存hatr id和dtb地址,hart id保存到a0,dtb地址保存到a1 */
mv s0, a0
mv s1, a1
la a2, boot_cpu_hartid
REG_S a0, (a2)

/* 初始化页表,然后重定向到虚拟地址 */
la sp, init_thread_union + THREAD_SIZE
mv a0, s1
call setup_vm // 跳转到C函数setup_vm,setup_vm会创建临时页表
#ifdef CONFIG_MMU
la a0, early_pg_dir
call relocate //重定向,实际就是开启MMU
#endif /* CONFIG_MMU */

call setup_trap_vector
/* 重载C环境 */
la tp, init_task
sw zero, TASK_TI_CPU(tp)
la sp, init_thread_union + THREAD_SIZE

#ifdef CONFIG_KASAN
call kasan_early_init
#endif
/* Start the kernel */
call soc_early_init
tail start_kernel //跳转到C函数start_kernel,开始C语言部分初始化

汇编中非常重要的一个部分就是页表的创建,关乎着后面的程序能不能继续往下跑。setup_vm创建页表后就会开始执行relocate重定向,这个重定向主要开启mmu,下面分析relocate的汇编。

relocate

relocate重定向,就是在开启mmu。开启mmu的操作就是将一级页表的地址以及权限写到 satp 寄存器中,这就算开启mmu了。

#ifdef CONFIG_MMU
la a0, early_pg_dir //跳转到relocate前,先把第一级页表early_pg_dir的地址存入a0
call relocate //跳转到relocate,开启MMU
#endif

relocate有两次开启mmu的操作,第一次开启mmu使用的是 setup_vm() 建立的 trampoline_gd_dir 页表,这页表保存的是 kernel 的前 2M 内存。第二次开启MMU使用的是 early_pg_dir 页表,这个页表映射了整个kernel内存以及 dtb 的4M空间。

如果 trampoline_pg_dir 或者 early_pg_dir 这两个页表的映射没弄好的话,开启MMU的时候就会失败,所以页表的建立十分关键。页表创建后续再深究,下面分析relocate汇编代码。

  • 计算返回地址

    返回地址就是 ra 加上虚拟地址和物理地址之间的偏移量,这个是固定偏移量。 PAGE_OFFSET kernel 入口地址对应的虚拟地址, _start 就是 kernel 入口地址的虚拟地址, PAGE_OFFSET - _start 就得到它们之间的偏移,然后再和ra相加,就是返回地址。

/* Relocate return address */
li a1, PAGE_OFFSET
la a2, _start
sub a1, a1, a2
add ra, ra, a1
  • 将异常入口 1f 的虚拟地址写入 stvec 寄存器

    因为一旦开启MMU,地址都变成了虚拟地址,原来访问的都是物理地址,开启MMU时,地址发生了改变, VA != PA ,从而进入异常,所以要先设置异常入口地址,此时的异常入口为 1f

/* Point stvec to virtual address of intruction after satp write */
la a2, 1f
add a2, a2, a1
csrw CSR_TVEC, a2
  • 提前计算切换到 early_pg_dir 页表要写入 satp 的值

再进入relocate之前,就已经把early_pg_dir赋值给a0了,所以a0是early_pg_dir。srl是逻辑右移,mmu使用的是sv39,虚拟地址39位,物理地址56位:

低12位是偏移量,所以 PAGE_SHIFT 等于12,将 early_pg_dir 地址右移12位存到 a2 。根据satp寄存器定义:

MODE 等于 0x8 代表使用 sv39 mmu 0x0 代表不进行地址翻译,即不开启 MMU 。这里 STAP_MODE sv39 ,即 0x8 。将 early_pg_dir 地址和 SATP_MODE 进行或运算后,即可得到写入 satp 寄存器的值,最后保存到 a2

/* Compute satp for kernel page tables, but don't load it yet */
srl a2, a0, PAGE_SHIFT
li a1, SATP_MODE //sv39 mmu
or a2, a2, a1
  • 第一次开启MMU,使用trampoline_pg_dir页表

satp 值的计算和上述是一样的。开启 MMU 之前,通过 sfence.vma 命令先刷新 TLB 。此时开启 MMU ,就会进入下面的标号为 1 的汇编段

	la a0, trampoline_pg_dir
srl a0, a0, PAGE_SHIFT
or a0, a0, a1
sfence.vma
csrw CSR_SATP, a0

进入异常 1f 段,重新设置异常入口为 .Lsecondary_park ,然后切换到 early_pg_dir 页表,相当于第二次开启MMU。此时,如果之前建立的 early_pg_dir 页表不对,则会就进入 .Lsecondary_park .Lsecondary_park 里面是个 wfi 指令,是个死循环。







请到「今天看啥」查看全文