在重新编译Linux内核与设备树后,需要使用uboot加载新的内核与设备树。
开发板启动后,进入uboot命令行,执行以下命令:

设置环境变量bootargs启动参数:

  • console=ttyAMA1,115200:指定了控制台输出设备为ttyAMA1,波特率为115200。这意味着内核将在ttyAMA1上输出调试信息和控制台输入。
  • earlycon=pl011,0x2800d000:指定了早期控制台设备为pl011,基地址为0x2800d000。早期控制台用于在内核初始化期间提供调试信息。
1
setenv bootargs 'console=ttyAMA1,115200 earlycon=pl011,0x2800d000 root=/dev/mmcblk0p1 rootwait rw'

加载内核至指定内存地址0x90100000

1
ext4load mmc 0:1 0x90100000 boot/Image

加载设备树至指定内存地址0x90000000

1
ext4load mmc 0:1 0x90000000 boot/phytiumpi_firefly.dtb

开始引导Linux系统,其中的-表明不使用initial RAM disk来启动内核,booti命令的使用细节可以参考uboot官方文档

1
booti 0x90100000 - 0x90000000

若需要在每次开机时uboot自动执行以上命令,可以设置uboot中的bootcmd环境变量

1
setenv bootcmd 'ext4load mmc 0:1 0x90100000 boot/Image; ext4load mmc 0:1 0x90000000 boot/phytiumpi_firefly.dtb; booti 0x90100000 - 0x90000000'

上述操作过程引发了很多疑惑:0x90100000、0x90000000和0x2800d000这个地址是怎么来的呢?pl011是什么东西?内核与设备树加载到了两块地址上,那内核是如何获取设备树信息的呢?

uboot中使用的地址

0x2800d000

首先是0x2800d000这个地址,通过查阅phytiumpi开发板的设备树文件,在pe220x.dtsi这个文件中找到了相关的信息,0x2800d000是uart1的映射地址。

为什么是这个地址?查阅飞腾派芯片手册,0x000_2800_0000 ~ 0x000_2FFF_FFFF这块地址空间分配给了低速设备。

其中0x000_2800_D000 ~ 0x000_2800_DFFF 的4KB地址空间分配给了UART1。

1
2
3
4
5
6
7
8
uart1: uart@2800d000 {
compatible = "arm,pl011","arm,primecell";
reg = <0x0 0x2800d000 0x0 0x1000>;
interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&sysclk_100mhz &sysclk_100mhz>;
clock-names = "uartclk", "apb_pclk";
status = "disabled";
};

不同的开发板有不同的设备树,因此这个地址是因设备的变化而变化的。

PL011指PrimeCell UART,是ARM公司设计的UART IP核,具体的文档可以在ARM官网查看。

0x90100000与0x90000000

在飞腾派中,0x000_8000_0000 ~ 0x000_FFFF_FFFF地址空间分配为Memory 空间,0x90100000与0x90000000均位于其中。

根据booti命令的参数,0x90000000为设备树的加载地址,0x90100000为Linux内核的加载地址。编译后的dtb大小为24.5KB,未压缩的内核大小为23.5MB,地址0x90000000与0x90100000之间的空间约为1048KB,足够设备树文件加载。经过查阅uboot源码,这两个地址是在移植uboot时进行设置的。

由于没有飞腾派移植uboot的源码,这里先展示以下rk3588的案例,u-boot/include/configs/rk3588_common.h中有如下配置:

1
2
3
4
5
6
7
8
9
10
11
#define ENV_MEM_LAYOUT_SETTINGS		\
"scriptaddr=0x00c00000\0" \
"script_offset_f=0xffe000\0" \
"script_size_f=0x2000\0" \
"pxefile_addr_r=0x00e00000\0" \
"kernel_addr_r=0x02000000\0" \
"kernel_comp_addr_r=0x0a000000\0" \
"fdt_addr_r=0x12000000\0" \
"fdtoverlay_addr_r=0x12100000\0" \
"ramdisk_addr_r=0x12180000\0" \
"kernel_comp_size=0x8000000\0"

可以看从中看出,内核加载地址为0x02000000,设备树加载地址为0x12000000。因此这部分地址也是根据不同的芯片而改变的。

booti,bootz与bootm

uboot能启动的内核格式:Image, zImage, uImage, fdt方式。

