1、引言
在实际项目开发中,经常遇见,uboot 引导 Linux 内核时,uboot 和 Linux 所处执行模式有所区别。uboot 无法直接通过跳转到 Linux 内核镜像地址运行。而是需要经过相关转换,将处理器执行模式(AArch64 或者 AArch32)进行切换,切换后再跳转运行。
2、vmlinux、Image、uImage 之间的关系
vmlinux
- 定义:Linux 内核编译后生成的最原始可执行文件,ELF 格式。
- 特点:
- 包含符号表、调试信息(可用于 gdb、objdump 调试和反汇编)。
- 一般不直接用于启动,而是作为内核构建过程的"中间产物"。
- 作用:
- 调试内核时使用(调试符号、反汇编)。
- 后续工具(objcopy 等)会基于它生成精简版本。
Image
- 定义:从 vmlinux 里提取出"纯粹的内核二进制映像"。
- 生成方式:
- 通过
objcopy -O binary vmlinux Image
得到。
- 通过
- 特点:
- 已经去掉了 ELF 头和调试信息,只剩下机器码。
- 可以直接被引导程序(如 bootloader)加载到内存并执行。
- 作用:
- 是最常见的内核启动镜像。
- 不含任何额外 header,仅仅是"裸内核映像"。
uImage
- 定义:在 Image 基础上,加上 U-Boot 专用的 64 字节头部,由 mkimage 工具生成。
- 生成方式:
mkimage -A <arch> -O linux -T kernel -C none -a <load_addr> -e <entry_point> -n "Linux Kernel" -d Image uImage
- 特点:
- 头部描述了架构、加载地址、入口地址、校验和、镜像类型等信息。
- 专门给 U-Boot 引导程序识别使用。
- 作用:
- U-Boot 启动 Linux 时,通常需要 uImage。
拓展:
比较常见的内核镜像文件,还有一个叫:zImage
-
zImage 是压缩过的内核镜像,主要用于减小内核体积,方便引导。
-
生成方式:
- 在内核编译时,Makefile 会调用压缩工具(gzip、LZMA、LZ4 等),对 Image 进行压缩,并在压缩数据前面加上一小段解压缩代码 (decompressor stub)。
- 所以 zImage = [解压缩头] + [压缩过的 Image]。
-
工作原理:
- Bootloader 把 zImage 加载到内存中。
- zImage 自带的解压缩头先运行,负责把压缩的内核解压缩到目标地址。
- 解压完毕后,跳转到内核入口,开始执行。
-
特点:
- 比 Image 体积小(适合存储空间紧张的嵌入式设备)。
- 内核启动时需要先自解压,启动时间略慢一些。
- 在很多嵌入式平台(包括 ARM)里历史上常用,因为存储/带宽有限。
2.1 mkimage
mkimage 与 Image / zImage 的关系
- mkimage 的作用:
- 不是区分 Image 或 zImage,它的功能就是在 任意二进制镜像(kernel / ramdisk / dtb 等)外面加上一个 U-Boot 专用的 64B 头部。
- 这个头部描述了:镜像类型(kernel/ramdisk)、压缩格式(none/gzip/lzma/...)、加载地址、入口点、校验和等。
- 所以:
mkimage -d Image uImage
→ 给 未压缩的内核 加 header。mkimage -d zImage uImage
→ 给 压缩过的内核 加 header。
- 区别在于镜像本身:
- 如果是 Image → 启动时,U-Boot 直接跳到内核入口执行。
- 如果是 zImage → 启动时,U-Boot 跳到解压缩 stub → stub 把内核解压到内存 → 跳到内核真正入口。
mkimage 的源码在 "uboot/tool" 目录下,在编译 uboot 时默认会编译出 mkimage 工具;
image_header_t
结构体定义在 uboot 源码的 image.h
中,和 mkimage工具的使用参数是对应关系,mkimage 工具就是构建image_header_t
这样一个64字节头;
c
/*
* Operating System Codes
*
* The following are exposed to uImage header.
* Do not change values for backward compatibility.
*/
enum {
IH_OS_INVALID = 0, /* Invalid OS */
IH_OS_OPENBSD, /* OpenBSD */
IH_OS_NETBSD, /* NetBSD */
IH_OS_FREEBSD, /* FreeBSD */
IH_OS_4_4BSD, /* 4.4BSD */
IH_OS_LINUX, /* Linux */
IH_OS_SVR4, /* SVR4 */
IH_OS_ESIX, /* Esix */
IH_OS_SOLARIS, /* Solaris */
IH_OS_IRIX, /* Irix */
IH_OS_SCO, /* SCO */
IH_OS_DELL, /* Dell */
IH_OS_NCR, /* NCR */
IH_OS_LYNXOS, /* LynxOS */
IH_OS_VXWORKS, /* VxWorks */
IH_OS_PSOS, /* pSOS */
IH_OS_QNX, /* QNX */
IH_OS_U_BOOT, /* Firmware */
IH_OS_RTEMS, /* RTEMS */
IH_OS_ARTOS, /* ARTOS */
IH_OS_UNITY, /* Unity OS */
IH_OS_INTEGRITY, /* INTEGRITY */
IH_OS_OSE, /* OSE */
IH_OS_PLAN9, /* Plan 9 */
IH_OS_OPENRTOS, /* OpenRTOS */
IH_OS_ARM_TRUSTED_FIRMWARE, /* ARM Trusted Firmware */
IH_OS_TEE, /* Trusted Execution Environment */
IH_OS_COUNT,
};
/*
* CPU Architecture Codes (supported by Linux)
*
* The following are exposed to uImage header.
* Do not change values for backward compatibility.
*/
enum {
IH_ARCH_INVALID = 0, /* Invalid CPU */
IH_ARCH_ALPHA, /* Alpha */
IH_ARCH_ARM, /* ARM */
IH_ARCH_I386, /* Intel x86 */
IH_ARCH_IA64, /* IA64 */
IH_ARCH_MIPS, /* MIPS */
IH_ARCH_MIPS64, /* MIPS 64 Bit */
IH_ARCH_PPC, /* PowerPC */
IH_ARCH_S390, /* IBM S390 */
IH_ARCH_SH, /* SuperH */
IH_ARCH_SPARC, /* Sparc */
IH_ARCH_SPARC64, /* Sparc 64 Bit */
IH_ARCH_M68K, /* M68K */
IH_ARCH_NIOS, /* Nios-32 */
IH_ARCH_MICROBLAZE, /* MicroBlaze */
IH_ARCH_NIOS2, /* Nios-II */
IH_ARCH_BLACKFIN, /* Blackfin */
IH_ARCH_AVR32, /* AVR32 */
IH_ARCH_ST200, /* STMicroelectronics ST200 */
IH_ARCH_SANDBOX, /* Sandbox architecture (test only) */
IH_ARCH_NDS32, /* ANDES Technology - NDS32 */
IH_ARCH_OPENRISC, /* OpenRISC 1000 */
IH_ARCH_ARM64, /* ARM64 */
IH_ARCH_ARC, /* Synopsys DesignWare ARC */
IH_ARCH_X86_64, /* AMD x86_64, Intel and Via */
IH_ARCH_XTENSA, /* Xtensa */
IH_ARCH_RISCV, /* RISC-V */
IH_ARCH_COUNT,
};
/*
* Image Types
*
* "Standalone
* Programs" are directly runnable in the environment
* provided by U-Boot; it is expected that (if they behave
* well) you can continue to work in U-Boot after return from
* the Standalone Program.
* "OS Kernel Images" are usually images of some Embedded OS which
* will take over control completely. Usually these programs
* will install their own set of exception handlers, device
* drivers, set up the MMU, etc. - this means, that you cannot
* expect to re-enter U-Boot except by resetting the CPU.
* "RAMDisk Images" are more or less just data blocks, and their
* parameters (address, size) are passed to an OS kernel that is
* being started.
* "Multi-File Images" contain several images, typically an OS
* (Linux) kernel image and one or more data images like
* RAMDisks. This construct is useful for instance when you want
* to boot over the network using BOOTP etc., where the boot
* server provides just a single image file, but you want to get
* for instance an OS kernel and a RAMDisk image.
*
* "Multi-File Images" start with a list of image sizes, each
* image size (in bytes) specified by an "uint32_t" in network
* byte order. This list is terminated by an "(uint32_t)0".
* Immediately after the terminating 0 follow the images, one by
* one, all aligned on "uint32_t" boundaries (size rounded up to
* a multiple of 4 bytes - except for the last file).
*
* "Firmware Images" are binary images containing firmware (like
* U-Boot or FPGA images) which usually will be programmed to
* flash memory.
*
* "Script files" are command sequences that will be executed by
* U-Boot's command interpreter; this feature is especially
* useful when you configure U-Boot to use a real shell (hush)
* as command interpreter (=> Shell Scripts).
*
* The following are exposed to uImage header.
* Do not change values for backward compatibility.
*/
enum {
IH_TYPE_INVALID = 0, /* Invalid Image */
IH_TYPE_STANDALONE, /* Standalone Program */
IH_TYPE_KERNEL, /* OS Kernel Image */
IH_TYPE_RAMDISK, /* RAMDisk Image */
IH_TYPE_MULTI, /* Multi-File Image */
IH_TYPE_FIRMWARE, /* Firmware Image */
IH_TYPE_SCRIPT, /* Script file */
IH_TYPE_FILESYSTEM, /* Filesystem Image (any type) */
IH_TYPE_FLATDT, /* Binary Flat Device Tree Blob */
IH_TYPE_KWBIMAGE, /* Kirkwood Boot Image */
IH_TYPE_IMXIMAGE, /* Freescale IMXBoot Image */
IH_TYPE_UBLIMAGE, /* Davinci UBL Image */
IH_TYPE_OMAPIMAGE, /* TI OMAP Config Header Image */
IH_TYPE_AISIMAGE, /* TI Davinci AIS Image */
/* OS Kernel Image, can run from any load address */
IH_TYPE_KERNEL_NOLOAD,
IH_TYPE_PBLIMAGE, /* Freescale PBL Boot Image */
IH_TYPE_MXSIMAGE, /* Freescale MXSBoot Image */
IH_TYPE_GPIMAGE, /* TI Keystone GPHeader Image */
IH_TYPE_ATMELIMAGE, /* ATMEL ROM bootable Image */
IH_TYPE_SOCFPGAIMAGE, /* Altera SOCFPGA CV/AV Preloader */
IH_TYPE_X86_SETUP, /* x86 setup.bin Image */
IH_TYPE_LPC32XXIMAGE, /* x86 setup.bin Image */
IH_TYPE_LOADABLE, /* A list of typeless images */
IH_TYPE_RKIMAGE, /* Rockchip Boot Image */
IH_TYPE_RKSD, /* Rockchip SD card */
IH_TYPE_RKSPI, /* Rockchip SPI image */
IH_TYPE_ZYNQIMAGE, /* Xilinx Zynq Boot Image */
IH_TYPE_ZYNQMPIMAGE, /* Xilinx ZynqMP Boot Image */
IH_TYPE_ZYNQMPBIF, /* Xilinx ZynqMP Boot Image (bif) */
IH_TYPE_FPGA, /* FPGA Image */
IH_TYPE_VYBRIDIMAGE, /* VYBRID .vyb Image */
IH_TYPE_TEE, /* Trusted Execution Environment OS Image */
IH_TYPE_FIRMWARE_IVT, /* Firmware Image with HABv4 IVT */
IH_TYPE_PMMC, /* TI Power Management Micro-Controller Firmware */
IH_TYPE_STM32IMAGE, /* STMicroelectronics STM32 Image */
IH_TYPE_SOCFPGAIMAGE_V1, /* Altera SOCFPGA A10 Preloader */
IH_TYPE_COUNT, /* Number of image types */
};
/*
* Compression Types
*
* The following are exposed to uImage header.
* Do not change values for backward compatibility.
*/
enum {
IH_COMP_NONE = 0, /* No Compression Used */
IH_COMP_GZIP, /* gzip Compression Used */
IH_COMP_BZIP2, /* bzip2 Compression Used */
IH_COMP_LZMA, /* lzma Compression Used */
IH_COMP_LZO, /* lzo Compression Used */
IH_COMP_LZ4, /* lz4 Compression Used */
IH_COMP_DETECT, /* Detect Compression Used */
IH_COMP_COUNT,
};
......
......
2.2 FIT image
uImage 的来源与局限
uImage 就是 mkimage 在 Image 或 zImage 上加了一个 64 字节 header。这个 header 里包含:
- 架构类型(ARM/MIPS/...)
- 加载地址
- 入口地址
- 镜像大小
- 校验和(CRC32)
- 压缩类型(gzip/lzma/...)
问题:
- 只能描述一个镜像 → 比如只能描述一个内核,没办法同时描述内核 + dtb + ramdisk。
- 不支持多配置 → 如果设备有多个 dtb(比如不同硬件型号),uImage 无法把它们组织到一起。
- 校验能力有限 → 只有简单的 CRC32,没有签名机制,无法满足安全启动需求。
FIT Image 的来源
FIT(Flattened Image Tree)格式就是 U-Boot 为了解决上面的问题设计的。名字里的 "Flattened" 来自于 FDT(Flattened Device Tree),即它的结构和设备树类似。
FIT Image 的实质:
- 用一个设备树描述文件(.its)来定义镜像的内容和属性;
- 然后用 mkimage 把这些内容打包成一个整体(.itb 文件)。
相比 uImage,FIT 有这些优势:
-
支持多组件打包。可以在一个 FIT Image 里同时包含:
- 内核镜像(kernel)
- 多个设备树(dtb)
- initramfs(ramdisk)
- 甚至 FPGA bitstream、固件 blob
-
U-Boot 在启动时可以根据配置选择合适的 dtb / ramdisk。
-
支持多配置(multi-configuration)
- 可以在一个 FIT 里打包多个"启动组合"。比如:
config@1: { kernel=kernel@1, fdt=fdt@1, ramdisk=ramdisk@1 } config@2:
{ kernel=kernel@1, fdt=fdt@2, ramdisk=ramdisk@1 }
- Bootloader 根据硬件情况选择 config@x,实现"一镜多机"。
- 更灵活
- uImage 的 header 固定 64B,字段死板;
- FIT Image 的描述信息写在"树"里,几乎可以无限扩展。
3、Bootm 引导命令
U-Boot 使用命令 bootm 来启动已经加载到内存中的内核。而 bootm 命令实际上调用的是 do_bootm 函数。
网上有很多说法,有 "bootm 加载镜像" 的说法,我认为这不准确。真正的 I/O 搬运动作,要用 mmc read、tftpboot 等命令先完成。而 bootm 只是去解析已经加载到内存中的内核镜像
u-boot-2018/cmd/bootm.c
c
U_BOOT_CMD(
bootm, CONFIG_SYS_MAXARGS, 1, do_bootm,
"boot application image from memory", bootm_help_text
);
U_BOOT_CMD
各项参数的意义如下:
- name:命令的名字,注意,它不是一个字符串(不要用双引号括起来);
- maxargs:最大的参数个数;
- repeatable:命令是否可以重复,可重复是指运行一个命令后,下次敲回车即可再次运行;
- command:对应的函数指针,类型为(*cmd)(struct cmd_tbl_s *, int, int, char *[]);
- usage:简单的使用说明,这是个字符串;
- help:较详细的使用说明,这是个字符串。
u-boot-2018/cmd/bootm.c
c
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
/*
* bootm 命令的第一个参数,即镜像加载地址(这时候,镜像已经被加载到内存中了)
* 从这个地址,就可以解析 image 镜像
*/
ulong os_load_addr = simple_strtoul(argv[1], NULL, 16);
......
return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
BOOTM_STATE_LOADOS |
#ifndef CONFIG_SUNXI_INITRD_ROUTINE
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
#ifndef CONFIG_SUNXI_UBIFS
BOOTM_STATE_RAMDISK |
#endif
#endif
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)
BOOTM_STATE_OS_CMDLINE |
#endif
BOOTM_STATE_OS_PREP | fake_go |
BOOTM_STATE_OS_GO, &images, 1);
}
函数入参:
- cmdtp:指向
cmd_tbl_t
结构体的指针,这个结构体包含了关于 bootm 命令本身的元信息,例如: - flag:命令执行的标志位,定义在
include/command.h
中 - argc:命令行参数的个数
- argv:命令行参数
do_bootm
最后调用的就是函数 do_bootm_states
,根据不同的 boot 状态执行不同的代码,函数代码如下:
u-boot-2018/common/bootm.c
c
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
int states, bootm_headers_t *images, int boot_progress)
{
......
/*
* Work through the states and see how far we get. We stop on
* any error.
*/
if (states & BOOTM_STATE_START)
ret = bootm_start(cmdtp, flag, argc, argv);
/* 主要工作是解析 image,获取一些基本参数信息赋值到 images 全局结构体中 */
if (!ret && (states & BOOTM_STATE_FINDOS))
ret = bootm_find_os(cmdtp, flag, argc, argv);
/* 获取一些其它信息,例如设备树地址等 */
if (!ret && (states & BOOTM_STATE_FINDOTHER))
ret = bootm_find_other(cmdtp, flag, argc, argv);
/* Load the OS */
/*
* 它的名字带个 "load",容易让人误解成 从存储介质加载,其实不是。它的主要工作是:
* 确认内核镜像在内存中的位置,如果需要的话,把压缩的内核解压到合适的物理内存位置
* 如果镜像是 uImage 格式,会检查 header,获取 load 地址
*/
if (!ret && (states & BOOTM_STATE_LOADOS)) {
iflag = bootm_disable_interrupts();
ret = bootm_load_os(images, 0);
if (ret && ret != BOOTM_ERR_OVERLAP)
goto err;
else if (ret == BOOTM_ERR_OVERLAP)
ret = 0;
}
......
/*
* 通过函数bootm_os_get_boot_func来查找系统启动 函数,参数images->os.os就是系统类型,根据这个系统类型来选择对应的启动函数
* 例如 Linux 内核镜像,解析出来的内核类型就是 IH_OS_LINUX,调用 IH_OS_LINUX 对应的系统启动函数为 do_bootm_linux
*/
boot_fn = bootm_os_get_boot_func(images->os.os);
......
/* Call various other states that are not generally used */
/* 一些不常用的其他各种状态 */
if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_BD_T))
ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_PREP)) {
#if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY)
if (images->os.os == IH_OS_LINUX)
fixup_silent_linux();
#endif
ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
}
......
/* Now run the OS! We hope this doesn't return */
/* 这里就是真的开始尝试启动 OS 了 */
if (!ret && (states & BOOTM_STATE_OS_GO))
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
images, boot_fn);
}
u-boot-2018/common/bootm_os.c
c
int boot_selected_os(int argc, char * const argv[], int state,
bootm_headers_t *images, boot_os_fn *boot_fn)
{
arch_preboot_os();
/* 最重要的就是这里的调用,例如 Linux 镜像,就会调用 do_bootm_linux 函数 */
boot_fn(state, argc, argv, images);
/* Stand-alone may return when 'autostart' is 'no' */
if (images->os.type == IH_TYPE_STANDALONE ||
IS_ENABLED(CONFIG_SANDBOX) ||
state == BOOTM_STATE_OS_FAKE_GO) /* We expect to return */
return 0;
bootstage_error(BOOTSTAGE_ID_BOOT_OS_RETURNED);
debug("\n## Control returned to monitor - resetting...\n");
return BOOTM_ERR_RESET;
}
arch\arm\lib\bootm.c
c
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
#ifdef CONFIG_ARM64
void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
void *res2);
/* fake 是一种假启动,只去解析/加载镜像,不去跳转运行 */
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
/* 函数 kernel_entry,此函数 Linux 内核入口 */
kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
void *res2))images->ep;
......
if (!fake) {
/* 初始化 PSCI 相关信息(不常用) */
#ifdef CONFIG_ARMV8_PSCI
armv8_setup_psci();
#endif
/* 完成安全态切换 */
do_nonsec_virt_switch();
update_os_arch_secondary_cores(images->os.arch);
/* u-boot 可能处于 EL3 级别时,当启动 linux 时会将异常级别降低到 el2 或者 el1 来启动 linux */
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
/* 强制要求进入 Linux 时 CPU 必须处于 EL1 状态 */
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
(u64)switch_to_el1, ES_TO_AARCH64);
#else
/* 进入 Linux 时 CPU 可以处于 EL2,然后由 Linux 内核自行执行相关切换到 EL1 */
if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
(images->os.arch == IH_ARCH_ARM))
/* 默认是 64-bit 平台,但要启动的是 32-bit Linux 内核 */
armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
(u64)images->ft_addr, 0,
(u64)images->ep,
ES_TO_AARCH32);
else
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
images->ep,
ES_TO_AARCH64);
#endif
}
#else /* ELSE CONFIG_ARM64 */
unsigned long machid = gd->bd->bi_arch_number;
char *s;
void (*kernel_entry)(int zero, int arch, uint params);
unsigned long r2;
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
kernel_entry = (void (*)(int, int, uint))images->ep;
......
if (!fake) {
#ifdef CONFIG_ARMV7_NONSEC
/*
* 使能 Non-Secure 模式 且 需要从 None-Secure 启动 Linux 内核
* armv7_boot_nonsec 会从 uboot 环境变量获取是否要从 None-Secure 启动
*/
if (armv7_boot_nonsec()) {
/* 初始化 ARMv7 的 Non-secure World 环境,并将当前核切换到 Non-secure 模式 */
armv7_init_nonsec();
/* 启动 Linux 内核 */
secure_ram_addr(_do_nonsec_entry)(kernel_entry,
0, machid, r2);
} else
#endif
/* 直接启动内核 */
kernel_entry(0, machid, r2);
}
#enidf /* END CONFIG_ARM64 */
}
int do_bootm_linux(int flag, int argc, char * const argv[],
bootm_headers_t *images)
{
/* No need for those on ARM */
if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
return -1;
if (flag & BOOTM_STATE_OS_PREP) {
boot_prep_linux(images);
return 0;
}
if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux(images);
boot_jump_linux(images, flag);
return 0;
}
4、U-boot 引导 OS
从上面的 bootm 指令的详细实现,已经可以对 uboot 引导不同类型的 kernel 初见端倪。下面我们详细讲解一下。
4.1 U-boot 引导 32 位 OS
c
if (!fake) {
#ifdef CONFIG_ARMV7_NONSEC
/*
* 使能 Non-Secure 模式 且 需要从 None-Secure 启动 Linux 内核
* armv7_boot_nonsec 会从 uboot 环境变量获取是否要从 None-Secure 启动
*/
if (armv7_boot_nonsec()) {
/* 初始化 ARMv7 的 Non-secure World 环境,并将当前核切换到 Non-secure 模式 */
armv7_init_nonsec();
/* 启动 Linux 内核 */
secure_ram_addr(_do_nonsec_entry)(kernel_entry,
0, machid, r2);
} else
#endif
/* 直接启动内核 */
kernel_entry(0, machid, r2);
}
_do_nonsec_entry
函数实现如下:
c
ENTRY(_do_nonsec_entry)
mov ip, r0
mov r0, r1
mov r1, r2
mov r2, r3
smc #0
ENDPROC(_do_nonsec_entry)
该函数实现是通过 SMC
指令,陷入 BL31 中去处理 non-secure 状态切换事宜。
4.2 U-boot 引导 64 位 OS
c
#ifdef CONFIG_ARMV8_SWITCH_TO_EL1
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
(u64)switch_to_el1, ES_TO_AARCH64);
#else
if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
(images->os.arch == IH_ARCH_ARM))
/* 默认是 64-bit 平台,但要启动的是 32-bit Linux 内核 */
armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
(u64)images->ft_addr, 0,
(u64)images->ep,
ES_TO_AARCH32);
else
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
images->ep,
ES_TO_AARCH64);
#endif
armv8_switch_to_el2 函数参数:
- 设备树地址
- 传递给目标执行环境的参数
- 传递给目标执行环境的参数(也可以是设备树地址)
- 传递给目标执行环境的参数
- Linux 内核镜像入口地址(也可以是其它函数地址)
- 根据需要启动的 Linux 内核镜像,指定目标异常级别的执行状态(AArch64 vs AArch32)
函数实现:
arch\arm\cpu\armv8\transition.S
c
.pushsection .text.armv8_switch_to_el2, "ax"
ENTRY(armv8_switch_to_el2)
switch_el x6, 1f, 0f, 0f
0:
cmp x5, #ES_TO_AARCH64
b.eq 2f
/*
* When loading 32-bit kernel, it will jump
* to secure firmware again, and never return.
*/
bl armv8_el2_to_aarch32
2:
/*
* x4 is kernel entry point or switch_to_el1
* if CONFIG_ARMV8_SWITCH_TO_EL1 is defined.
* When running in EL2 now, jump to the
* address saved in x4.
*/
br x4
1: armv8_switch_to_el2_m x4, x5, x6
ENDPROC(armv8_switch_to_el2)
它根据当前运行的异常等级,确定需要执行的分支。由于 uboot 可以执行在 EL1、EL2 或 EL3 下,因此这里对其分别执行不同的处理。通过下面 switch_el 的定义,可知当前运行异常等级不同时,其跳转分支分别如下:
c
.macro switch_el, xreg, el3_label, el2_label, el1_label
mrs \xreg, CurrentEL
cmp \xreg, 0xc // EL3
b.eq \el3_label
cmp \xreg, 0x8 // EL2
b.eq \el2_label
cmp \xreg, 0x4 // EL1
b.eq \el1_label
.endm
- 当前异常等级为 EL3 时,通过
armv8_switch_to_el2_m
切换到 EL2 并启动内核 - 当前异常等级为 EL1 或 EL2,根据目标内核运行状态 aarch32 还是 aarch64
a. 如果是 aarch32 ,则调用armv8_el2_to_aarch32
,切换到 aarch32
b. 如果是 aarch64,若未定义CONFIG_ARMV8_SWITCH_TO_EL1
,则该函数会直接跳转到内核入口函数处启动内核,此时内核的异常等级与 uboot 当前运行的异常等级相同;若定义了CONFIG_ARMV8_SWITCH_TO_EL1
,则会跳转到switch_to_el1
接口,cpu 先切换到 EL1,然后再启动内核
以 armv8_el2_to_aarch32
函数为例,可以看到,该函数实现是通过 SMC
指令,陷入 BL31 中去处理 AArch64 EL2 to AArch32 EL2 切换事宜。
c
/*
* Switch from AArch64 EL2 to AArch32 EL2
* @param inputs:
* x0: argument, zero
* x1: machine nr
* x2: fdt address
* x3: input argument
* x4: kernel entry point
* @param outputs for secure firmware:
* x0: function id
* x1: kernel entry point
* x2: machine nr
* x3: fdt address
*/
ENTRY(armv8_el2_to_aarch32)
mov x3, x2
mov x2, x1
mov x1, x4
ldr x0, =0xc000ff04
smc #0
ret
ENDPROC(armv8_el2_to_aarch32)
以 armv8_switch_to_el2_m
为例:
c
/*
* Switch from EL3 to EL2 for ARMv8
* @ep: kernel entry point
* @flag: The execution state flag for lower exception
* level, ES_TO_AARCH64 or ES_TO_AARCH32
* @tmp: temporary register
*
* For loading 32-bit OS, x1 is machine nr and x2 is ftaddr.
* For loading 64-bit OS, x0 is physical address to the FDT blob.
* They will be passed to the guest.
*/
.macro armv8_switch_to_el2_m, ep, flag, tmp
......
/* Check switch to AArch64 EL2 or AArch32 Hypervisor mode */
cmp \flag, #ES_TO_AARCH32
b.eq 1f
/*
* The next lower exception level is AArch64, 64bit EL2 | HCE |
* RES1 (Bits[5:4]) | Non-secure EL0/EL1.
* and the SMD depends on requirements.
*/
#ifdef CONFIG_ARMV8_PSCI
ldr \tmp, =(SCR_EL3_RW_AARCH64 | SCR_EL3_HCE_EN |\
SCR_EL3_RES1 | SCR_EL3_NS_EN)
#else
ldr \tmp, =(SCR_EL3_RW_AARCH64 | SCR_EL3_HCE_EN |\
SCR_EL3_SMD_DIS | SCR_EL3_RES1 |\
SCR_EL3_NS_EN)
#endif
msr scr_el3, \tmp
/* Return to the EL2_SP2 mode from EL3 */
ldr \tmp, =(SPSR_EL_DEBUG_MASK | SPSR_EL_SERR_MASK |\
SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\
SPSR_EL_M_AARCH64 | SPSR_EL_M_EL2H)
msr spsr_el3, \tmp
msr elr_el3, \ep
eret
1:
/*
* The next lower exception level is AArch32, 32bit EL2 | HCE |
* SMD | RES1 (Bits[5:4]) | Non-secure EL0/EL1.
*/
ldr \tmp, =(SCR_EL3_RW_AARCH32 | SCR_EL3_HCE_EN |\
SCR_EL3_SMD_DIS | SCR_EL3_RES1 |\
SCR_EL3_NS_EN)
msr scr_el3, \tmp
/* Return to AArch32 Hypervisor mode */
ldr \tmp, =(SPSR_EL_END_LE | SPSR_EL_ASYN_MASK |\
SPSR_EL_IRQ_MASK | SPSR_EL_FIQ_MASK |\
SPSR_EL_T_A32 | SPSR_EL_M_AARCH32 |\
SPSR_EL_M_HYP)
msr spsr_el3, \tmp
msr elr_el3, \ep
eret
.endm
可以看到,不仅仅是切换了处理器的执行状态,也切换了 SCR_EL3_NS_EN
处理器的安全状态。通常 Uboot 引导 Linux 后,Linux 系统都会处于 Non-Secure 状态。
4.3 两个重要的寄存器
4.3.1 SCR_EL3
ARMv8 架构下,有一个 SCR_EL3
寄存器。该寄存器只能在 EL3 等级去访问。
通过修改 RW bit 位,来控制下一级异常等级(ELx)是 AArch32 还是 AArch64。
4.3.1 ELR_EL3

该寄存器,保存从 EL3 返回时,需要返回的地址。
从这 SCR_EL3、ELR_EL3两个寄存器,我们可以知道:
- EL3 状态下,可以配置并控制低等级 EL(EL2/EL1/EL0)的执行状态
- 而 EL3 自身的状态(32/64 位)由硬件/BootROM 决定,软件不能切换
5、总结
文章主要总结了,bootm 命令相关知识。要注意的是,本文是从官方 uboot 的角度来看待 bootm 命令。对于不同的板卡来说,bootm 的命令实现都有可能不同,因为厂商会去自定义修改相关函数实现。同样的,BL31 也是每个厂家都有可能不同。