三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

嵌入式开发实战:从零开始交叉编译Linux内核(ARM架构)

嵌入式开发实战:从零开始交叉编译Linux内核(ARM架构)

1. 为什么我们需要交叉编译内核?

如果你是一个嵌入式开发者,或者正在为树莓派、路由器、NAS等ARM/MIPS架构的设备折腾系统,那么“交叉编译Linux内核”这个操作,大概率是你绕不开的一道坎。很多人第一次接触这个概念时,会觉得既神秘又麻烦:为什么不能直接在目标设备上编译?为什么需要一套专门的工具链?那些复杂的配置选项又该怎么选?

我经历过无数次这样的场景:手头有一块全新的开发板,官方提供的内核版本太旧,缺少我需要的驱动或功能;或者,我需要为特定硬件优化内核,裁剪掉不必要的模块以节省资源。直接在资源有限的嵌入式设备上编译一个完整的内核,动辄几小时甚至十几小时,而且常常因为内存不足而失败。这时,交叉编译就成了唯一的、也是最高效的路径——它允许你在性能强劲的x86开发机上,为ARM、MIPS等不同架构的设备生成可运行的内核镜像。

这个过程看似只是换了个编译器,实则涉及工具链选择、内核配置适配、设备树编译、模块处理等一系列环环相扣的步骤。任何一个环节的疏漏,都可能导致最终生成的内核无法启动。网上教程很多,但往往只给出命令,缺少背后的“为什么”,让新手照猫画虎却频频掉坑。今天,我就结合自己多年的踩坑经验,为你拆解一次完整的、细节拉满的交叉编译过程,目标是让你不仅能“做出来”,更能“弄明白”。

2. 环境准备:工具链与内核源码的“门当户对”

工欲善其事,必先利其器。交叉编译的第一步,也是最重要的一步,就是准备正确的工具链(Toolchain)和内核源码。这里的“正确”指的是架构匹配、版本兼容,绝非随便下载一个就可以。

2.1 交叉编译工具链的选择与安装

工具链的核心是交叉编译器(例如arm-linux-gnueabihf-gcc),它运行在x86主机上,但生成的二进制代码是ARM指令集的。选择工具链时,必须考虑目标设备的CPU架构、ABI(应用二进制接口)和浮点运算单元(FPU)。

  • 架构(Architecture):常见的有arm(ARMv5及以下)、armel/armhf(ARMv6/v7,硬浮点)、aarch64(ARM64)。
  • ABIgnueabi(旧式)、gnueabihf(硬浮点,性能更好)。
  • 厂商:有Linaro、ARM官方、芯片原厂(如Rockchip、Allwinner)提供的工具链,也有通用的crosstool-NG自编译工具链。

对于大多数通用的ARMv7-A设备(如树莓派2/3、许多IoT设备),我推荐使用Linaro的gcc-linaro-arm-linux-gnueabihf。它的稳定性、兼容性和社区支持都很好。

实操步骤:下载与配置

假设我们的目标设备是ARMv7-A架构,支持硬浮点(hard-float)。我们将在Ubuntu 22.04 LTS主机上进行操作。

  1. 下载工具链

    # 创建一个工作目录 mkdir -p ~/cross_compile cd ~/cross_compile # 下载 Linaro 7.5.0 版本工具链(这是一个经过长期考验的稳定版本) wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz # 解压 tar -xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz # 将工具链的bin目录加入PATH环境变量,方便直接调用 echo 'export PATH=$PATH:~/cross_compile/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin' >> ~/.bashrc source ~/.bashrc
  2. 验证安装

    arm-linux-gnueabihf-gcc --version

    如果成功输出gcc版本信息,且前缀是arm-linux-gnueabihf,说明工具链已就位。

注意:永远不要使用主机系统自带的gcc来编译给其他架构用的内核,那会导致编译出的内核根本无法在目标设备上运行。这是新手最容易犯的致命错误。

2.2 获取目标设备对应的内核源码