uboot提供了三种命令来启动内核:booti, bootz, bootm三个命令,分别用于对应Image, zImage, ulmage/FIT三种内核格式:

  • Image: 内核的原始烧录镜像文件, 也可以对他进行压缩, 根据不同的压缩算法有不同的文件名, gzip算法为Image.gz.
  • zImage: 压缩后的内核烧录镜像文件, 采用的是自压缩算法(不需要额外的解压器).
  • uImage: 这是uboot提供的一种内核Wrapper, 包含了内核烧录镜像与其他信息。由于uImage存在很大限制与安全隐患, 已不被官方支持, 现称其为legacy image format。
  • FIT: Flat Image Tree (FIT)是最新的uImage文件, 为了更好的支持单个固件的通用性,类似于kernel device tree机制,uboot也需要对这种uImage固件进行支持。FIT uImage中加入多个dtb文件,和ramdisak文件,当然如果需要的话,同样可以支持多个kernel文件。这样的目的就是能够使同一个uImage就能够在uboot中选择特定的kernel/dtb和ramdisk进行启动了,达成一个uImage可以通用多个板型的目的。

Boot流程分析

uboot是如何引导Boot image的?

接下来将对uboot源码进行分析,最新以及历史的uboot源码可以在Bootlin的Elixir Cross Referencer中查看

Boot流程主要用了两个结构体:bootm_info与bootm_headers,定义均位于/include/bootm.h文件中。

bootm_info结构体用于存储Boot系统所需的信息:镜像地址、ramdisk地址、fdt设备树地址等。

1
2
3
4
5
6
7
8
9
10
struct bootm_info {
const char *addr_img;
const char *conf_ramdisk;
const char *conf_fdt;
bool boot_progress;
struct bootm_headers *images;
const char *cmd_name;
int argc;
char *const *argv;
};

bootm_headers记录了image镜像的具体信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
* Legacy and FIT format headers used by do_bootm() and do_bootm_<os>() routines.
*/
struct bootm_headers {
/*
* Legacy os image header, if it is a multi component image
* then boot_get_ramdisk() and get_fdt() will attempt to get
* data from second and third component accordingly.
*/
struct legacy_img_hdr *legacy_hdr_os; /* image header pointer */
struct legacy_img_hdr legacy_hdr_os_copy; /* header copy */
ulong legacy_hdr_valid;

/*
* The fit_ members are only used with FIT, but it involves a lot of
* #ifdefs to avoid compiling that code. Since FIT is the standard
* format, even for SPL, this extra data size seems worth it.
*/
const char *fit_uname_cfg; /* configuration node unit name */

void *fit_hdr_os; /* os FIT image header */
const char *fit_uname_os; /* os subimage node unit name */
int fit_noffset_os; /* os subimage node offset */

void *fit_hdr_rd; /* init ramdisk FIT image header */
const char *fit_uname_rd; /* init ramdisk subimage node unit name */
int fit_noffset_rd; /* init ramdisk subimage node offset */

void *fit_hdr_fdt; /* FDT blob FIT image header */
const char *fit_uname_fdt; /* FDT blob subimage node unit name */
int fit_noffset_fdt;/* FDT blob subimage node offset */

void *fit_hdr_setup; /* x86 setup FIT image header */
const char *fit_uname_setup; /* x86 setup subimage node name */
int fit_noffset_setup;/* x86 setup subimage node offset */

#ifndef USE_HOSTCC
struct image_info os; /* os image info */
ulong ep; /* entry point of OS */

ulong rd_start, rd_end;/* ramdisk start/end */

char *ft_addr; /* flat dev tree address */
ulong ft_len; /* length of flat device tree */

ulong initrd_start;
ulong initrd_end;
ulong cmdline_start;
ulong cmdline_end;
struct bd_info *kbd;
#endif

int verify; /* env_get("verify")[0] != 'n' */

#define BOOTM_STATE_START 0x00000001
#define BOOTM_STATE_FINDOS 0x00000002
#define BOOTM_STATE_FINDOTHER 0x00000004
#define BOOTM_STATE_LOADOS 0x00000008
#define BOOTM_STATE_RAMDISK 0x00000010
#define BOOTM_STATE_FDT 0x00000020
#define BOOTM_STATE_OS_CMDLINE 0x00000040
#define BOOTM_STATE_OS_BD_T 0x00000080
#define BOOTM_STATE_OS_PREP 0x00000100
#define BOOTM_STATE_OS_FAKE_GO 0x00000200 /* 'Almost' run the OS */
#define BOOTM_STATE_OS_GO 0x00000400
#define BOOTM_STATE_PRE_LOAD 0x00000800
#define BOOTM_STATE_MEASURE 0x00001000
int state;

#if defined(CONFIG_LMB) && !defined(USE_HOSTCC)
struct lmb lmb; /* for memory mgmt */
#endif
};

