Linux启动流程之arm (一)

2021/10/24 7:09:38

本文主要是介绍Linux启动流程之arm (一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

老的内核版本,不带dts内核:

1. 机器 ID,启动参数

启动文件head.S,主要完成如下几件事:

  • (0)判断是否支持此CPU
  • (1)如何比较机器ID是:(判断是否支持单板)
  • (3)创建页表。
  • (4)使能MMU。
  • (5)跳转到 start_kernel (它就是内核的第一个 C 函数)

2.分析内核源代码

1.通过make uImage V=1详细查看内核编译时的最后一条命令可知。

  • 内核中排布的第一个文件是:arch/arm/kernel/head.S
  • 链接脚本:arch/arm/kernel/vmlinux.lds

UBOOT启动时首先在内存里设置了一大堆参数。

接着启动内核:

theKernel(0, bd->bi_arch_number, bd->bi_boot_params);

theKernel 就是内核的入口地址。有3 个参数。

  • 参1为0 r0
  • 参2为机器ID r1
    • machine numbers:linux/arch/arm/tools/mach-types
  • 参3是上面那些参数所存放的地址r2
    所以,内核一上来肯定要处理这些参数。

3.内核启动

内核启动:最终目标是就运行应用程序。对于Linux 来说应用程序在根文件系统中,要挂接。

3.1.老内核,处理UBOOT 传入的参数

内核中排布的第一个文件是:arch/arm/kernel/head.S

arch/arm/kernel/head.S
arch/arm/boot/compressed/head.S

内核编译出来后比较大,可以压缩很小。在压缩过的内核前部加一段代码“自解压代码”。
这样内核运行时,先运行“自解压代码”。然后再执行解压缩后的内核。

我们看不用解压的head.S文件

mrc p15, 0, r9, c0, c0     @get processor id
bl __lookup_processor_type @r5=procinfo r9=cpuid

__lookup_processor_type查找处理器类型内核能够支持哪些处理器,是在编译内核时定义下来的。内核启动时去读寄存器:获取ID。

__lookup_processor_type:
    adr r3, __lookup_processor_type_data
    ldmia   r3, {r4 - 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:  ldmia   r5, {r3, r4}            @ value, mask
    and r4, r4, r9          @ mask wanted bits
    teq r3, r4
    beq 2f
    add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    cmp r5, r6
    blo 1b
    mov r5, #0              @ unknown processor
2:  mov pc, lr
ENDPROC(__lookup_processor_type)

注意:前面的内容是连续的。能够对虚拟地址到物理地址进行线性映射。虚拟地址+虚拟到物理地址的偏移=物理地址

首先 r3 等于__lookup_processor_type_data的地址如下。这是实际存在的地址。UBOOT 启动内核时,MMU 还没启动。所以这是物理地址。

  • ldmia r3, {r4 - r6},将r3依次赋值给r4-r6,这是相当于long类型数组r4=. ,r5=__proc_info_begin,r6=__proc_info_end,size = . - __lookup_processor_type_data是虚拟地址
  • sub r3, r3, r4: r4 等于“.”虚拟地址,r3是物理地址,得到virt和phys之间的偏移. r3最后等于offset
  • add r5, r5, r3add r6, r6, r3 :它们加上这个偏差值后,就变成了__proc_info_begin__proc_info_end的真正物理地址了。

__lookup_processor_type_data内容如下:

/*  
 * 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没有在内核源码中定义,是在 链接脚本 中定义的(vmlinux.lds)。

	__proc_info_begin = .;
	__arch_info_begin = .;
	__proc_info_end = .;

中间夹着 *(.arch .info .init),*表示所有文件。这里是指所有文件的 .arch .info .init段。(架构、信息、初始化)即架构相关的初始化信息全放在这里。它开始地址
是__arch_info_begin,结束地址是 __arch_info_end。这两个地址上从. = (0xc0000000) + 0x00008000(虚拟地址)一路的增涨下来的。

. = (0xc0000000) + 0x00008000;
.text.haed: {
	_stext = .;
	_sinittext = .;
	*(.text.head)
}
.init : { /*Init code and data*/
	*(.init.text)
	_einittext = .;
	__proc_info_begin = .;
	__arch_info_begin = .;
	__proc_info_end = .;
	__arch_info_begin = .;
	*(.arch.info.init)
	__arch_info_end = .;
	_tagtable_begin = .;
	*(.taglist.init)
	_tagtable_end = .;
}

详细看下节内核
看处理器ID 后,看内核是否可以支持这个处理器。若能支持则继续运行,不支持则跳到“_error_p”中去:

    beq __error_p @yes , error 'p'
    adr r0, str_p1
    bl  printascii
    mov r0, r9
    bl  printhex8
    adr r0, str_p2
    bl  printascii
    b   __error 
str_p1: .asciz  "\nError: unrecognized/unsupported processor variant (0x"
str_p2: .asciz  ").\n"
    .align
__error:
1:  mov r0, r0
    b   1b

__error这是个死循环。

一个编译好的内核能支持哪些单板,都是定下来的。(make CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm vexpress_defconfig编译内核时候制定)内核上电后会检测下看是否支持当前的
单板。若可以支持则继续往下跑,不支持则 __error_a 跳到死循环。
内核支持多少单板,就有多少个这种以 “MACHINE_START”开头,以“MACHINE_END”
定义起来的代码。每种单板都有机器ID(结构体中的nr),机器ID 是整数。
UBOOT 传来这个参数:

theKernel(0, bd->bi_arch_numer, bd->bi_boot_params);

与内核的这部分刚好对应(如下部分),内核将这部分代码编译进去了,就支持这段代码定义的 单板。上面这段代码中的结构体有
一个属性,它的段被强制设置到了体被放在 vmlinux.lds 定义的:

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

2410,2440,qt2410 的单板代码都强制放在这个地方。内核启动时,会从 __arch_info_begin = . 开始读,读到 __arch_info_end = . 一个一个的将单板信息取出来。将里面的机器ID 和UBOOT 传进来的机器ID 比较。相同则表示内核支持这个
单板。

下面就是比较机器ID了。(内核中的和UBOOT 传进来的)看arch\arm\kernel\head-common.S
从“__lookup_machine_type:”中可知

r5=__arch_info_begin
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
  • r5 是: __arch_info_begin
  • r1 是UBOOT 传来的参数:bi_arch_number
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

最后比较成功后,会回到:head.S

bl __lookup_machine_type @r5 = machinfo
movs r8, r5 @invliad machine (r5=0)?

以上 单板机器ID 比较完成。

3.2 新内核,处理UBOOT 传入的参数

r2决定使用atags还是使用dtb(CONFIG_OF_FLATTREE)

  • ATAG_CORE:探测RAM前16k内存,要求指针对齐.
  • CONFIG_OF_FLATTR:如果使能,则接收dtb地址
head.S->head_common.S __vet_atags:
# r2 either valid atags pointer, valid dtb pointer, or zero

__vet_atags:
    tst r2, #0x3               @ aligned?
    bne 1f

    ldr r5, [r2, #0]
#ifdef CONFIG_OF_FLATTREE
    ldr r6, =OF_DT_MAGIC        @ is it a DTB?
    cmp r5, r6
    beq 2f
#endif
    cmp r5, #ATAG_CORE_SIZE     @ is first tag ATAG_CORE?
    cmpne   r5, #ATAG_CORE_SIZE_EMPTY
    bne 1f
    ldr r5, [r2, #4]
    ldr r6, =ATAG_CORE
    cmp r5, r6
    bne 1f

2:  mov pc, lr              @ atag/dtb pointer is ok

1:  mov r2, #0
    mov pc, lr
ENDPROC(__vet_atags)

4.内核

4.1 老内核定义

知道上面这个关系后,我们则一定要知道在代码里面谁定义了.arch.info.init 这些东西。在内核中搜索它们。grep *.arch.info.init * -rR
查它们的定义:include/asm-arm/mach/arch.h +53

/*
 * 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				\
};

例如:sm2440

MACHINE_START(S3C2440, "SMDK2440")
	/* Maintainer: Ben Dooks <ben-linux@fluff.org> */
	.atag_offset	= 0x100,

	.init_irq	= s3c2440_init_irq,
	.map_io		= smdk2440_map_io,
	.init_machine	= smdk2440_machine_init,
	.init_time	= smdk2440_init_time,
MACHINE_END

上段代码展开:

static const struct machine_desc __mach_desc_S3C2440 
	__used	
	__attributed__((__section__(".arch.info.init"))) = { 
		.nr = MACH_TYPE_S3C2440,
		.name = "SMDK2440",
	.phys_io = S3C2410_PA_UART,
	.io_pg_offset = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
	.boot_params = S3C2410_SDRAW_PA + 0x100,
	.init_irq = s3c24xx_init_irq,
	.map_io = _smkd2440_map_io,
	.init_machine = smdk2440_machine_init,
	.timer = &s3c24xx_timer,
};

4.2 新内核定义

// init/main.c
setup_arch(&cmd_line);

// arch/arm/kernel/setup.c
void __init setup_arch(char **cmd_line_p)
{
  mdesc = setup_machine_fdt(__atags_pointer);
}

//arch/arm/kernel/devtree.c
struct machine_desc *__init setup_machine_fdt(unsigned int dt_phys)
{
    struct boot_param_header *devtree = phys_to_virt(dt_phys);
	/* check device tree validity */
    if (be32_to_cpu(devtree->magic) != OF_DT_HEADER)
        return NULL;

	/* Search the mdescs for the 'best' compatible value match */
	initial_boot_params = devtree;
	dt_root = of_get_flat_dt_root();
	for_each_machine_desc(mdesc) {
	  score = of_flat_dt_match(dt_root, mdesc->dt_compat);
	  if (score > 0 && score < mdesc_score) {
		mdesc_best = mdesc;
		mdesc_score = score;
	  }
	}
}
  • 其中:__atags_pointer r2 dtb地址
  • dt_rootinitial_boot_params由__atags_pointer计算得到,即相当于从r2传入

for_each_machine_desc:对应宏

/*        
 * Machine type table - also only accessible during boot
 */           
extern struct machine_desc __arch_info_begin[], __arch_info_end[];
#define for_each_machine_desc(p)            \
    for (p = __arch_info_begin; p < __arch_info_end; p++)

这里mdesc对应lds中变量,即DT_MACHINE_START中的信息

//linux\arch\arm\mach-at91\at91rm9200.c
static void __init at91rm9200_dt_device_init(void)
{
	//注意这里解析 dts
	of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);

	arm_pm_idle = at91rm9200_idle;
	arm_pm_restart = at91rm9200_restart;
	at91rm9200_pm_init();
}

static const char *at91rm9200_dt_board_compat[] __initconst = {
	"atmel,at91rm9200",
	NULL
};

DT_MACHINE_START(at91rm9200_dt, "Atmel AT91RM9200")
	.init_time      = at91rm9200_dt_timer_init,
	.map_io		= at91_map_io,
	.init_machine	= at91rm9200_dt_device_init,
	.dt_compat	= at91rm9200_dt_board_compat,
MACHINE_END

看看结构体:machine_desc

struct machine_desc {
	unsigned int		nr;		/* architecture number	机器ID*/
	const char		*name;		/* architecture name	*/
	unsigned long		atag_offset;	/* tagged list (relative) */
	const char *const 	*dt_compat;	/* array of device tree
						 * 'compatible' strings	*/

	unsigned int		nr_irqs;	/* number of IRQs */

#ifdef CONFIG_ZONE_DMA
	phys_addr_t		dma_zone_size;	/* size of DMA-able area */
#endif

	unsigned int		video_start;	/* start of video RAM	*/
	unsigned int		video_end;	/* end of video RAM	*/

	unsigned char		reserve_lp0 :1;	/* never has lp0	*/
	unsigned char		reserve_lp1 :1;	/* never has lp1	*/
	unsigned char		reserve_lp2 :1;	/* never has lp2	*/
	enum reboot_mode	reboot_mode;	/* default restart mode	*/
	unsigned		l2c_aux_val;	/* L2 cache aux value	*/
	unsigned		l2c_aux_mask;	/* L2 cache aux mask	*/
	void			(*l2c_write_sec)(unsigned long, unsigned);
	struct smp_operations	*smp;		/* SMP operations	*/
	bool			(*smp_init)(void);
	void			(*fixup)(struct tag *, char **);
	void			(*dt_fixup)(void);
	void			(*init_meminfo)(void);
	void			(*reserve)(void);/* reserve mem blocks	*/
	void			(*map_io)(void);/* IO mapping function	*/
	void			(*init_early)(void);
	void			(*init_irq)(void);
	void			(*init_time)(void);
	void			(*init_machine)(void);
	void			(*init_late)(void);
	void			(*restart)(enum reboot_mode, const char *);
};

3.3 创建页表

bl __create_page_tables

内核的链接地址从虚拟地址

. = (0xc00000000) + 0x00008000;

开始。这个地址并不代表真实存在的内存。我们的内存是从 0x3000 0000 开始的。故这里面要建立一个页表,启动 MMU 。

3.4 使能MMU

ldr r13, __switch_data @ address to jump to after mmu has been enabled
adr lr, __enbable_mmu @ return (PIC) address
add pc, r10, #PROCINFO_INITFUNC

mmu has been enabled 当MMU 使能后,会跳到 __switch_data 中去。
如何跳到 __switch_data ,则看:__enable_mmu

	.type __switch_data, %object
__switch_data:
	.long __mmap_switched
	.long __data_loc @ r4
	.long __data_start @ r5
	.long __bss_start @ r6
	.long __end 	  @ r7
	.long processor_id @ r4
	.long __machine_arch_type @ r5
	.long cr_alignment @ r6
	.long init_thread_union + THREAD_START_SP @ sp

在head_common.S 文件中,__switch_data 后是:__mmap_switched

__mmap_switched:
	adr r3, __switched_data + 4
	ldmia r3!, {r4, r5, r6, r7}
	cmd 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

	ldmia r3, {r4, r4, r6, sp}
	str r9, [r4] @ Save processor ID
	str r1, [r5] @ Save machine type
	bic r4, f0, #CR_A
	stmia r6, {r0, r4}
	b start_kernel @跳转到第一个C函数中来处理UBOOT传来的参数

4.start_kernel(它就是内核的第一个C函数)

UBOOT 传进来的启动参数,参2:机器ID 在head.Skh 中会比较。
参3:传进来的参数,就是在这个第一个C函数start_kernel 中处理
分析第一个C 函数 start_kernel :在 main.c 文件中

asmlinkage __visible void __init start_kernel(void)


这篇关于Linux启动流程之arm (一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程