ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

交叉编译Linux内核详解:从工具链搭建到ARM板部署

交叉编译Linux内核详解:从工具链搭建到ARM板部署 每个人玩嵌入式或者学习 Linux 内核迟早都会撞上“交叉编译”这堵墙。明明在 PC 上编译一个 hello world 顺手得很可一旦换成 Linux 内核各种 ARCH、CROSS_COMPILE、工具链路径的问题能让老手也挠头。这篇文章就围绕“交叉编译环境下对 linux 内核编译”这个核心主题把我自己从踩坑到理顺的全过程、关键参数、容易翻车的地方一次讲透。适合正在学习嵌入式 Linux、准备给 ARM 开发板比如 orangepi 这类板子编译内核、或者想在 Ubuntu 虚拟机里搭一套交叉编译环境的同学参考。很多人在刚开始接触交叉编译时脑子里最大的问号是我的电脑是 x86_64 架构目标板是 ARM 架构两边指令集都不一样凭什么 PC 上编出来的内核镜像能直接在 ARM 板上跑这就要从交叉编译的本质说起。我们平时在本机编译本机运行的程序叫“本地编译”而在 x86 主机上编译出 ARM 目标平台能运行的程序就叫“交叉编译”。这个“交叉”体现在编译工具链的运行架构与目标架构不一致上所以工具链的安装与配置就成了整个内核编译工作的第一道关卡。1. 先搞清楚交叉编译这件事1.1 为什么不能直接在板子上编译内核很多人一开始会有个很朴素的想法既然内核最终要在 ARM 板上跑那我在板子上直接编译不就行了理论上确实可以前提是你的板子性能足够强、存储空间足够大、编译依赖的软件环境足够完整。但实际开发中ARM 板往往内存只有 1GB 到 4GBCPU 主频也就 1GHz 上下编译一次完整的内核动辄半个小时甚至更久再加上磁盘空间紧张很容易在中途把板子搞到卡死。在 x86 的 PC 或服务器上交叉编译则可以充分借助高性能 CPU 和多核并行编译把编译时间压缩到几分钟。另外开发机的调试、配置、版本管理也都比在板子上操作方便得多。交叉编译的核心价值就在这里它在“开发环境”和“运行环境”之间搭了一座桥让你用最高效的方式产出目标平台需要的二进制产物。1.2 交叉编译工具链是什么由哪些部分组成交叉编译工具链不是单个程序而是一整套工具的组合。以 ARM 交叉编译为例最常见的工具链前缀是arm-linux-gnueabihf-这里面每个部分都有自己的分工。arm-linux-gnueabihf-gccC 语言交叉编译器负责把源码编译成 ARM 指令集的汇编与机器码。arm-linux-gnueabihf-ld链接器把多个目标文件和库文件链接成最终的 ELF 可执行文件或内核镜像。arm-linux-gnueabihf-objcopy格式转换工具用于把 ELF 文件转换成二进制镜像比如把内核的 vmlinux 转成 zImage 的过程中就会用到。arm-linux-gnueabihf-objdump反汇编与查看目标文件信息的工具调试内核时常用。arm-linux-gnueabihf-gdb远程调试器配合开发板上的 gdbserver 做内核或应用的源码级调试。工具链名字里的“gnueabihf”表示使用的是 glibc 库并且支持硬件浮点hf hard float。如果目标平台不支持硬件浮点那就要选择arm-linux-gnueabi软浮点用错的话编译出来的程序可能在执行时直接报非法指令。1.3 为什么要用交叉编译工具链而不是普通 gcc这个问题其实藏着一个很深的坑。普通 gcc 是严格按照你当前系统的架构生成机器码的在 x86 的 Ubuntu 上执行gcc test.c -o test得到的test文件用file命令查看会显示 “ELF 64-bit LSB executable, x86-64”这个文件拷贝到 ARM 板子上系统根本无法识别。交叉编译工具链的 gcc 则不同它自身虽然运行在 x86 主机上但生成的目标文件是针对 ARM 平台的。用交叉编译器编译同一个test.c生成的test文件会显示 “ELF 32-bit LSB executable, ARM, EABI5”放到板子上才能执行。这里 “交叉” 两个字的含义就在这里工具的宿主架构和目标架构是分开的。2. 搭建交叉编译环境的完整指南2.1 两种主流的工具链获取方式获取交叉编译工具链有两种主流方式适用场景不同可以按需选择。第一种是直接用发行版软件源里自带的工具链比如 Ubuntu 里执行sudo apt install gcc-arm-linux-gnueabihf就能装好。这种方式最大的优点是省事缺点也很明显版本可能偏旧但大部分内核版本都能兼容而且依赖库处理得很完整。如果你只是日常学习、验证一个内核配置完全够用。第二种是去 ARM 官方或者 Linaro 等渠道下载预编译工具链。这类工具链版本更新、优化更好但使用前往往需要手动设置环境变量比如export PATH/opt/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/bin:$PATH。对于正式项目、量产环境我更推荐这种因为可复现性更好团队成员可以统一版本。2.2 在 Ubuntu 虚拟机里安装工具链的完整步骤这里以 Ubuntu 20.04 虚拟机为例演示完整安装过程。我自己的习惯是先建一个工作目录把所有交叉编译相关的东西集中放在一起方便后面清理和管理。# 第一步更新软件源装一些后续编译需要用到的依赖 sudo apt update sudo apt install -y build-essential bc bison flex libssl-dev libncurses5-dev # 第二步安装 ARM 交叉编译工具链 sudo apt install -y gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf # 第三步验证工具链是否安装成功 arm-linux-gnueabihf-gcc -v看到最后一条命令输出中带有Target: arm-linux-gnueabihf的字段说明工具链已经正常工作。第二步中安装binutils-arm-linux-gnueabihf这一点很多人会忽略它提供的就是ld、objcopy这些配套工具缺了它们后面编译内核时会出现找不到链接器之类的错误。2.3 工具链命名规则与 PATH 配置交叉编译工具链的程序名都带一个前缀这个前缀就是工具链的目标平台描述。arm-linux-gnueabihf-gcc中的arm是目标架构linux是目标系统类型gnueabihf是 C 库类型和浮点 ABI。内核编译时Makefile 会根据你传的CROSS_COMPILE参数去拼接完整的命令名。如果工具链不是装在系统标准路径下编译内核前一定要把它加入 PATH否则会出现arm-linux-gnueabihf-gcc: command not found。我通常会把这类环境变量写进~/.bashrc里避免每次开终端都要重新导出。export PATH/opt/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/bin:$PATH export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-这里的ARCH和CROSS_COMPILE是内核编译最重要的两个变量提前导成环境变量之后后面执行make的时候就可以少敲参数也不容易漏写。不过为了严谨我还是建议在具体编译每个目标时都显式传入它们后面章节会展示完整的写法。3. 内核源码的下载与准备工作3.1 怎么选择合适的内核版本内核版本的选择取决于你的目标平台和业务需求。如果使用的是厂商提供的开发板直接使用厂商维护的 BSP 内核是最稳妥的因为驱动、设备树都已经适配好了。如果是跟着社区主线学习就选择最新的 LTS长期支持版本比如 5.10、6.1 这些。我个人不太建议初学者一上来就追最新版。LTS 版本经过长时间验证资料多、坑少社区遇到问题的人多更容易搜到解决方案。最新版本往往某些驱动接口改了使用旧配置时会出现编译不过的情况对新手来说排查起来会比较痛苦。3.2 下载和解压源码从内核官网kernel.org或者国内的镜像站下载源码包这里以 linux-6.1.28 为例。cd ~/work wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.28.tar.xz tar -xJf linux-6.1.28.tar.xz cd linux-6.1.28解压之后可以先看一下目录结构。arch/目录下面有各个架构相关的代码x86、arm、arm64、riscv 都在这里drivers/是所有设备驱动的源码Documentation/里有很多硬核资料遇到不懂的配置项时查这里比搜索引擎还靠谱。对交叉编译来说arch/arm/和arch/arm64/是重点关注的目录。3.3 确认源码目录干净如果你下载的是 tar.xz 包解压出来的源码目录都是干净的不需要额外处理。但如果你拿到的是别人的代码仓库或者之前已经编译过建议先执行一次make mrproper把所有的历史编译产物和配置文件清掉避免残留的.config影响到这次编译。make mrproper这一条命令会把.config、include/config、各个子目录里的.o文件全部清理干净。特别是.config文件它记录了之前的内核配置旧配置和新源码不匹配时编译会报各种莫名其妙的错误“找不到未定义的类型”“宏未定义”之类的。养成编译前mrproper的习惯可以帮你排除掉一大类隐形问题。4. 内核配置阶段决定“要什么”的关键一步4.1 配置内核的两种常见玩法内核编译前必须先做配置。配置的本质是生成一个.config文件这个文件里用CONFIG_XXXy或CONFIG_XXXm这种形式记录了几千个编译开关比如CONFIG_USB_SUPPORT、CONFIG_EXT4_FS决定了哪些功能会被编进内核、哪些功能会编成模块、哪些直接不编。配置内核有两种常见思路。第一种是使用厂商提供的默认配置一般叫xxx_defconfig。这种配置文件的位置在arch/arm/configs/ARM 平台或arch/arm64/configs/ARM64 平台文 件名通常带有开发板型号的特征。第二种是自己从头配置对新手来说不太现实几千个配置项很容易让人崩溃。最合理的路径是先用厂商给的defconfig生成基础配置再通过menuconfig做加减法。4.2 用厂商 defconfig 快速生成基础配置以香橙派orangepi这类开发板常见的配置方式为例假设我们要为一块基于 ARM 平台的板子编译内核make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- orangepi_zmp_defconfig执行完之后源码根目录下会出现一个.config文件。可以打开看看里面每一行一个配置项比如CONFIG_ARMy CONFIG_MMUy CONFIG_USBy CONFIG_EXT4_FSy如果你不确定某个配置项的作用可以在.config里直接搜关键词也可以去Documentation/kbuild/目录查阅说明文档。4.3 menuconfig 界面化配置与保存menuconfig是一个基于文本的图形化配置界面依赖libncurses5-dev库。进入方式make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig界面里面按/可以搜索配置项这是一个非常好用的功能。比如你想开启某个网卡的驱动支持但不知道配置项叫什么名字直接按/输入网卡型号关键字就会列出对应的配置路径。改好配置后选 Exit 退出界面会询问是否保存新的.config选 Yes 即可。4.4 保存你的内核配置配置完成后建议把.config备份一份命名为config_myboard。这样下次清掉编译产物或者换了一台机器可以直接用这个文件恢复配置不用重新去 menuconfig 里一项项找。cp .config config_myboard这个习惯在开发中特别有用。内核配置项太多有时候你花几个小时一点点调出来的配置一次mrproper就全没了有了备份就能一键找回。5. 内核编译的完整实操流程5.1 正式编译前的检查清单在敲编译命令之前我强烈建议先做三件事。第一确认工具链已经被系统找到执行which arm-linux-gnueabihf-gcc如果有路径输出说明安装正常。第二确认当前目录是内核源码根目录不要跑到子目录里去执行make。第三确认ARCH和CROSS_COMPILE传参正确这一点很多人都会栽跟头少传一个变量内核就会拿本机的 x86 工具链去编译出来的镜像完全不能用。5.2 可执行文件格式的选择vmlinux、zImage、uImage编译内核时不同的目标产物对应不同的编译命令。vmlinux是未经压缩的内核 ELF 文件带有完整的符号表主要用来调试不能直接引导启动。zImage是自解压格式的内核镜像体积比 vmlinux 小U-Boot 可以直接引导。uImage是在 zImage 前面加了一个 U-Boot 头部的镜像老版本的 U-Boot 需要用这种格式新版本 U-Boot 大多可以直接引导 zImage。针对 ARM 平台完整的内核镜像编译命令是make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage -j8-j8表示用 8 个并行任务来编译。这个数字不是随便定的一般取你 CPU 线程数的 1~2 倍比较合理。如果你的开发机是 8 核 16 线程用-j16没问题如果是虚拟机并且只分配了 2 核用-j16反而会因为频繁切换线程导致性能下降。5.3 编译设备树DTBARM 平台的内核还需要配套的设备树文件.dtb它描述了硬件平台的设备信息CPU、内存大小、外设地址等都在里面。在设备树源文件.dts目录下搜索你的板子型号对应的文件find arch/arm/boot/dts -name *orangepi*找到对应的 dts 文件后编译命令make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs -j8编译出的 dtb 文件会在arch/arm/boot/dts/目录下。设备树是嵌入式内核开发中非常容易出错的地方节点地址写错、属性名拼错内核启动时就会卡在硬件初始化阶段。5.4 编译内核模块内核里被配置成CONFIG_XXXm的功能都会以.ko模块的形式存在。编译模块的命令make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules -j8编译完成之后模块是分散在各个源码子目录下的需要安装到一个统一的目录方便后续拷贝到开发板make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- INSTALL_MOD_PATH../modules_install modules_install上面的命令会把所有模块按照目录结构安装到../modules_install下里面会有lib/modules/6.1.28/这样的目录。之后把整个modules_install/lib/modules/拷贝到板子的根文件系统对应路径下即可。5.5 查看编译产物编译完成后到arch/arm/boot/目录下检查一下产物ls -lh arch/arm/boot/正常情况下会有zImage文件。再确认一下文件类型file arch/arm/boot/zImage输出中如果显示 “ARM” 或者 “ARM64” 字样说明交叉编译成功了。如果这里显示 “x86-64”那基本可以确定你忘了指定CROSS_COMPILE需要清理后重新编译。6. 内核模块的独立编译与调试6.1 单独编译一个内核模块的方法实际开发中你往往需要单独编译一个驱动模块反复调试而不希望每次都把整个内核重新编一遍。内核提供了单独编译模块的能力但前提是你已经有一次完整的内核编译记录并且源码树保持干净。假设你的模块源码在~/work/mydriver/目录下里面有一个mydriver.c和对应的MakefileMakefile 内容如下obj-m : mydriver.o KERNEL_DIR : ~/work/linux-6.1.28 all: make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -C $(KERNEL_DIR) M$(PWD) modules clean: make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -C $(KERNEL_DIR) M$(PWD) clean然后执行make这里的关键在于-C $(KERNEL_DIR)指定了内核源码目录M$(PWD)指定了模块源码目录。内核 Makefile 会先切换到内核源码目录读取里面的配置信息再回到你指定的模块目录去编译。如果内核源码没有编译过或者modules目标没有执行过这里会直接报错。6.2 模块编译成功后的检查编译成功后目录下会生成mydriver.ko。查看它的架构信息file mydriver.ko输出中应该有 “ARM” 字样。再查看它的模块信息arm-linux-gnueabihf-modinfo mydriver.ko可以看 到模块名、描述、依赖的其他模块等关键信息。如果模块依赖于某个符号但对应的模块没有编进内核加载时会提示 “Unknown symbol”。6.3 动态加载与 file_operations 拦截的思路很多做内核安全或者驱动开发的朋友会用到动态加载模块去拦截open、read、write这类系统调用。基本思路是实现一个内核模块注册自己的file_operations替换掉目标设备的操作函数指针。需要注意的是内核从 5.7 版本开始直接修改系统调用表被限制得越来越严格基于 ftrace、kprobe 等方式做 hook 会更稳妥。static struct file_operations orig_fops; static ssize_t fake_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk(KERN_INFO read intercepted\n); return orig_fops.read(file, buf, count, ppos); }这类模块编译时同样要指定交叉编译工具链。加载到板子上后用insmod mydriver.ko加载用rmmod mydriver卸载。调试时注意 dmesg 里的内核日志几乎所有的模块加载失败信息都会打印在那里。7. 把内核部署到开发板的实操方法7.1 镜像与设备树的拷贝位置编译好的zImage和.dtb文件要放到开发板能够读取的位置。对于使用 SD 卡启动的板子一般 SD 卡上会有一个 FAT32 格式的启动分区里面放着内核镜像和设备树文件。在 Ubuntu 虚拟机里插入 SD 卡读卡器会自动挂载到这个分区直接拷贝即可。sudo cp arch/arm/boot/zImage /media/你的用户名/boot/zImage sudo cp arch/arm/boot/dts/你的板子.dtb /media/你的用户名/boot/你的板子.dtb拷贝完成后先给 SD 卡执行一次sync确保数据写回硬件然后安全卸载插到板子上上电测试。7.2 模块安装到根文件系统内核模块要拷贝到根文件系统的/lib/modules/目录下。如果你的根文件系统也在这个 SD 卡上直接拷贝整个lib/modules/6.1.28目录到目标位置即可。sudo cp -r ../modules_install/lib/modules/6.1.28 /media/你的用户名/rootfs/lib/modules/如果没有提前建立根文件系统目录也可以临时挂载根文件系统分区后再拷贝但要注意别把分区格式搞坏操作前做好备份。7.3 启动参数与内核日志内核部署完成后可以通过串口观察启动日志。常见的启动参数有consolettyS0,115200指定串口终端、root/dev/mmcblk0p2指定根文件系统分区、rootwait等待根设备就绪。这些参数在 U-Boot 的环境变量bootargs里配置或者在 distro boot 的方式下写到配置文件里。启动过程中如果卡住多半是设备树和实际硬件不匹配。这时先在 U-Boot 里检查bootargs是否正确再检查.dtb文件是否拷对位置。如果内核启动到一半就 panic大概率是root参数指定错误或者根文件系统里缺少内核需要的驱动模块。8. 交叉编译过程中的常见问题与排查技巧8.1 编译报错速查表这里整理一份我自己经常遇到的报错和解决办法可以收藏备用。错误信息可能原因解决方法arm-linux-gnueabihf-gcc: command not found工具链不在 PATH 中重新 export PATH 或安装工具链arch/arm/Makefile:190: recipe for target zImage failed编译过程中有具体文件报错往前翻看真正的错误找到真正的错误文件多半是源码或配置问题No rule to make target menuconfig缺少 ncurses 库安装libncurses5-dev/bin/sh: 1: bison: not found缺少 bison 解析器apt install bison flexCannot use CONFIG_CC_STACKPROTECTOR_STRONG工具链不支持栈保护器在 menuconfig 里关闭栈保护相关选项undefined reference to__aeabi_unwind_cpp_pr0工具链 ABI 不匹配检查 gnueabi 与 gnueabihf 的选择错误: 在 .config 中未知的 CONFIG_xxx旧配置文件与新源码不匹配执行make mrproper后重新配置8.2 工具链版本与内核版本不匹配的处理工具链版本和内核版本不匹配时最典型的症状是编译到一半报出一堆“没有定义的宏”或“隐式函数声明”的错误。比如老内核4.x用非常新的 GCC12.xGCC 的检查变严格了会把原来只是警告的问题升级成错误。一种思路是更换工具链版本找到内核发布年代相近的工具链。另一种思路是绕过具体错误比如在 Makefile 的全局配置里追加KBUILD_CPPFLAGS -Wno-error让编译器不要用“把警告当错误”的标准。这种方式能让你先把内核编出来但我不建议长期依赖治标不治本。8.3 内存不足与编译性能优化-j并行参数开得太大容易在虚拟机里触发 OOM内存不足表现就是编译进程突然被系统 kill 掉或者虚拟机直接卡死。应对办法第一检查虚拟机分配的内存至少 4GB推荐 8GB第二编译前查看free -h确认可用内存第三适当减小-j数值。另外可以在编译时临时限制并行度make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j4 zImage如果内存确实不够还有一个办法是使用make的-l参数按系统负载动态限制并行任务但这在虚拟机里效果一般不如直接减-j来得直接。8.4 编译完成后内核起不来的排查思路内核编译成功只是第一步真正放到板子上能不能启动是另一回事。如果启动卡在“Starting kernel ...”先不要急着怀疑内核大概率是设备树不匹配或者 bootargs 参数有问题。排查路径我一般按这个顺序走先用串口线连接板子确认串口波特率设置正确然后在 U-Boot 环境里打印printenv查看bootcmd和bootargs接着确认内核镜像确实是从你刚编译生成的 zImage 加载的很多板子会从 emmc 或网络加载你明明刚拷了 SD 卡它却从别的地方启动最后确认设备树文件里外设节点和板子实际硬件一致。9. 关于 Qt、Boost 等库的交叉编译扩展9.1 Qt 交叉编译的常见套路热词里看到有“orangepi cm5 安装 qt5 交叉编译”“qt-everywhere-src-5.15.10 交叉编译”这类搜索说明很多人是嵌入式 GUI 方向。Qt 的交叉编译本质上和内核交叉编译是同一个思路但要额外多配几步。下载 Qt 源码后需要先指定工具链文件toolchain.cmake 或 qmake 参数里的-xplatform并设置 sysroot也就是开发板根文件系统的目录。比如./configure -prefix /usr/local/qt5-arm -xplatform linux-arm-gnueabihf-g \ -sysroot /path/to/rootfs -opensource -confirm-license -nomake examples这里-sysroot指向开发板的根文件系统编译 Qt 时它需要从里面找 zlib、png 这类依赖库的头文件和库文件。所以一个完整的根文件系统是 Qt 交叉编译的前提否则会在 configure 阶段就报找不到某些库的错误。9.2 Boost 等 C 库的交叉编译思路Boost 库的交叉编译核心是修改user-config.jam指定编译器为交叉编译器再通过b2指定参数./b2 --with-date_time --prefix/opt/boost-arm toolsetgcc-arm \ cxxflags-fPIC linkstatic具体配置项因 Boost 版本而异但思路都一样告诉构建系统“编译器是交叉编译器”“目标平台根目录在哪里”。这些库交叉编译的经验本质上和内核编译是相通的都是把本机的编译环境映射到目标平台上。9.3 为什么这些场景都离不开交叉编译Qt、Boost、内核这些看起来不相干的东西在嵌入式开发里都会被交叉编译这个概念串联起来。原因很简单目标平台跑着精简系统很难有完整的本地编译环境而 PC 上构建工具齐、性能强、生态好。交叉编译就是这种不平衡的资源分配下最自然的解决方案。搞明白了内核的交叉编译其他库的交叉编译都只是在不同构建系统里重复同一套思维。10. 结束语一些个人的经验之谈在我刚开始接触交叉编译内核的时候踩过最大的坑就是漏传ARCH和CROSS_COMPILE导致编出来的镜像放到 ARM 板上完全无法启动。后来养成了一个习惯把完整的编译命令写成一个脚本文件放在工程目录里每次编译只管执行脚本不仅不用记忆长长的参数还能保证所有团队成员用同一个编译流程。#!/bin/bash export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make mrproper make orangepi_zmp_defconfig make -j8 zImage dtbs modules另外一个小技巧是在 kernel 源码根目录建一个build.sh把上述内容放进去用bash build.sh执行。后续不管你是换了一个交叉编译器还是换了目标架构只需要修改脚本开头几行的变量不用去记忆每一个 make 命令的参数细节。交叉编译 Linux 内核这件事说难确实难头一次接触时面对那么多配置项和编译参数容易发懵说简单也简单总结下来无非就是搭好工具链、定好ARCH/CROSS_COMPILE、配好内核选项、编译产物部署到板子。只要理解了这四步的逻辑任何平台的内核编译对你来说都只是换一个ARCH参数的问题。最后再分享一句遇到内核起不来先别慌串口日志永远是最好的老师。
返回列表