本文使用了booti命令来启动内核,命令源码uboot/cmd/booti.c文件中。当在uboot命令行中输入booti时,调用了do_booti函数。

do_booti主要进行了如下操作:

  1. 初始化bootm_info结构体, 其中调用了bootm_init函数;
  2. 调用booti_start函数加载image镜像至内存中;
  3. 关闭中断, 调用bootm_run_states函数启动Linux系统, bootm_run_states函数根据不同的state参数进行不同的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int do_booti(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
struct bootm_info bmi;
int states;
int ret;

/* Consume 'booti' */
argc--; argv++;

bootm_init(&bmi); //初始化bmi结构体,设置为空
if (argc)
bmi.addr_img = argv[0];
if (argc > 1)
bmi.conf_ramdisk = argv[1];
if (argc > 2)
bmi.conf_fdt = argv[2];
bmi.boot_progress = true;
bmi.cmd_name = "booti";
/* do not set up argc and argv[] since nothing uses them */

if (booti_start(&bmi)) //booti_start运行成功,返回0
return 1;

/*
* We are doing the BOOTM_STATE_LOADOS state ourselves, so must
* disable interrupts ourselves
*/
bootm_disable_interrupts();

images.os.os = IH_OS_LINUX; //启动镜像类型
if (IS_ENABLED(CONFIG_RISCV_SMODE))
images.os.arch = IH_ARCH_RISCV;
else if (IS_ENABLED(CONFIG_ARM64))
images.os.arch = IH_ARCH_ARM64; //选择启动镜像架构

states = BOOTM_STATE_MEASURE | BOOTM_STATE_OS_PREP |
BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO;
if (IS_ENABLED(CONFIG_SYS_BOOT_RAMDISK_HIGH))
states |= BOOTM_STATE_RAMDISK;

ret = bootm_run_states(&bmi, states); //启动Linux系统

return ret;
}

booti_start函数进行了如下操作:

  1. 调用bootm_run_states函数, state参数设置为BOOTM_STATE_START, 执行bootm_start函数,初始化bootm_header结构体;

  2. 调用image_decomp_type检测image镜像是否被压缩, 若被压缩则进行解压;

  3. 调用booti_setup函数检测image镜像是否正确,返回Linux aarch64 Image的起始地址和大小;

  4. 调用bootm_find_images函数检测image镜像是否加载正确.

综上, booti_start函数作用为配置并加载image镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
static int booti_start(struct bootm_info *bmi)
{
struct bootm_headers *images = bmi->images;
int ret;
ulong ld;
ulong relocated_addr;
ulong image_size;
uint8_t *temp;
ulong dest;
ulong dest_end;
unsigned long comp_len;
unsigned long decomp_len;
int ctype;

ret = bootm_run_states(bmi, BOOTM_STATE_START); //初始化

/* Setup Linux kernel Image entry point */
if (!bmi->addr_img) {
ld = image_load_addr;
debug("* kernel: default image load address = 0x%08lx\n",
image_load_addr);
} else {
ld = hextoul(bmi->addr_img, NULL);
debug("* kernel: cmdline image address = 0x%08lx\n", ld);
}

temp = map_sysmem(ld, 0);
ctype = image_decomp_type(temp, 2); //返回镜像的压缩类型,0表示未压缩
if (ctype > 0) {
dest = env_get_ulong("kernel_comp_addr_r", 16, 0);
comp_len = env_get_ulong("kernel_comp_size", 16, 0);
if (!dest || !comp_len) {
puts("kernel_comp_addr_r or kernel_comp_size is not provided!\n");
return -EINVAL;
}
if (dest < gd->ram_base || dest > gd->ram_top) {
puts("kernel_comp_addr_r is outside of DRAM range!\n");
return -EINVAL;
}

debug("kernel image compression type %d size = 0x%08lx address = 0x%08lx\n",
ctype, comp_len, (ulong)dest);
decomp_len = comp_len * 10;
ret = image_decomp(ctype, 0, ld, IH_TYPE_KERNEL,
(void *)dest, (void *)ld, comp_len,
decomp_len, &dest_end);
if (ret)
return ret;
/* dest_end contains the uncompressed Image size */
memmove((void *) ld, (void *)dest, dest_end);
}
unmap_sysmem((void *)ld);

ret = booti_setup(ld, &relocated_addr, &image_size, false); //检测image地址和大小是否正确,0表示正确
if (ret)
return 1;

/* Handle BOOTM_STATE_LOADOS */
if (relocated_addr != ld) {
printf("Moving Image from 0x%lx to 0x%lx, end=%lx\n", ld,
relocated_addr, relocated_addr + image_size);
memmove((void *)relocated_addr, (void *)ld, image_size);
}

images->ep = relocated_addr;
images->os.start = relocated_addr;
images->os.end = relocated_addr + image_size;

lmb_reserve(&images->lmb, images->ep, le32_to_cpu(image_size));

/*
* Handle the BOOTM_STATE_FINDOTHER state ourselves as we do not
* have a header that provide this informaiton.
*/
if (bootm_find_images(image_load_addr, bmi->conf_ramdisk, bmi->conf_fdt,
relocated_addr, image_size)) //加载image、ramdisk、fdt,如果加载成功,返回0
return 1;

return 0;
}

