公司动态

从零构建嵌入式Linux系统镜像:U-Boot、内核与根文件系统全流程解析

📅 2026/7/30 12:15:45
从零构建嵌入式Linux系统镜像:U-Boot、内核与根文件系统全流程解析
1. 项目缘起为什么需要自己动手制作嵌入式Linux系统镜像在嵌入式开发领域尤其是基于ARM、RK3568这类SoC的项目中我们经常会遇到一个场景拿到一块全新的开发板或者自己设计的核心板上面空空如也。官方的SDK可能只提供了一个编译好的、功能庞杂的“通用”镜像或者干脆只有一个基础的U-Boot。这时候如果你想定制一个精简、高效、完全符合自己产品需求的系统或者想彻底搞明白从硬件上电到系统启动的每一个环节那么亲手从零开始制作一个能烧录到SD卡并成功启动的嵌入式Linux系统镜像就是一项绕不开的核心技能。这不仅仅是“烧录一个系统”那么简单。它意味着你对Bootloader如U-Boot、Linux内核、设备树Device Tree、根文件系统Rootfs以及它们之间的衔接关系有了完全的控制权。你可以决定内核包含哪些驱动、根文件系统里预装哪些应用、系统以何种方式挂载存储。当产品出现启动失败、驱动不匹配、存储空间不足等问题时这项技能能让你快速定位到是镜像的哪个环节出了问题而不是对着黑屏的串口输出束手无策。网络上关于“嵌入式Linux系统镜像制作”的资料很多但往往分散、步骤跳跃或者严重依赖某个特定厂商的打包工具让人知其然不知其所以然。今天我就以最通用的方式基于SD卡这一最常用的启动介质带你走一遍完整的流程。我们会从准备编译环境开始一步步构建U-Boot、内核和根文件系统最后将它们打包成一个完整的、可启动的SD卡镜像。过程中我会穿插这些年我踩过的坑和总结的经验目标是让你做完一遍后不仅能成功启动系统更能理解每个步骤背后的“为什么”。2. 环境搭建与基础概念扫盲动手之前我们需要一个稳定、高效的编译环境。对于嵌入式Linux开发强烈建议使用一台运行Linux操作系统的物理机或虚拟机。Ubuntu 20.04/22.04 LTS是社区支持最广泛的版本能避免很多因库版本差异导致的编译问题。2.1 宿主机开发环境配置首先通过SSH连接到你的Ubuntu系统更新软件源并安装一系列必要的开发工具和库。这些工具是后续编译U-Boot、内核和根文件系统的基础。sudo apt update sudo apt upgrade -y # 安装基础编译工具链 sudo apt install -y build-essential git cmake automake autoconf libtool pkg-config # 安装交叉编译工具链以ARM 32位为例这是最常见的情况 sudo apt install -y gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf # 安装设备树编译工具 sudo apt install -y device-tree-compiler # 安装文件系统制作工具 sudo apt install -y genext2fs bmap-tools u-boot-tools # 安装QEMU等模拟工具用于测试 sudo apt install -y qemu-system-arm安装完成后可以通过arm-linux-gnueabihf-gcc -v命令验证交叉编译工具链是否安装成功。这里选择gnueabihf后缀的编译器表示它针对ARM硬浮点Hard Float架构进行优化能显著提升浮点运算性能是现代ARM Cortex-A系列处理器的标配。注意不同的芯片架构可能需要不同的工具链。例如对于64位的ARMv8AArch64芯片工具链包名通常是gcc-aarch64-linux-gnu。务必根据你的目标处理器架构选择正确的工具链。如果你使用的是RK3568这类64位芯片就需要安装64位的工具链。2.2 理解SD卡镜像的物理布局一张能启动的SD卡其镜像文件并非简单地把几个文件扔进去。它有着严格的分区布局通常如下所示分区序号起始扇区 (示例)大小文件系统内容作用未分区区域08KB ~ 64KB无Boot ROM代码芯片上电后固化在CPU内部的Boot ROM会从SD卡的这个固定位置读取初始引导代码。分区1 (boot)16384 (32KB后)64MB ~ 256MBFAT32/VFATU-Boot、内核镜像(zImage/uImage)、设备树(.dtb)这是一个可以被大多数Bootloader识别和读取的分区用于存放启动所需的二进制文件。分区2 (rootfs)末尾剩余全部空间EXT4根文件系统包含Linux系统的所有目录/bin, /etc, /lib等和应用程序。为什么是这个布局前部预留空间许多SoC如Allwinner、Rockchip系列的Boot ROM要求引导代码必须存放在SD卡的固定起始扇区例如8KB偏移处。这个区域在分区表之前所以我们需要在制作镜像时先将U-Boot的SPLSecondary Program Loader写在这个位置。FAT32分区U-Boot本身通常支持读取FAT32文件系统。将内核和设备树放在一个独立的FAT分区方便我们在开发阶段通过U-Boot命令行直接替换内核或设备树文件进行测试而无需重新烧录整个镜像。EXT4根文件系统这是Linux系统运行时使用的分区EXT4在可靠性和性能上比较均衡是嵌入式系统的常见选择。理解了布局我们制作镜像的实质就是创建一个空镜像文件 - 按照上述布局进行分区 - 格式化分区 - 将编译好的文件放入对应分区。3. 三大核心组件编译详解一个可启动的Linux系统离不开Bootloader、Kernel和Rootfs这“三驾马车”。我们将分别编译它们。3.1 编译U-Boot系统的引路人U-Boot是嵌入式领域事实标准的Bootloader。它负责初始化最基本的硬件如DRAM、时钟、串口然后从存储设备SD卡、eMMC、SPI Flash加载Linux内核和设备树到内存中并跳转到内核入口点执行。首先获取U-Boot源码。建议从官方仓库或芯片厂商的Git仓库获取。git clone https://source.denx.de/u-boot/u-boot.git # 或者对于特定芯片例如RK3568从厂商仓库获取 # git clone https://github.com/rockchip-linux/u-boot.git -b master cd u-boot接下来是配置和编译。这里以模拟的vexpress_ca9x4板子ARM Versatile Express为例因为它可以在QEMU中模拟方便大家无硬件验证流程。对于真实板子如树莓派、RK3568等需要找到对应的配置文件*_defconfig。# 清理旧配置 make distclean # 加载默认配置这里以vexpress_ca9x4为例 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- vexpress_ca9x4_defconfig # 启动图形化配置界面可选用于深度定制 # make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig # 开始编译 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)编译成功后会在当前目录生成几个关键文件u-boot ELF格式的可执行文件包含调试信息。u-boot.bin 纯二进制镜像是烧录到存储设备的主要文件。u-boot.img 在某些平台如Rockchip上可能需要这个打包了特定头部信息的镜像。实操心得编译U-Boot最常遇到的错误是工具链不匹配。确保CROSS_COMPILE前缀与你安装的工具链完全一致。如果工具链是aarch64-linux-gnu-那么这里就应该是CROSS_COMPILEaarch64-linux-gnu-。另一个坑是依赖缺失如果编译报错找不到python或某些库根据错误信息安装对应的python3-dev或libssl-dev等包即可。3.2 编译Linux内核系统的核心引擎内核负责管理硬件资源、提供系统调用接口是承上启下的核心。我们同样需要为目标板配置并编译内核。获取内核源码以长期支持版本为例git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git -b linux-5.10.y cd linux配置内核。嵌入式系统通常基于一个接近目标板的默认配置进行微调。# 清理 make ARCHarm distclean # 导入默认配置这里仍以vexpress为例 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- versatile_defconfig # 进入图形化配置界面进行定制如增加驱动、裁剪模块 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig在menuconfig中你可以根据需求启用或禁用驱动和功能。对于嵌入式系统一个重要的原则是“按需裁剪”移除不需要的驱动、文件系统支持、网络协议等以减小内核体积和启动时间。配置完成后保存退出。开始编译内核和设备树make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage dtbs -j$(nproc) # 如果需要模块 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules -j$(nproc)编译产物arch/arm/boot/zImage 压缩的内核镜像是我们要使用的。arch/arm/boot/dts/*.dtb 编译生成的设备树二进制文件。设备树以一种数据结构的形式描述了板级的硬件信息如内存地址、外设寄存器地址、中断号等使得同一个内核可以支持不同的硬件平台。踩坑记录内核配置是门学问。如果你发现系统启动后找不到网卡、SD卡或串口大概率是内核配置中对应的驱动没有启用。此时需要重新执行make menuconfig在相应目录下如Device Drivers - Network device support找到并启用它。另一个常见问题是内核版本与U-Boot不兼容导致U-Boot无法引导新版内核的zImage这时可以尝试改用uImage格式make uImage或者检查U-Boot的版本是否需要更新。3.3 构建根文件系统系统的家园根文件系统是内核挂载的第一个文件系统包含了系统运行所必需的所有目录结构、命令行工具、系统配置和用户程序。构建根文件系统主要有三种方式BusyBox、Buildroot和Yocto。这里我们使用最轻量、最直接的BusyBox来构建一个最小系统。第一步编译和安装BusyBoxBusyBox集成了上百个最常用的Linux命令和工具如ls, cp, sh, mount到一个单一可执行文件中通过创建符号链接来调用不同的功能极大地节省了空间。wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 # 配置为静态编译避免依赖动态库的麻烦 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig # 在 menuconfig 中进入 Settings - Build Options选中 “Build static binary (no shared libs)” make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc) make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- install安装后会在_install目录下生成基本的bin,sbin,usr目录以及指向BusyBox的链接。第二步创建完整的根文件系统目录结构我们需要在一个单独的目录例如rootfs下构建出完整的根文件系统骨架。# 创建根文件系统目录 mkdir -p rootfs cd rootfs # 拷贝BusyBox生成的文件 cp -r /path/to/busybox-1.36.1/_install/* . # 创建必要的系统目录 mkdir -p proc sys dev tmp etc/init.d lib # 如果是动态链接还需要拷贝工具链中的动态库。静态编译则不需要此步。 # arm-linux-gnueabihf-gcc -print-sysroot # 查看工具链路径 # cp -P /usr/arm-linux-gnueabihf/lib/*.so* lib/第三步创建初始启动脚本内核启动后会尝试执行/sbin/init或/etc/init等初始化程序。我们创建一个最简单的init脚本。创建文件etc/init.d/rcS并赋予可执行权限#!/bin/sh # 挂载虚拟文件系统 mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /tmp # 创建必要的设备节点 mdev -s # 设置主机名 hostname MyEmbeddedLinux # 启动shell /bin/sh然后创建/sbin/init的符号链接指向BusyBoxcd rootfs ln -s ../bin/busybox sbin/init chmod x etc/init.d/rcS至此一个最简化的根文件系统就准备好了。它包含了基本的命令和一个可以交互的Shell。4. 镜像制作、打包与烧录全流程有了三大组件现在我们来把它们组装成一个完整的SD卡镜像文件。4.1 使用dd和fdisk创建并分区镜像文件我们创建一个大小为1GB的镜像文件sd.img。# 创建一个全零的1GB文件 dd if/dev/zero ofsd.img bs1M count1024 # 给这个文件加上一个DOS分区表 sudo fdisk sd.img在fdisk交互界面中依次输入以下命令n # 新建分区 p # 主分区 1 # 分区号1 # 起始扇区默认通常从2048开始为前面预留空间 256M # 分区大小256MB给boot分区 n # 新建第二个分区 p # 主分区 2 # 分区号2 # 起始扇区默认接在上一个分区之后 # 结束扇区默认到文件末尾即剩余所有空间 t # 更改分区类型 1 # 选择分区1 c # 类型为W95 FAT32 (LBA) w # 写入并退出现在sd.img文件内部就有了一个分区表。我们需要让系统能够识别这些分区并格式化它们。# 使用 loop 设备将镜像文件虚拟成块设备 sudo losetup -fP sd.img # 查看分配的loop设备假设是 /dev/loop0 sudo losetup -a # 让内核重新读取分区表 sudo partprobe /dev/loop0 # 格式化分区 sudo mkfs.vfat -F 32 -n boot /dev/loop0p1 sudo mkfs.ext4 -L rootfs /dev/loop0p24.2 向镜像中写入U-Boot和文件首先将U-Boot写入SD卡镜像最开始的预留位置非分区内。这步很关键决定了板子能否从SD卡启动。# 假设你的U-Boot二进制文件是 u-boot.bin # 对于很多ARM SoC需要写入到偏移64KB即65536字节处 sudo dd ifu-boot/u-boot.bin of/dev/loop0 bs512 seek64 convnotrunc这里的seek64表示跳过前64个扇区每个扇区512字节即64*51232768字节。这个偏移量因芯片而异Allwinner系列可能是8KBseek16Rockchip系列可能是32KBseek64。务必查阅你的芯片数据手册或U-Boot板级配置。接下来挂载两个分区并将文件拷贝进去。# 创建挂载点 mkdir -p mnt/boot mnt/rootfs # 挂载分区 sudo mount /dev/loop0p1 mnt/boot sudo mount /dev/loop0p2 mnt/rootfs # 拷贝内核和设备树到boot分区 sudo cp linux/arch/arm/boot/zImage mnt/boot/ sudo cp linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb mnt/boot/ # 拷贝根文件系统内容到rootfs分区 sudo cp -r rootfs/* mnt/rootfs/ # 如果是动态编译的BusyBox还需要拷贝动态库 # sudo cp -P /usr/arm-linux-gnueabihf/lib/*.so* mnt/rootfs/lib/ # 同步并卸载 sync sudo umount mnt/boot mnt/rootfs # 断开loop设备 sudo losetup -d /dev/loop04.3 使用QEMU进行启动验证无硬件调试在将镜像烧录到物理SD卡前我们可以先用QEMU进行模拟启动验证整个流程是否正确。这能节省大量反复插拔SD卡的时间。# 使用QEMU模拟ARM Versatile Express板子加载我们的镜像 qemu-system-arm -M vexpress-a9 -m 512M -kernel linux/arch/arm/boot/zImage -dtb linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -append root/dev/mmcblk0p2 rw consolettyAMA0 -sd sd.img -nographic -serial mon:stdio命令参数解析-M vexpress-a9指定机器模型需与内核和设备树匹配。-kernel和-dtb直接指定内核和设备树文件QEMU会直接加载它们到内存模拟了U-Boot的工作。这步跳过了U-Boot的测试。-append传递给内核的启动参数。root/dev/mmcblk0p2指定根文件系统在SD卡的第二个分区rw可读写consolettyAMA0指定串口控制台。-sd sd.img将我们的镜像文件作为SD卡提供给虚拟机。-nographic -serial mon:stdio将串口输出重定向到当前终端。如果一切顺利你将看到内核启动日志最后出现类似/ #的BusyBox shell提示符。恭喜你一个最基本的嵌入式Linux系统已经成功在模拟器中跑起来了4.4 烧录到物理SD卡并上电测试模拟器测试通过后就可以烧录到真实的SD卡了。请务必确认你的SD卡设备路径错误操作会清空你的硬盘# 1. 插入SD卡使用 lsblk 命令查看确认SD卡设备名例如是 /dev/sdb lsblk # 2. 卸载SD卡上所有已挂载的分区 sudo umount /dev/sdb* # 3. 将制作好的镜像直接写入SD卡此操作会清空SD卡所有数据 sudo dd ifsd.img of/dev/sdb bs4M statusprogress oflagdirect sync将SD卡插入开发板连接串口调试线到电脑使用串口终端工具如minicom,picocom,PuTTY打开对应的串口如/dev/ttyUSB0设置波特率如115200。给开发板上电你应该能在串口终端中看到U-Boot的启动信息随后是内核解压和启动的日志最终进入BusyBox shell。5. 进阶配置与深度排错指南一个能启动的系统只是起点。要让其成为一个可用的产品原型还需要进行大量配置和优化。5.1 配置U-Boot环境变量与自动启动U-Boot有自己的环境变量可以用来定义启动命令。我们可以在U-Boot命令行中设置或者编译时预设。一个典型的启动命令序列bootcmd可能是setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rootwait rw load mmc 0:1 ${kernel_addr_r} /zImage load mmc 0:1 ${fdt_addr_r} /vexpress-v2p-ca9.dtb bootz ${kernel_addr_r} - ${fdt_addr_r}bootargs传递给Linux内核的参数。load mmc 0:1 ...从SD卡mmc设备0的第一个分区FAT分区加载文件到内存地址。bootz启动zImage格式的内核。你可以将这些命令在U-Boot命令行中逐条执行也可以将它们写入bootcmd环境变量并保存实现上电自动启动。setenv bootcmd ‘上述命令序列’ saveenv5.2 内核启动参数bootargs精讲bootargs是连接U-Boot和Linux内核的桥梁至关重要。root/dev/mmcblk0p2指定根文件系统设备。mmcblk0是第一个MMC/SD设备p2是第二个分区。如果你的根文件系统在eMMC上可能是/dev/mmcblk1p2。rootwait让内核等待根设备就绪后再挂载对于慢速SD卡很有必要。rw以读写方式挂载根文件系统。consolettyAMA0,115200指定内核控制台为第一个串口波特率115200。这个ttyAMA0是内核中的设备名需要与你的板子实际串口驱动匹配。在设备树中定义。5.3 常见启动失败问题排查链路当系统无法启动时不要慌张按照以下链路排查能解决90%的问题无任何输出串口无信息检查硬件电源是否稳定串口线是否连接正确TX/RX是否接反波特率设置是否正确检查U-Boot是否写入正确确认dd命令写入的偏移量seek参数是否正确。这是最常出错的一步。用hexdump -C sd.img | head -100查看镜像开头是否有U-Boot的魔数或可读字符串。检查芯片启动模式开发板的启动拨码开关是否设置为从SD卡启动U-Boot启动后卡住无法加载内核U-Boot命令行下使用mmc list和fatls mmc 0:1命令查看是否能识别SD卡和列出boot分区下的文件。如果不能可能是SD卡兼容性或分区格式问题。检查文件路径和名称确认load命令中的文件名zImage,.dtb与boot分区中的完全一致包括大小写。检查内存地址${kernel_addr_r}等环境变量是否已正确设置可以用printenv查看。内核解压后卡住或报错最常见的错误内核恐慌Kernel Panic提示VFS: Unable to mount root fs。检查root参数确认指定的根设备分区号是否正确。检查根文件系统根文件系统是否完整是否包含了/init或/sbin/init如果是动态链接是否包含了所有必要的动态库可以在U-Boot中尝试用ext4ls mmc 0:2 /查看根文件系统内容。检查文件系统类型内核是否支持你根文件系统的格式如EXT4在内核配置File systems中确认已启用。串口无输出或乱码检查console参数中的串口设备名是否正确波特率是否与内核配置、U-Boot配置一致。5.4 从最小系统到实用系统我们构建的BusyBox系统只是一个最小外壳。一个实用的系统可能还需要网络支持在内核中启用网卡驱动在根文件系统中添加/etc/network/interfaces配置安装udhcpcBusyBox自带或dhcpcd来获取IP。永久存储环境变量为根文件系统创建/etc/profile或/etc/bash.bashrc。安装自定义应用将你的应用程序可执行文件拷贝到/usr/bin或/home目录并创建启动脚本。使用Buildroot/Yocto对于更复杂的项目建议使用Buildroot或Yocto来构建根文件系统。它们能自动解决库依赖、生成更完整的系统包括udev、systemd等并方便地集成你的软件包。制作嵌入式Linux系统镜像是一个系统工程涉及硬件、固件、内核、文件系统等多个层面。通过这次基于SD卡的手动实践我希望你不仅掌握了具体的命令和步骤更重要的是理解了各个组件之间的依赖关系和启动流程。下次当你再遇到“系统起不来”的问题时你就能像侦探一样沿着启动链路从U-Boot到内核参数再到根文件系统一步步缩小范围最终找到问题的根源。这才是这项技能最大的价值所在。