文章目录
-
- 前言
- 一、内核的编译与剪裁
-
- [1.1 相关概念](#1.1 相关概念)
- [1.2 软件安装(lzop)](#1.2 软件安装(lzop))
- [1.3 编译流程](#1.3 编译流程)
- 二、uboot之后的Linux内核启动流程
-
- [2.1 Linux内核启动的前置条件](#2.1 Linux内核启动的前置条件)
- [2.2 Linux内核启动流程](#2.2 Linux内核启动流程)
- 三、根文件系统
-
- [3.1 正点原子根文件系统代码阅读](#3.1 正点原子根文件系统代码阅读)
- [3.2 制作自己的根文件系统](#3.2 制作自己的根文件系统)
- [四、Linux 完整启动流程](#四、Linux 完整启动流程)

前言
在上篇文章IMX6ULL Linux系统移植】SD卡分区 | uboot详解中,在使用SD卡进行系统移植情况下,我们完成了对SD卡分区的了解,并且对uboot的启动流程有了一定的掌握。在这篇文章中,我们将学习内核的编译与剪裁,根文件系统rootfs,以及对Linux系统启动流程的完整梳理。
一、内核的编译与剪裁
1.1 相关概念
内核是操作系统的核心功能代码,是一个大型的开源项目,在嵌入式设备中,不需要这么多的功能,并且也装不下这么大的系统,因此,我们需要对这个源码进行裁剪、编译。我们使用的内核版本为Linux4.1.15。
1.2 软件安装(lzop)
lzop 是基于 LZO 算法的高速无损压缩程序。
用途:
✅ Linux 内核编译:生成 zImage 内核镜像,很多嵌入式内核配置依赖 lzop,缺少它编译会报错。
✅ 嵌入式开发、bootloader 加载 lzo 压缩的内核。
1.3 编译流程
-
把linux内核复制到虚拟机,进行解压操作:
tar -zxvf linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek.tar.gz -
进入内核文件夹:
cd linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/ -
使用编译命令进行编译
cmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_emmc_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig #内核地图,驱动,模块和c文件的关系 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean:
大清理,清除编译产生的中间文件,以及执行文件等;make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_emmc_defconfig:
生成.config,内核编译的核心配置文件;make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig:
图形化配置界面(终端窗口),用来修改内核配置 ,如图所示,

在这里面进行参与编译文件的选择就称之为内核的裁剪。make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16:
编译全套产物:zImage 内核镜像 + 设备树 dtb + 内核模块 ko
-j16:多线程编译,16 个任务并行,数字一般填 CPU 核心数
-
生成结果
结果文件主要为2个必备文件以及一些.ko文件:- 内核镜像文件: arch/arm/boot/zImage
- 设备树文件: arch/arm/boot/dts/imx6ull-alientek-emmc.dtb文件
- 相关的驱动文件 *.ko
-
将编译好的两个系统相关的文件拷贝到tftp管理目录下
-
重新启动开发板,每次从Ubuntu管理目录下载最新版本的内核和设备树文件
二、uboot之后的Linux内核启动流程
U-Boot 是引导加载程序(Bootloader),负责硬件初始化,找到并把 Linux 内核加载到内存,最后启动内核;内核接管之后,U-Boot 就退出,不再运行。
2.1 Linux内核启动的前置条件
-
MMU 必须关闭
U-Boot 跳转内核瞬间,不能开启 MMU 。- MMU 一开就是虚拟地址,而内核刚入口
stext汇编是按照物理地址运行的; - 内核
head.S里面会自己去初始化页表、由内核自己打开 MMU; - 如果 U-Boot 带着 MMU 直接跳过去,地址翻译错乱,内核直接崩溃。
- MMU 一开就是虚拟地址,而内核刚入口
-
D-cache(数据缓存)必须关闭
D-cache 是读写内存用的缓存。- 如果 D-cache 开启,缓存中的脏数据和真实 DDR 内存不一致;
- 内核初始化内存、BSS 清零、操作设备树的时候,会读到脏缓存,引发随机 bug。
-
I-Cache(指令缓存)无所谓
I-cache 只缓存代码指令。- 打开 I-cache 只是加速取指令,不会影响数据一致性;
- 内核
stext汇编里会自行处理 I-cache,所以 U-Boot 开或者关都可以。
-
r0 = 0
ARM Linux 启动 ABI 强制约定,r0 必须置 0,是历史 ABI 保留项。 -
r1 = machine nr(机器 ID)
现在设备树 dtb 方式下,这个 machine nr可以填 - 1,内核会忽略 r1,直接解析 r2 里的 dtb。IMX6ULL 就是用 dtb 方式。 -
r2 = atags /dtb 首地址
- ATAGS:老式,把板载信息(内存大小、串口、根分区)打包成标签列表,放在内存,r2 指向这个 atags 起始地址;
- DTB 设备树:现代方案(IMX6ULL 用这个),r2 存放 dtb 在内存中的物理起始地址。
内核 stext 汇编拿到 r2,后续 C 代码
start_kernel就会解析这个 dtb,获取硬件信息。
2.2 Linux内核启动流程
接下来就是Linux启动流程了。与uboot的流程类似,我们可以通过lds链接脚本来对linux启动的流程进行梳理:
打开arch/arm/kernel/vmlinux.lds:

[_stext, _etext].text代码段:内核可执行指令(所有内核函数代码),只读,运行期间永久保留,不会释放。[_sdata, _edata].data数据段:已经初始化的全局变量,可读可写,内核全程使用。
我们跟进stext去看Linux内核相关的代码,刚从 U-Boot 跳过来的时候,CPU 还处在裸机环境,C 语言根本跑不起来。必须先用汇编stext做最底层的硬件 / 寄存器环境搭建,环境就绪后,才能进入 C 代码环境。ENTRY(stext)就是标记这一段汇编是内核的入口,因此我们使用grep -Rin 'ENTRY(stext)'指令来找到这个入口:

head.S:带 MMU 的内核启动汇编(IMX6ULL 用这个)head-nommu.S:无 MMU 版本,不使用
我们进入head.S:

往下翻,我们可以看到带有注释的一段代码:

这段代码作用是:先保存 MMU 开启后的目标入口__mmap_switched,设置返回地址,把临时页表基址存入 r8;然后从proc_info结构体取出当前 CPU 对应的__cpu_flush函数地址并跳转。__cpu_flush负责配置 TTBR 页表寄存器、开启 MMU 和 Cache;MMU 打开完成后返回,继续执行__mmap_switched,完成 BSS 清零、准备 C 栈,最终调用start_kernel:

此时,我们要再跟入会发现start_kernel不再此文件夹了,因为start_kernel就是大量的初始化,所以,它位于linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/init目录下,去此目录下查找:



翻到start_kernel最好发现执行的函数为rest_init(),跟进去:

rest_init()创建了 PID=1 的 kernel_init(用户态 init 进程前身)和内核线程管家 kthreadd(PID=2),然后主线程变成 idle 空闲线程,进入 cpu idle 循环。

kernel_init 是 PID1 的入口,负责做完内核最后的初始化,释放 init 段内存。到此,Linux内核就启动起来了。最后,还需去挂载根文件系统、启动用户态 init 程序。
三、根文件系统
根文件系统是Linux操作系统组成的基础,是内核启动时所要挂载的第一个文件系统,我们的一些配置文件、库函数、系统命令都保存在文件系统中,同时也可以通过文件系统实现用户与操作系统功能的交互。
由于我们在开发阶段使用第三种系统烧录的方法(uboot烧写到SD卡 + zImage通过tftp下载 +rootfs通过nfs挂载)比较便捷,因此,需要将rootfs复制到nfs目录下:
c
cd nfs
mkdir rootfs
sudo tar -xvf rootfs.tar.bz2 #解压的时候, 加上sudo, 不然可能报错
因为需要从上位机中通过nfs下载根文件系统,因此,在交互界面中一定要确认下载地址以及ip等信息:

除此以外,还要确认ubuntu中nfs设置正确:

在完成上述操作后,开发板就可以成功从ubuntu中下载rootfs了。
3.1 正点原子根文件系统代码阅读
在Linux内核启动后,就是加载rootfs根系统文件了:

- 蓝色(内核侧):内核启动 → 加载 rootfs → 执行 linuxrc。linuxrc 本质上就是 init 进程,是系统里第一个用户态进程(PID=1),是内核与用户空间的分界点。
- 青色(用户空间初始化):
- /etc/inittab 是 init 的"配置清单",决定要跑哪些启动脚本、重启脚本、以及按下 ctrl+alt+del 时执行什么;
- /etc/init.d/rcS 是其中最核心的启动脚本,负责设置环境变量和系统参数;
- mount -a 依据 /etc/fstab 把 sd、flash 等剩余文件系统全部挂载上;
- 最后执行 /etc/profile 配置好用户环境。
- 终点:运行 bash shell,进入交互模式,用户可以输入命令。
3.2 制作自己的根文件系统
根文件系统的制作常见有如下几种: busybox构建根文件系统、 Buildroot构建根文件系统、Yocto构建根文件系统。
这里采用busybox构建根文件系统,其特点是易于构建简单的根文件系统,而且文件系统占用空间小,但是需要自己配置库文件,易于理解移植过程。
操作步骤:
- 将源码拷贝到Ubuntu主机下。(从busybox官网下载源码)

- 然后将其解压
tar -vxjf busybox-1.29.0.tar.bz2 - 修改busybox顶层的Makfile
- 先进入Makefile
cd busybox-1.29.0/
vim Makefile - 修改内容:
CROSS_COMPILE ?=------>CROSS_COMPILE ?= arm-linux-gnueabihf-
ARCH ?= $(SUBARCH)------>ARCH ?= arm
- 先进入Makefile

-
配置busybox,进入menuconfig图形界面
make defconfig //使用默认配置make menuconfig //打开图形化配置 -
通过图形界面配置busybox工具
配置路径如下:
cLocation: -> Settings -> Build static binary (no shared libs)
cLocation: -> Settings -> vi-style line editing commands//选中
cLocation: -> Linux Module Utilities -> Simplified modutils
cLocation: -> Linux System Utilities -> mdev (16 kb) //确保下面的全部选中, 默认都是选中的
-
创建一个用来存放生成根文件系统的目录,并编译busybox
cmkdir /home/linux/nfs/rootfs //创建存放根文件系统的目录 make //编译 make install CONFIG_PREFIX=/home/linux/nfs/rootfs //安装 ls -l /home/linux/nfs/rootfs //查看根文件系统是否创建成功
-
此时的文件系统还缺少了一些运行的库文件,需要将工具链中的libc库拷贝过去。
cd /home/linux/nfs/rootfsmkdir libcp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/libc/lib/*so* /home/linux/nfs/rootfs/lib/ -dcp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/libc/lib/*.a /home/linux/nfs/rootfs/lib/ -d
-
将符号链接库删除,并拷贝真实的库文件
rm /home/linux/nfs/rootfs/lib/ld-linux-armhf.so.3cp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/libc/lib/ld-linux-armhf.so.3 /home/linux/nfs/rootfs/lib
-
将工具链下的lib库拷贝过去
cp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/lib/*so* /home/linux/nfs/rootfs/lib/ -dcp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/lib/*.a /home/linux/nfs/rootfs/lib/ -d
-
将工具链的usr/lib库拷贝过去
mkdir /home/linux/nfs/rootfs/usr/lib -pcp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/libc/usr/lib/*so* /home/linux/nfs/rootfs/usr/lib -dcp ~/tools/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/armlinuxgnueabihf/libc/usr/lib/*.a /home/linux/nfs/rootfs/usr/lib -d
-
进入根目录文件系统查看这两个目录的大小
ccd /home/linux/nfs/rootfs du ./lib ./usr/lib -sh
与上图所示大小一样即为正确。
-
创建其余文件夹
cmkdir dev mkdir proc mkdir mnt mkdir sys mkdir tmp mkdir root mkdir lib/modules -
我们还需要完善下根我们系统中的配置文件,这些配置文件可以进一步完善Linux系统中的功能
-
创建/etc/init.d/rcS文件,并将文件修改为如下内容,该文件是一个shell脚本, Linux内核启动后需要启动的一些服务都可以通过该脚本进行配置。
cmkdir ~/nfs/rootfs/etc/init.d -p touch ~/nfs/rootfs/etc/init.d/rcS vim ~/nfs/rootfs/etc/init.d/rcS修改文件内容如下:
c#!/bin/sh PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib export PATH LD_LIBRARY_PATH mount -a mkdir /dev/pts mount -t devpts devpts /dev/pts echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s -
修改文件权限
chmod 0777 ~/nfs/rootfs/etc/init.d/rcS -
创建/etc/fstab文件,并将文件修改为如下内容,在rcS脚本中执行mount -a,挂载所有文件系统,就会找到fstab文件,并进行文件挂载。
ctouch ~/nfs/rootfs/etc/fstab vim ~/nfs/rootfs/etc/fstab修改内容如下:
c#<file system> <mount point> <type> <options> <dump> <pass> proc /proc proc defaults 0 0 tmpfs /tmp tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0字段 作用 file system 待挂载设备 / 伪文件系统名称,可以是块设备 /dev/mmcblk0p2,也可以是内核虚拟文件标识proc、sysfsmount point 挂载点目录,目录必须提前创建好,否则 mount -a会报错type 文件系统类型: proc/sysfs/tmpfs/ext4等options 挂载选项, defaults等价于rw,suid,dev,exec,auto,nouser,asyncdump dump 备份工具标记;嵌入式统一写 0,代表不做备份pass fsck 开机磁盘检查;嵌入式统一写 0,代表开机不做磁盘自检 -
创建/etc/inittab文件,该文件指定启动后一些代码的运行方式。
ctouch ~/nfs/rootfs/etc/inittab vim ~/nfs/rootfs/etc/inittab修改文件内容如下:
c#etc/inittab ::sysinit:/etc/init.d/rcS console::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r ::shutdown:/sbin/swapoff -a内容的格式为:
c<id>:<runlevels>:<action>:<process> <id>:每个指令的标识符, 不能重复。 对于busybox而言<id>用来指定启动进程的控制tty, 一般 我们将串口或者LCD屏幕设置为控制tty。 <runlevels>:对busybox来说此项说明完成没用 <action>: 动作, 用于指定<process>可能使用到的动作。 如下图所示 <process>:具体的动作, 如程序、 脚本、 命令等。action动作可配置为如下参数:

-
-
接下来就可以通过nfs挂载测试了。
四、Linux 完整启动流程

- bootROM 阶段
IMX6ULL 平台在芯片 0x0 地址接了 96K bootROM,里面是厂商固化的启动代码,上电启动后触发复位异常
根据用户设置的启动方式,将 EMMC 或 SD 卡存储器的前 4K 代码拷贝到 SRAM 中执行,其中包括:IVT 镜像入口地址(0X877FF400,0x8780 0000 -3K)、DCD(DDR 基础初始化代码)、BootData(镜像加载地址及大小)
根据 DCD 中的内容完成对 DDR 的基础初始化
根据 BootData 将 SPL 加载到 OCRAM 中(on-chip ram)
- SPL 阶段
在 OCRAM 中运行 SPL 代码
对 DDR 做深度初始化
将完整的 uboot 拷贝到 0x87800000 地址
重新定位链接地址(board_init_f → relocate_code, relocate_vectors)
- uboot 阶段
重新设置异常向量表,执行 reset 复位异常
运行 uboot 完成对硬件设备初始化
与用户交互,执行 bootcmd 加载内核
- 内核阶段
运行内核代码
- 文件系统阶段
执行 init 进程 → 启动配置文件 /etc/inittab → 运行 /etc/init.d/rcS 脚本 → 挂载 /etc/fstab 文件 → 启动终端