Linux 内核裁剪与移植

📅 2026/8/3 10:23:16 👁️ 阅读次数 📝 编程学习
Linux 内核裁剪与移植

Linux 内核裁剪与移植

作者:Leilei Sun
日期:2026-07-23
适用读者:嵌入式 Linux 开发者、BSP 工程师、驱动工程师


目录

  1. 为什么要做内核裁剪与移植?
  2. 内核裁剪与移植的原理依据
  3. 内核裁剪的具体步骤
  4. 内核移植的具体步骤
  5. 验证方法
  6. 最佳实践与常见陷阱

1. 为什么要做内核裁剪与移植?

1.1 资源约束 —— 嵌入式设备的内存和存储非常有限

Linux 主线内核的全量编译产物(allyesconfig)vmlinux 镜像超过500MB,即使defconfig也通常有8~15MB。而嵌入式设备的 Flash 和 RAM 都非常紧张:

注:嵌入式设备中 Flash 和 RAM 的大小关系取决于 Flash 类型。

  • NOR Flash(支持 XIP 片上执行):价格昂贵、容量小(通常 ≤ 256MB),通常 RAM > Flash
  • NAND / eMMC(块读写):价格便宜、容量大(GB 级),通常 Flash >> RAM,类似 PC 的硬盘/内存关系
┌────────────────────────────────────────────────────────────────┐ │ 嵌入式设备典型资源 —— NOR Flash 场景 │ ├──────────────┬────────────┬──────────┬──────────┬─────────────┤ │ 设备类型 │ Flash (NOR)│ RAM │ 允许内核 │ Flash vs RAM │ ├──────────────┼────────────┼──────────┼──────────┼─────────────┤ │ IoT 传感器 │ 8MB │ 32MB │ < 2MB │ Flash < RAM │ │ 家用路由器 │ 16MB │ 64MB │ < 3MB │ Flash < RAM │ │ 工业控制板 │ 64MB │ 256MB │ < 5MB │ Flash < RAM │ │ 高端交换机 │ 256MB │ 2GB │ < 8MB │ Flash < RAM │ └──────────────┴────────────┴──────────┴──────────┴─────────────┘ ┌────────────────────────────────────────────────────────────────┐ │ 嵌入式设备典型资源 —— eMMC / NAND Flash 场景 │ ├──────────────┬────────────────┬──────────┬──────────┬─────────┤ │ 设备类型 │ Flash (eMMC) │ RAM │ 允许内核 │ 关系 │ ├──────────────┼────────────────┼──────────┼──────────┼─────────┤ │ 智能手机 │ 128GB │ 12GB │ < 20MB │ Flash >> RAM │ │ 车载信息娱乐 │ 32GB │ 4GB │ < 12MB │ Flash >> RAM │ │ ARM 开发板 │ 8GB (SD 卡) │ 512MB │ < 8MB │ Flash >> RAM │ │ x86 PC (对比) │ 256GB+ │ 16GB+ │ 15MB │ Flash >> RAM │ └──────────────┴────────────────┴──────────┴──────────┴─────────┘

无论哪种场景,留给内核的存储空间都极其有限(最后一列),必须裁剪。

必须裁剪,否则内核放不进 Flash,或者占满了存储导致 rootfs 没空间。

1.2 按需取舍 —— 不需要的功能不该编译进去

一块 ARM Cortex-A7 的工业控制板上有: ✅ UART (调试串口) ✅ Ethernet (工业以太网) ✅ SPI NOR Flash (存储) ✅ GPIO (控制继电器) ❌ SATA (没有硬盘) ❌ USB Host (不需要) ❌ WiFi (有线就够了) ❌ Sound Card (没声卡) ❌ GPU (没显示器) ❌ Bluetooth (不需要)

不裁剪的后果:浪费 Flash 空间、增加启动时间、引入不必要的安全漏洞面、增加内存占用(某些子系统启动时会申请内存)。

