1. 先把两个文件的分工掰清楚
第一次碰内核编译的人,最容易犯的错就是把.config和xxx_defconfig当成一回事,或者以为改完其中一个另一个会自动同步。我当年交叉编译一块 ARM 开发板的内核,就是因为在arch/arm/configs/下改了半天配置,然后直接敲make,结果编出来的镜像里串口驱动死活没有——折腾了快一个钟头才反应过来,我改的是模板文件,真正参与编译的是根目录下那个.config。这个教训值一篇博文,所以下面把这两个文件的关系从头讲透。
1.1 .config 是编译真正读的那一份
内核源码根目录下的.config是整个编译过程的唯一配置输入。Makefile 在编译时,会把.config里的每一行CONFIG_XXX=y、CONFIG_XXX=m或者未设置的状态,转换成编译参数和环境变量,决定哪些目录进、哪些文件编、哪些代码被条件编译掉。换句话说,你在源码里看到的#ifdef CONFIG_XXX,最终能不能成立,全看根目录.config里那一行是怎么写的。
这个文件是自动生成的,它的头部通常有一行注释:# Automatically generated file; DO NOT EDIT.。这行提示不是说绝对不能手改,而是告诉你:手改风险很高,因为内核配置项之间存在大量依赖与联动,你手动把某个开关拨到y,但它的依赖项没开,下次跑make oldconfig或者make menuconfig时,工具会毫不留情地把它改回去。所以规范做法是:用配置工具改,让工具帮你写.config。
.config不随源码提交到版本库,每个开发者的.config都可能不一样,这也解释了为什么同一份内核源码,不同人编出来的大小和功能差异巨大。
1.2 defconfig 是给板子用的"配置清单"
defconfig的全称是 default configuration,它放在arch/<架构>/configs/目录下,命名规范一般是<板子名>_defconfig,比如arch/arm64/configs/defconfig、arch/arm/configs/imx_v7_defconfig。它代表"某块具体板子或某个平台的标准配置",用来描述这套硬件需要哪些驱动、哪些子系统、哪些特性。
关键点在于:defconfig 不是完整的 .config。它是一份"精简版差异清单",只记录那些偏离内核默认值的配置项。内核里成千上万个配置项都有各自的默认值(由 Kconfig 里的default语句决定),defconfig 只写你需要改动的部分。这样做的好处是文件短、易读、好维护,一个平台几百行就能描述清楚,而不是像完整.config那样动辄上万行。
当你执行make xxx_defconfig时,配置工具会读这份 defconfig,先把所有配置项置为默认值,再把你列出来的项覆盖上去,最后生成根目录的.config。这就是两个文件之间的桥梁动作。
1.3 Kconfig 才是背后发号施令的那一层
理解.config和 defconfig 的关系,绕不开第三个角色:Kconfig。Kconfig 是分散在各个源码目录里的文本文件,它定义了每个配置项的名字、类型(bool/tristate/int/string)、提示语、默认值、依赖关系depends on和反向选择select。你可以把它理解为"配置项的定义语言"。
一个完整的链路是这样:源码里的 Kconfig 文件描述了"有哪些配置项、它们怎么关联";defconfig描述了"这块板子在这些项上取什么值";配置工具读取前两者,生成最终的.config;Makefile 再读.config来驱动编译。三者各司其职,缺一不可。
所以当你遇到"配置项找不到"或者"配置项被自动关掉"这类问题时,根源往往在 Kconfig 的依赖定义里,而不是.config本身写错了。把这三个文件的关系理清,后面所有配置操作都会顺畅很多。
2. 拆开 .config 看每一行到底写了什么
很多人拿到一份.config直接懵:上万行,全是CONFIG_开头,看不出重点。其实它的语法非常规整,掌握几种状态和几条关键规则,你就能快速读懂乃至审核一份配置。
2.1 y、m、未设置:三种状态各有含义
.config里的配置项基本只有三种写法,对应三种结果:
CONFIG_XXX=y # 编译进内核(built-in) CONFIG_XXX=m # 编译成可加载模块(module) # CONFIG_XXX is not set # 未启用y表示这段代码直接编进最终的内核镜像,开机就在,不依赖任何外部文件;m表示编译成一个独立的.ko模块文件,运行时用insmod或modprobe动态加载。这个区别在实践中影响巨大:选y会增大内核体积、占用常驻内存,但启动即生效;选m让内核更小、更灵活,但需要根文件系统里有对应模块文件,且加载时机要自己控制。
# CONFIG_XXX is not set这种写法是工具生成的规范形式,等价于"关闭"。你手写.config时如果直接删掉某行,效果和显式写 not set 有时不完全一样——配置系统会把"缺失"当作需要重新询问的状态。所以手工改配置时,关闭一项最好用工具,别随手删行。
需要注意的坑是:不是所有配置项都能取 m。只有类型声明为tristate的项才支持 y/m/n 三种状态,bool类型的项只有 y/n,你给它写=m,工具会报错或者忽略。
2.2 依赖和 select:为什么你开的项会自己消失
配置项之间有两类关系,理解它们能省下大量排查时间:
- depends on:表示"我需要别人先成立"。比如某网卡驱动
depends on PCI,如果你没开 PCI 支持,这个网卡选项在 menuconfig 里根本不会出现,或者出现也是灰的。 - select:表示"我一开启,就强制把别人打开"。比如某功能
select CRC32,你开了它会自动把 CRC32 拉上。
问题就出在这里。depends on是自上而下的门槛,select是自下而上的强拉。当你执行make oldconfig或make menuconfig时,工具会重新做一遍依赖求解:任何依赖不满足的项会被强制关掉。你明明在.config里写了CONFIG_XXX=y,编译时却发现没生效,八成是因为它的某个depends on项没开。
举个真实场景:我曾想给一块板子开某个特定的文件系统,反复在.config里置y,但工具每次都给我关回去。后来查 Kconfig 才发现,这个文件系统depends on BLOCK,而我在裁剪时把块设备层关掉了。开回CONFIG_BLOCK=y,问题立刻消失。这就是"配置项自己消失"的典型原因。
2.3 一份 .config 里值得重点关注的几类配置
面对上万行配置,没必要逐行看,但有几类必须心里有数:
| 类别 | 典型配置项 | 关注理由 |
|---|---|---|
| 平台与架构 | CONFIG_ARM64、CONFIG_SOC_xxx | 决定能不能在这块 SoC 上跑 |
| 启动与命令行 | CONFIG_CMDLINE、CONFIG_CMDLINE_FORCE | 影响内核启动参数是否被 bootloader 覆盖 |
| 存储与文件系统 | CONFIG_BLK_DEV_INITRD、CONFIG_EXT4_FS | 决定根文件系统能否挂载 |
| 网络协议栈 | CONFIG_INET、CONFIG_NETFILTER | 影响网络功能和体积 |
| 调试与日志 | CONFIG_DEBUG_INFO、CONFIG_PRINTK | 调试期开,量产前务必关 |
| 内核配置访问 | CONFIG_IKCONFIG、CONFIG_IKCONFIG_PROC | 开启后可从/proc/config.gz读回当前配置 |
其中CONFIG_IKCONFIG_PROC特别实用。开启它,内核会把自身配置压缩后暴露在/proc/config.gz,用zcat /proc/config.gz就能导出当前运行内核的完整配置。这在排查"线上机器的内核到底开了什么"时简直是救命稻草。发行版内核通常还会把这份配置放到/boot/config-$(uname -r),同样可以直接查看。
另外,CONFIG_DEBUG_INFO会让内核带完整调试符号,体积可能翻好几倍,还显著拉长编译时间。开发阶段留着方便用 gdb 调,但量产固件里一定要关掉,否则 Flash 都被符号表占满了。
3. defconfig 的来龙去脉与移植实操
搞懂了.config,再回头看 defconfig 就简单了:它就是生成.config的种子。但在实际项目里,defconfig 的维护和移植才是真正花时间的地方,尤其是给一块新板子做适配。
3.1 make xxx_defconfig 背后发生了什么
当你在源码根目录执行:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig配置系统做的事情大致分三步:第一,找到arch/arm64/configs/defconfig这个文件;第二,把所有配置项扫描一遍,先按 Kconfig 里的default填上默认值;第三,把 defconfig 里显式列出的项覆盖上去;最后把结果写成根目录.config。
这里有个容易忽略的点:defconfig 里没写的项,不一定是关闭,而是回到默认值。内核里很多项默认就是y,所以你的 defconfig 看起来很干净,生成出来的.config却很长。这也解释了为什么"我 defconfig 里根本没写这个功能,怎么编出来它有"——因为人家默认是开的。
如果你执行make xxx_defconfig报错提示找不到目标,先确认文件路径和名字拼写,arch/<架构>/configs/下必须有对应的xxx_defconfig。老版本内核里这个目录可能叫arch/<架构>/configs/,也有平台把它放在arch/<架构>/mach-xxx/下,找的时候可以全局搜一下。
3.2 用 savedefconfig 维护自己的配置文件
改完配置想保存成新的 defconfig,千万别直接复制.config。因为.config是全量文件,一万多行,里面大量项都是默认值的冗余记录,拿它当 defconfig,可读性和可维护性都极差。
正确姿势是用savedefconfig:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- savedefconfig这条命令会对比当前.config和内核默认值,只把差异部分输出到根目录的defconfig文件里,通常只有几百行。然后你把它拷到arch/arm64/configs/下改名即可:
cp defconfig arch/arm64/configs/myboard_defconfig这套流程是我现在维护板级配置的标准做法:先用 menuconfig 把要开的项都调好,跑一遍编译验证功能正常,再savedefconfig导出精简版,提交到版本库。这样别人拿到后make myboard_defconfig就能一键复现我的环境,不会有任何遗漏。
注意:
savedefconfig生成的只是"差异清单",它必须配合对应版本的内核源码才能还原出完整.config。跨内核版本搬运 defconfig 时,一定要用make olddefconfig让工具补齐新版本的配置项。
3.3 拿相近板子的 defconfig 做移植的完整思路
给一块全新的板子做内核适配,从零写 defconfig 是不现实的,标准做法是找一块最相近的现成板子改。具体步骤我一般是这样:
- 选定参考板。SoC 厂商通常会提供参考 defconfig,比如同系列芯片的评估板配置。优先选同架构、同 SoC 家族、外设接近的那份。
- 复制改名。
cp arch/arm64/configs/vendor_evb_defconfig arch/arm64/configs/myboard_defconfig。 - 改核心平台项。把 SoC 型号、CPU 类型、时钟、中断控制器这些跟芯片强相关的项调整到目标平台。这一步改错,内核根本起不来。
- 调外设驱动。串口(调试口必开)、存储控制器(eMMC/SPI Flash)、网络 PHY、显示、触摸等,按硬件原理图逐项核对。
- 裁剪冗余。把参考板上用不到的外设驱动、调试选项、多余文件系统关掉,减少体积和攻击面。
- 编译验证。编完下板,看串口有没有输出、根文件系统能不能挂、网卡能不能起来。
- 导出精简版。功能确认后
savedefconfig,提交。
整个流程里最耗时的是第 4 步,因为驱动和硬件是对应的,原理图上多一颗芯片,配置里就要多开一个驱动。我的经验是:先让内核能启动(串口 + 存储 + 根文件系统),再逐步加外设。一上来就把所有驱动开齐,一旦起不来,排查面太大。
4. 配置工具链与完整编译流程
配置有很多种改法,工具也五花八门,不同场景选不同工具能明显提效。下面把常用的几种和完整编译流程串一遍,给一套可以直接照抄的操作方案。
4.1 menuconfig、oldconfig、allyesconfig 各自适合什么场景
常用的配置目标有这么几个,各有适用场合:
make menuconfig:最常用的交互式配置界面,基于 ncurses 的文本菜单,SSH 环境下就能用。适合人工挑选配置项、查依赖、看提示。进出都很快。make nconfig:menuconfig 的升级版,界面更友好,支持搜索、颜色高亮,我一般优先用它。make xconfig:图形界面,需要 Qt 环境,本地桌面开发时体验最好,服务器上基本用不上。make oldconfig:读取已有.config,只对新增的配置项逐个询问,其他保持不变。升级内核版本后必跑。make olddefconfig:和 oldconfig 类似,但新增项一律采用默认值,不交互。适合自动化脚本和 CI 环境。make allyesconfig:所有能开的项全开。体积巨大、启动慢,主要用来做编译覆盖测试,别拿它当产品配置。make allnoconfig:全关。用来做最小系统实验,或者验证某个配置项是不是真的被依赖着。
选择逻辑很简单:日常调配置用 menuconfig/nconfig;换内核版本用 olddefconfig 打底再用 menuconfig 微调;做测试才用 allyesconfig/allnoconfig。
顺带一提,menuconfig 里的搜索功能(按/)极其好用。输入配置项名字或关键字,它会告诉你这个项在哪、当前值是什么、依赖哪些项。遇到"某个选项找不到"时,先搜索,看清它的depends on,再去开前置项。
4.2 一次完整编译的操作步骤
把配置和编译串成一条完整流程,我平时是这么操作的,你可以直接照抄(以 arm64 为例):
# 1. 生成 .config make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- myboard_defconfig # 2. 按需微调(可选) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig # 3. 同步新增配置项(升级内核后必做) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig # 4. 编译,-j 后面跟 CPU 核心数 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) # 5. 确认产物 ls -lh arch/arm64/boot/Image ls -lh arch/arm64/boot/dts/*.dtb第 4 步的-j$(nproc)是并行编译,nproc 会返回当前机器的 CPU 核心数。我实测在一台 8 核机器上,全量编译一个 arm64 内核大概十几分钟,不加-j要四五十分钟。内存小的机器别把并行度开太大,容易 OOM,一般取核心数的 1 到 1.5 倍比较稳。
第 5 步的产物要认清楚:Image是压缩后的内核镜像,Image.gz是再压缩一层供某些 bootloader 使用,dts/目录下的.dtb是设备树二进制,三者缺一不可(具体用哪个看你的启动方式)。
4.3 增量编译和配置变更的坑
内核编译有一套依赖追踪机制,改了一个.c文件,只会重编受影响的部分,这就是增量编译。但配置变更的处理比较微妙,几个坑必须知道:
改配置后必须重编,且改动可能触发大面积重编。你打开一个影响面广的配置项(比如某个核心子系统),内核会认为相关代码的编译条件变了,可能触发成百上千个文件重编。所以配置尽量在动手编译前定好,别编到一半反复改。
改了 CONFIG 一定要让 Makefile 感知。内核的编译系统会监控.config的时间戳,正常make会自动处理。但如果你手动vi .config改了内容,然后又跑make oldconfig之外的命令,有些版本可能不触发重新求解依赖,导致实际生效的配置和你写的不一致。稳妥做法是:手改完.config后跑一次make olddefconfig。
清理要分清 clean 和 mrproper。make clean删掉编译产物但保留.config;make mrproper连.config和编辑器备份一起删。很多人想重新配置却误用mrproper,把辛苦调好的配置也清了,只能重来。想彻底重来用 mrproper,只想重编用 clean。
5. 常见问题排查与实战避坑
配置这事儿,出问题的地方高度集中。我把这些年踩过的坑和排查看家本领整理出来,遇到问题对照着查,基本能定位。
5.1 改了配置却不生效的几种典型原因
这是最高频的困惑,原因通常逃不出下面几种,我按出现概率排了序:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 改了没反应 | 改的是 defconfig 但没重新生成 .config | 确认根目录 .config 内容是否变了 |
| 改了又变回去 | 依赖项不满足,被工具强制关闭 | 用 menuconfig 搜索该配置项,看 depends on |
| 编译报未定义 | 配置项类型是 bool,却写了 =m | 查 Kconfig 里的类型声明 |
| 功能没启用 | 同名配置项被更高优先级覆盖 | 全局搜该配置项,确认没有多处定义 |
| 线上内核和源码不一致 | 编译用的不是当前 .config | 用/proc/config.gz导出实际配置对比 |
第一行那个坑我重复犯过不止一次。新人往往盯着 defconfig 看,以为自己改对了,但真正参与编译的.config还是旧的。永远以根目录.config为准,这是铁律。
第四行那个也比较隐蔽:内核里偶尔存在同名配置项在不同目录重复定义的情况,最终值取决于加载顺序。遇到"明明开了却没生效",用grep -rn "config XXX" --include=Kconfig .全局搜一遍,看是不是有别的定义在捣乱。
5.2 依赖报错和循环依赖怎么查
依赖问题分两类。一类是依赖不满足,表现为配置项在 menuconfig 里灰色不可选,或者选了之后又变回去。解决办法是用 menuconfig 的搜索功能定位该配置项,把提示里列出的依赖项逐个开上。
另一类是循环依赖,表现为配置工具报错,比如提示某两个项互相depends on。这种一般不是内核本身的 bug,而是你在裁剪时把某个中间项关掉了,导致依赖链断裂又被select反向拉起。处理方法通常是找到那个中间项重新开启,让依赖链恢复完整。
我个人的排查顺序是:先make olddefconfig看工具会不会自动修好;修不好就menuconfig搜索目标项看依赖;还不行就全局搜 Kconfig 定义,画出依赖关系。画依赖关系时,一张纸一支笔,把 depends on 和 select 的箭头画出来,循环在哪个环节一眼就能看见。
5.3 我踩过的坑和攒下的经验
最后这条是最有价值的,都是文档里不会写的:
别在生产配置里留 DEBUG_INFO。我见过一个项目忘记关CONFIG_DEBUG_INFO,编出来的内核比正常大四倍,刷进 8MB 的 Flash 直接满了,还占了大量内存。量产前一定要检查调试类配置。
根文件系统驱动和根文件系统类型要一起配。光开CONFIG_BLK_DEV_INITRD不够,还得开对应的文件系统(ext4、squashfs 等),否则内核起不来就卡在挂载根文件系统那一步,串口只打印一句VFS: Cannot open root device。
配置改动后先本地编一遍再提交。defconfig 提交到版本库前,务必用干净环境跑一次make xxx_defconfig && make -j,确认能编过。我吃过亏:本地.config里有历史遗留项,savedefconfig导出时带了进去,别人拉下来编不过,排查半天。
用脚本做配置对比。当你怀疑两次编译配置不一致时,把两次的.config都导出,用diff对比,比人眼扫强一百倍:
diff -u config_old config_new | grep '^[+-]CONFIG'这条命令只看配置项的增减,改动一目了然,是我排查"怎么突然坏了"的利器。
建立配置基线。项目稳定后,把确认可用的.config和 defconfig 都归档一份,标注内核版本和日期。后面再出问题,直接跟基线对比,能快速判断是不是配置改动导致的回退。
说到底,.config和 defconfig 一个负责"最终真相",一个负责"模板种子",把 Kconfig 这张底图看懂,配置这件事就没有玄学了。我现在拿到一块新板子,基本能在一天内把内核跑起来,靠的不是运气,而是这套流程跑熟了。你要是也在做嵌入式内核适配,建议从吃透一份现成的 defconfig 开始,逐项对照原理图看,几块板子下来就有感觉了。