泰安新闻频道在线直播/seo营销推广公司
在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
文章目录
- 1 内核启动总体流程
- 1.1 核心目的
- 1.2 总体流程
- 2 stext剖析
- 2.1 流程分析
- 2.2 __cpu_flush函数
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) @ 定义全局标号stextmsr 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 enabledadr 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_READb __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, r0mcr p15, 0, r0, c1, c0, 0 @ write control regmrc p15, 0, r3, c0, c0, 0 @ read id regmov r3, r3mov r3, r3mov pc, r13 /* 这里是关键,会返回第97行执行__switch_data函数*/
Tips:总结一下97行到100行代码的难度到底在那里。首先,在于C语言定义的结构体和汇编定义的同一个结构体只有偏移相同,但是名称不同;再次,在于汇编语言对寄存器的分时复用思想,当某个或某些寄存器的时间跨度过大后,对代码的可读性会产生重大破坏,比如上述的r13,中间横跨的代码太多了,不建议这么使用;最后,难度在于大量使用了函数指针,而且还是在汇编中。
不过,这种代码见得多了,有了经验,下次遇到也就不觉得繁琐了。
<完>