linux-kernel 启动过程 一

本文解析了Linux内核4.1.15版本的启动过程,包括内核编译步骤、目录结构介绍,以及通过链接脚本vmlinux.lds定位内核入口函数stext。深入分析了stext函数如何进行大小端设置、中断关闭、页表创建等关键操作。

概述

前边的章节我们分析了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(Uboot在跳转到内核前会关闭MMU、cache这些),同时stext的入口参数有3个,第一个是0,第二个机器id,第三个是设备树的地址(此地址为物理地址)。
在这里插入图片描述

  • 第81行:设置为大端模式,其实ARM_BE8是个宏,定义在arch/arm/include/asm/assembler.h,如下
    在这里插入图片描述
    如上图,arm是支持修改大小端的,默认是小端,因此如果需要修改成大端则会执行指令 setend be,将其设置为大端模式
  • 第83~86行:切换到thumb指令集,启动默认是ARM指令模式,内核配置了使能THUMB2才会切换指令集,thumb2是长短指令集混合模式,16/32位指令都有。
  • 第88~90行:若启动了虚拟化,此处未启用
  • 第92行:调用函数safe_svcmode_maskall,定义在arch/arm/include/asm/assembler.h中,确保CPU 处于 SVC 模式,并且关闭了所有的中断。因为刚进入到内核时的模式取决于uboot的模式,uboot通常也是在svc模式下运行,这里是内核做了保险。
  • 第94行:从cp15协处理器的c0寄存器读取硬件的CPU ID号,保存在r9中
  • 第95行:调用函数__lookup_processor_type,检查当前系统是否支持此 CPU(只匹配处理器型号,例如是ARMV7的A7、A8、A15还是什么),如果支持的就获 取 procinfo 信 息,会将其保存到 r5 寄存器中 。 procinfo 是 proc_info_list 类 型 的 结 构 体 , proc_info_list 在 文 件
    arch/arm/include/asm/procinfo.h 中的定义如下
    在这里插入图片描述
  • 第96~98行:若获取到的cpu信息为0,则跳转到__error_lpae,最终死循环
  • 第100~106行:确认系统是否使能并支持大内存,普通ARM最大支持4G内存,如果使用超过4G,则需要硬件支持,这里通过读CP15来确认如果使能了大内存,相应的硬件是否支持。

在这里插入图片描述

  • 第108~115行:未定义XIP,需要计算出物理地址和虚拟地址的差值。因为内核链接的时候是指定了运行的虚拟地址,而在这时MMU还未使能,只能使用物理地址,内核被搬运到了DDR的哪个地址上这时候内核自己是不知道的,因此用adr指令获取到符号2f处的物理地址,然后再读取PAGE_OFFSET变量(它是内核的链接起始地址,是个虚拟地址),这样就知道了虚拟地址和物理地址的相对偏移地址存到r8里,后边在MMU打开之前,访问的地址都需要加上这个偏移才可以。
  • 第121行:校验uboot给内核的传参ATAGS(r2)格式是否正确,定义在arch/arm/kernel/head-common.S(DTB第一个数是魔法数,通过检测这个来确认)
  • 第122~124行:当你编译了一个支持多核的内核,但又想让它能在单核 CPU 上正常运行时,就用它。暂不关注
  • 第125~127行:内核链接时的地址和实际运行地址不一致,二者之间有一个偏移值,在编译时在链接脚本里有需要修正的物理地址的段(pv_table),对这里边的地址修正即可。通常是影响虚拟地址和物理地址的转换函数。 把编译时写死的物理内存起始地址替换成当前板子真正的物理起始地址。
  • 第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之前的准备工作,比如cache clean、配置mmu页表、设置内存属性、配置CP15控制寄存器等, 执行完之后返回到lr寄存器的地址继续执行,即__enable_mmu函数,在head.s中

在这里插入图片描述

  • 第429~433行:根据配置使能或禁止地址对齐错误检测
  • 第434~436行:根据配置使能或禁止Dcache
  • 第437~439行:是否关闭分支预测执行,通常不会关,很影响效率
  • 第440~442行:根据配置使能或禁止Icache
  • 第443~448行:设置访问权限,把所有域设为 Manager(管理员)这样内核才有权访问所有内存
  • 第449行:r4 = 页表物理地址,写入 CP15 c2,告诉 MMU 页表在哪
  • 第451行:调用了__turn_mmu_on打开mmu
    在这里插入图片描述
  • 第472行:指令同步屏障
  • 第473行:根据配置设置SCTLR寄存器,打开mmu使能位,cache等
  • 第477~478行:由于mmu已经使能,因此这里可以运行虚拟地址的函数了,跳转到r13的寄存器内,即__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变量(r6)中(此地址为物理地址,使用需转换)

  • 第102~103行:把cp15的c1的寄存器的值(r0)存放到cr_alignment变量中

  • 第104行:跳转到start_kernel开始启动内核

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值