- 内核复位后在裸机上运行的代码,即在不使用操作系统的情况下运行的代码。这是首次启动芯片或系统时经常遇到的情况。
- bootloader如何加载和运行Linux内核。
从裸机启动
芯片复位后,将在异常向量表中复位向量的位置开始执行。复位操作的代码必须做以下事情:
- 在多核系统中,使非主核进入睡眠状态
- 初始化异常向量。
- 初始化内存系统,包括MMU。
- 初始化核心模式堆栈和寄存器。
- 初始化任何关键的 I/O 设备。
- 执行NEON 或VFP 的任何必要初始化。
- 启用中断。
- 更改核心模式或状态。
- 处理安全世界所需的任何设置(参见第 21 章)。
- 调用main() 应用程序。
GNU 汇编器中的 _start 指令告诉链接器将代码定位在特定地址,并可用于将代码放置在向量表中。初始向量表将位于非易失性存储器中,并且可以包含跳转到自我指令(除了复位向量),因为此时预计不会出现异常。通常,复位向量包含指向 ROM 中引导代码的分支。
ROM 可以别名为异常向量的地址。然后,ROM 写入一些将 RAM 映射到地址 0 的内存重映射外设,并将真正的异常向量表复制到 RAM 中。这意味着处理重新映射的引导代码部分必须与位置无关,因为只能使用 PC 相对寻址。
下一步是设置缓存、MMU 和分支预测器。
MMU TLB 必须无效。分支目标预测器硬件可能不必显式失效,但必须由引导代码启用。此时可以安全地启用分支预测;这将提高性能。
在此之后,您可以创建一些翻译表,如示例 13-4 的示例代码所示。变量 ttb_address 用于表示要用于初始转换表的地址。这必须是一个 16KB 的内存区域(其起始地址与 16KB 边界对齐),此代码可以向其中写入 L1 转换表。
接下来的步骤将取决于系统的确切性质。例如,可能需要对将保存未初始化 C 变量的内存进行零初始化,将其他变量的初始值从 ROM 映像复制到 RAM,并设置应用程序堆栈和堆空间。可能还需要初始化 C 库函数、调用顶级构造函数(用于 C 代码)和其他标准嵌入式 C 初始化。
Booting Linux
如果选择了 HIVECS(称为高向量),那么了解内核从复位中出来并在异常基地址 0x00000000 或 0xFFFF0000 处执行其第一条指令会发生什么是很有用的,直到出现 Linux 命令提示符为止。
当内核存在于内存中时,基于 ARM 处理器的系统上的序列类似于台式计算机上可能发生的序列。但是,引导加载过程可能非常不同,因为基于 ARM 处理器的手机或更深入的嵌入式设备可能缺少硬盘驱动器或类似 PC 的 BIOS。
通常,当您打开系统电源时会发生硬件特定的引导代码从闪存或 ROM 运行。此代码初始化系统,包括任何必要的硬件外围代码,然后启动引导加载程序(例如 U-Boot)。这会初始化主内存并将压缩的 Linux 内核映像复制到主内存中(从闪存设备、板上的内存、MMC、主机 PC 或其他地方)。引导加载程序将某些初始化参数传递给内核。然后,Linux 内核会自行解压并初始化其数据结构和运行的用户进程,然后再启动命令 shell 环境。让我们更详细地看看这些过程中的每一个。
Reset handler
通常有少量特定于系统的引导监控代码,用于配置内存控制器并执行其他系统外围设备初始化。它在内存中设置堆栈,通常将自身从 ROM 复制到 RAM,然后更改硬件内存映射,以便 RAM 映射到异常向量地址,而不是 ROM。本质上,此代码独立于要在板上运行的操作系统并执行类似于 PC BIOS 的功能。当它完成执行后,它将调用一个 Linux 引导加载程序,例如 U-Boot。
Bootloader
Linux 需要执行一定数量的代码才能完成重置,以初始化系统。这将执行内核启动所需的基本任务:
- 初始化内存系统和外围设备。
- 将内核映像加载到内存中的适当位置(也可能是初始 RAM 磁盘)。
- 生成要传递给内核的引导参数(包括机器类型)。
- 为内核设置控制台(视频或串行)。
- 进入内核。
不同引导加载程序所采取的具体步骤有所不同,因此有关详细信息,请参阅您要使用的引导加载程序的文档。U-Boot 是一个广泛使用的示例,但其他可能的引导加载程序包括 Apex、Blob、Bootldr 和 Redboot。
当引导加载程序启动时,它通常不存在于主存储器中。它必须首先分配堆栈并初始化核心(例如使其缓存无效)并将其自身安装到主内存。它还必须为全局数据和 malloc() 使用分配空间,并将异常向量条目复制到适当的位置。
Initialize memory system
这在很大程度上是一块板或系统特定的代码。Linux 内核不负责系统中 RAM 的配置。它显示了物理内存布局,但没有其他关于内存系统的知识。在许多系统中,可用 RAM 及其位置是固定的,并且引导加载程序任务很简单。在其他系统中,必须编写代码来发现系统中可用的 RAM 量。
Kernel images
构建过程中的内核映像通常以 zImage 格式压缩(可引导内核映像的常规名称)。它的头代码包含一个魔术字,用于验证解压的完整性,加上开始和结束地址。内核代码与位置无关,可以位于内存中的任何位置。按照惯例,它被放置在距离物理 RAM 基数 0x8000 的偏移处。这为放置在 0x100 偏移处的参数块提供了空间(用于转换表等)。
许多系统需要一个初始 RAM 磁盘 (initrd),因为这可以让您拥有一个可用的根文件系统,而无需设置其他驱动程序。引导加载程序可以将初始 ramdisk 映像放入内存,并使用 ATAG_INITRD2(描述压缩 RAM 磁盘映像的物理位置的标签)和 ATAG_RAMDISK 将其位置传递给内核。
引导加载程序通常会在目标中设置一个串行端口,使内核串行驱动程序能够检测该端口并将其用于控制台。在某些系统中,可以将另一个输出设备(例如视频驱动程序)用作控制台。内核命令行参数console=可以用来传递信息。
Kernel parameters using ATAGs
从历史上看,传递给内核的参数是以标记列表的形式,放置在物理 RAM 中,寄存器 R2 保存列表的地址。标签头包含两个 32 位无符号整数,第一个给出标签的字大小,第二个提供标签值(指示标签的类型)。有关可以传递的参数的完整列表,请参阅相应的文档。示例包括描述物理内存映射的 ATAG_MEM 和描述压缩 ramdisk 映像所在位置的 ATAG_INITRD2。引导加载程序还必须提供 ARM Linux 机器类型号 (MACH_TYPE)。这可以是硬编码的值,或者引导代码可以检查可用的硬件并相应地分配一个值。
有一种更灵活或更通用的方法可以使用扁平设备树 (FDT) 传递此信息。
Kernel parameters using Flattened Device Trees
为 PowerPC 内核引入了 Linux 设备树或 FDT 支持,作为 32 位和 64 位内核合并的一部分,通过使用适用于所有 PowerPC 平台、服务器、台式机和嵌入式的开放固件接口来标准化固件接口 . 它已成为 PowerPC、Micro Blaze 和 SPARC 架构的 Linux 内核中使用的配置方法。
设备树是描述硬件配置的数据结构。它包括有关处理器、内存大小和组、中断配置和外围设备的信息。数据结构被组织成一棵树,有一个名为 / 的根节点。除根节点外,每个节点都有一个父节点。每个节点都有一个名称,并且可以有任意数量的子节点。节点还可以包含具有任意数据的命名属性值,它们以键值对表示。
Kernel entry
内核执行必须从处于固定状态的内核开始。bootloader通过直接跳转到它的第一条指令(arch/arm/boot/compressed/head.S中的开始标签)来调用内核映像。必须禁用 MMU 和数据缓存。内核必须处于超级用户模式,并设置 CPSR寄存器的 I 和 F 位(禁用 IRQ 和 FIQ)。R0 必须包含 0,R1 是 MACH_TYPE 值,R2 是标记参数列表的地址。
内核工作的第一步是解压缩它。这是独立于架构的。保存从bootloader传递的参数并启用缓存和MMU。在调用arch/arm/boot/compressed/misc.c 中的decompress_kernel() 之前,会检查解压后的图像是否会覆盖压缩后的图像,清除缓存然后再次禁用。然后支到 arch/arm/kernel/head.S 中的内核启动入口点。
Platform-specific actions
首先使用__lookup_processor_type()
检查内核类型,该函数返回一个码,指定它在哪个内核上运行。然后使用函数__lookup_machine_type()
来查找机器类型。然后定义一组基本的转换表,映射内核代码。然后初始化缓存和MMU并设置其他控制寄存器。数据段被复制到 RAM 并调用start_kernel()。
Kernel start-up code
原则上,启动顺序的其余部分在任何架构上都是相同的,但实际上某些功能仍然依赖于硬件。
- 使用 local_irq_disable() 禁用 IRQ 中断,而 lock_kernel() 用于阻止 FIQ 中断中断内核。它初始化tick control、内存系统和特定于体系结构的子系统,并处理bootloader传递的命令行选项。
- 设置堆栈并初始化 Linux 调度程序。
- 设置各种内存区域并分配页面。
- 设置中断和异常表和处理程序,以及 GIC
- 系统计时器已设置,此时 IRQ 已启用。进行额外的内存系统初始化,然后使用一个名为 BogoMips 的值来校准核心时钟速度。
- 设置内核的内部组件,包括文件系统和初始化进程,然后是创建内核线程的线程守护进程。
- 内核解锁(启用 FIQ)并启动调度程序
- 调用函数 do_basic_setup() 来初始化驱动程序、sysctl、工作队列和网络套接字。此时,执行到用户模式的切换。