• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    (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
    • 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

    (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
    
    • 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

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    传参含义
    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
    • 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

    (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,这里是递归调用
    
    • 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

    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
    ~ # 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    命令作用
    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节点的名字
  • 相关阅读:
    一位3年经验的测试工程师水平能差到什么程度?面试后,感叹都是人才呀...
    Python5
    图像处理Scharr 算子
    【面试题】Ajax
    ElementUI之CUD+表单验证
    TSINGSEE青犀视频平台Linux云存储挂载工具使最新配置与部署方式
    【Hyperledger Fabric 学习】私有数据(Key Concepts: Private data)
    基于协作搜索算法的函数寻优及工程优化
    深度学习零基础学习之路——第三章 数据可视化TensorBoard和TorchVision的介绍
    MySQL索引特性(上)
  • 原文地址:https://blog.csdn.net/weixin_42031299/article/details/125941276