内核源码的版本和配置必须尽可能贴近你的目标设备。最理想的来源是设备制造商或社区维护的Git仓库(如树莓派的github.com/raspberrypi/linux)。如果找不到,则使用主线内核(kernel.org),但可能需要自己移植驱动和设备树。

  1. 获取源码(以树莓派4B,即BCM2711平台为例):

    cd ~/cross_compile git clone --depth=1 https://github.com/raspberrypi/linux.git -b rpi-5.15.y cd linux

    这里--depth=1只克隆最新提交,节省时间和空间。-b rpi-5.15.y指定了树莓派维护的5.15长期支持版本分支,稳定性好。

  2. 源码结构初窥: 进入linux目录,你会看到arch/,drivers/,include/等目录。arch/目录下存放着不同CPU架构的代码,我们即将用到的交叉编译配置就在这里。

3. 内核配置:从默认配置到精细化定制

内核配置(.config文件)决定了哪些功能、驱动、协议会被编译进内核或编译为模块。这是交叉编译中最具技术含量、也最容易出错的环节。

3.1 应用默认配置与指定架构

大多数嵌入式平台都提供了默认的配置文件(defconfig),这是一个极好的起点。

# 确保当前在linux源码根目录 cd ~/cross_compile/linux # 关键步骤:指定架构和交叉编译器前缀 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- # 应用树莓派4B的默认配置 make bcm2711_defconfig
  • ARCH=arm:告诉make系统,我们要编译的是ARM架构的内核。
  • CROSS_COMPILE=arm-linux-gnueabihf-:告诉make系统,我们使用的交叉编译器前缀是什么。make在调用gcc、ld等工具时,会自动在前面加上这个前缀(例如arm-linux-gnueabihf-gcc)。
  • make bcm2711_defconfig:将arch/arm/configs/bcm2711_defconfig这个预设配置文件复制到源码根目录,并命名为.config

执行成功后,会生成.config文件。千万不要直接编辑这个文本文件,内容过于庞杂且容易出错。

3.2 使用menuconfig进行可视化配置

接下来,我们需要一个图形化界面来查看和修改配置。最常用的是menuconfig

# 继续使用之前设置的ARCH和CROSS_COMPILE环境变量 make menuconfig

如果系统提示缺少libncurses5-dev等库,请先安装:

sudo apt-get install libncurses5-dev libncursesw5-dev

执行make menuconfig后,会进入一个基于终端的菜单界面。你可以使用方向键导航,空格键选中/取消选中([*]表示编译进内核,[M]表示编译为模块,[ ]表示不编译),/键搜索配置项。

几个关键的配置区域与实战心得:

  1. General setup -> Local version:可以在这里添加自定义版本后缀,如-mycustom-rpi4。这样编译出的内核版本号会包含这个信息,便于区分。
  2. Device Drivers:这是驱动的大本营。你需要根据目标设备的实际硬件来启用或禁用驱动。
    • 网络设备:如果你的设备有有线网卡(如SMSC LAN9514)或无线网卡(如Broadcom BCM43455),需要在这里找到并启用对应驱动(编译进内核或模块)。
    • USB支持USB support是必须的。根据设备上的USB控制器类型(如DWC2、DWC3)选择驱动。
    • 存储设备:SD/MMC、SATA、NVMe等控制器驱动。
    • 图形显示:对于树莓派,DRM驱动下的VC4驱动是关键。
    • 实战心得“宁缺毋滥,按需启用”。对于嵌入式设备,不必要的驱动会增大内核体积,延长启动时间,甚至可能引起冲突。如果你不确定某个驱动是否需要,可以先保持为模块([M]),这样它不会增大内核镜像(zImage)的体积,可以在系统启动后根据需要动态加载。
  3. File systems:选择你根文件系统使用的格式。例如,如果根文件系统是ext4,则需要启用ext4支持。如果使用了squashfs只读文件系统,也需要启用。
  4. Kernel Features:这里可以配置内核的通用行为。对于嵌入式设备,有时需要启用Preemption ModelPreemptible Kernel以获得更好的实时性,或者调整Timer frequency
  5. Boot options:可以设置默认的启动命令行参数 (CONFIG_CMDLINE)。例如,为树莓派指定串口控制台:console=ttyAMA0,115200

配置完成后,选择<Save>保存到.config文件,然后退出。