do_booti最后一步调用bootm_run_states时states参数设置为BOOTM_STATE_MEASURE | BOOTM_STATE_OS_PREP |BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO, bootm_run_states进行了如下操作:

  1. 调用bootm_measure函数, 对image镜像进行测量(进行哈希处理并存储到安全内存(例如TPM PCR)的过程),用于安全认证;
  2. 调用boot_fn = bootm_os_get_boot_func(images->os.os), 获取启动特定操作系统的函数指针(这里是Linux),结构体参数 images->os.os 就是系统类型,根据这个系统类型来选择对应的启动函数,在 do_booti 中设置了 images.os.os= IH_OS_LINUX。函数返回值就是找到的系统启动函数,这里找到的 Linux 系统启动函数为 do_bootm_linux。因此 boot_fn=do_bootm_linux,后面执行 boot_fn 函数的地方实际上是执行的 do_bootm_linux 函数;
  3. 调用boot_fn(BOOTM_STATE_OS_PREP, bmi),进行启动image前的一些初始化工作,boot_prep_linux 主要用于处理环境变量 bootargs,bootargs 保存着传递给 Linux kernel 的参数;
  4. boot_selected_os(BOOTM_STATE_OS_FAKE_GO, bmi, boot_fn),fake go用于完成执行跳转到OS image前的初始化,这个步骤用于保证能够追踪trace uboot系统记录直至最后一刻;
  5. 最后一步,调用boot_selected_os(BOOTM_STATE_OS_GO, bmi, boot_fn)函数,启动操作系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
int bootm_run_states(struct bootm_info *bmi, int states)
{
struct bootm_headers *images = bmi->images;
boot_os_fn *boot_fn;
ulong iflag = 0;
int ret = 0, need_boot_fn;

images->state |= states;

/*
* Work through the states and see how far we get. We stop on
* any error.
*/
if (states & BOOTM_STATE_START)
ret = bootm_start();

if (!ret && (states & BOOTM_STATE_PRE_LOAD))
ret = bootm_pre_load(bmi->addr_img);

if (!ret && (states & BOOTM_STATE_FINDOS))
ret = bootm_find_os(bmi->cmd_name, bmi->addr_img); //Find the OS to boot,返回0表示正确

if (!ret && (states & BOOTM_STATE_FINDOTHER)) {
ulong img_addr;

img_addr = bmi->addr_img ? hextoul(bmi->addr_img, NULL)
: image_load_addr;
ret = bootm_find_other(img_addr, bmi->conf_ramdisk,
bmi->conf_fdt);
}

if (IS_ENABLED(CONFIG_MEASURED_BOOT) && !ret &&
(states & BOOTM_STATE_MEASURE))
bootm_measure(images);

/* Load the OS */
if (!ret && (states & BOOTM_STATE_LOADOS)) {
iflag = bootm_disable_interrupts();
//加载image镜像,由于之前已经加载过了,因此没有设置BOOTM_STATE_LOADOS,不会执行到这一步
ret = bootm_load_os(images, 0);
if (ret && ret != BOOTM_ERR_OVERLAP)
goto err;
else if (ret == BOOTM_ERR_OVERLAP)
ret = 0;
}

/* Relocate the ramdisk */
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
if (!ret && (states & BOOTM_STATE_RAMDISK)) {
ulong rd_len = images->rd_end - images->rd_start;
//加载ramdisk,由于之前已经加载过了,因此没有设置BOOTM_STATE_RAMDISK,不会执行到这一步
ret = boot_ramdisk_high(&images->lmb, images->rd_start,
rd_len, &images->initrd_start, &images->initrd_end);
if (!ret) {
env_set_hex("initrd_start", images->initrd_start);
env_set_hex("initrd_end", images->initrd_end);
}
}
#endif
#if CONFIG_IS_ENABLED(OF_LIBFDT) && defined(CONFIG_LMB)
if (!ret && (states & BOOTM_STATE_FDT)) {
//加载设备树,由于之前已经加载过了,因此没有设置BOOTM_STATE_FDT,不会执行到这一步
boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr);
ret = boot_relocate_fdt(&images->lmb, &images->ft_addr,
&images->ft_len);
}
#endif

