MEMORY | INTERRUPT | TIMER | 并发与同步 | 进程管理 | 调度 | uboot | DTB | ARMV8 | ATF | Kernel Data Structure | PHY | LINUX2.6 | 驱动合集 | UART子系统 | USB专题 |

linux内核链接脚本vmlinux.lds分析续篇之---* (.proc.info.init)、* (.arch.info.init)、*(.taglist.init)段的分析(十二)

序言

为什么这里又单独写一篇关于内核连接脚本中各种段的分析。因为在内核启动中有如下三个比较重要的步骤:

  • 校验处理器ID,检验内核是否支持该处理器;若不支持,则停止启动内核。 - - -> *(.proc.info.init)段
  • 校验机器码,检验内核是否支持该机器;若不支持,则停止启动内核 - - -> *(.arch.info.init)段
  • 解析uboot传入的tag - - -> *(.taglist.init)段

一. 基础知识

r0,r1,r2三个寄存器的设置
uboot启动内核时,会设置r0,r1,r2三个寄存器:

  • r0一般设置为0;
  • r1一般设置为machine id (在使用设备树时该参数没有被使用);
  • r2一般设置ATAGS或DTB的开始地址;

这里的machine id,是让内核知道是哪个CPU,从而调用对应的初始化函数。
(1) 在没有使用设备树时,需要uboot传一个machine id给内核(现在使用设备树的话,这个参数就不需要设置)。
(2) r2要么是以前的ATAGS开始地址,要么是现在使用设备树后的DTB文件开始地址。

附注:由于我手里目前只有一块mini6410的开发板(s3c6410是ARMv6架构的),所以本节还是以没有使用设备树的情景为例进行讲解。当然当前主流的开发基本上都是基于设备树的,因为它的优点很多(会放在设备树专题进行讲解)。(6月将会入手一块基于ARMV8架构的cortex-A53,选型中!!!*,到时候再补上基于设备树的启动方式)

二. * (.proc.info.init)段

# arch/arm/kernel
...
mrc	p15, 0, r9, c0, c0		@ get processor id
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid 

1. 如何把CPU对应的proc_info_list结构放入 * (.proc.info.init)段

在内核源码中,定义了若干个proc_info_list结构,以此来表示它支持的CPU。其中proc_info_list 结构体定义如下:

# arch/arm/include/asm/procinfo.h
/*
 * Note!  struct processor is always defined if we're
 * using MULTI_CPU, otherwise this entry is unused,
 * but still exists.
 *
 * NOTE! The following structure is defined by assembly
 * language, NOT C code.  For more information, check:
 *  arch/arm/mm/proc-*.S and arch/arm/kernel/head.S
 */
struct proc_info_list {
	unsigned int		cpu_val;
	unsigned int		cpu_mask;
	unsigned long		__cpu_mm_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_io_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_flush;		/* used by head.S */
	const char		*arch_name;
	const char		*elf_name;
	unsigned int		elf_hwcap;
	const char		*cpu_name;
	struct processor	*proc;
	struct cpu_tlb_fns	*tlb;
	struct cpu_user_fns	*user;
	struct cpu_cache_fns	*cache;
};

对于ARM架构的CPU,表示支持的CPU的源码定义在在arch/arm/mm/目录下。比如arch/arm/mm/proc-v6.S中有如下代码,它表示所有ARMv6架构CPU的proc_info_list结构。

	.section ".proc.info.init", #alloc, #execinstr

	/*
	 * Match any ARMv6 processor core.
	 */
	.type	__v6_proc_info, #object
__v6_proc_info:
	.long	0x0007b000 /*cpu_val*/
	.long	0x0007f000 /*cpu_mask*/
	...

不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在链接内核镜像时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:

__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;

2. 如何获取CPUDID

通过读协处理器CP15的寄存器C0获取CPUID,并放在r9中。以确定内核是否支持当前CPU。如果支持,r5寄存器返回一个用来描述处理器的结构体的地址,否则r5为0。CPU ID格式如图所示:
在这里插入图片描述