重要提示menuconfig会自动处理依赖关系。如果你启用了一个需要其他功能支持的选项,它会自动帮你选中依赖项。反之,如果你禁用了一个被其他选项依赖的功能,依赖它的选项也会被自动禁用。这是手动编辑.config文件无法做到的,也是推荐使用menuconfig的主要原因。

4. 编译过程详解:内核镜像、模块与设备树

配置完成后,就进入了编译阶段。这个过程会消耗大量CPU和内存资源,但好在是在高性能主机上运行。

4.1 启动编译并理解输出产物

使用-j参数指定并行编译的作业数,通常设置为CPU核心数的1到2倍,以充分利用多核性能。

# 开始编译内核镜像、模块和设备树 make -j$(nproc)

$(nproc)会自动获取你主机的CPU核心数。

编译过程会持续一段时间(取决于主机性能和内核配置的复杂度)。期间,控制台会输出大量信息。编译成功后,在源码目录下会生成几个关键文件:

  • arch/arm/boot/zImage:这是压缩后的内核镜像,是引导加载程序(如U-Boot)最终要加载并执行的核心文件。对于ARM架构,它通常就是我们要的最终内核文件。
  • arch/arm/boot/dts/目录:这里存放着编译好的设备树二进制文件(.dtb)。设备树(Device Tree)是一种描述硬件资源的数据结构,用于将硬件的详细信息传递给内核,替代了旧式架构中冗杂的硬编码。你的目标设备(如bcm2711-rpi-4-b.dtb)对应的.dtb文件就在这里。
  • modules:所有标记为[M]的驱动,会被编译成.ko文件。它们位于源码树的各个子目录中,需要被安装到指定位置。

4.2 安装内核模块到临时目录

内核模块不能随意放置。我们需要将它们安装到一个临时的目录结构中,这个结构应该与目标设备上根文件系统的目录结构一致。

# 创建一个临时目录作为模块的安装根目录 mkdir -p ~/cross_compile/modules_output # 安装模块,INSTALL_MOD_PATH指定了安装的“根目录” make modules_install INSTALL_MOD_PATH=~/cross_compile/modules_output

执行后,模块会被安装到~/cross_compile/modules_output/lib/modules/<kernel-version>/目录下。这里的<kernel-version>就是你编译的内核版本号(包含之前设置的local version)。

关键检查点: 进入该目录,使用file命令检查一个.ko文件:

file ~/cross_compile/modules_output/lib/modules/5.15.84-v8+/kernel/drivers/usb/host/xhci-plat-hcd.ko

你应该看到类似ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=..., not stripped的输出。特别注意ARM这个字段,这确认了它是为ARM架构编译的模块。如果这里显示的是x86-64,那说明之前的ARCHCROSS_COMPILE设置有问题,编译出来的是错误架构的模块。

5. 部署与测试:将编译产物放到目标设备

编译完成只是成功了一半,将正确的文件放到目标设备的正确位置,并成功启动,才是最终的胜利。

5.1 准备SD卡与分区

假设目标设备使用SD卡启动。将SD卡通过读卡器连接到主机。

  1. 识别SD卡设备:使用lsblksudo fdisk -l命令,确认SD卡对应的设备名(如/dev/sdb)。务必谨慎,选错设备会格式化你的硬盘!
  2. 分区结构:典型的树莓派SD卡有两个分区:
    • 第一个分区 (boot):FAT32格式,通常较小(256MB足够),存放引导文件(kernel.img/zImage,*.dtb,config.txt,cmdline.txt等)。
    • 第二个分区 (rootfs):ext4格式,存放根文件系统。

5.2 拷贝内核与设备树到boot分区

挂载SD卡的boot分区(假设挂载到/mnt/boot)。

