当你在路上遇到一位绝世的丽人,你首先会很好奇她从哪里来,是什么样的成长环境造就了这旷世的美丽。进而你开始搜肠刮肚的想怎么样跟这美女套点近乎,建立起沟通的连廊,走进去她的内心,最终抱得美人归?
同样的道理,当你接触linux内核这位大美女的时候,你也会有上面的小心思,那我们就来看:
- linux内核从哪儿来? -- 内核是如何运行起来的,它的启动过程是怎么样的?
本文或者后续文章代码均参考自:linux 4.9.230版本。
有接触过的朋友知道,linux内核的启动函数叫 start_kernel, 位置 init/main.c
asmlinkage __visible void __init start_kernel(void)
备注:asmlinkage, __visible和__init都是gcc编译器扩展,我们暂且把它们当透明人,不管他们,我下来会写篇文章统一讲这个部分。
就像人类孜孜不倦的寻找自己的历史一样,内核的起点真的是从C语言的start_kernel开始的吗?
很显然不是。
在start_kernel之前还有漫漫的史前时代,而这个时代的描绘者却是汇编代码。所以不同的体系架构上,这部分代码是不同的:
对于x86 i386架构,这部分代码在arch/x86/kernel/head_32.S
对于x86 x64架构,这部分代码在arch/x86/kernel/head_64.S
而对于arm架构,则位于 arch/arm/kernel/head.S
但是共同点也是明显的,这些不同架构的汇编代码最终都会调用start_kernel。所以,start_kernel并非linux内核的起点,把start_kernel称为linux内核架构无关的通用入口,才是最合适的描述。
那start_kernel以前的这部分代码做了什么呢?处理器相关的硬件初始化。
我们以arm架构为例,arm架构下运行linux内核,执行的第一行代码在哪里?
就在这里了:arch/arm/kernel/head.S (linux 4.9.230)
你可以不懂汇编代码,但是不懂英文简直是寸步难行。作为对内核终极起点的描述,我们把上面的注释好好看一下吧,以上注解透漏几个信息:
- 这部分代码是被内核解压代码调用的。我们知道内核有压缩的机制,所以看起来内核一解压,就跑到这里来了。所以,如果算上内核的解压程序,是不是内核的解压程序启动比这部分代码还早?是的。所以,内核的终极起点又要提前了,那就是内核自解压程序的入口。
- 以上汇编代码执行的时候,硬件的状态是这样的:MMU = off, D-cache = off, I-cache = don care, r0 = 0,* r1 = machine nr, r2 = atags或dtb指针* r1 = machine nr, r2 = atags或dtb指针。后续的处理器初始化都是基于这个前提进行的。
- 这部分代码是位置无关代码,所以你在编译的时候的链接地址是0xc0008000,那么在执行的时候就可以这样调用:__pa(0xc0008000)。
- 查看arch/arm/tools/mach-types获取内核支持的machine types.这个是做什么的呢?如果你要新增你自己的board,那么也要登记到这里。
了解以上信息后,我们大概看一下arc/arm/kernel/head.S干了什么事情,其实不一定要能看懂汇编代码,我们看注释就够了,我把head.S里的原文地址代码注释总结如下:
/* 部分源代码分析 */
/* 内核入口点 */
ENTRY(stext)
/* 设置处理器模式 */
ARM_BE8(setend be ) @ ensure we are in BE8 mode
THUMB( badr r9, 1f ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )
/* 判断CPU类型,查找运行的CPU ID值与Linux编译支持的ID值是否支持 */
bl __lookup_processor_type
/* 处理uboot传递过来的参数,获取memory大小等 */
bl __vet_atags
#ifdef CONFIG_SMP_ON_UP
bl __fixup_smp
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
bl __fixup_pv_table
#endif
/* 为内核运行创建页目录表和页表*/
bl __create_page_tables
/* 跳转到start_kernel函数 */
b start_kernel
我们总结一下,内核的启动分为三个阶段:
- 内核自解压阶段。解压程序解压内核之后,进入 arch/arm/kernel/head.S
- 内核引导阶段。执行head.S汇编代码,初始化处理器并创建页目录表和页表,然后enable mmu,最后进入start_kernel。一定要注意:enable mmu之后,所用的地址就都是虚拟地址了,而非物理地址。
- 内核初始化部分,通过start_kernel执行内核各个部件的初始化。这个部分比较长,且需要有一定的基础,我们放到后面再讲。