3. 如何判断内核是否支持当前的CPU

__lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPUID(存入r9寄存器),并尝试从这些proc_info_list结构中找出匹配目标,以确定内核是否支持当前的CPU。代码如下:

# arch/arm/kernel/head-common.S
/*
 * Read processor ID register (CP#15, CR0), and look up in the linker-built
 * supported processor list.  Note that we can't use the absolute addresses
 * for the __proc_info lists since we aren't running with the MMU on
 * (and therefore, we are not in the correct address space).  We have to
 * calculate the offset.
 *
 *	r9 = cpuid
 * Returns:
 *	r3, r4, r6 corrupted
 *	r5 = proc_info pointer in physical address space
 *	r9 = cpuid (preserved)
 */
__lookup_processor_type:
	/* adr指令是采用相对地址,因此这里r3装的实际上是标号 __lookup_processor_type_data 对应的物理地址 */
	adr	r3, __lookup_processor_type_data              
	
	/* LDM是多寄存器存取的意思,IA表示数据传输后地址增加(increase after);(IB:increase before, DA: decrease after, DB: decrease before)
	  意思是r3指示的内存数据依次加载到寄存器r4,r5,r6中去
		@r4 = . 的地址(虚拟地址)
		@r5= __proc_info_begin (虚拟地址)
		@r6 = __proc_info_end  (虚拟地址)
 */
	ldmia	r3, {r4 - r6} 
	sub	r3, r3, r4			@ get offset between virt&phys// 得到虚拟地址和物理地址的差值
	add	r5, r5, r3			@ convert virt addresses to   // 得到 __proc_info_begin 对应的物理地址
	add	r6, r6, r3			@ physical address space      // 得到 __proc_info_end 对应的物理地址

1:	ldmia	r5, {r3, r4}	@ value, mask       //将proc_info_list结构中cpu_val、cpu_mask分别存放在r3, r4中
	and	r4, r4, r9			@ mask wanted bits  //r4 = cpu_mask&CPU_ID
	teq	r3, r4                                  //比较
	beq	2f                                      //如果相等,找到匹配的proc_info_list结构,跳转到标号2处,并返回
	add	r5, r5, #PROC_INFO_SZ	@ sizeof(proc_info_list) /如果不相等,指向下一个proc_info_list结构
	cmp	r5, r6                                           //判断是否到达__proc_info_end 
	blo	1b                                               //没有则跳转到标号1,继续比较
	mov	r5, #0				@ unknown processor
2:	mov	pc, lr
ENDPROC(__lookup_processor_type)

/*
 * Look in <asm/procinfo.h> for information about the __proc_info structure.
 */
	.align	2
	.type	__lookup_processor_type_data, %object
__lookup_processor_type_data:
	.long	.
	.long	__proc_info_begin
	.long	__proc_info_end
	.size	__lookup_processor_type_data, . - __lookup_processor_type_data

注意:
__proc_info_begin、 __proc_info_end和“.”这三个数据都是在链接内核时确定的,它们都是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行代码在编译链接后的虚拟地址。因为MMU没有开启,所以我们此时还不能直接使用这些地址。所以在访问proc_info_list结构前,需要先将它的虚拟地址转化为物理地址。

4. 小节

  • __lookup_processor_type函数首先将标号__lookup_processor_type_data的物理地址加载到r3,
  • 然后将标号__lookup_processor_type_data的虚拟地址载入到r4,编译时生成的 __proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6。由于r3和r4分别存储的是同一位置标号__lookup_processor_type_data的物理地址和虚拟地址,所以两者相减即得到虚拟地址和物理地址之间的offset。
  • 利用此offset,将r5和r6中保存的虚拟地址转变为物理地址,然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r5置0并返回。

注意,在arch/arm/mm/Makefile中有如下行,需要配置CONFIG_CPU_V6 = y(在配置菜单中, Processor Type->)

obj-$(CONFIG_CPU_V6)		+= proc-v6.o

三. * (.arch.info.init)段