1.3 启动速度 —— 内核越小,启动越快

内核镜像 2MB: 解压 + 启动 ≈ 0.5 秒 内核镜像 8MB: 解压 + 启动 ≈ 2.0 秒 内核镜像 15MB: 解压 + 启动 ≈ 4.0 秒

对于要求上电 2 秒内进入工作状态的工业设备,必须裁剪。

1.4 安全性 —— 减少攻击面

每个编译进内核的子系统都是一条潜在的攻击路径。裁剪掉不需要的网络协议、文件系统、驱动模块,就消除了这些模块可能存在的漏洞。

1.5 移植的必要性 —— 换一块 SoC,所有东西都变了

旧板子: NXP i.MX6ULL (ARM Cortex-A7) 新板子: Allwinner T113 (ARM Cortex-A7 双核 + RISC-V) 虽然都是 ARM,但: - 内存映射完全不同 (DDR 起始地址变了) - 中断控制器不同 (GICv2 → GICv2 但寄存器布局不同) - 时钟树完全不同 (CCM 寄存器全变了) - 外设 IP 不同 (UART 驱动能复用, 但 base address 变了) - 启动流程不同 (BootROM → SPL → U-Boot → kernel)

移植 = 让同一套 Linux 内核在新的硬件平台上跑起来。


2. 内核裁剪与移植的原理依据

2.1 Kconfig / Kbuild 体系 —— 裁剪的核心机制

Linux 内核的编译系统是一个三层结构

┌──────────────────────────────────────────────────────┐ │ 用户执行: make menuconfig │ │ ↓ │ │ Kconfig 文件 (分散在 ~1800 个目录中) │ │ 定义每个选项的: 名称、类型、依赖、帮助文本 │ │ ↓ │ │ .config 文件 (最终输出) │ │ CONFIG_NET=y │ │ CONFIG_USB=m │ │ # CONFIG_SOUND is not set │ │ ↓ │ │ Makefile (顶层 + 各子目录) │ │ obj-$(CONFIG_NET) += net/ │ │ obj-$(CONFIG_USB) += usb/ │ │ ↓ │ │ 编译产物: vmlinux / zImage / modules │ └──────────────────────────────────────────────────────┘

例如drivers/usb/Makefile

obj-$(CONFIG_USB) += usb.o obj-$(CONFIG_USB_EHCI_HCD) += host/ehci-hcd.o

.configCONFIG_USB=y,变量展开为obj-y,usb.o 编入内核。
.config# CONFIG_USB is not set,变量展开为obj-n,不编译。

2.2 设备树(Device Tree)—— 移植的核心机制

设备树是将硬件描述从内核代码中分离出来的机制:

┌──────────── 没有设备树的时代 (ARM Linux < 3.x) ────────────┐ │ arch/arm/mach-imx/board-mx6q-sabresd.c │ │ → 硬编码: UART 基址 0x021E8000, 寄存器布局, 引脚配置 │ │ → 每块板子一个 C 文件,内核里塞了几百个板级文件 │ │ → 换块板子 = 改 C 代码 = 重新编译内核 │ └────────────────────────────────────────────────────────────┘ ┌──────────── 设备树时代 (ARM Linux ≥ 3.x) ──────────────────┐ │ arch/arm/boot/dts/imx6q-sabresd.dts │ │ → 描述硬件: UART 基址 0x021E8000, 引脚 pinctrl, 时钟 │ │ → 内核只包含驱动代码,不包含硬件数据 │ │ → 换块板子 = 换个 .dtb 文件 = 内核代码不用改! │ └────────────────────────────────────────────────────────────┘

设备树源码(.dts)的结构:

