[内核学习]内核构建系统

/ 0评 / 0赞

内核构建是很多人的第一堵墙:make 一下几千个文件哗哗过,改一行重编四十秒,摸一个头文件二十分钟.这篇把 Kconfig 和 Kbuild 拆开讲,所有数字都是实测,机器是 8 核 x64 交叉编译同一份 6.18.56 源码,两份配置:板上内核和给 QEMU 编的 defconfig.

这篇回答几件事:

先说配置这半边,主角是 Kconfig.全部配置入口汇总成一颗依赖树,Documentation/kbuild/kconfig-language.rst 是语法说明书.日常的循环是三步:make defconfig(或板厂的配置)生成 .config;make menuconfig 人工调;最后编译期 Kbuild 把 .config 翻译成 include/generated/autoconf.h 里的 CONFIG_XXX 宏,C 文件里 #ifdef 的就是它.反向还有个好东西:make savedefconfig,从当前 .config 压出一份最小描述文件,实测 1755 行就能完整重现一个 defconfig 内核 — 因为背后有大量符号有默认值,只存差异.板卡厂商给bsp 配置时,与其传一整份 .config,不如传 savedefconfig 的产物,可读可 diff.

配置之外是编译这半边,Kbuild.它的本质是把"哪些文件要编,怎么编"全写在 Makefile 里,靠两层推导:obj-y/obj-m 决定文件进 vmlinux 还是变成模块;每个 .o 编译时,fixdep 顺手把这个文件真正用到的依赖(头文件 + CONFIG 符号)记进隐藏的 .cmd 文件.看一眼 kernel/sched/.core.o.cmd,整个 gcc 命令行原样躺着:

savedcmd_kernel/sched/core.o := aarch64-linux-gnu-gcc -Wp,-MMD,kernel/sched/.core.o.d 
	-nostdinc -I./arch/arm64/include ... -include ./include/linux/kconfig.h ...

增量判断就看这个:时间戳没变,命令行没变,配置依赖没变,就不重编.注意命令行本身也是条件 — 同一个文件换了编译选项照样重编,这比 make 的裸时间戳机制精细一档.

精细到什么程度,实测三组数据,-j8:

场景编译链接步数耗时
touch kernel/sched/core.c639.4 s
什么都不改012.3 s
touch include/linux/sched.h117701194 s

三个数字各有话说.改一个 .c,真正编的只有它自己加少量生成物,39 秒里大头是 vmlinux 重新链接.什么都不改也要 12 秒,是 Kbuild 全树遍历比对的开销,树大就是这价.最狠的是头文件:sched.h 几乎被半个内核 include,一步摸出 11770 个编译动作,20 分钟没了.所以内核开发圈的经验法则 — 攴动接口才动头文件,能放 .c 的别放 .h,头文件里少 include 别的头文件 — 不是玄学,是这 20 分钟换来的.反过来 CONFIG 符号级别的改动通常温和得多,因为 fixdep 记的依赖精确到符号,不碰那个符号的文件不理你.头文件依赖和配置依赖是两条不同的依赖通道,别混为一谈.

编出来两份产物.vmlinux 是带符号表带调试信息的 ELF,给链接器和 gdb 用;Image 是引导加载器真正搬运的裸二进制.转换就一行规则,arch/arm64/boot/Makefile:17:

OBJCOPYFLAGS_Image := -O binary -R .note -R .note.gnu.build-id -R .comment -S

objcopy 抽出可加载段,-S 剥符号.两份构建的对比很说明问题:板上内核 vmlinux 23MB,Image 17.9MB;给 QEMU 编的 defconfig 开了 DEBUG_INFO(DWARF),vmlinux 直接 159MB,但 Image 41MB — 调试信息只住在 vmlinux 里,objcopy 时全剥掉,运行时根本不需要.要小要大的诉求可以同时满足:Image 追求小,vmlinux 留着肥,一个给板子烧一个给 gdb 用.顺带一提 arm64 没有 x86 那套 bzImage 自解压流程,Image 就是裸的,压缩是 bootloader 或 EFI zboot 那边的事.

vmlinux 里还藏着一张表:kallsyms,全部符号和地址编译期嵌进内核映像,运行时 /proc/kallsyms 读的就是它.另一份 System.map 是链接期生成的文本表.两个应该一致 — 前提是 KASLR 没动.实测拿板上内核对:firefly 构建树 System.map 里 start_kernel 在 ffff800080da07dc,ssh 到板上 grep /proc/kallsyms,start_kernel 还是 ffff800080da07dc,分毫不差.这一个对照说明两件事:板上跑的就是本地这份构建;以及这次启动内核没被 KASLR 平移(kallsyms 给的是运行时真地址,滑了就对不上).uboot 没喂 kaslr-seed,熵不够,内核就老实待在链接地址.以后看 Oops 地址,先对 System.map,对不上先想 KASLR,再想是不是板子换了内核.

最后说模块怎么防"错配".模块是独立编译的 ELF,加载时内核要确认它真是给"这个"内核编的.第一道叫 vermagic,一串拼接的版本指纹,include/linux/vermagic.h 里定义.有意思的细节:里面那段 preempt 不是总有,代码写得很明白,CONFIG_PREEMPT_BUILD 或 PREEMPT_RT 才拼上.所以板上的 PREEMPT_VOLUNTARY 内核,模块 vermagic 是 "6.18.56 SMP mod_unload aarch64",而给 QEMU 编的 defconfig 是全抢占,拼出来 "6.18.56 SMP preempt mod_unload aarch64" — 同一份源码,一个词之差.

拿 QEMU 树编的 hello.ko 到板上 insmod,现场教学:

insmod: ERROR: could not insert module hello.ko: Invalid module format

dmesg 里才是真正的拒绝理由:

module hello: .gnu.linkonce.this_module section size must match the kernel's
built struct module size at run time

这是比 vermagic 更硬的一道闸:模块里内嵌的 struct module(驱动作者填 name,init 那个结构体的壳)必须和内核的 struct module 布局完全一致,而 struct module 里一堆字段是 CONFIG 条件编的 — 配置不同,结构体大小就不同.所以"同一个内核版本"从来不等于模块能混用,版本 + 配置 + 工具链,一个都不能差.Invalid module format 这个 errno 是个大筐,vermagic 不对,struct 大小不对,架构不对都往里装,排查时别猜,dmesg 会把具体原因打出来.老内核还有第三道 CONFIG_MODVERSIONS 的符号 CRC 校验,这套板上和 QEMU 都没开,碰到开了的发行版内核,跨版本模块连同名符号都得 CRC 一致才放行.

最后留几个容易忽略的细节:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注