sudo mount /dev/sdb1 /mnt/boot
  1. 备份原内核:先将SD卡上原有的内核镜像(如kernel.img,kernel7.img,kernel7l.img等)备份。

    cd /mnt/boot sudo mv kernel7l.img kernel7l.img.backup

    (对于树莓派4B,默认内核镜像名是kernel7l.img

  2. 拷贝新内核:将我们编译好的zImage拷贝过去,并重命名为目标设备期望的名字。

    sudo cp ~/cross_compile/linux/arch/arm/boot/zImage /mnt/boot/kernel7l.img
  3. 拷贝设备树:将对应设备的.dtb文件拷贝到boot分区。

    sudo cp ~/cross_compile/linux/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb /mnt/boot/

    注意:设备树文件必须与硬件完全匹配。树莓派4B就用bcm2711-rpi-4-b.dtb,树莓派3B+就用bcm2837-rpi-3-b-plus.dtb。用错了会导致内核无法识别硬件而启动失败。

5.3 拷贝内核模块到rootfs分区

挂载SD卡的rootfs分区(假设挂载到/mnt/rootfs)。

sudo mount /dev/sdb2 /mnt/rootfs

将之前安装好的模块目录,整个拷贝到目标根文件系统的/lib/modules下。

sudo cp -r ~/cross_compile/modules_output/lib/modules/* /mnt/rootfs/lib/modules/

权限与所有权:由于是使用sudo拷贝的,文件的权限和所有权通常是正确的(root)。但为了保险,可以检查一下:

sudo chown -R root:root /mnt/rootfs/lib/modules/*

5.4 卸载与上电测试

sudo umount /mnt/boot sudo umount /mnt/rootfs

将SD卡插入目标设备,上电启动。如果一切顺利,你应该能看到内核启动日志(如果连接了串口或屏幕)。使用uname -a命令可以验证运行的内核版本是否与你编译的版本(包含local version)一致。

6. 排错指南:当内核无法启动时

即使步骤再详细,第一次尝试也难免遇到问题。内核启动失败通常表现为黑屏、卡在某个日志信息、或者反复重启。以下是系统的排查思路:

  1. 检查串口输出(最有效):绝大多数嵌入式开发板都预留了UART串口。通过USB转TTL串口线连接主机的串口终端(如minicom,picocom,screen),在启动瞬间就能看到最原始的引导和内核日志。错误信息会直接打印在这里,这是定位问题的黄金通道。常见的错误有:

    • Failed to execute /init:通常是根文件系统挂载失败,检查cmdline.txt中的root=参数是否正确指定了根分区(如root=/dev/mmcblk0p2),以及内核是否支持该文件系统类型(如ext4)。
    • Error loading device treeBad device tree:设备树文件错误、不匹配或损坏。确认拷贝的.dtb文件是否正确,并尝试用dtc(设备树编译器)反编译检查:dtc -I dtb -O dts bcm2711-rpi-4-b.dtb > test.dts
    • Kernel panic - not syncing: VFS: Unable to mount root fs:根文件系统问题。除了上述root=参数,还要检查内核配置中是否启用了对应的文件系统驱动、块设备驱动(如MMC/SD)以及必要的SCSI或USB存储驱动(如果根文件系统在USB设备上)。
  2. 验证交叉编译工具链:再次确认ARCHCROSS_COMPILE环境变量在整个配置和编译过程中都正确设置。一个快速验证方法是去检查任意一个编译出的.o.ko文件:

    file ~/cross_compile/linux/vmlinux # 查看未压缩的内核文件架构

    输出应明确显示为ARM

  3. 简化配置:如果你在默认配置的基础上做了大量修改后启动失败,可以尝试回溯。先用make bcm2711_defconfig恢复默认配置,直接编译并测试这个“纯净版”内核能否启动。如果能,说明问题出在你后续的修改上。然后,通过make menuconfig一项项地、小批量地启用你需要的功能,每做一次修改就编译测试一次,以定位是哪个具体的配置项导致了问题。

  4. 检查依赖与头文件:确保主机系统安装了必要的开发库。除了之前提到的libncurses,有时还需要libssl-dev(用于内核模块签名)、flexbison等。编译过程中的错误信息通常会提示缺少什么。

交叉编译内核是一个需要耐心和细致观察的过程。每一次失败的信息都是通往成功的路标。当你亲手编译的内核在目标设备上成功跑起来,并完美驱动了所有硬件时,那种成就感是对所有繁琐步骤的最佳回报。记住这个流程的核心:正确的工具链、适配的配置、完整的产物部署。掌握了这三点,你就掌握了为任意嵌入式设备定制内核的钥匙。

← 返回列表