一起分析Linux系统设计思想——03内核启动流程分析(一)

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 内核启动总体流程

分析内核启动流程时一定要 抓大放小 ,千万不要上来就深入到每一个函数,每一个细节。如果这样就会只见树木不见森林,逐渐迷路。正确的做法是先掌握根本目的和大体流程,然后再逐个击破。

1.1 核心目的

无论看多少代码或写多少代码,一定不要忘记最终的目的。Linux内核启动的最终目的就是——运行应用程序。

根据这个最终目的再去理解内核启动流程中的关键步骤就容易多了。比如,运行应用程序首先要找到应用程序存储的地方吧,这就需要挂载根文件系统。再比如,运行应用程序需要加载到内存中运行吧,这就需要初始化页表并打开MMU。

1.2 总体流程

首先我们梳理一下Linux内核启动的总体流程,在脑海中建立起关键步骤的 枝干 ,后续我会根据需要挑拣 枝叶 再进行详细分析。

先上图:

在这里插入图片描述

我向来不赞成死记硬背,凡事都遵循 因果报应 ,计算机也不例外,只不过这里叫 逻辑 。只要你想明白了,流程自然就应该是这样的。比如,bootloader给内核传递了两类参数,一个是机器ID,一个是启动参数。那既然bootloader给内核传递了这两个参数,那内核就肯定会处理对吧,那就必然有对机器ID的处理步骤和对启动参数的解析。进一步的,我们还可以 刨根问底 ,bootloader为什么要传递这两类参数,为什么不是其他的?这两类参数中机器ID是必须要传递的,那咱就先分析这个为啥必须传递。其实,原因也很简单,它们之前其实是一体的,后来才分工的,可以将bootloader理解成内核的 开道先锋 ,所以,bootloader开的道必须是内核想走的路才对,这里的路就是硬件啦。每一款硬件是用机器ID来进行区分。因此机器ID必须由bootloader传递给内核,这是他们的 接头暗号

2 stext剖析

该节用到的汇编指令汇总如下表:

指令功能说明
msrMove register to PSR status/flags操纵PSR寄存器时才会使用该指令
mrcMove from coprocessor register to CPU register读协处理器时才会使用该指令
mov(s)Move register or constant加s代表会影响标志位
ldrLoad register or constant加载绝对地址(链接地址)
adrAddress of register or constant加载小范围地址(pc+offset)
blBranch with link该指令会在跳转到标号之前保存当前pc(r15)指针内容到lr(r14)寄存器中,用于函数调用返回使用
beqBranch该指令中eq为执行b指令的条件——CPSR中的Z=1
addaddsum = add1 + add2

2.1 流程分析

stext是Linux内核的第一个入口函数,代码都是位置无关码(PIC)。在Linux内核启动之前bootloader已经将0放到了r0中,将机器ID放到了r1中。

Tips:在分析内核启动代码前了解当前寄存器中的值和含义是十分必要的,因为汇编语言的特点就是充分利用了 分时复用 的思想,不同的时刻,寄存器中的值是不同的,有着不同的含义。汇编语言比较繁琐的一点就在于这里——一句话不会在一行说完,想理解当前行的意思,必须查看上下文,综合去理解分析。

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 * crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */
	.section ".text.head", "ax" @ 定义一个属性为“可分配可运行”的名为“.text.head”的节
	.type	stext, %function @ 声明stext标号为一个函数
ENTRY(stext)	@ 定义全局标号stext
	msr	cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
				@ and irqs disabled 其实,这里的工作bootloader都已经做了,这里再做一遍保证万无一失。
	mrc	p15, 0, r9, c0, c0		@ get processor id arm9只有一个协处理器,处理器id存储在c0寄存器,将c0中的cpuid存储到r9中。
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid,调用函数__lookup_processor_type,该函数中会根据r9的内容匹配内核代码支持的procinfo,并把procinfo存储在r5中。
	movs	r10, r5				@ invalid processor (r5=0)?将r5的内容移动到r10,并且影响CPSR的Z位,如果r5=0,则Z=1,否则Z=0。
	beq	__error_p			@ yes, error 'p',如果Z=1,则跳转到__error_p停止启动。
	bl	__lookup_machine_type		@ r5=machinfo,调用__lookup_machine_type函数,该函数中会根据r1的内容匹配内核代码支持的机器ID,并把machinfo存储在r5中。
	movs	r8, r5				@ invalid machine (r5=0)?,该行代码和下一行配合,用来判断r5是否为0,如果为0则跳入__error_a打印错误信息并停止执行。
	beq	__error_a			@ yes, error 'a'
	bl	__create_page_tables @ 调用创建临时一级页表函数

	/* 该段代码比较难以理解,下一小节展开说明
	 * The following calls CPU specific code in a position independent
	 * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
	 * xxx_proc_info structure selected by __lookup_machine_type
	 * above.  On return, the CPU will be ready for the MMU to be
	 * turned on, and r0 will hold the CPU control register value.
	 */
	ldr	r13, __switch_data		@ address to jump to after,将__switch_data所在地址赋值给r13
						@ mmu has been enabled
	adr	lr, __enable_mmu		@ return (PIC) address,将__enable_mmu地址赋值给lr。
	add	pc, r10, #PROCINFO_INITFUNC 

