U-Boot 多 CPU 执行状态引导

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
  1. 当前异常等级为 EL3 时,通过 armv8_switch_to_el2_m 切换到 EL2 并启动内核
  2. 当前异常等级为 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 也是每个厂家都有可能不同。

相关推荐
侠客行031713 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪13 小时前
深入浅出LangChain4J
java·langchain·llm
子兮曰13 小时前
OpenClaw入门:从零开始搭建你的私有化AI助手
前端·架构·github
吴仰晖13 小时前
使用github copliot chat的源码学习之Chromium Compositor
前端
1024小神13 小时前
github发布pages的几种状态记录
前端
较劲男子汉14 小时前
CANN Runtime零拷贝传输技术源码实战 彻底打通Host与Device的数据传输壁垒
运维·服务器·数据库·cann
老毛肚14 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
wypywyp14 小时前
8. ubuntu 虚拟机 linux 服务器 TCP/IP 概念辨析
linux·服务器·ubuntu
风流倜傥唐伯虎15 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Doro再努力15 小时前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim