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

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

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_OFFSETkernel入口地址对应的虚拟地址,_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 mmu0x0代表不进行地址翻译,即不开启MMU。这里STAP_MODEsv39,即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指令,是个死循环。

完整relocate汇编代码:

relocate:
  /* Relocate return address */
  li a1, PAGE_OFFSET
  la a2, _start
  sub a1, a1, a2
  add ra, ra, a1
  /* Point stvec to virtual address of intruction after satp write */
  la a2, 1f
  add a2, a2, a1
  csrw CSR_TVEC, a2
  /* Compute satp for kernel page tables, but don't load it yet */
  srl a2, a0, PAGE_SHIFT
  li a1, SATP_MODE
  or a2, a2, a1
  /*
   * Load trampoline page directory, which will cause us to trap to
   * stvec if VA != PA, or simply fall through if VA == PA.  We need a
   * full fence here because setup_vm() just wrote these PTEs and we need
   * to ensure the new translations are in use.
   */
  la a0, trampoline_pg_dir
  srl a0, a0, PAGE_SHIFT
  or a0, a0, a1
  sfence.vma
  csrw CSR_SATP, a0
.align 2
1:
  /* Set trap vector to spin forever to help debug */
  la a0, .Lsecondary_park
  csrw CSR_TVEC, a0
  /* Reload the global pointer */
.option push
.option norelax
  la gp, __global_pointer$
.option pop
  /*
   * Switch to kernel page tables.  A full fence is necessary in order to
   * avoid using the trampoline translations, which are only correct for
   * the first superpage.  Fetching the fence is guarnteed to work
   * because that first superpage is translated the same way.
   */
  csrw CSR_SATP, a2
  sfence.vma
  ret

总结

以上就是RISC-V Linux的汇编启动流程,虽说RISC-V的指令不复杂,但要理解这个汇编启动的部分,还是需要一点基础和时间。另外,大多数人工作中基本用不上汇编,只有真正用上了理解才会比较深。希望本文能够帮助到有需要的人。

猜你喜欢:

写给新手的MMU工作原理

OpenSBI三种固件的区别

RISC-V 入门笔记(新手必看!)

内核调试之devmem直接读写寄存器

教你在QEMU上运行RISC-V Linux

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
相关文章
|
3月前
|
Linux Shell 网络安全
Linux 系统启动过程
Linux 系统启动过程
66 2
|
7天前
|
存储 运维 监控
Linux--深入理与解linux文件系统与日志文件分析
深入理解 Linux 文件系统和日志文件分析,对于系统管理员和运维工程师来说至关重要。文件系统管理涉及到文件的组织、存储和检索,而日志文件则记录了系统和应用的运行状态,是排查故障和维护系统的重要依据。通过掌握文件系统和日志文件的管理和分析技能,可以有效提升系统的稳定性和安全性。
26 7
|
10天前
|
监控 安全 Linux
启用Linux防火墙日志记录和分析功能
为iptables启用日志记录对于监控进出流量至关重要
|
30天前
|
搜索推荐 Linux
深入理解Linux操作系统的启动过程
本文旨在揭示Linux操作系统从开机到完全启动的神秘面纱,通过逐步解析BIOS、引导加载程序、内核初始化等关键步骤,帮助读者建立对Linux启动流程的清晰认识。我们将探讨如何自定义和优化这一过程,以实现更高效、更稳定的系统运行。
|
2月前
|
缓存 算法 Linux
Linux内核中的调度策略优化分析####
本文深入探讨了Linux操作系统内核中调度策略的工作原理,分析了不同调度算法(如CFS、实时调度)在多核处理器环境下的性能表现,并提出了针对高并发场景下调度策略的优化建议。通过对比测试数据,展示了调度策略调整对于系统响应时间及吞吐量的影响,为系统管理员和开发者提供了性能调优的参考方向。 ####
|
3月前
|
存储 Linux Shell
深入理解Linux操作系统的启动过程
【10月更文挑战第21天】本文将深入浅出地介绍Linux操作系统的启动过程,包括BIOS、引导加载程序、内核初始化和系统服务启动等环节。通过阅读本文,您将了解到Linux启动过程中的关键步骤和相关概念,以及如何优化启动速度。
|
3月前
|
安全 Linux
探索Linux操作系统的启动过程
在这篇文章中,我们将深入探讨Linux系统的启动流程,从电源开启到登录界面呈现的每一个步骤。我们将揭示BIOS、引导加载器、内核以及初始化进程如何协同工作,使Linux系统顺利启动。通过了解这些过程,读者将能更好地理解Linux系统的工作原理,并为可能出现的启动问题提供解决思路。
99 14
|
3月前
|
安全 Ubuntu Linux
Linux系统无法启动或启动过程中卡住
【10月更文挑战第5天】
614 3
|
5月前
|
Linux C语言
深度探索Linux操作系统 —— 编译过程分析
深度探索Linux操作系统 —— 编译过程分析
43 2
|
5月前
|
存储 Unix Linux
Linux 内核源代码情景分析(四)(下)
Linux 内核源代码情景分析(四)
33 2

热门文章

最新文章