/* From now on, we need the OS boot function */
if (ret)
return ret;
boot_fn = bootm_os_get_boot_func(images->os.os);
need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
if (boot_fn == NULL && need_boot_fn) {
if (iflag)
enable_interrupts();
printf("ERROR: booting os '%s' (%d) is not supported\n",
genimg_get_os_name(images->os.os), images->os.os);
bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
return 1;
}

/* Call various other states that are not generally used */
if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
ret = boot_fn(BOOTM_STATE_OS_CMDLINE, bmi);
if (!ret && (states & BOOTM_STATE_OS_BD_T))
ret = boot_fn(BOOTM_STATE_OS_BD_T, bmi);
if (!ret && (states & BOOTM_STATE_OS_PREP)) {
int flags = 0;
/* For Linux OS do all substitutions at console processing */
if (images->os.os == IH_OS_LINUX)
flags = BOOTM_CL_ALL;
ret = bootm_process_cmdline_env(flags);
if (ret) {
printf("Cmdline setup failed (err=%d)\n", ret);
ret = CMD_RET_FAILURE;
goto err;
}
ret = boot_fn(BOOTM_STATE_OS_PREP, bmi);
}

#ifdef CONFIG_TRACE
/* Pretend to run the OS, then run a user command */
if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
char *cmd_list = env_get("fakegocmd");

ret = boot_selected_os(BOOTM_STATE_OS_FAKE_GO, bmi, boot_fn);
if (!ret && cmd_list)
ret = run_command_list(cmd_list, -1, 0);
}
#endif

/* Check for unsupported subcommand. */
if (ret) {
printf("subcommand failed (err=%d)\n", ret);
return ret;
}

/* Now run the OS! We hope this doesn't return */
if (!ret && (states & BOOTM_STATE_OS_GO)) //启动系统
ret = boot_selected_os(BOOTM_STATE_OS_GO, bmi, boot_fn);

/* Deal with any fallout */
err:
if (iflag)
enable_interrupts();

if (ret == BOOTM_ERR_UNIMPLEMENTED) {
bootstage_error(BOOTSTAGE_ID_DECOMP_UNIMPL);
} else if (ret == BOOTM_ERR_RESET) {
printf("Resetting the board...\n");
reset_cpu();
}

return ret;
}

do_bootm_linux即boot_fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int do_bootm_linux(int flag, struct bootm_info *bmi)
{
struct bootm_headers *images = bmi->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;
}

boot_selected_os是对boot_fn的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int boot_selected_os(int state, struct bootm_info *bmi, boot_os_fn *boot_fn)
{
arch_preboot_os();
board_preboot_os();

boot_fn(state, bmi);

/* Stand-alone may return when 'autostart' is 'no' */
if (bmi->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;
}

boot_jump_linux是启动Linux系统的函数, boot_jump_linux根据不同的芯片架构有不同的实现, 下面所展示的代码为ARM架构的boot_jump_linux函数, 位于arch/arm/lib/bootm.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void boot_jump_linux(struct bootm_headers *images, int flag)
{
#ifdef CONFIG_ARM64
void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
void *res2);
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
void *res2))images->ep;

debug("## Transferring control to Linux (at address %lx)...\n",
(ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);

announce_and_cleanup(fake);

if (!fake) {
#ifdef CONFIG_ARMV8_PSCI
armv8_setup_psci();
#endif
do_nonsec_virt_switch();

update_os_arch_secondary_cores(images->os.arch);

#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))
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
}
#endif
}

