前几天朋友发来三段编译日志,说在龙芯2K3000的板子上编Linux 4.19内核,编一半就报错,来回折腾一天没解决。我看完日志第一反应不是去猜哪个函数写错了,而是反问他三个问题:源码从哪拉下来的?交叉编译器用的哪套?make ARCH设成了什么?结果他一个都答不上来。这个场景我见得太多了——龙芯平台上报Linux 4.19编译出错,十个里有八个不是代码问题,而是整条编译链路从源头就错了。
这篇文章就把我在龙芯平台上编4.19内核踩过的坑、排查过的错误捋一遍。4.19至今还是很多国产化工控项目的锚定版本,AFC这类场景选它很常见,但生态不像x86那么顺滑。下面按“为什么容易错—动手前确认什么—逐条对着日志排错—编译完怎么保证能启动”的顺序讲,尽量让新人和被板子折磨过的老手都能对号入座。
1. 为什么Linux 4.19在龙芯上编译,报错会比x86平台多一个量级
1.1 官方主线4.19里根本找不到LoongArch目录
Linux 4.19是2018年底发布的LTS内核。LoongArch是龙芯后来才公开的指令集架构,并不存在于4.19时代的官方主线中。这意味着你跑到kernel.org拉一个4.19.xxx的原版压缩包,解压之后进arch目录翻到底,也只能看到mips下的loongson64,绝对翻不出loongarch目录。
这种情况下无论怎么调整编译命令,Makefile都会直接告诉你:
arch/loongarch/Makefile: No such file or directory make[1]: *** No rule to make target 'arch/loongarch/Kconfig'. Stop.这条报错基本等于直说:源码不对,不是编译姿势不对。所以排错第一件事,永远先看arch目录下有没有对应架构,有LoongArch目录,后面的讨论才有意义。
| 内核来源 | 架构目录 | 结果 |
|---|---|---|
| kernel.org 4.19 原版 | arch/mips/loongson64(仅MIPS龙芯) | 编LoongArch必失败 |
| Loongnix/龙芯社区4.19分支 | arch/loongarch | 可编3A5000/2K1000/2K3000 |
| 板卡厂商BSP 4.19 | 视厂商而定 | 通常可用,但要做二次确认 |
1.2 龙芯自己就有两套架构,选错defconfig等于白做
这个问题比源代码分支更隐蔽。龙芯早期处理器用MIPS指令集,3A3000、3B4000都是;后来新处理器切到LoongArch指令集,3A5000、3A6000、2K1000、2K2000、2K3000这些都是。同样是“4.19内核”,MIPS龙芯走arch/mips/loongson64,LoongArch龙芯走arch/loongarch,两条线代码不同,defconfig也不同。
LoongArch板子常见的是loongson2k_defconfig或loongson64_defconfig,MIPS龙芯常见的是loongson3_defconfig。不同维护分支可能改名,务必先ls一下arch/loongarch/configs(或arch/mips/configs),以仓库里实际文件名为准。我见过有人拿3A3000的MIPS配置硬套到2K3000板子上,编译器一会儿报这一会儿报那,就是.config里全是MIPS选项,CPU型号、串口地址、中断控制器全对不上。这种错误靠看日志很难定位,因为报错点分散在各个驱动里,其实源头就是一个。
1.3 工具链错位导致的报错最隐蔽
编译x86内核时,大家习惯用发行版自带gcc就行。到了龙芯4.19,发行版自带gcc是给本机架构编的,交叉编译器又经常版本不对:
- 用x86的gcc直接编:链接阶段疯狂报错或机器码不对。
- 用mips64el工具链去编loongarch:汇编器不认LoongArch指令。
- 用太老的gcc:gcc 8之前对LoongArch支持很差,某些-march参数根本不存在,头文件里也能报语法错。
这类错看起来像代码错,实际是工具链错。内核源码里的汇编文件对binutils版本尤其敏感,老版本binutils不识别LoongArch伪指令,会在head.S这类文件上报一堆莫名其妙的错。工具链问题后面单独说,这里先记结论:在龙芯上编4.19,工具链的可信度比源码还值得怀疑。
2. 翻车前先确认三件事:源码分支、目标架构、工具链版本
2.1 先摸清板子上的CPU型号
拿到板子后不要急着解压源码,先上板子确认CPU。最简单的方法是uname -m和cat /proc/cpuinfo。在板子有串口或SSH的情况下,一条命令就能看到processor信息。有设备树的再cat /proc/device-tree/model,通常会带loongson字样。
- uname -m输出是loongarch64,说明是LoongArch处理器。
- 如果是mips64,说明是MIPS龙芯。
这一步不搞清楚,后面所有defconfig都选不对,报错也会千奇百怪。
2.2 找到匹配的4.19源码
正确源码从哪来?我的经验优先级:
- 板卡厂商BSP里带的源码包,他们调过板级配置,最贴近硬件。
- Loongnix仓库里的kernel-4.19分支。
- 龙芯开源社区维护的linux-4.19分支。
下载后立刻检查arch目录,判断标准是:ls arch | grep loongarch能出结果,且make kernelversion输出是4.19开头。我之前遇到过有人拿了一个5.10的内核,然后硬套4.19的defconfig来编,报错后到处查,最后发现版本号压根不对,白折腾半天。
2.3 交叉编译器怎么准备
方案A:直接用龙芯官方发布的交叉编译器,一般命名类似loongarch64-linux-gnu-,解压后放进/opt或某个工具目录,加入PATH。
方案B:用crosstool-ng自建。版本选择上,gcc建议9以上,binutils建议2.32以上,否则很容易触发汇编错误。我实测下来gcc 10.x加binutils 2.38的组合比较稳。如果是MIPS龙芯,就配mips64el-linux-gnu-的工具链,别搞混。工具链前缀在不同版本里可能有差异,比如loongarch64-unknown-linux-gnu-,验证方法很简单:
loongarch64-linux-gnu-gcc -dumpmachine输出为loongarch64-linux-gnu之类就基本对。再写一个hello.c编一下看能不能过,能过说明工具链基础可用。
2.4 x86主机上交叉编译需要装的系统依赖
在x86的Ubuntu/Debian主机上交叉编译,确保有这些包:
apt install build-essential flex bison libncurses-dev libssl-dev bc kmod cpio缺libncurses-dev会卡在make menuconfig,缺libssl-dev会在签名阶段报错,缺bc会在scripts/kconfig里报错。这些错在报错信息里都算明显,但第一次遇到容易愣住:明明在编内核,为什么去找ncurses库?因为配置界面要用curses库画菜单。这类“报错点和原因隔着一条街”的问题,在龙芯平台上特别多,所以心态上要先接受一个事实:日志里的信息是准确的,但它不会告诉你该去换源码还是换工具链。
3. 从编译日志逐行倒推的排错实战
这一章是核心。我按错误类型分别列一下,每类都给出“现象—排查—解决”的完整链路,你对着日志找自己那条就行。
3.1 缺失架构目录的错误
现象:
make ARCH=loongarch loongarch_defconfig *** Can't find default configuration "arch/loongarch/configs/loongarch_defconfig"!或者更直接:
Makefile: No rule to make target 'arch/loongarch/Kconfig'. Stop.排查思路:不要急着修改配置文件,先看arch目录里到底有什么。如果arch下没有loongarch目录,说明源码不是龙芯维护的4.19分支,后面的一切操作都没意义。解决方法是换源码,而不是新建一个空目录。
3.2 汇编错误几乎都是工具链版本问题
一个很常见的场景,head.S里的伪指令不被识别:
arch/loongarch/kernel/head.S: Assembler messages: arch/loongarch/kernel/head.S: Error: unknown pseudo-op: `.dword'.dword是64位下的常见伪指令,绝大多数LoongArch工具链都能支持。如果这里报unknown pseudo-op,基本可以断定binutils过老,或者根本就是用错了工具链,比如拿mips工具链来编loongarch。解决方法是换编译器,不要尝试改源码绕过去。
排查时先看工具链版本:
loongarch64-linux-gnu-as --version | head -1如果binutils版本在2.32以下,直接换新版本。还有一类隐藏情况是PATH里同时存在多个工具链,调用的as不是你以为的那个。用which确认一下实际路径,或者用绝对路径调用工具链。
3.3 menuconfig阶段的ncurses报错
现象:
Unable to find the ncurses libraries.或者:
make[1]: *** [scripts/kconfig/conf] Error 1然后提示缺少curses.h。解决就是装libncurses-dev。这个比较基础,但需要提醒的是,有些精简系统连gcc都不一定装全,别光盯着内核报错,先看系统依赖。在龙芯板子本机编译时,如果用的是Loongnix或统信这类国产系统,软件包管理器里装依赖的命令可能不叫apt,遇到报错先看当前系统是什么发行版。
3.4 链接时的undefined reference
现象类似:
drivers/misc/foo.o: undefined reference to `__udivdi3'__udivdi3是编译器在目标平台没有硬件除法指令或ABI限制时,对64位除法生成的libgcc辅助函数调用。普通内核链接时一般不会出现,出现了多半说明用的是软浮点或32位ABI配置,和当前架构不匹配。解决方向:检查.config里的CPU类型、ABI选项,比如CONFIG_64BIT是否打开,以及工具链是否支持硬件除法。
还有一种常见情况是外部模块单独编译时,编进模块的配置选项和当前内核不一致,触发modpost的undefined!。我之前在2K1000上调一个GPIO驱动模块,就遇到过这种:
ERROR: modpost: "gpiochip_get_data" [drivers/gpio/foo.ko] undefined!内核里函数确实存在,但编译模块时用的头文件或配置符号和内核本体不一致。这种情况重新用内核源码的make modules_prepare再编模块,通常能解决。如果还不行,就去查模块对应的Kconfig有没有被打开。
3.5 一次完整的报错到修复过程
我把前面几个问题连成一个浓缩案例。假设你拿到一块2K3000板子、一份官方4.19原版源码、一套x86默认gcc。
第一步,执行:
make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- loongarch_defconfig报错:找不到arch/loongarch/...。此时不怪编译器,怪源码。换龙芯4.19维护分支。
第二步,配置通过,开始编译,报head.S汇编错。查工具链版本,发现gcc 8.3.0太老。换gcc 10加binutils 2.38的组合。
第三步,再编译,报缺少libssl-dev导致的sign-file编译失败。装依赖。
第四步,终于编出vmlinux。但U-Boot引导时卡死,没有串口输出。排查发现设备树没编进镜像,U-Boot里fdt地址没有对应二进制。编译对应的dtb后重新打包,启动正常。
这个案例里每一步都不是深奥的内核知识,但顺序错、链路不完整时,就会误以为是代码问题。我始终强调看日志倒推,就是因为报错点和真正根因经常隔了两三层。
4. 内核编译成功不等于能启动:镜像、设备树和启动参数
4.1 先弄清楚板子的引导方式
各板子的U-Boot/GRUB加载格式不一样。有人只make出vmlinux就烧进去,U-Boot不认,报bad magic number,然后以为内核没编对。本质上vmlinux是ELF格式,U-Boot若要求uImage就得用mkimage包一层。命令大概是:
make uImage LOADADDR=0x9000000000004000LOADADDR要按板子内存布局填,不是随手抄的。不同的龙芯板子,DDR映射起始地址可能不相同,2K3000和3A5000的地址空间也不一样,这个值必须看BSP文档或U-Boot里环境变量。如果用GRUB引导,直接放vmlinuz即可。这块不能省,全凭板卡资料。
4.2 设备树要不要单独编
LoongArch下很多设备都靠设备树描述。4.19的龙芯分支会在arch/loongarch/boot/dts下放各板级dts,比如2K3000的板子可能叫loongson2k3000.dts,具体以仓库实际文件名为准。
编译时如果没编dtb,刷机后在U-Boot里依然显示加载内核成功,可一旦跳转后没有任何串口输出,大概率是内核根本不知道串口在哪、内存在哪。要注意的是不同维护分支的dts路径不同,先在dts目录下ls看看有没有对应板型,没有就对照同系列dts改一版。
AFC这类嵌入式工控场景,GPIO控制、看门狗、RTC这些外设全挂在设备树下面。缺了某个节点虽然不会让编译报错,但运行时候模块加载不出来,表现就是“驱动明明编了却没生效”,这个坑非常隐蔽。
4.3 rootfs和cmdline的坑
内核起来了,rootfs起不来也是常见现象。常见表现:
- 串口无输出:console没设对,比如板子默认ttyS0,cmdline里写ttyAMA0,自然黑屏。
- VFS: Unable to mount root fs:检查root=指向的设备,以及内核里对应的文件系统和磁盘驱动是否内建。如果rootfs在sda,但sata/ahci驱动被编成模块且没有initramfs,mount阶段就会失败。
所以与启动相关的驱动尽量以内建方式编入,不要图省事用模块。这是嵌入式场景和服务器场景一个很重要的差别。启动参数示例:
console=ttyS0,115200 root=/dev/sda2 rw具体串口号和波特率看板子原理图或BSP文档。
4.4 工控稳定性的编译期准备
AFC项目往往要7x24小时运行。编译内核时需要考虑:
- 开启CONFIG_WATCHDOG并选对看门狗驱动,配合应用层喂狗。
- RTC驱动内建,保证断电后时间基准还在。
- 串口、GPIO、CAN等工业接口按实际需求编入。
- 不需要的驱动别乱开。很多报错来自把desktop板子的config直接拿来编,编出来驱动和板子对不上,启动时IRQ冲突、驱动崩溃全来了。
我见过一个案例,有人用3A5000的配置文件编2K3000,编译全过,启动到一半卡在USB控制器初始化。查了半天,是config里开了SATA相关的一个新驱动,而2K3000的SATA控制器和3A5000不完全一样,导致探测逻辑跑偏。后来回到板卡基线config,再逐一裁剪,问题消失。
5. 一张避坑检查表和我平时构建内核的习惯
5.1 检查表
| 检查项 | 正确做法 | 错误后果 |
|---|---|---|
| 源码来源 | 4.19龙芯维护分支 | 编译直接找不到loongarch |
| CPU型号 | uname/cpuinfo确认 | defconfig全乱 |
| 工具链 | loongarch64交叉编译器,gcc>=9,binutils>=2.32 | 汇编符号、链接错误 |
| 系统依赖 | libncurses-dev libssl-dev bc flex bison等 | 各阶段命令失败 |
| ARCH/CROSS_COMPILE | 每次make都要带,或写环境变量 | 编到x86去了 |
| dtb | 编译对应板级dtb并打包 | U-Boot跳转后无输出 |
| cmdline | console对应板子串口,root指定正确 | 黑屏或挂不上rootfs |
| 启动驱动 | 关键驱动内建 | initramfs缺失时挂载失败 |
5.2 我平时构建内核的几个习惯
不直接改源码目录下的.config,用make O=build_dir把构建目录单独拆出去。这样同一份源码可以同时维护多个配置,出问题也不会污染源码。我经常同时维护debug版和release版两个构建目录,切来切去很方便。
保存日志。编译命令带上tee:
make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- -j$(nproc) 2>&1 | tee build.log排查时grep -i error,比在终端翻页强多了。
make -j$(nproc)要注意内存,特别是2K3000板子内存不大时,本机编译容易OOM。交叉编译时-j16没问题,但编dtb或模块时偶尔有依赖顺序问题,先串行编一次保底。
每次改配置后执行make olddefconfig,把新配置项合并进来。4.19这种老内核上如果直接硬改.config,很容易出现配置项被忽略的情况。
编完内核后,模块使用的内核版本要一致。很多“编译成功但insmod报错”的问题,都是内核和模块版本错位。
5.3 给新手的一个小验证脚本
把下面这个脚本放在内核源码根目录,编译前先跑一遍,能在第一步拦截掉一半问题:
#!/bin/bash CROSS=loongarch64-linux-gnu- if [ ! -d arch/loongarch ]; then echo "[ERROR] arch/loongarch not found, check kernel source branch" exit 1 fi if ! command -v ${CROSS}gcc >/dev/null 2>&1; then echo "[ERROR] ${CROSS}gcc not found, check toolchain path" exit 1 fi ${CROSS}gcc -dumpmachine ${CROSS}gcc --version | head -1 ${CROSS}as --version | head -1 make ARCH=loongarch kernelversion这个脚本不复杂,但每次开始新的板子项目都能帮我省不少时间。踩过几次坑之后,我现在拿到任何一块龙芯板子,第一件事就是写板卡信息备忘:CPU型号、源码分支、工具链版本、defconfig、dtb文件、启动参数。全部记下来之后才去动编译。
这个习惯不只在4.19上有用,在之后升级内核版本、换BSP时,对照这份备忘就能知道哪些变量变了、哪些报错是因为哪个环节变了。龙芯平台这几年用得越来越多,AFC、电力、交通这些对长期稳定要求很高的场景都在往国产平台迁移,工具链和内核版本更新也不会停。把编译链路里的每个环节都当成独立变量去验证,比事后对着报错猜根因要靠谱得多。