ENTRY的展开如下:

/* /include/linux/linkage.h 文件 */

#define __ALIGN		.align 4,0x90 /*4:4字节对其;0x90:NOP指令的机器码,用于填充到指定的对齐字节*/
#define ALIGN __ALIGN

#ifndef ENTRY
#define ENTRY(name) \
  .globl name; \ /* 声明该标号为全局标号 */
  ALIGN; \ /* 4字节对齐,使用nop进行填充 */
  name: /* 定义标号 */
#endif

2.2 __cpu_flush函数

下面的代码使用的技巧性比较强,单独拿出来分析一下。

/*
  * The following calls CPU specific code in a position independent
  * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
  * xxx_proc_info structure selected by __lookup_machine_type
  * above.  On return, the CPU will be ready for the MMU to be
  * turned on, and r0 will hold the CPU control register value.
  */
097:  ldr	r13, __switch_data		@ address to jump to after,将__switch_data所在地址赋值给r13
098:  @ mmu has been enabled
099:  adr	lr, __enable_mmu		@ return (PIC) address,将__enable_mmu地址赋值给lr。
100:  add	pc, r10, #PROCINFO_INITFUNC 

从第100行代码开始分析,主要意思是改变pc指针,这就意味着程序的流程会发生改变,那下一个问题就是变到哪里?

pc = r10 + PROCINFO_INITFUNC

前面的初始化保证,寄存器r10指向procinfo,在文件arch/arm/kernel/asm-offsets.c文件中宏PROCINFO_INITFUNC的定义如下:

 DEFINE(PROCINFO_INITFUNC,     offsetof(struct proc_info_list, __cpu_flush));

我们再来看一下结构体proc_info_list:

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

通过上面结构体的注释,我们知道,该结构体的真实定义位置是arch/arm/mm/proc-*.S。此处,我们以proc-arm920.S文件为例分析。

/* 真实的结构体定义如下 */

__arm920_proc_info:
	.long	0x41009200
	.long	0xff00fff0
	.long   PMD_TYPE_SECT | \
		PMD_SECT_BUFFERABLE | \
		PMD_SECT_CACHEABLE | \
		PMD_BIT4 | \
		PMD_SECT_AP_WRITE | \
		PMD_SECT_AP_READ
	.long   PMD_TYPE_SECT | \
		PMD_BIT4 | \
		PMD_SECT_AP_WRITE | \
		PMD_SECT_AP_READ
	b	__arm920_setup			/*和C语言中对照偏移,这个就是我们关注的字段*/
	.long	cpu_arch_name
	.long	cpu_elf_name
	.long	HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
	.long	cpu_arm920_name
	.long	arm920_processor_functions
	.long	v4wbi_tlb_fns
	.long	v4wb_user_fns
#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
	.long	arm920_cache_fns
#else
	.long	v4wt_cache_fns
#endif
	.size	__arm920_proc_info, . - __arm920_proc_info

我们发现,此数据结构中,C语言结构体中定义的__ cpu_flush成员,在汇编语言中被定义为一条调用指令:b __arm920_setup。

因此 add pc, r10, #PROCINFO_INITFUNC 这句指令辗转(难点就在于名字不对应,但偏移对应)调用了 __arm920_setup 函数。到此,我们不再继续跟踪。


第100行代码调用结束之后,pc = lr,也就是执行 __enable_mmu 函数。

该函数调用了 __turn_mmu_on 函数,关键点在于该函数的最后一行。

pc = r13 会返回第97行执行 __switch_data 函数。

	.align	5
	.type	__turn_mmu_on, %function
__turn_mmu_on:
	mov	r0, r0
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	mov	r3, r3
	mov	r3, r3
	mov	pc, r13  /* 这里是关键,会返回第97行执行__switch_data函数*/

Tips:总结一下97行到100行代码的难度到底在那里。首先,在于C语言定义的结构体和汇编定义的同一个结构体只有偏移相同,但是名称不同;再次,在于汇编语言对寄存器的分时复用思想,当某个或某些寄存器的时间跨度过大后,对代码的可读性会产生重大破坏,比如上述的r13,中间横跨的代码太多了,不建议这么使用;最后,难度在于大量使用了函数指针,而且还是在汇编中。

不过,这种代码见得多了,有了经验,下次遇到也就不觉得繁琐了。


<完>

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值