ARM64架构的内核的入口是标号head,直接跳转到stext。在stext中会读取引导程序传递的四个参数,并且保存在boot_args中。并且设置处理器一场级别,创建页表映射,并调用函数cpu_setup ,为开启处理器的内存管理单元做准备,初始化处理器。调用函数primary_switch为主处理器开启内存管理单元,搭建c语言执行环境,进入c语言部分的入口函数start_kernel。

下图为bootz命令的调用流程图,bootz命令与booti类似,可以进行参考。

uboot-boot

uboot与Linux内核

设备树

设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被 bootloader(uboot) 传递到内核,内核可以从设备树中获取硬件信息。以下为和设备树有关的文件:

  • dts是一种 ASII 文本格式的设备树描述,一般一个.dts 文件对应一个硬件平台。

  • dtsi内容和dts相似,是指由芯片厂商提供,是同一芯片平台“共用”的设备树文件。

  • dtb是设备树源码编译生成的文件,类似于uboot源码编译后的bin文件。

芯片厂商提供的dtsi几乎包含了芯片中所有设备及外设接口,在使用时,我们只需要在我们板卡的dts设备树源文件中 #includedtsi就可以导入指定芯片所有的设备,然后我们再根据板卡上的外设来修改dts文件即可。

Bootargs是如何传递给内核的?

uboot与内核的数据交换由boot_prep_linux进行, boot_prep_linux函数实现了启动image前的一些初始化工作,boot_prep_linux 主要用于处理环境变量 bootargs,bootargs 保存着传递给 Linux kernel 的参数;

uboot与内核之间传递数据的方式有两种: FDT与TAGS.

TAGS传参方式类似于在uboot与Linux中定义一个相同的结构体, uboot先对结构体赋值, 在uboot启动Linux内核时将该结构体的内存地址传递给Linux内核; 而FDT方式是将参数信息加入设备树中, 随设备树一同传递给Linux内核.

boot_prep_linux函数首先判断uboot与内核的传递方式, 若开启了FDT方式, 则跳转至FDT相关函数; 若未开启FDT而开启了TAGS方式, 则跳转至TAGS相关函数; 若两者均为开启, 则报错.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void boot_prep_linux(struct bootm_headers *images)
{
char *commandline = env_get("bootargs");

if (CONFIG_IS_ENABLED(OF_LIBFDT) && IS_ENABLED(CONFIG_LMB) && images->ft_len) {
debug("using: FDT\n");
if (image_setup_linux(images)) {
panic("FDT creation failed!");
}
} else if (BOOTM_ENABLE_TAGS) {
debug("using: ATAGS\n");
setup_start_tag(gd->bd);
if (BOOTM_ENABLE_SERIAL_TAG)
setup_serial_tag(&params);
if (BOOTM_ENABLE_CMDLINE_TAG)
setup_commandline_tag(gd->bd, commandline);
if (BOOTM_ENABLE_REVISION_TAG)
setup_revision_tag(&params);
if (BOOTM_ENABLE_MEMORY_TAGS)
setup_memory_tags(gd->bd);
if (BOOTM_ENABLE_INITRD_TAG) {
/*
* In boot_ramdisk_high(), it may relocate ramdisk to
* a specified location. And set images->initrd_start &
* images->initrd_end to relocated ramdisk's start/end
* addresses. So use them instead of images->rd_start &
* images->rd_end when possible.
*/
if (images->initrd_start && images->initrd_end) {
setup_initrd_tag(gd->bd, images->initrd_start,
images->initrd_end);
} else if (images->rd_start && images->rd_end) {
setup_initrd_tag(gd->bd, images->rd_start,
images->rd_end);
}
}
setup_board_tags(&params);
setup_end_tag(gd->bd);
} else {
panic("FDT and ATAGS support not compiled in\n");
}

board_prep_linux(images);
}

若使用FDT方式进行传参, 则会进入image_setup_linux函数, 该函数用于配置ramdisk, fdt. image_setup_linux的步骤如下:

  • 获取image镜像的LMB (logical memory blocks). lmb为uboot下的一种内存管理机制,用于管理镜像的内存。lmb所记录的内存信息最终会传递给kernel。在/include/lmb.h和/lib/lmb.c中有对lmb的接口和定义的具体描述。

  • 调用boot_fdt_add_mem_rsv_regions为设备树预留内存地址空间, 这里的地址为do_booti函数中从命令参数中获取的fdt地址;

  • 调用boot_get_cmdline初始化内核命令行;

  • 调用boot_relocate_fdt将fdt搬移至指定内存地址;

  • 调用image_setup_libfdt函数, 为设备树增加参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Set up the FDT to use for booting a kernel
