在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
1 内核启动总体流程
分析内核启动流程时一定要 抓大放小 ,千万不要上来就深入到每一个函数,每一个细节。如果这样就会只见树木不见森林,逐渐迷路。正确的做法是先掌握根本目的和大体流程,然后再逐个击破。
1.1 核心目的
无论看多少代码或写多少代码,一定不要忘记最终的目的。Linux内核启动的最终目的就是——运行应用程序。
根据这个最终目的再去理解内核启动流程中的关键步骤就容易多了。比如,运行应用程序首先要找到应用程序存储的地方吧,这就需要挂载根文件系统。再比如,运行应用程序需要加载到内存中运行吧,这就需要初始化页表并打开MMU。
1.2 总体流程
首先我们梳理一下Linux内核启动的总体流程,在脑海中建立起关键步骤的 枝干 ,后续我会根据需要挑拣 枝叶 再进行详细分析。
先上图:
我向来不赞成死记硬背,凡事都遵循 因果报应 ,计算机也不例外,只不过这里叫 逻辑 。只要你想明白了,流程自然就应该是这样的。比如,bootloader给内核传递了两类参数,一个是机器ID,一个是启动参数。那既然bootloader给内核传递了这两个参数,那内核就肯定会处理对吧,那就必然有对机器ID的处理步骤和对启动参数的解析。进一步的,我们还可以 刨根问底 ,bootloader为什么要传递这两类参数,为什么不是其他的?这两类参数中机器ID是必须要传递的,那咱就先分析这个为啥必须传递。其实,原因也很简单,它们之前其实是一体的,后来才分工的,可以将bootloader理解成内核的 开道先锋 ,所以,bootloader开的道必须是内核想走的路才对,这里的路就是硬件啦。每一款硬件是用机器ID来进行区分。因此机器ID必须由bootloader传递给内核,这是他们的 接头暗号 。
2 stext剖析
该节用到的汇编指令汇总如下表:
指令 | 功能 | 说明 |
---|---|---|
msr | Move register to PSR status/flags | 操纵PSR寄存器时才会使用该指令 |
mrc | Move from coprocessor register to CPU register | 读协处理器时才会使用该指令 |
mov(s) | Move register or constant | 加s代表会影响标志位 |
ldr | Load register or constant | 加载绝对地址(链接地址) |
adr | Address of register or constant | 加载小范围地址(pc+offset) |
bl | Branch with link | 该指令会在跳转到标号之前保存当前pc(r15)指针内容到lr(r14)寄存器中,用于函数调用返回使用 |
beq | Branch | 该指令中eq为执行b指令的条件——CPSR中的Z=1 |
add | add | sum = 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,中间横跨的代码太多了,不建议这么使用;最后,难度在于大量使用了函数指针,而且还是在汇编中。
不过,这种代码见得多了,有了经验,下次遇到也就不觉得繁琐了。
<完>