最近在移植一款蓝牙芯片的过程中用到了设备树,一开始在研究kernel是怎么解析DTB的,后来就很好奇kernel是怎么找到DTB的,所以就有了这篇文章,纯粹记录一下自己的学习过程吧。
下面我就从两个阶段来讲述,第一个阶段就是uboot是怎么将DTB的地址传递给kernel;第二个阶段就是kernel怎么根据DTB的地址,并解析DTB的。
uboot的启动流程我就不在这里具体分析,网上很多的文章,这里只分析在uboot是怎么传递DTB的地址的。在uboot启动的最后阶段,会去执行bootcmd中的命令,其中就是使用下面的命令来启动kernel:
bootm ${loadaddr} //loadaddr是kernel的地址
熟悉uboot的人会知道,这条命令最终会执行到某个具体的函数:
U_BOOT_CMD(
bootm, CONFIG_SYS_MAXARGS, 1, do_bootm,
"boot application image from memory", bootm_help_text
);
所以最终执行到do_bootm函数
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
...
nRet = do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
BOOTM_STATE_LOADOS |
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)
BOOTM_STATE_OS_CMDLINE |
#endif
BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
BOOTM_STATE_OS_GO, &images, 1);
...
}
继续跟踪到do_bootm_states()里面
//uboot\common\bootm.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)
{
...
if (!ret && (states & BOOTM_STATE_FINDOTHER)) {
ret = bootm_find_other(cmdtp, flag, argc, argv); //找到fdt的最终地址
argc = 0; /* consume the args */
}
...
boot_fn = bootm_os_get_boot_func(images->os.os); //跳转到kernel
...
}
这里我们只关心两个主要的函数,分别是
(1)bootm_find_other:找到fdt的最终地址
(2)bootm_os_get_boot_func:找到跳转kernel的函数,并执行它boot_fn()
我们先看一下怎么找到fdt地址的。
//uboot\common\bootm.c
bootm_find_other
bootm_find_ramdisk_fdt
bootm_find_fdt
跟踪到最终的 bootm_find_fdt()函数,我们可以看到fdt的地址最终设置到images.ft_addr里面。
/*
*uboot\common\bootm.c
*/
#if defined(CONFIG_OF_LIBFDT)
static int bootm_find_fdt(int flag, int argc, char * const argv[])
{
int ret;
/* find flattened device tree */
#ifdef CONFIG_DTB_MEM_ADDR
unsigned long long dtb_mem_addr = -1;
char *ft_addr_bak;
ulong ft_len_bak;
if (getenv("dtb_mem_addr"))
dtb_mem_addr = simple_strtoul(getenv("dtb_mem_addr"), NULL, 16);
else
dtb_mem_addr = CONFIG_DTB_MEM_ADDR;
ft_addr_bak = (char *)images.ft_addr;
ft_len_bak = images.ft_len;
images.ft_addr = (char *)map_sysmem(dtb_mem_addr, 0);
images.ft_len = fdt_get_header(dtb_mem_addr, totalsize);
#endif
printf("load dtb from 0x%lx ......\n", (unsigned long)(images.ft_addr));
...
if (ret) {
puts("Could not find a valid device tree\n");
return 1;
}
set_working_fdt_addr(images.ft_addr);
return 0;
}
#endif
上面既然找到了合法的fdt地址,后面肯定就是看怎么传递给kernel了,所以我们再回到bootm_os_get_boot_func()。看一下里面做了什么动作。
/*
* uboot\common\bootm_os.c
*/
boot_os_fn *bootm_os_get_boot_func(int os)
{
...
return boot_os[os];
}
其实,boot_os是一个函数数组,会根据传递进来的os值,调用对应的函数
/*
* uboot\common\bootm_os.c
*/
static boot_os_fn *boot_os[] = {
...
#ifdef CONFIG_BOOTM_LINUX
[IH_OS_LINUX] = do_bootm_linux,
#endif
...
}
所以最终调用到do_bootm_linux()函数里面,并最终调用到kernel_entry(),并去执行kernel。
/*
*uboot\arch\arm\lib\bootm.c
*/
do_bootm_linux
boot_jump_linux(images, flag);
kernel_entry(images->ft_addr, NULL, NULL, NULL); //从uboot跳到执行kernel,第一个参数就是fdt的地址
分析完上面的uboot阶段后,我们已经知道fdt的地址是作为参数1传递到kernel。下面看一下kernel阶段怎么获取这个地址值的。
大致看一下head.S里面的内容吧,全是汇编,只能懂个大概(囧~)。下面贴出来的代码可以看出来,uboot的传过来的地址是怎么传给kernel来的,其它的汇编代码可以参考我给出的链接。
/*
* kernel\arch\arm64\kernel\head.S
* 可以参考下面两篇文章:
* https://www.cnblogs.com/keanuyaoo/p/3299536.html
* http://www.wowotech.net/215.html
*/
ENTRY(stext)
mov x21, x0 // x21=FDT,uboot阶段我们知道fdt的地址放在了寄存器x0,这里赋值给x21
bl el2_setup // Drop to EL1, w20=cpu_boot_mode
bl __calc_phys_offset // x24=PHYS_OFFSET, x28=PHYS_OFFSET-PAGE_OFFSET
bl set_cpu_boot_mode_flag
mrs x22, midr_el1 // x22=cpuid
mov x0, x22
bl lookup_processor_type
mov x23, x0 // x23=current cpu_table
cbz x23, __error_p // invalid processor (x23=0)?
bl __vet_fdt
bl __create_page_tables // x25=TTBR0, x26=TTBR1
...
__switch_data:
.quad __mmap_switched
.quad __bss_start // x6
.quad _end // x7
.quad processor_id // x4
.quad __fdt_pointer // x5,用来指向FDT的首地址
.quad memstart_addr // x6
.quad init_thread_union + THREAD_START_SP // sp
/*
* The following fragment of code is executed with the MMU on in MMU mode, and
* uses absolute addresses; this is not position independent.
*/
__mmap_switched:
adr x3, __switch_data + 8
ldp x6, x7, [x3], #16
1: cmp x6, x7
b.hs 2f
str xzr, [x6], #8 // Clear BSS
b 1b
2:
ldp x4, x5, [x3], #16
ldr x6, [x3], #8
ldr x16, [x3]
mov sp, x16
str x22, [x4] // Save processor ID
str x21, [x5] // Save FDT pointer,保存FDT的地址到x5指向的地址
str x24, [x6] // Save PHYS_OFFSET
mov x29, #0
b start_kernel //跳转到C代码继续执行
ENDPROC(__mmap_switched)
在汇编的阶段,大概可以看出来用变量__fdt_pointer指向FDT的首地址,执行完汇编的阶段就会调到C代码的流程里面了。
asmlinkage void __init start_kernel(void)
{
...
setup_arch(&command_line); //设置架构相关的内容
...
}
我们只关心读取fdt的相关内容,直接进到setup_ arch()函数。
void __init setup_arch(char **cmdline_p)
{
/*
* Unmask asynchronous aborts early to catch possible system errors.
*/
local_async_enable();
setup_processor();
setup_machine_fdt(__fdt_pointer); //根据fdt地址,进行fdt的扫描
}
终于看到fdt的地址被用起来了,传递进了setup_machine_fdt()函数里面。
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
if (!dt_phys || !early_init_dt_scan(phys_to_virt(dt_phys))) {
early_print("\n"
"Error: invalid device tree blob at physical address 0x%p (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and passed in the first 512MB of memory\n"
"\nPlease check your bootloader.\n",
dt_phys, phys_to_virt(dt_phys));
while (true)
cpu_relax();
}
machine_name = of_flat_dt_get_machine_name();
}
因为传进来的fdt地址是物理地址,所以用phys_to_virt()函数转换为虚拟地址,并进入到early_init_dt_scan()进行fdt的扫描
bool __init early_init_dt_scan(void *params)
{
if (!params)
return false;
/* Setup flat device-tree pointer */
initial_boot_params = params;
/* check device tree validity */
if (be32_to_cpu(initial_boot_params->magic) != OF_DT_HEADER) {
initial_boot_params = NULL;
return false;
}
/* Retrieve various information from the /chosen node */
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
return true;
}
具体的扫描函数就不分析了,有兴趣的同学可以继续跟踪代码。