linux内核启动阶段对设备树的解析
Posted 正在起飞的蜗牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核启动阶段对设备树的解析相关的知识,希望对你有一定的参考价值。
1、前言
(1)设备树dts文件格式讲解参考博客:《linux设备树dts文件详解》;
(2)本文对设备树的讲解是基于hi3516dv300芯片的uboot和kernel源码进行详解,uboot版本是2016.11,内核版本是4.9.37;
(3)在dv300芯片用的uboot和内核中,uboot启动内核传参是传统tag方式,内核是采用的设备树技术,镜像构成是zImage+dtb;
2、uboot启动linux
2.1、do_bootm_linux()函数
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;
//处理tag或者fdt
boot_prep_linux(images);
//跳转执行内核
boot_jump_linux(images, flag);
return 0;
(1)do_bootm_linux()函数是uboot启动内核的最终阶段。到此函数为止,已经将内核重定位到内存,并且解析出内核镜像的头信息;
(2)do_bootm_linux()函数主要功能就是找到machId和内核启动参数,然后调用内核入口把机器码和tag/dtb地址传给内核;
(3)在dv300芯片采用的uboot中,uboot传的是tag;
2.2、boot_jump_linux()函数
static void boot_jump_linux(bootm_headers_t *images, int flag)
//通过全局变量gd获取的机器码,这是通过配置文件指定的
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;
//判断环境变量里是否有指定machid,环境变量的优先级高于代码指定machid的优先级
s = getenv("machid");
if (s)
if (strict_strtoul(s, 16, &machid) < 0)
debug("strict_strtoul failed!\\n");
return;
printf("Using machid 0x%lx from environment\\n", machid);
debug("## Transferring control to Linux (at address %08lx)" \\
"...\\n", (ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
announce_and_cleanup(fake);
//判断当前uboot给内核传参是采用设备树还是传统的tag
if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
r2 = (unsigned long)images->ft_addr; //采用设备树,r2寄存器的值是dtb的地址
else
r2 = gd->bd->bi_boot_params; //传统tag,r2寄存器的值是tag的地址
if (!fake)
#ifdef CONFIG_ARMV7_NONSEC
if (armv7_boot_nonsec())
armv7_init_nonsec();
secure_ram_addr(_do_nonsec_entry)(kernel_entry,
0, machid, r2);
else
#endif
//启动内核:(0,机器码,dtb/tag)
kernel_entry(0, machid, r2);
(1)boot_jump_linux()函数是uboot启动内核的最后一个函数,主要功能就是根据内核镜像头找到内核入口地址,然后启动内核;
(2)重点:uboot启动内核的时,r0寄存器的值是0,r1寄存器的值是机器码,r2寄存器的值是tag的启动地址或者是dtb;(在dv300芯片的uboot采用的tag传参)
3、内核对设备树解析的流程
4、内核解压阶段
4.1、解压阶段的流程图
4.2、解压缩阶段源码摘选
start:
//定义了一些变量,这些符号都可以在链接脚本找到
.word _magic_sig @ Magic numbers to help the loader
.word _magic_start @ absolute load/run zImage address
.word _magic_end @ zImage end address
.word 0x04030201 @ endianness flag
#ifdef CONFIG_ARM_APPENDED_DTB
/* 对到此阶段各个寄存器里保存值的含义做了说明
* r0 = delta
* r2 = BSS start
* r3 = BSS end
* r4 = final kernel address (possibly with LSB set)
* r5 = appended dtb size (still unknown)
* r6 = _edata zImage结束地址,也就是dtb的起始地址
* r7 = architecture ID
* r8 = atags/device tree pointer
* r9 = size of decompressed image
* r10 = end of this image, including bss/stack/malloc space if non XIP
* r11 = GOT start
* r12 = GOT end
* sp = stack pointer
*
* if there are device trees (dtb) appended to zImage, advance r10 so that the
* dtb data will get relocated along with the kernel if necessary.
*/
ldr lr, [r6, #0] //读取dtb的最开始的4个字节,里面是dtb格式的特殊头(是一个魔数)
//区分内核当前是大端模式还是小端模式
#ifndef __ARMEB__
ldr r1, =0xedfe0dd0 @ sig is 0xd00dfeed big endian
#else
ldr r1, =0xd00dfeed
#endif
cmp lr, r1 //比较dtb的头4个字节是否是对应的魔术,如果不是则代表不是dtb格式文件
bne dtb_check_done @ not found
#ifdef CONFIG_ARM_ATAG_DTB_COMPAT
/*
* OK... Let's do some funky business here.
* If we do have a DTB appended to zImage, and we do have
* an ATAG list around, we want the later to be translated
* and folded into the former here. No GOT fixup has occurred
* yet, but none of the code we're about to call uses any
* global variable.
*/
/* Get the initial DTB size */
ldr r5, [r6, #4] //读取dtb的第四到第八字节到r5寄存器,这4个字节标明dtb的大小
stmfd sp!, r0-r3, ip, lr
mov r0, r8 //tag地址
mov r1, r6 //_edata,内核镜像的结束地址,也是dtb的开始地址
mov r2, r5 //appended dtb size,dtb的大小
bl atags_to_fdt //解析uboot传过来的tag并生成相应的dtb节点
······
ldmfd sp!, r0-r3, ip, lr
sub sp, sp, r5
#endif
mov r8, r6 @ use the appended device tree
/* Get the current DTB size */
ldr r5, [r6, #4]
dtb_check_done:
#endif
/*
* The C runtime environment should now be setup sufficiently.
* Set up some pointers, and start decompressing.
* r4 = kernel execution address
* r7 = architecture ID
* r8 = atags pointer
*/
mov r0, r4 //内核解压后存放的地址
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7 //architecture ID:机器码
bl decompress_kernel //执行解压zImage为elf格式的可执行kernel镜像
bl cache_clean_flush
bl cache_off
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
b __enter_kernel //调用解压后的内核
__enter_kernel:
mov r0, #0 @ must be 0
ARM( mov pc, r4 ) @ call kernel 解压内核时已经把解压地址保存到r4寄存器中
M_CLASS( add r4, r4, #1 ) @ enter in Thumb mode for M class
THUMB( bx r4 ) @ entry point is always ARM for A/R classes
4.3、CONFIG_ARM_APPENDED_DT
(1)这个宏标明dtb是接续在zImage镜像后面的,整个内核烧录镜像构成如上图;
(2)zImage就是平时的zImage镜像,不同之处就是zImage镜像后面接续了二进制dtb数据;
(3)在arch/arm/boot/目录下会产生两个镜像,分别是zImage和zImage-dtb,用比对软件可以分析得到zImage-dtb就是比zImage尾部多了dtb数据;
4.4、CONFIG_ARM_ATAG_DTB_COMPAT宏
(1)前面说过,dv300芯片的uboot采用的传统的tag传参方式并没有用设备树技术,但是内核是用的设备树,dtb镜像是接续在zImage镜像后面的;
(2)内核启动需要的信息都来自dtb文件,包括启动参数、内存信息、设备信息等,但是dts文件没有配置启动参数、内存信息等,在启动节点需要先将uboot的tag传参里内核启动信息、内存信息等,从tag格式转换成dtb格式;
(3)tag结构体转换成dtb格式,具体工作在函数atags_to_fdt()里进行;
4.5、atags_to_fdt()函数
4.5.1、atags_to_fdt函数调用
stmfd sp!, r0-r3, ip, lr //将r0-r3、ip、lr寄存器保存到栈中
mov r0, r8 //tag地址
mov r1, r6 //_edata,内核镜像的结束地址,也是dtb的开始地址
mov r2, r5 //appended dtb size,dtb的大小
bl atags_to_fdt //解析uboot传过来的tag并生成相应的dtb节点
int atags_to_fdt(void *atag_list, void *fdt, int total_space);
传参 | 含义 |
---|---|
atag_list | uboot传递的tag参数链表的地址 |
fdt | dtb数据所在地址 |
total_space | dtb数据的总大小 |
(1)atags_to_fdt()函数是被汇编语句调用,其中汇编调用C语言函数传参是通过寄存器,将需要传递的参数保存到寄存器中,其中传递的三个参数分别保存到r0、r1、r2寄存器;
(2)因为r0、r1、r2寄存器会被调用C语言函数时占用,所以提前将寄存器的值备份到栈中,后面从栈里回复寄存器的值;
4.5.2、atags_to_fdt函数源码分析
(1)首先判断tag参数链表地址处是否已经是dtb数据,tag参数地址就是uboot启动内核时传递的r2寄存器的值,可能是tag参数也可能是dtb数据,这里判断头4个字节是不是dtb格式文件的魔数(FDT_MAGIC)即可;
(2)判断tag是不是合法的,也就是tag链表第一个tag是不是ATAG_CORE类型;
(3)调用fdt_open_into()函数打开dtb格式的数据,dtb格式本质是二进制数据,具体是何种格式没去研究过,但是内核已经提供了相关函数,我们只需要按照函数传参要求调用即可;
(4)遍历tag参数,如果是有必要的tag参数就解析并合入到dtb数据中,比较典型的就是解析bootargs成chosen节点的属性;
(5)tag转换成dtb也是调用内核提供的函数,setprop_xxx()函数族,里面有很多setprop开头的函数,专门用于tag转dtb,根据不同类型的tag调用不同的函数;
(6)最终效果就是原始的dtb数据中,对应的节点、属性会被tag参数中的值替换掉,具体如何替换并不用关心,能看懂内核提供的setprop_xxx()函数族传参即可;
4.6、解压缩部分涉及的源文件
(1)zImage是压缩的镜像,这里讲解的是未压缩的头部分如何解压缩内核镜像,以及对dtb的前期处理;
(2)涉及源文件目录:arch/arm/boot/compressed/;
4.7、内核的解压缩
参考博客:《内核的解压缩过程详解》;
5、内核启动汇编阶段
5.1、汇编阶段涉及的文件
(1)链接脚本:arch/arm/kernel/vmlinux.lds;
(2)主要汇编文件:arch/arm/kernel/head.S;
(3)汇编程序的入口:ENTRY(stext),分析链接脚本可以知道;
5.2、汇编代码摘选
__INIT
__mmap_switched:
adr r3, __mmap_switched_data
//将r4*-r7寄存器的值保存到__data_loc到_end变量中
ldmia r3!, r4, r5, r6, r7
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
//将r4*-r7寄存器的值保存到processor_id到init_thread_union变量中
//下面ARM态和THUMB态语句的效果是一样的
ARM( ldmia r3, r4, r5, r6, r7, sp) //如果CPU处于ARM态就执行该语句
THUMB( ldmia r3, r4, r5, r6, r7 ) //如果CPU处于THUMB态就执行该语句
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel //跳转执行start_kernel函数,开始C语言阶段
ENDPROC(__mmap_switched)
.align 2
.type __mmap_switched_data, %object
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4:该变量的值来自于r4寄存器,是CPU的ID
.long __machine_arch_type @ r5:该变量的值来自于r4寄存器,是机器码
.long __atags_pointer @ r6:该变量的值来自于r6寄存器,是tag或者dtb的地址
#ifdef CONFIG_CPU_CP15
.long cr_alignment @ r7
#else
.long 0 @ r7
#endif
.long init_thread_union + THREAD_START_SP @ sp
.size __mmap_switched_data, . - __mmap_switched_data
(1)在汇编阶段对dtb没有做特别的处理,就检验了dtb的合法性,这里分析一些重要的全局变量,全局变量在汇编阶段定义并赋值,后面C语言阶段会使用到;
(2)__mmap_switched_data标号就相当于汇编定义了一个数组,成员变量包含__data_loc到init_thread_union;
(3)在上面的汇编代码中,在解析过程中将寄存器的值写到对应全局变量中;
(4)上面和设备树关系较紧密的是__atags_pointer变量,其实在这里已经是dtb数据的地址,因为在前面已经将uboot传递的tag转换成了dtb数据;
6、内核启动C语言阶段
6.1、函数调用关系
start_kernel
setup_arch
setup_machine_fdt //解析dtb数据,得到匹配的struct machine_desc结构体,这是用来描述板级配置的
early_init_dt_verify //校验dtb数据
of_flat_dt_match_machine //匹配最符合的struct machine_desc结构体
get_next_compat(arch_get_next_mach) //读取编译进内核的struct machine_desc结构体的dt_compat属性
of_flat_dt_match //将struct machine_desc结构体的dt_compat属性和根节点的"compatible"属性进行匹配,匹配度越好返回的分数越小
early_init_dt_scan_nodes //从dtb数据中解析出一些关键的信息来引导内核启动
of_scan_flat_dt //遍历dtb的所有节点,并调用回调函数it解析节点
unflatten_device_tree //将dtb数据节点解析成struct device_node结构体,根节点保存到of_root变量
//将dtb数据解析成struct device_node结构体数据
of_platform_default_populate_init //将struct device_node结构体转换成总线上的device,比如转换成struct amba_device(amba总线)、struct platform_device(platform总线)
of_have_populated_dt //判断节点是否已经被转换
of_platform_default_populate //
of_platform_populate //将该节点以及子节点都转换成总线上的device
for_each_child_of_node //遍历节点的每一个子节点
of_platform_bus_create //为节点创建对应的总线设备
of_get_property //判断节点是否有"compatible"属性,因为是根据这个属性来判断是创建哪种总线的device
of_device_is_compatible //"compatible"属性是否匹配上amba总线
of_amba_device_create //匹配上则创建amab总线的device,实例化一个struct amba_device结构体并注册到amba总线
of_platform_device_create_pdata //如果匹配不是amba总线,则创建platform总线的设备
of_device_alloc //读取device_node节点信息,实例化一个platform_device结构体
of_device_add //将platform_device结构体注册到platform总线
for_each_child_of_node //遍历节点的子节点
of_platform_bus_create //对每个子节点都创建对应总线device,这里是递归调用
6.2、对dtb数据的处理流程
6.2、由dtb数据匹配struct machine_desc结构体
6.3、从dtb格式数据中解析出bootargs
6.4、dtb格式到device node结构体的转换
6.5、device node结构体转换成platform_device结构体
7、proc文件系统中查看设备树节点
~ # ls /proc/device-tree/
#address-cells interrupt-controller@10300000
#size-cells media
aliases memory
chosen model
clock@12010000 name
compatible soc
cpus syscounter
~ #
~ # cat /proc/device-tree/model
Hisilicon HI3516DV300 DEMO Board
~ #
~ # cat /proc/device-tree/compatible
hisilicon,hi3516dv300
~ #
~ # ls /proc/device-tree/chosen/
bootargs name
~ #
~ # cat /proc/device-tree/chosen/bootargs
mem=512M console=ttyAMA0,115200 root=/dev/mmcblk0p3 rootfstype=ext4 rw rootwait blkdevparts=mmcblk0:5M(boot),10M(kernel),200M(rootfs),200M(userdata),-(user)
~ #
~ # cat /proc/device-tree/chosen/name
chosen
~ #
命令 | 作用 |
---|---|
ls /proc/device-tree/ | 查看设备树的所有节点 |
cat /proc/device-tree/model | 查看根节点的model属性 |
cat /proc/device-tree/compatible | 查看根节点的compatible属性 |
ls /proc/device-tree/chosen/ | 查看chosen节点下的所有属性 |
cat /proc/device-tree/chosen/bootargs | 查看chosen节点的bootargs属性 |
cat /proc/device-tree/chosen/name | 查看chosen节点的名字 |
以上是关于linux内核启动阶段对设备树的解析的主要内容,如果未能解决你的问题,请参考以下文章