*
* This performs ramdisk setup, sets up the FDT if required, and adds
* paramters to the FDT if libfdt is available.
*
* @param images Images information
* Return: 0 if ok, <0 on failure
*/
int image_setup_linux(struct bootm_headers *images)
{
ulong of_size = images->ft_len;
char **of_flat_tree = &images->ft_addr;
struct lmb *lmb = images_lmb(images);
int ret;

/* This function cannot be called without lmb support */
if (!IS_ENABLED(CONFIG_LMB))
return -EFAULT;
if (CONFIG_IS_ENABLED(OF_LIBFDT))
boot_fdt_add_mem_rsv_regions(lmb, *of_flat_tree);

if (IS_ENABLED(CONFIG_SYS_BOOT_GET_CMDLINE)) {
ret = boot_get_cmdline(lmb, &images->cmdline_start,
&images->cmdline_end);
if (ret) {
puts("ERROR with allocation of cmdline\n");
return ret;
}
}

if (CONFIG_IS_ENABLED(OF_LIBFDT)) {
ret = boot_relocate_fdt(lmb, of_flat_tree, &of_size);
if (ret)
return ret;
}

if (CONFIG_IS_ENABLED(OF_LIBFDT) && of_size) {
ret = image_setup_libfdt(images, *of_flat_tree, lmb);
if (ret)
return ret;
}

return 0;
}

image_setup_libfdt用于在FDT中增加要传给linux内核的参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
int image_setup_libfdt(struct bootm_headers *images, void *blob,
struct lmb *lmb)
{
ulong *initrd_start = &images->initrd_start;
ulong *initrd_end = &images->initrd_end;
int ret, fdt_ret, of_size;

if (IS_ENABLED(CONFIG_OF_ENV_SETUP)) {
const char *fdt_fixup;

fdt_fixup = env_get("fdt_fixup");
if (fdt_fixup) {
set_working_fdt_addr(map_to_sysmem(blob));
ret = run_command_list(fdt_fixup, -1, 0);
if (ret)
printf("WARNING: fdt_fixup command returned %d\n",
ret);
}
}

ret = -EPERM;

if (fdt_root(blob) < 0) {
printf("ERROR: root node setup failed\n");
goto err;
}
//传递bootargs参数
if (fdt_chosen(blob) < 0) {
printf("ERROR: /chosen node create failed\n");
goto err;
}
if (arch_fixup_fdt(blob) < 0) {
printf("ERROR: arch-specific fdt fixup failed\n");
goto err;
}

fdt_ret = optee_copy_fdt_nodes(blob);
if (fdt_ret) {
printf("ERROR: transfer of optee nodes to new fdt failed: %s\n",
fdt_strerror(fdt_ret));
goto err;
}

/* Store name of configuration node as u-boot,bootconf in /chosen node */
if (images->fit_uname_cfg)
fdt_find_and_setprop(blob, "/chosen", "u-boot,bootconf",
images->fit_uname_cfg,
strlen(images->fit_uname_cfg) + 1, 1);

/* Update ethernet nodes */
fdt_fixup_ethernet(blob);
#if IS_ENABLED(CONFIG_CMD_PSTORE)
/* Append PStore configuration */
fdt_fixup_pstore(blob);
#endif
if (IS_ENABLED(CONFIG_OF_BOARD_SETUP)) {
const char *skip_board_fixup;

skip_board_fixup = env_get("skip_board_fixup");
if (skip_board_fixup && ((int)simple_strtol(skip_board_fixup, NULL, 10) == 1)) {
printf("skip board fdt fixup\n");
} else {
fdt_ret = ft_board_setup(blob, gd->bd);
if (fdt_ret) {
printf("ERROR: board-specific fdt fixup failed: %s\n",
fdt_strerror(fdt_ret));
goto err;
}
}
}
if (IS_ENABLED(CONFIG_OF_SYSTEM_SETUP)) {
fdt_ret = ft_system_setup(blob, gd->bd);
if (fdt_ret) {
printf("ERROR: system-specific fdt fixup failed: %s\n",
fdt_strerror(fdt_ret));
goto err;
}
}

if (fdt_initrd(blob, *initrd_start, *initrd_end))
goto err;

if (!ft_verify_fdt(blob))
goto err;

/* after here we are using a livetree */
if (!of_live_active() && CONFIG_IS_ENABLED(EVENT)) {
struct event_ft_fixup fixup;

fixup.tree = oftree_from_fdt(blob);
fixup.images = images;
if (oftree_valid(fixup.tree)) {
ret = event_notify(EVT_FT_FIXUP, &fixup, sizeof(fixup));
if (ret) {
printf("ERROR: fdt fixup event failed: %d\n",
ret);
goto err;
}
}
}

/* Delete the old LMB reservation */
if (lmb)
lmb_free(lmb, map_to_sysmem(blob), fdt_totalsize(blob));

ret = fdt_shrink_to_minimum(blob, 0);
if (ret < 0)
goto err;
of_size = ret;

/* Create a new LMB reservation */
if (lmb)
lmb_reserve(lmb, map_to_sysmem(blob), of_size);

#if defined(CONFIG_ARCH_KEYSTONE)
if (IS_ENABLED(CONFIG_OF_BOARD_SETUP))
ft_board_setup_ex(blob, gd->bd);
#endif

return 0;
err:
printf(" - must RESET the board to recover.\n\n");

return ret;
}

