活动地址:CSDN21天学习挑战赛
思考
uboot 启动 kernel,这是一个众所周知的事实。但是,你有没有思考过,这两个独立的镜像实体,是如何做到 A 启动 B 的呢?如果是同一份代码中的 func_a() 调用 func_b(),还好理解,因为是在同一份程序中(或者说是同一份指令中),func_a 是知道 func_b() 的地址的,直接跳转过去就行了(设置 PC 指针指向 func_b 的地址)。
但是 uboot 和 kernel 不在同一份指令中,这是两个独立的程序,那么是怎样实现 程序 A
“调用” 程序 B
的呢?
bootz
在 uboot 命令行下,可以使用 bootz 命令启动 kernel,我们就以此为切入点,探究 uboot 启动 kernel 的底层原理。
表象
使用如下命令,就能完成 uboot 启动 kernel。
tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-evk.dtb
bootz 80800000 - 83000000
这里简单介绍下命令作用。
前两条是通过 tftp 命令,分别将 kernel 镜像 zImage 和设备树二进制文件下载到内存的 0x80800000 和 0x83000000 位置。
第三条是使用 bootz 命令启动 kernel。
bootz
接下来,就分析下 bootz 启动 kernel 的原理。
通过 uboot 命令行,向 bootz 传递了三个参数 “80800000”、“-”、“83000000”,它们分别是 “kernel 镜像在内存中的首地址”、“无效参数”、“设备树文件首地址”,
bootz 命令对应的执行函数为 do_bootz(),在该函数中,首先设置 kernel 首地址和设备树首地址(其实这就是 uboot 能够启动 kernel 的原因:有了地址就能跳转过去运行)。
images->ep = simple_strtoul(argv[0], NULL, 16);
变量 ep 用来存储 kernel 镜像在内存中的首地址,该参数由 argv[0] 赋值,argv[0] 就是刚刚在命令行传递给 bootz 的第一个参数 80800000
,
images.ft_addr = argv[2];
变量 ft_addr 用来存储设备树在内存中的首地址,该参数由 argv[2] 赋值,为 83000000
。
kernel_entry()
do_bootz() 最终会调用 kernel_entry(),kernel_entry 并不是在 uboot 中定义的函数,它是一个地址转换来的,如下:
kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
void *res2))images->ep;
而 image->ep 就是 kernel 在内存中的首地址,这不就联系上了嘛🤞。
uboot 将设备树传递给 kernel
刚才分析出 images.ft_addr 存放设备树在内存中的首地址,那又是如何将这个地址传递给 kernel 呢?这里不像函数传参,当成实参就传递过去了。
一来 uboot 和 kernel 不是同一份程序,不共用同一个堆栈;二来 kernel 一开始的代码是汇编代码,C 函数也没办法直接将自己的实参传递给汇编函数。
那要怎么才能实现 uboot 中的一个变量传递给 kernel 呢?答案是使用寄存器。
uboot 生命周期的最后一个函数调用如下
r2 = (unsigned long)images->ft_addr;
kernel_entry(0, machid, r2);
使用 r2 寄存器存储设备树文件的首地址。
kernel 接收设备树
再来看 kernel 是怎么接收这个地址的呢?
kernel 的入口函数是 head.S 中的 stext
ENTRY(stext)
...
ldr r13, =__mmap_switched @ address to jump to after
__mmap_switched:
...
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
...
总体流程是:
stext 调用 __mmap_switched,在 __mmap_switched 中将 r2 的值赋给全局变量 __atags_pointer,然后调用 start_kernel()。
start_kernel() 调用 setup_arch(),在该函数中解析设备树,如下:
mdesc = setup_machine_fdt(__atags_pointer);
总结
uboot 启动 kernel:
uboot 拿到 kernel 在内存中的首地址,就能跳转到该地址运行。
uboot 将设备树传递给 kernel:
通过 r2 寄存器。