/dts-v1/; / { compatible = "fsl,imx6q-sabresd", "fsl,imx6q"; #address-cells = <1>; #size-cells = <1>; memory@10000000 { device_type = "memory"; reg = <0x10000000 0x40000000>; /* 1GB DDR3 */ }; soc { uart1: serial@021e8000 { compatible = "fsl,imx6q-uart", "fsl,imx21-uart"; reg = <0x021e8000 0x4000>; /* 寄存器基址 + 大小 */ interrupts = <0 27 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks 160>, <&clks 161>; status = "okay"; }; }; };

内核启动时,bootloader 把.dtb传给内核,内核动态解析设备树并初始化对应的硬件。

2.3 内核模块化机制 —— y / m / n 三元选择

┌──────────────────────────────────────────────────────┐ │ CONFIG_XXX=y → 编入内核镜像 (built-in) │ │ 占用 Flash, 不可卸载, 启动即加载 │ │ 适用: 串口驱动, 根文件系统驱动 │ ├──────────────────────────────────────────────────────┤ │ CONFIG_XXX=m → 编译为独立模块 (.ko) │ │ 在 rootfs 中存储, 按需 insmod 加载 │ │ 适用: 非必需驱动, 调试用 │ ├──────────────────────────────────────────────────────┤ │ # CONFIG_XXX is not set │ │ → 完全不编译 │ │ 适用: 这个板子没有的硬件 │ └──────────────────────────────────────────────────────┘

裁剪策略:能裁掉就裁掉(=n),必须开机就用的选 =y,偶尔用的选 =m

2.4 架构抽象层(arch/)—— 跨 CPU 的隔离

linux/ ├── arch/ │ ├── arm/ ← ARM 32bit (Cortex-A7, A9, etc.) │ │ ├── boot/dts/ ← 板级设备树 │ │ ├── mach-*/ ← 历史遗留的板级代码 (逐步被 DTS 替代) │ │ ├── kernel/ ← ARM 特有的异常处理、上下文切换 │ │ └── Kconfig ← ARM 架构特有的配置选项 │ ├── arm64/ ← ARM 64bit (Cortex-A53, A72, etc.) │ ├── riscv/ ← RISC-V │ ├── x86/ │ └── ... ├── drivers/ ← 驱动代码 (跨架构复用) ├── kernel/ ← 核心调度、信号、定时器等 └── include/ ← 头文件

移植时主要改动在arch//boot/dts/drivers/中新增驱动。

常见疑问:arch/arm64/arch/riscv/arch/x86/等其他架构目录可以删除吗?

编译层面:不需要。设置ARCH=arm后,Kbuild 只编译arch/arm/下的代码,其他架构目录完全不会被编译进内核镜像。内核镜像的大小取决于编译了什么,而不是源码树里有多少文件。

源码层面:技术上可以删(不会影响ARCH=arm的编译),但不推荐——会破坏git status、丢失未来移植到其他架构的能力,且节省的磁盘空间有限(arch/ 目录在完整源码中占比不大)。


3. 内核裁剪的具体步骤

3.1 获取内核源码

# 方式一: kernel.org 主线wgethttps://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xztarxf linux-6.6.tar.xz&&cdlinux-6.6# 方式二: SoC 厂商 SDK (推荐, 厂商已打好基础补丁)# 例如 NXP: git clone https://github.com/nxp-imx/linux-imx.git# 方式三: 你的 Ubuntu 虚拟机上的当前内核源码aptsourcelinux-image-$(uname-r)

3.2 选择基准配置文件

不需要自己写脚本。内核源码树的arch/<arch>/configs/目录下已经自带了大量参考配置文件:

# 看看 ARM 架构有哪些现成的配置文件lsarch/arm/configs/# 输出示例:# multi_v7_defconfig ← ARMv7 多平台通用配置# imx_v6_v7_defconfig ← NXP i.MX6/7 专用# sunxi_defconfig ← Allwinner 专用# exynos_defconfig ← Samsung Exynos 专用# ...几十个...

操作步骤(一步到位,不需要脚本):

# 1. 设置环境变量 (本机编译可省略 ARCH 和 CROSS_COMPILE)exportARCH=armexportCROSS_COMPILE=arm-linux-gnueabihf-# 2. 选择合适的 defconfig 生成 .configmakemulti_v7_defconfig# 方式一: 架构通用配置# 或makeimx_v6_v7_defconfig# 方式二: SoC 厂商提供的配置# 或 (在 x86 虚拟机上做裁剪实验)cp/boot/config-$(uname-r).config# 方式三: 复制当前运行内核的配置makeolddefconfig# 把旧配置适配到当前内核版本

可选:创建一个build.sh避免每次手输环境变量。这不是必须的,但可以方便反复编译:

#!/bin/bashexportARCH=armexportCROSS_COMPILE=arm-linux-gnueabihf-make-j$(nproc)"$@"

之后用./build.sh zImage替代make zImage

3.3make menuconfig逐项裁剪

# 需要安装 ncursessudoaptinstalllibncurses5-dev flex bison# 启动图形化配置界面makemenuconfig
┌────────────────── Linux/arm 6.6.0 Kernel Configuration ──────────────────┐ │ Arrow keys navigate the menu. <Enter> selects submenus ---> │ │ Press <Y> to include, <M> to module, <N> to exclude. │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ General setup ---> │ │ │ │ [*] Enable loadable module support ---> │ │ │ │ -*- Enable the block layer ---> │ │ │ │ Processor type and features ---> │ │ │ │ Power management options ---> │ │ │ │ Bus support ---> │ │ │ │ Executable file formats ---> │ │ │ │ Memory Management options ---> │ │ │ │ [*] Networking support ---> │ │ │ │ Device Drivers ---> │ │ │ │ File systems ---> │ │ │ │ Security options ---> │ │ │ │ -*- Cryptographic API ---> │ │ │ │ Library routines ---> │ │ │ │ Kernel hacking ---> │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘

3.4 裁剪策略 —— 从大到小,五轮进行

第 1 轮 — 子系统级 (收益最大, 一轮可裁掉 30%~50%) ├── General Setup: 关闭 cgroups (如果不用容器)、关闭 namespace ├── Networking: 关闭 Hamradio / NFC / CAN / Wireless / Bluetooth ├── File Systems: 关闭 NFS / CIFS / XFS / Btrfs / FUSE (只留 ext4 和 tmpfs) ├── Device Drivers: 关闭 SATA / SCSI / USB / Sound / GPU / Infiniband └── Security: 关闭 SELinux / AppArmor / IMA 第 2 轮 — 驱动级 ├── Network device support: 只保留目标板子的网卡驱动 ├── Input device support: 关闭触摸屏、游戏手柄 (嵌入式通常没有) ├── MTD (Memory Technology Device): 只保留目标 Flash 类型 └── HID: 关闭 USB HID (如果没有 USB) 第 3 轮 — 特性级 ├── Kernel hacking: 关闭 KGDB / KDB / debug info ├── Profiling: 关闭 OProfile / perf events (不对客户开放) └── Printk: 只保留 KERN_ERR 以上级别 (CONFIG_CONSOLE_LOGLEVEL) 第 4 轮 — 模块化 ├── 不常用的驱动: =m → 编为 .ko, insmod 时加载 └── 根文件系统驱动: 保持 =y (否则无法挂载 rootfs) 第 5 轮 — 验证 ├── 编译 → 烧录 → 启动 → 看日志 → 看镜像大小 └── 如果裁剪过度: 加回来, 重复

3.5 编译

不需要自己写脚本。内核自带的顶层 Makefile 就是完整的构建系统。裁剪完成后,几条make命令即可:

# ====== 编译内核镜像 ======# 前提: 已完成 ARCH 和 CROSS_COMPILE 的设置 (同 3.2 节)# 在 x86 虚拟机本机编译时不需要设置这两个变量make-j$(nproc)# 编译所有 (vmlinux + modules + dtbs)# 或分步编译:make-j$(nproc)zImage# ARM 32bit: 生成 arch/arm/boot/zImagemake-j$(nproc)Image.gz# ARM 64bit: 生成 arch/arm64/boot/Image.gz# ====== 编译内核模块 ======make-j$(nproc)modules# 编译所有 =m 的内核模块 (.ko 文件)# ====== 安装模块到 rootfs ======makemodules_installINSTALL_MOD_PATH=/path/to/rootfs# 模块会被安装到 /path/to/rootfs/lib/modules/<kernel-version>/# ====== 编译设备树 ======makedtbs# 编译所有设备树 (.dts → .dtb)# 产物在 arch/arm/boot/dts/*.dtb

产物在哪里?

架构内核镜像设备树
ARM 32bitarch/arm/boot/zImagearch/arm/boot/dts/<your-board>.dtb
ARM 64bitarch/arm64/boot/Image(或Image.gz)arch/arm64/boot/dts/<vendor>/<your-board>.dtb

编译时间参考:全量编译(defconfig)在 8 核 CPU 上约 5~15 分钟。裁剪后(大量 =n)可缩短到 1~3 分钟。

3.6 常见裁剪项速查表

裁剪项Kconfig 选项典型收益
无线协议栈CONFIG_WIRELESSCONFIG_BT~500KB
USB 子系统CONFIG_USB_SUPPORT~1.5MB
声卡CONFIG_SOUND~800KB
GPU/DRMCONFIG_DRM~3MB
调试信息CONFIG_DEBUG_INFO~100MB(vmlinux)
内核调试CONFIG_KGDBCONFIG_KDB~200KB
文件系统 (XFS/Btrfs/NFS)CONFIG_XFS_FS~1MB/个
SELinuxCONFIG_SECURITY_SELINUX~400KB
IPv6CONFIG_IPV6~300KB
模块签名CONFIG_MODULE_SIG~100KB
内核压缩CONFIG_KERNEL_GZIP/LZMA/XZXZ 比 gzip 小 ~20%

3.7 裁剪效果示例

┌────────────────┬──────────┬──────────┬──────────────┐ │ 配置类型 │ zImage │ modules │ 说明 │ ├────────────────┼──────────┼──────────┼──────────────┤ │ multi_v7_defconfig │ 6.1MB │ 15MB │ 全功能 ARM │ │ NXP i.MX6 参考 │ 4.2MB │ 8MB │ 厂商裁剪后 │ │ 手工裁剪后 │ 2.1MB │ 2MB │ 按需裁剪 │ │ 极致裁剪 (IoT) │ 0.8MB │ 0KB │ 单功能设备 │ └────────────────┴──────────┴──────────┴──────────────┘

4. 内核移植的具体步骤

4.1 移植前的准备工作

在动手之前,必须拿到以下资料:

┌─────────────────────────────────────────────────────┐ │ 必备资料 │ ├──────────────────────────────┬──────────────────────┤ │ SoC Reference Manual │ 寄存器地址、时钟、中断 │ │ Board Schematic │ 引脚连接、外设型号 │ │ DDR Datasheet │ 内存起始地址和大小 │ │ 交叉编译工具链 │ arm/arm64/riscv gcc │ │ Bootloader (U-Boot) 支持 │ 至少能加载 kernel │ └──────────────────────────────┴──────────────────────┘

4.2 编写设备树 (.dts)

这是移植中最重要的一步。一个典型的新板级设备树:

/dts-v1/; #include "soc-vendor.dtsi" /* 包含 SoC 公用的节点定义 */ / { model = "My Company MyBoard v1.0"; compatible = "mycompany,myboard", "vendor,soc-model"; /* 1. 内存描述 */ memory@80000000 { device_type = "memory"; reg = <0x80000000 0x20000000>; /* 512MB DDR3 */ }; /* 2. 选择启动用的串口 */ chosen { stdout-path = "serial0:115200n8"; bootargs = "console=ttyS0,115200 earlyprintk"; }; /* 3. LED (用 GPIO 控制) */ leds { compatible = "gpio-leds"; status_led: led-0 { label = "status"; gpios = <&gpio3 12 GPIO_ACTIVE_HIGH>; }; }; }; /* 4. 使能 SoC 上要用到的外设 (在 .dtsi 中默认为 disabled) */ &uart1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart1>; status = "okay"; /* ← 关键: 从 disabled 改为 okay */ }; &i2c1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c1>; status = "okay"; /* 挂在 I2C 总线上的设备 */ eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; }; }; &usdhc2 { /* eMMC / SD 卡 */ pinctrl-names = "default"; pinctrl-0 = <&pinctrl_usdhc2>; bus-width = <8>; no-1-8-v; non-removable; status = "okay"; }; &fec1 { /* 以太网 */ pinctrl-names = "default"; pinctrl-0 = <&pinctrl_enet>; phy-mode = "rgmii"; phy-reset-gpios = <&gpio1 25 GPIO_ACTIVE_LOW>; status = "okay"; };

4.3 在 arch/ 下注册新板子

# ARM 32bitvimarch/arm/boot/dts/Makefile# 添加一行:dtb-$(CONFIG_SOC_VENDOR)+=myboard.dtb# ARM 64bitvimarch/arm64/boot/dts/vendor/Makefile dtb-$(CONFIG_ARCH_VENDOR)+=myboard.dtb

4.4 移植关键子系统

这是最需要底层功底的部分,按优先级排序:

优先级 1 — 串口 (UART) └── 有了串口, 才能看 boot log, 才能调试后续工作 └── 配置 pinctrl (引脚复用) + 时钟 + UART 驱动 优先级 2 — 中断控制器 (GIC / NVIC) └── 几乎所有驱动都依赖中断 └── 通常在 SoC 的 .dtsi 中已定义, 确认 compatible 匹配即可 优先级 3 — 时钟控制器 (CCF: Common Clock Framework) └── 所有外设驱动都依赖时钟 └── 移植 clk 驱动, 通常由 SoC 厂商提供 优先级 4 — 定时器 (ARM Generic Timer / SoC 私有定时器) └── 调度器的"心跳", 一定要有 └── ARMv7/v8 通常用 arch timer, 配置 device tree 即可 优先级 5 — pinctrl (引脚复用) └── 没有 pinctrl, 所有外设的 GPIO 功能都是错的 └── 通常由 SoC 厂商提供驱动, 只需在 .dts 中添加 pinmux 配置 优先级 6 — 存储控制器 (eMMC / NAND / SPI Flash) └── rootfs 在上面, 必须通

4.5 交叉编译工具链配置

# 下载工具链 (以 ARM 为例)# Linaro: https://releases.linaro.org/components/toolchain/binaries/wgethttps://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xztarxf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz# 设置环境变量exportPATH=/path/to/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATHexportARCH=armexportCROSS_COMPILE=arm-linux-gnueabihf-# 验证arm-linux-gnueabihf-gcc--version

4.6 生成可启动镜像

# 编译make-j$(nproc)# ARM 32bit:# 产物在 arch/arm/boot/# zImage (可自解压内核)# dts/*.dtb (设备树)# ARM 64bit:# 产物在 arch/arm64/boot/# Image (未压缩内核)# Image.gz (压缩内核)# dts/vendor/*.dtb# 最终烧录到板子上 (以 SD 卡为例):# fat32 分区: zImage + myboard.dtb# ext4 分区: rootfs (含 /lib/modules/)

4.7 整个移植过程的架构图

┌──────────────────────────────────────────────────────┐ │ 移植工作流 │ ├──────────────────────────────────────────────────────┤ │ │ │ 拿到板子 → 查阅 Datasheet → 编写 .dts │ │ ↓ ↓ │ │ 确认交叉工具链 注册到 arch/.../dts/Makefile│ │ ↓ ↓ │ │ make defconfig → make menuconfig (裁剪) │ │ ↓ │ │ make zImage + make dtbs │ │ ↓ │ │ 烧录到板子 → 串口看 boot log │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ earlyprintk 有输出? │ │ │ │ ├── YES → 继续 │ │ │ │ └── NO → 检查 UART pinctrl/时钟│ │ │ └─────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ 挂载 rootfs 成功? │ │ │ │ ├── YES → 继续 │ │ │ │ └── NO → 检查存储驱动/文件系统 │ │ │ └─────────────────────────────────┘ │ │ ↓ │ │ 逐个调通外设驱动 → 完成! │ │ │ └──────────────────────────────────────────────────────┘

5. 验证方法

5.1 串口输出验证(最重要的第一步)

# 用 minicom / picocom / putty 连接板子串口# 波特率: 115200, 8N1, 无硬件流控# 上电后应该看到的最早输出:## Booting kernel from Legacy Image at 80800000 ...## Image Name: Linux-6.6.0## Data Size: 2156480 Bytes = 2.1 MB## Starting kernel ...#### [ 0.000000] Booting Linux on physical CPU 0x0## [ 0.000000] Linux version 6.6.0 (user@host) (gcc version 12.3.0)## [ 0.000000] CPU: ARMv7 Processor [410fc075] revision 5 (ARMv7)## [ 0.000000] OF: fdt: Machine model: My Company MyBoard v1.0#### ← 看到 "Machine model" 与你的 .dts 中 model 属性一致 = 设备树加载成功!

5.2 内核启动日志分析

# 进入系统后dmesg|less# 关键检查项:dmesg|grep-i"error\|fail\|warn"# 查找异常dmesg|grep"Machine model"# 确认设备树正确dmesg|grep"Memory policy"# 确认内存大小正确dmesg|grep"root"# 确认 rootfs 挂载cat/proc/meminfo# 可用内存cat/proc/mtd# 已识别的 MTD 分区

5.3 设备树验证

# 检查内核解析后的设备树ls/proc/device-tree/cat/proc/device-tree/compatible# 应输出 "mycompany,myboard..."cat/proc/device-tree/memory@80000000/reg|xxd# 十六进制看内存地址和大小# 把 dtb 反编译回 dts, 检查是否正确dtc-Idtb-Odts-o/tmp/decompiled.dts /proc/device-tree

5.4 驱动加载验证

# 已加载模块lsmod# 已识别设备cat/proc/devices# 查看设备树中哪些节点成功绑定了驱动ls-l/sys/bus/platform/devices/ls-l/sys/class/net/# 网卡ls-l/sys/class/tty/# 串口

5.5 文件系统挂载验证

df-h# 看分区是否都挂载上了mount# 看挂载选项cat/proc/filesystems# 内核支持的文件系统列表

5.6 功能测试

# 网络ping-c38.8.8.8# GPIO (如果导出到 sysfs)echo12>/sys/class/gpio/exportechoout>/sys/class/gpio/gpio12/directionecho1>/sys/class/gpio/gpio12/value

5.7 镜像大小对比

# 各阶段产物大小对比ls-lhvmlinux# 未裁剪的 ELFls-lhvmlinux-o# 裁剪后的 ELFls-lharch/arm/boot/zImage# 最终烧录的镜像size vmlinux# 分析各段 (text/data/bss) 的占用# 更精细的分析: 用 bloat-o-meter (内核源码自带)scripts/bloat-o-meter vmlinux-old vmlinux-new# 输出示例:# add/remove: 0/145 grow/shrink: 2/15 up/down: 48/-125630 (-125582)# ← 裁剪掉了 122KB

6. 最佳实践与常见陷阱

6.1 裁剪过度的后果

症状: 内核编译成功, 但启动到某一步就 kernel panic 常见原因: ├── 关闭了 CONFIG_PRINTK → 没有任何内核日志 !! ├── 关闭了 CONFIG_BLOCK → 无法访问块设备 (eMMC/SD 卡) ├── 关闭了 CONFIG_TMPFS → systemd 无法创建 tmpfs (启动失败) ├── 关闭了 CONFIG_INOTIFY_USER → systemd 无法监控文件变化 ├── 关闭了 CONFIG_SYSFS → /sys 目录为空 (大量用户态工具失效) └── 关闭了 CONFIG_PROC_FS → /proc 目录为空 黄金规则: 不确定的选项保持 =y, 验证通过后再裁

6.2 Kconfig 依赖关系陷阱

# drivers/net/ethernet/Kconfig config NET_VENDOR_STMICRO bool "STMicroelectronics devices" default y depends on HAS_IOMEM # ← 如果目标架构没有 IOMEM, 这个驱动不会出现! config STMMAC_ETH tristate "STMicroelectronics 10/100/1000/EQOS Ethernet" depends on HAS_IOMEM && HAS_DMA select PHYLIB # ← 选中 STMMAC 会自动选中 PHYLIB select CRC32 # ← 自动选中 CRC32

陷阱 1:select可以绕过depends on—— 如果 AselectB, 但 B 的依赖条件不满足, Kconfig 会警告但可能被忽略。

陷阱 2:depends on缺失的依赖会导致编译错误 —— 比如某个驱动depends on NET, 网络中有一个 API, 关闭CONFIG_NET后驱动编译失败。

调试方法:

# 查看某个配置为什么被意外选中的完整依赖链makemenuconfig# 在选项上按 ? → 显示 Depends on 和 Selected by# 搜索配置依赖grep-r"select SOC_VENDOR"arch/arm/ drivers/|head-20

6.3 设备树与驱动匹配失败的调试

常见症状: 设备树中有节点, 但驱动没有 probe 诊断: 1. 检查 compatible 字符串是否与驱动的 of_match_table 完全一致 2. 检查 status = "okay" (不是 "ok" 或 "disabled") 3. 检查 pinctrl 是否正确 (引脚冲突或未配置) 4. 检查时钟是否正确使能 调试命令: cat /sys/kernel/debug/devices_deferred ← 列出延迟 probe 的设备

6.4 增量裁剪方法论

不要一次裁掉 50 个选项然后编译 → 烧录 → 启动失败 → 不知道哪个裁错了 正确做法: 第1次: 裁 5 项 → 编译 → 启动 → 验证 第2次: 裁 5 项 → 编译 → 启动 → 验证 ... 每次只改少量选项, 出了问题能快速定位。 进阶: 用 git 管理 .config 的变更 git add .config && git commit -m "enable USB, disable SATA" git diff HEAD~1 .config ← 精确看到改了哪些选项

6.5 总结要点

┌──────────────────────────────────────────────────────┐ │ Linux 内核裁剪与移植 核心要点 │ ├──────────────────────────────────────────────────────┤ │ │ │ 裁剪的三层原理: │ │ Kconfig (定义选项) ──► .config (选择) ──► Makefile │ │ │ │ 移植的两大支柱: │ │ 设备树 (.dts) —— 描述硬件"是什么" │ │ 驱动 (.c) —— 定义硬件"怎么用" │ │ │ │ 验证的三级递进: │ │ earlyprintk 有输出 → rootfs 能挂载 → 外设全正常 │ │ │ │ 黄金规则: │ │ 1. 不确定的选项不要裁 │ │ 2. 每次只改少量 │ │ 3. 用 git 追踪 .config 变更 │ │ 4. 先跑通再优化 │ │ │ └──────────────────────────────────────────────────────┘

参考文献

  • Linux Kernel Documentation: https://www.kernel.org/doc/html/latest/
  • Device Tree Specification: https://www.devicetree.org/specifications/
  • Bootlin Embedded Linux Training: https://bootlin.com/training/embedded-linux/
  • 《Linux 设备驱动开发详解》(宋宝华 著)