从uboot环境变量bootargs获取需要传递的参数,uboot引导内核时会根据bootargs环境变量值,修改内存设备树里面的bootargs参数。通过/boot/fdt_support.c中的fdt_chosen函数实现:

  • str = board_fdt_chosen_bootargs();获取bootargs参数;
  • 调用fdt_setprop(fdt, nodeoffset, "bootargs", str,strlen(str) + 1);将bootargs参数插入设备树中.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* board_fdt_chosen_bootargs - boards may override this function to use
* alternative kernel command line arguments
*/
__weak char *board_fdt_chosen_bootargs(void)
{
return env_get("bootargs");
}

int fdt_chosen(void *fdt)
{
struct abuf buf = {};
int nodeoffset;
int err;
char *str; /* used to set string properties */

err = fdt_check_header(fdt);
if (err < 0) {
printf("fdt_chosen: %s\n", fdt_strerror(err));
return err;
}

/* find or create "/chosen" node. */
nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
if (nodeoffset < 0)
return nodeoffset;

if (IS_ENABLED(CONFIG_BOARD_RNG_SEED) && !board_rng_seed(&buf)) {
err = fdt_setprop(fdt, nodeoffset, "rng-seed",
abuf_data(&buf), abuf_size(&buf));
abuf_uninit(&buf);
if (err < 0) {
printf("WARNING: could not set rng-seed %s.\n",
fdt_strerror(err));
return err;
}
}

str = board_fdt_chosen_bootargs();

if (str) {
err = fdt_setprop(fdt, nodeoffset, "bootargs", str,
strlen(str) + 1);
if (err < 0) {
printf("WARNING: could not set bootargs %s.\n",
fdt_strerror(err));
return err;
}
}

/* add u-boot version */
err = fdt_setprop(fdt, nodeoffset, "u-boot,version", PLAIN_VERSION,
strlen(PLAIN_VERSION) + 1);
if (err < 0) {
printf("WARNING: could not set u-boot,version %s.\n",
fdt_strerror(err));
return err;
}

return fdt_fixup_stdout(fdt, nodeoffset);
}

uboot是如何将设备树文件传递给Linux的?

在boot_jump_linux函数中, 启动64位linux内核时调用了armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,images->ep, ES_TO_AARCH64);, 该函数用汇编实现, 用于启动Linux内核并将设备树地址传递给内核, 该函数的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* armv8_switch_to_el2() - switch from EL3 to EL2 for ARMv8
*
* @args: For loading 64-bit OS, fdt address.
* For loading 32-bit OS, zero.
* @mach_nr: For loading 64-bit OS, zero.
* For loading 32-bit OS, machine nr
* @fdt_addr: For loading 64-bit OS, zero.
* For loading 32-bit OS, fdt address.
* @arg4: Input argument.
* @entry_point: kernel entry point
* @es_flag: execution state flag, ES_TO_AARCH64 or ES_TO_AARCH32
*/
void __noreturn armv8_switch_to_el2(u64 args, u64 mach_nr, u64 fdt_addr,
u64 arg4, u64 entry_point, u64 es_flag);

参考资料

完全理解ARM启动流程:Uboot-Kernel

ARMs PL011 UART

How U-boot loads Linux kernel

Device Tree

FIT IMAGE

Secure Boot

Uboot启动流程