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_listuboot传递的tag参数链表的地址
fdtdtb数据所在地址
total_spacedtb数据的总大小

(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结构体

参考博客:《设备树(dtb数据)匹配struct machine_desc结构体》

6.3、从dtb格式数据中解析出bootargs

参考博客:《从设备树(dtb格式数据)中解析出bootargs》

6.4、dtb格式到device node结构体的转换

参考博客:《设备树——dtb格式到struct device node结构体的转换》

6.5、device node结构体转换成platform_device结构体

参考博客:《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内核启动阶段对设备树的解析的主要内容,如果未能解决你的问题,请参考以下文章

设备树学习:内核对设备树的处理

从设备树(dtb格式数据)中解析出bootargs

RK3399平台开发系列讲解(内核设备树篇)3.5Linux内核对DTB文件的解析

Linux-设备树编译器DTC

Linux的内核源码树的根目录下的每个文件的含义简介

Linux学习 :Uboot 移植