# arch/arm/kernel
	...
	bl	__lookup_machine_type	@ r5=machinfo               
	movs	r8, r5				@ invalid machine (r5=0)?

1.如何把描述机器(开发板)的machine_desc 结构放入 *(.arch.info.init)段

内核中对每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数,如机器的类型ID、起始I/O物理地址、Bootloader传入的参数地址、中断初始化函数、I/O映射函数等。对于mini6410开发板在arch/arm/mach-s3c64xx/ mach-mini6410.c有如下代码:

MACHINE_START(MINI6410, "MINI6410")
	/* Maintainer: Ben Dooks <ben-linux@fluff.org> */
	.boot_params	= S3C64XX_PA_SDRAM + 0x100,

	.init_irq	= s3c6410_init_irq,
	.map_io		= mini6410_map_io,
	.init_machine	= mini6410_machine_init,
	.timer		= &s3c24xx_timer,
MACHINE_END

其中:宏MACHINE_START、MACHINE_END在arch/arm/include/asm/mach/arch.h文件中定义:

/*
 * Set of macros to define architecture features.  This is built into
 * a table by the linker.
 */
#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,

#define MACHINE_END				\
};

将宏展开:

static const struct machine_desc __mach_desc_MINI6410	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_MINI6410,		\
	.name		=MINI6410,
	.boot_params	= S3C64XX_PA_SDRAM + 0x100,

	.init_irq	= s3c6410_init_irq,
	.map_io		= mini6410_map_io,
	.init_machine	= mini6410_machine_init,
	.timer		= &s3c24xx_timer,
};

不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在链接接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的段定义:

__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;

2. 如何判断内核是否支持当前的机器(开发板)

uboot 启动内核准备阶段时,会在r1寄存器中给出开发板的标记即机器类型ID。__lookup_machine_type函数将这个值与.arch.info.init段中定义的若干个machine_desc结构中的nr值进行逐一比较,如果两者相等则表示找到匹配的machine_desc结构,并把该machine_desc结构中的nr值保存到r5中。如果__arch_info_begin、__arch_info_end之间所有machine_desc结构的nr值都不等于r1寄存器中的值,则返回0,即r5中值为0。分析方法与* (.proc.info.init)段相同,这里只给出源代码:

# arch/arm/kernel/head-common.S
/*
 * Lookup machine architecture in the linker-build list of architectures.
 * Note that we can't use the absolute addresses for the __arch_info
 * lists since we aren't running with the MMU on (and therefore, we are
 * not in the correct address space).  We have to calculate the offset.
 *
 *  r1 = machine architecture number
 * Returns:
 *  r3, r4, r6 corrupted
 *  r5 = mach_info pointer in physical address space
 */
__lookup_machine_type:
	adr	r3, __lookup_machine_type_data
	ldmia	r3, {r4, r5, r6}
	sub	r3, r3, r4			@ get offset between virt&phys
	add	r5, r5, r3			@ convert virt addresses to
	add	r6, r6, r3			@ physical address space
