2. 交叉编译工具链构建

说实话,很多初学者拿到内核源码第一反应就是懵。几万个文件,几百个目录,从哪看起?我当年也一样,对着 linux-2.6 的 tarball 发呆了半天。后来做项目多了,慢慢摸清了门道——内核源码的结构其实很有规律,你只要抓住几个关键目录和两套核心机制,剩下的就是按图索骥。

2.1 内核源码目录:别被数量吓到

先看顶层目录。我习惯用 tree -L 1 扫一眼,大概十几个目录。真正核心的,其实就下面这几个:

目录 干什么的 我常去翻什么
arch/ 体系结构相关代码 arch/arm/、arch/arm64/,板级文件、启动代码都在这里
drivers/ 设备驱动,最大的一块 drivers/i2c/、drivers/spi/、drivers/net/,驱动开发的主战场
fs/ 文件系统实现 fs/ext4/、fs/proc/、fs/sysfs/,虚拟文件系统也在这
kernel/ 核心调度、进程管理 一般不动,除非改调度策略
mm/ 内存管理 调 OOM 或者改内存分配策略时会看
net/ 网络协议栈 TCP/IP、Netfilter 都在这里
include/ 全局头文件 include/linux/ 下的 device.h、platform_device.h 是常客
Documentation/ 内核文档 遇到不懂的配置项,先来这里翻翻

嗯,这里要注意:arch/ 和 drivers/ 是嵌入式工程师最常打交道的两个目录。你想想看,移植内核到一块新板子,改的基本就是 arch/arm/mach-xxx/ 下的板级文件;写驱动,基本就在 drivers/ 下新建目录。

我的习惯:拿到一份新内核源码,先 cd arch/arm/configs/ 看看有没有现成的 defconfig 文件。如果有,直接拿来做基础配置,能省不少事。

2.2 Kconfig 与 Makefile:内核的“骨架”和“肌肉”

内核源码能组织得这么清晰,靠的就是两套机制:Kconfig 和 Makefile。说白了,Kconfig 负责“有什么选项”,Makefile 负责“怎么编译”。

2.2.1 Kconfig:配置菜单的“剧本”

每个支持配置的目录下都有一个 Kconfig 文件。它定义了 menuconfig 里看到的菜单项、依赖关系和帮助信息。举个例子:

config CONFIG_MY_DRIVER
    tristate "My custom driver support"
    depends on I2C
    help
      This is a custom driver for my sensor.
      Say Y to build it into kernel, M for module.

这里 tristate 表示三种状态:Y(编入内核)、M(编译为模块)、N(不编译)。depends on I2C 表示这个驱动依赖 I2C 子系统——如果 I2C 没开,这个选项就灰掉。

我曾经踩过的坑:写了一个 SPI 驱动,Kconfig 里忘了写 depends on SPI。结果 menuconfig 里选项亮着,选了 Y 编译直接报错,说找不到 SPI 核心头文件。查了半天才发现是依赖没写。所以,依赖关系一定要写清楚。

2.2.2 Makefile:告诉编译器怎么干活

同目录下的 Makefile 则负责把 Kconfig 的配置翻译成编译指令:

obj-$(CONFIG_MY_DRIVER) += my_driver.o

如果 CONFIG_MY_DRIVER 为 y,就编译进内核;为 m,就编译成 .ko 模块;为 n,就不编译。就这么简单。

多文件驱动也很常见:

obj-$(CONFIG_MY_DRIVER) += my_driver.o
my_driver-objs := main.o i2c_ops.o sensor_core.o

这样就把三个 .c 文件打包成一个驱动模块。

2.3 menuconfig:内核配置的“控制面板”

配置内核,我几乎只用 make menuconfig。它基于 ncurses 的文本界面,在终端里就能操作,不需要图形桌面。

基本操作流程:

  1. 加载默认配置:make defconfig 或 make ARCH=arm xxx_defconfig
  2. 进入菜单:make menuconfig(如果交叉编译,加 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-)
  3. 导航:方向键移动,回车进入子菜单,空格/Y/M/N 切换选项状态
  4. 搜索:按 / 键输入关键词,快速定位配置项
  5. 保存退出:生成 .config 文件
核心技巧:按 / 搜索时,会显示该配置项被哪些其他配置依赖,以及依赖了哪些配置。调试驱动时,如果某个选项灰掉,用搜索功能查依赖链,基本能定位问题。

我个人的工作流是这样的:先 make ARCH=arm myboard_defconfig 加载板级默认配置,然后 make menuconfig 微调,把需要的驱动选上,不需要的关掉。最后 diff .config .config.old 看看改了哪些,心里有数。

2.4 内核编译与烧录:从源码到跑起来

配置好了,接下来就是编译。嵌入式环境基本都是交叉编译:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
make -j4 zImage
make -j4 modules
make -j4 dtbs

这里 -j4 表示 4 线程并行编译,能快不少。编译产物:

  • arch/arm/boot/zImage:压缩后的内核镜像
  • arch/arm/boot/dts/xxx.dtb:设备树二进制文件
  • 各个目录下的 .ko 文件:内核模块

烧录方式取决于板子。我常用的几种:

方式 适用场景 命令示例
TFTP 网络加载 开发调试阶段,频繁改内核 tftp 0x82000000 zImage; bootz 0x82000000
SD 卡烧录 量产或现场部署 dd if=zImage of=/dev/sdb1
fastboot Android 或支持 fastboot 的板子 fastboot flash boot zImage
JTAG/SWD 裸机或 bootloader 损坏时救砖 配合 OpenOCD 或 JLink 工具
调试阶段我推荐 TFTP:编译完直接 cp zImage /tftpboot/,在 U-Boot 里 tftp 加载,10 秒就能看到新内核启动。不用反复插拔 SD 卡,效率高很多。

烧录完重启,串口输出里看到 Starting kernel ... 然后进入文件系统,嗯,这感觉就对了。

2.5 本章知识结构图

下面这张图把内核源码结构、配置机制和编译流程串起来了,你可以对照着看:

Linux 内核源码结构·配置·编译·烧录 总览 内核源码顶层目录 arch/ drivers/ fs/ include/ kernel/ net/ Kconfig + Makefile Kconfig Makefile 定义配置选项 定义编译规则 menuconfig 生成 .config make zImage / .ko / .dtb 烧录到目标板 TFTP SD卡/fastboot 💡 核心流程总结 1. 源码目录按功能划分,arch/ 和 drivers/ 是嵌入式开发主战场 2. Kconfig 定义配置选项,Makefile 定义编译规则,两者配合驱动整个构建系统 3. menuconfig 生成 .config,make 编译出镜像/模块/设备树,最后烧录到板子

从源码目录到 Kconfig/Makefile,再到 menuconfig 配置、make 编译、最后烧录运行,这条链路就是嵌入式 Linux 系统构建的核心流程。你把这个链条跑通了,后面写驱动、调系统,心里就有底了。