概述
前边的章节我们分析了UBOOT是怎么加载内核的,最终是通过bootz等指令跳转到了一个地址,就开始进入到内核,要分析内核的起始点也是得先从链接脚本开始分析,因此我们先编译下内核,然后从链接脚本开始分析(注:内核版本4.1.15,后续系列均为此版本)
1、内核目录结构及编译
1.1 内核编译
1 #!/bin/sh
2 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
3 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_emmc_defconfig
4 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
5 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
- 首先clean下工程,删除无用配置
- 然后选择一个配置文件来进行编译,跟uboot类似,配置文件位置在arch/arm/configs/
- 然后make menuconfg 图形化界面进行内核配置
- 最后make 进行编译
1.2 内核目录结构
- arch:架构有关的目录,比如 arm、 arm64、 avr32、 x86 等等架构。
- block:块设备目录,像 SD 卡、 EMMC、 NAND、硬盘等存储设备就属于块设备
- COPYING:版权声明
- crypro:加密相关,比如常见的 crc、 crc32、 md4、 md5、 hash 等加密算法
- Documentation:此目录里面存放着 Linux 相关的文档,如果要想了解 Linux 某个功能模块或驱动架构的功能,就可以在 Documentation 目录中查找有没有对应的文档。
- drivers:驱动相关,根据驱动类型的不同,分门别类进行整理,比如 drivers/i2c 就是 I2C相关驱动目录, drivers/gpio 就是 GPIO 相关的驱动目录,这是学习的重点
- firmware:固件相关,用于存放固件
- fs:文件系统相关,存放文件系统,比如 fs/ext2、 fs/ext4、 fs/f2fs 等
- include:头文件相关
- init:初始化相关,内核启动的时候初始化代码
- ipc:进程间通讯相关
- Kbuild:Makefile 会读取此文件
- Kconfig:图形化配置界面的配置文件
- kernel:内核相关
- lib:库文件,一些公用的库函数
- MAINTAINERS:维护者名单
- Makefile:linux顶层makefile
- make_mx6ull_alpha_emmc.sh:笔者加的编译脚本
- mm:内存管理文件
- modules.* Modules.* :一系列文件,和模块有关,编译生成的文件
- net:网络相关文件
- README:Linux 描述文件,详细讲解了如何编译 Linux 源码,以及 Linux 源码的目录信息
- REPORTING-BUGS: BUG 上报指南
- samples:例程相关
- scripts:脚本相关,Linux 编译的时候会用到很多脚本文件,这些脚本文件就保存在此目录
- security:安全相关
- sound:音频处理相关,音频驱动文件并没有存放到 drivers 目录中,而是单独的目录
- System.map:符号表,编译生成的文件
- tools:工具相关,存放一些编译的时候使用到的工具
- usr:与 initramfs 相关的目录,用于生成initramfs
- virt:提供虚拟机技术(KVM),存放虚拟机相关文件
- vmlinux:编译出来的、未压缩的 ELF 格式Linux 文件,编译生成
- vmlinux.dis:vmlinux的反汇编文件,编译生成
- vmlinux.o:vmlinux的obj文件,编译生成
此外还有部分隐藏文件,列举几个比较重要的:
- .config文件:跟 uboot 一样, .config 保存着 Linux 最终的配置信息,编译 Linux 的时候会读取此文件中的配置信息。最终根据配置信息来选择编译 Linux 哪些模块,哪些功能,编译生成
- .vmlinux.cmd:cmd 文件,用于连接生成 vmlinux
编译生成的image文件保存在arch/arm/boot,如下 注:Image是编译生成的内核镜像二进制文件,ZImage是将Image进行了压缩的,通常使用的也是ZImage。
2、启动流程
2.1 链接脚本vmlinux.lds
首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以找到 Linux 内核的第一行程序是从哪里执行的,如下 我们就看关键的一些信息,ENTRY指定了入口函数,即stext(链接指定入口点的几个方式,在其他的bolg中有介绍)。
2.2 入口函数stext
stext 定义在文件arch/arm/kernel/head.S中,如下 首先说明了在进入到kernel的入口函数之前要关闭mmu,D-cache,同时stext的入口参数有3个,第一个是0,第二个机器id,第三个是设备树的地址(此地址为物理地址)。
- 第81行:设置为大端模式,其实ARM_BE8是个宏,定义在arch/arm/include/asm/assembler.h,如下
如上图,arm是支持修改大小端的,默认是小端,因此如果需要修改成大端则会执行指令 setend be,将其设置为大端模式 - 第83~86行:切换到thumb指令集
- 第88~90行:若启动了虚拟化,此处未启用
- 第92行:调用函数safe_svcmode_maskall,定义在arch/arm/include/asm/assembler.h中,确保CPU 处于 SVC 模式,并且关闭了所有的中断
- 第94行:从cp15协处理器的c0寄存器读取硬件的CPU ID号,保存在r9中
- 第95行:调用函数__lookup_processor_type,检查当前系统是否支持此 CPU,如果支持的就
获 取 procinfo 信 息,会将其保存到 r5 寄存器中 。 procinfo 是 proc_info_list 类 型 的 结 构 体 , proc_info_list 在 文 件 arch/arm/include/asm/procinfo.h 中的定义如下 - 第96~98行:若获取到的cpu信息为0,则跳转到__error_lpae,最终死循环
- 第100~106行:若定义3级页表才需要,此处未定义
- 第108~115行:未定义XIP,需要计算出物理地址和虚拟地址的差值。
- 第121行:校验uboot给内核的传参ATAGS(r2)格式是否正确,定义在arch/arm/kernel/head-common.S
- 第122~124行:多核的一些检测
- 第125~127行:给物理地址打补丁,转换成虚拟地址
- 第128行:调用函数__create_page_tables 创建页表,以便后续打开mmu
注:linux内核本身被链接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态,但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。因此kernel想了一个好办法,就是:建立页表分两步走。第一步,kernel先建立一个段式页表(和uboot之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表。段式页表本身比较好建立(段式页表1MB一个映射,4GB的空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(以4KB为单位),然后启动新的细页表,废除第一步建立的段式映射页表。 在内核启动的早期,建立的段式页表,并在内核启动的前期使用;在内核启动的后期,就会再次建立细页表并启用。等内核启动工作起来之后就只有细页表了。
- 第137行:取得__mmap_switched的链接地址,保存在r13中(此时mmu未开启,无法跳转到该地址运行)
- 第139行:将1f处的地址保存到lr寄存器中,即lr地址是__enable_mmu
- 第140行:将上文计算的r4(PHYS_OFFSET - PAGE_OFFSET)存储到r8中
- 第141行:把cpu对应proc info中的__cpu_flush存放到r12寄存器中,r10存储的是cpuinfo(这个__cpu_flush成员存放的是cpu对应架构的setup函数的地址)
- 第142行:与r10相加后,的得setup函数的物理地址
- 第143行:执行setup函数,定义在arch/arm/mm/proc-v7.s中,主要是一些打开mmu之前的准备工作, 执行完之后就跳转到__enable_mmu函数
- 第429~433行:根据配置使能或禁止地址对齐错误检测
- 第434~436行:根据配置使能或禁止数据cache
- 第437~439行:控制位选择淘汰算法
- 第440~442行:根据配置使能或禁止指令cache
- 第443~448行:设置访问权限
- 第449行:设置页表地址c2
- 第451行:调用了__turn_mmu_on打开mmu
- 第472行:指令同步屏障
- 第473行:根据配置设置SCTLR寄存器,打开mmu使能位,cache等
- 第477~478行:由于mmu已经使能,因此这里可以运行虚拟地址的函数了,跳转到__mmap_switched
__mmap_switched函数定义在arch/arm/kernel/head-common.S 中,函数代码如下:
-
第82行:加载__mmap_switched_data的地址到r3中 -
第84行:设置寄存器值,如下 相当于R4=_data_loc(数据存放的位置),R5=_sdata(是数据开始的位置),R6=__bss_start(bss开始的位置),R7=_end(bss结束的位置, 也是内核结束的位置) -
第85~89行:比较R4和R5,判断数据存储的位置和数据的开始的位置是否相等,如果不相等,则需要搬运数据,从 __data_loc 将数据搬到 _sdata. 其中 __bss_start 是bss的开始的位置,也标志了 data 结束的位置,因而用其作为判断数据是否搬运完成 -
第91行:初始化栈基指针fp -
第92~94:清空bss段数据 -
第96行:重新设置寄存器值 -
R4=processor_id(cpu处理器ID地址,其变量定义在arch/arm/kernel/setup.c中) -
R5=__machine_arch_type(machine id地址,其变量定义在arch/arm/kernel/setup.c中) -
R6=__atags_pointer(dtb指针的地址,其变量定义在arch/arm/kernel/setup.c中) -
R7=cr_alignment(cp15的c1寄存器的值的地址,也就是mmu控制寄存器的值,其变量定义在arch/arm/kernel/entry-armv.S中) -
sp=init_thread_union + THREAD_START_SP(init_thread_union可在System.map中获取,笔者的是0x809e6000,这样一来相当于SP的初值是0x809e6000 + 0x2000 - 8) -
第97~98行:thumb指令的实现,意义同第86行,同时只有一处生效 -
第99行:把cpu处理器id(r9)放到processor_id变量中 -
第100行:把mechine id(r1)存放到__machine_arch_type变量中 -
第101行:把dtb的地址指针(r2)存放到__atags_pointer变量中(此地址为物理地址,使用需转换) -
第102~103行:把cp15的c1的寄存器的值(r0)存放到cr_alignment变量中 -
第104行:跳转到start_kernel开始启动内核
|