1:	ldr	r3, [r5, #MACHINFO_TYPE]	@ get machine type
	teq	r3, r1				@ matches loader number?
	beq	2f				@ found
	add	r5, r5, #SIZEOF_MACHINE_DESC	@ next machine_desc
	cmp	r5, r6
	blo	1b
	mov	r5, #0				@ unknown machine
2:	mov	pc, lr
ENDPROC(__lookup_machine_type)

/*
 * Look in arch/arm/kernel/arch.[ch] for information about the
 * __arch_info structures.
 */
	.align	2
	.type	__lookup_machine_type_data, %object
__lookup_machine_type_data:
	.long	.
	.long	__arch_info_begin
	.long	__arch_info_end
	.size	__lookup_machine_type_data, . - __lookup_machine_type_data

注意:
对于mini6410开发板uboot传入的机器ID为2520,对应machine_desc结构在arch/arm/mach-s3c64xx/ mach-mini6410.c文件中定义,所以这个文件要编进内核中,因此需要配置CONFIG_MACH_MINI6410=y:
在这里插入图片描述

四. * (taglist.init)段

*(.taglist.init) 段存放的是 uboot 传递到内核的 tag 的处理函数。在 uboot 中,定义了一个 tag 结构体,里面存放要传递给内核的信息,uboot 将 tag 依次排放在和内核约定的地址,如 mini6410是 0x50000100 处,排放顺序是有要求的,必须以 ATAG_CORE 标记的 tag 开头,以 ATAG_NONE 为标记的 tag 结尾。

1. 如何把tag结构放入 * (taglist.init)段

在内核中,使用 __tagtable 来将处理 tag 的函数放到 *(.taglist.init) 段。如下:

# arch/arm/kernel/setup.c
__tagtable(ATAG_CORE, parse_tag_core);
__tagtable(ATAG_MEM, parse_tag_mem32);
__tagtable(ATAG_RAMDISK, parse_tag_ramdisk);
__tagtable(ATAG_SERIAL, parse_tag_serialnr);
__tagtable(ATAG_REVISION, parse_tag_revision);
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);

宏定义在arch/arm/include/asm/setup.h

#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn __tag = { tag, fn }

以 __tagtable(ATAG_CORE, parse_tag_core)为例,把宏展开:

static struct tagtable __tagtable_parse_tag_core __used __attribute__((__section__(".taglist.init"))) = {
  ATAG_CORE,
  parse_tag_core
}

其中struct tag结构体定义在arch/arm/include/asm/setup.h中:

# arch/arm/include/asm/setup.h
struct tag {
	struct tag_header hdr;
	union {
		struct tag_core		core;
		struct tag_mem32	mem;
		struct tag_videotext	videotext;
		struct tag_ramdisk	ramdisk;
		struct tag_initrd	initrd;
		struct tag_serialnr	serialnr;
		struct tag_revision	revision;
		struct tag_videolfb	videolfb;
		struct tag_cmdline	cmdline;

		/*
		 * Acorn specific
		 */
		struct tag_acorn	acorn;

		/*
		 * DC21285 specific
		 */
		struct tag_memclk	memclk;
	} u;
};

2. uboot中传入这些tag的方法

	setup_start_tag (bd);    /*设置ATAG_CORE标志*/
	setup_memory_tags (bd);                     /*设置内存标记*/
	setup_commandline_tag (bd, commandline);  /*设置命令行标记*/
	...
	setup_end_tag (bd);      /*设置ATAG_NONE标志 */

关于uboot如何传入这些tag,我放在uboot启动系列篇中讲解,这里只给出调用的函数。

(3)内核如何解析uboot传入的tag
调用链如下:

# init/main.c
|--- start_kernel
     |--- setup_arch(&command_line)
  		  |--- mdesc = setup_machine(machine_arch_type)//获取machine_desc,见上一小节
          |--- tags = phys_to_virt(mdesc->boot_params) //获取定义的boot_params内存地址,并赋值给tags
          |--- parse_tags(tags)                        //解析这些tags
               |--- parse_tag(t)    //遍历__tagtable_begin ~ __tagtable_end中的各种类型tag,并调用该类型tag真正的解析函数
                     |--- t->parse(tag)	 //调用该类型tag真正的解析函数   

在内核启动过程中,会使用 parse_tags 来处理 tag ,它最终会调用到 parse_tag ,取出 __tagtable_begin 和 __tagtable_end 之间的每一个 tagtable ,比较它们的类型,如果相同则调用 tagtable 里的处理函数来处理这个tag 。对于例子中的tag就是要调用parse_tag_core来处理这个 tag。

总结

内核通过链接脚本链接成内核映像文件时,指定了很多段,这些段的摆放顺序都是有一定的要求的,特别是涉及到内核启动准备阶段的段。关于这些段的解析和使用方法都是大同小异。参考前面的就能弄明白了。以后再涉及到段的使用,我就不再一步步的讲解了。

posted on 2022-11-02 22:23  BSP-路人甲  阅读(199)  评论(0编辑  收藏  举报

导航