目录

我们先看一下整个流程逻辑:
make xxx_defconfig
│
▼
读取 configs/xxx_defconfig
(人为配置的最小默认配置)
│
▼
Kconfig 系统开始工作
├─ 收集所有 Kconfig 文件
│ Kconfig
│ arch/*/Kconfig
│ drivers/*/Kconfig
│ cmd/*/Kconfig
│
├─ 注册所有 CONFIG 符号
├─ 解析 depends on / select
├─ 计算 default 值
│
▼
生成完整的 .config
(所有 CONFIG 都有确定取值)
│
▼
include .config
(Makefile & 子 Makefile)
│
▼
Makefile 系统决定编译行为
├─ 选择 ARCH / CPU / SoC
├─ 选择 board 目录
├─ 选择 drivers / cmd
├─ 选择链接脚本 .lds
│
▼
各目录源码编译
*.c / *.S → *.o
│
▼
链接生成 u-boot(ELF)
(使用 u-boot.lds)
│
▼
objcopy / mkimage
│
▼
生成最终镜像
├─ u-boot.bin
├─ u-boot.img
├─ u-boot-dtb.bin
└─ spl/u-boot-spl.bin
uboot执行指令为:make xxx_deconfig,那么我们就是要搞清楚,这个make中它做了什么,怎么就得到了u-boot.bin文件。
1、deconfig
先看一下deconfig文件,它是编译入口,每一个 xxx_defconfig = 一块板子 / 一个 SoC 的默认配置,这个文件是我们自己人为配置,格式为CONFIG_xxx=y/n/m/str,决定了架构(ARM / ARM64)、是否启用 SPL、使用哪些驱动、命令、文件系统。
注意defconfig 不是源码,只是“开关清单”
2、Kconfig
Kconfig 是 配置解释器,在进入deconfig后会在makefile检测到其依赖与Kconfig文件,那我们来看看Kconfig是怎么回事,其格式如下:
config SYMBOL_NAME # 配置符号名(对应CONFIG_XXX)
type "prompt text" # 类型和提示文本
depends on DEPENDENCY # 依赖条件
select OTHER_SYMBOL # 反向选择
default VALUE # 默认值
range MIN MAX # 数值范围
help # 帮助文本
Detailed description...
分布在:Kconfig arch/*/Kconfigdrivers/*/Kconfig cmd/*/Kconfig
决定了哪些目录会参与编译、哪些 .c 文件会被加入
解析 defconfig,并结合所有 Kconfig 文件,生成一个“满足依赖关系、取值完整”的 .config。
即根据规则(Kconfig),把 defconfig 展开成一个“完整、合法、不冲突”的
.config,即“完整的 .config”。
最终 .config 具有这些特征:所有 CONFIG 都有值(y / n / m)、不违反依赖关系、默认项已补齐、可直接被 Makefile / C 代码使用。
3、.config
.config这个文件便是我们最终的配置文件,是由 defconfig + Kconfig 生成,是编译全过程的“总配置文件”,是 Makefile 实际读取的配置。
# 规则1:启用选项(编译进内核)
CONFIG_SMP=y
# 规则2:编译为模块
CONFIG_SOUND=m
# 规则3:禁用选项(两种等价写法)
CONFIG_DEBUG_FS=n
# 或者更常见的:
# CONFIG_DEBUG_FS is not set
# 规则4:设置字符串值
CONFIG_LOCALVERSION="-mykernel"
CONFIG_DEFAULT_HOSTNAME="(none)"
CONFIG_CMDLINE="console=ttyS0,115200"
# 规则5:设置整数值
CONFIG_LOG_BUF_SHIFT=18
CONFIG_NR_CPUS=8
# 规则6:设置十六进制值
CONFIG_PHYSICAL_START=0x1000000
4、 make menuconfig
make menuconfig,这个是在实际常使用到的,那么这个指令是做了什么呢,通过UI中的选择哪些文件被更改了呢。make menuconfig 实际上是,调用 scripts/kconfig/mconf后使用 ncurses UI最终以图形化方式修改 .config
它的本质是:
menuconfig =
.config的可视化编辑器
对的,就是直接对.config修改,不是对deconfig修改,可是又有疑问了,make xxx_deconfig下来不是会将.config进行重新覆盖吗,对的,会被覆盖,所以一般我们是先编写deconfig后进行make其一次,然后再进行make menuconfig进行UI的选择,即执行如下:
make xxx_defconfig # 生成 .config
make menuconfig # 可视化改 .config
make savedefconfig # 生成最小 defconfig
cp defconfig configs/xxx_defconfig
make # 编译
那么其核心实现逻辑是什么呢
make menuconfig 的真实执行链路:
当你在 U-Boot 源码目录执行:make menuconfig ,真实发生的是下面这条链路:
make menuconfig
↓
顶层 Makefile
↓
调用 scripts/kconfig/Makefile
↓
编译并执行 mconf
↓
mconf 读取 Kconfig
↓
mconf 读 / 写 .config
在 U-Boot 顶层 Makefile 中,menuconfig 是一个 伪目标(phony target),本质是:
menuconfig: $(Q)$(MAKE) -C scripts/kconfig menuconfig
也就是说:
顶层 Makefile 并不实现 menuconfig,只是“转发请求”。
真正执行的是这个程序:
scripts/kconfig/mconf
语言:C
UI:ncurses
来源:Linux Kconfig 系统(U-Boot 复用)
那么mconf 是怎么来的?第一次执行 make menuconfig 时,会发生两步:先编译 mconf 本身
scripts/kconfig/
├── mconf.c ← menuconfig 主程序
├── conf.c
├── symbol.c
├── menu.c
├── expr.c
├── util.c
└── Makefile
执行:gcc mconf.c symbol.c menu.c expr.c ...
生成:scripts/kconfig/mconf
再运行 mconf : ./scripts/kconfig/mconf Kconfig
那mconf 执行时到底做了什么?首先读取所有 Kconfig 文件
Kconfig arch/*/Kconfig drivers/*/Kconfig cmd/*/Kconfig
之后解析配置符号:config XXX、depends on、select、default
再加载当前 .config(如果存在)已配置项 → 显示当前状态,未配置项 → 使用 default。
如此你看到的菜单,就是 menu.c 动态生成的。
最后当你按 Save:直接写 .config,不修改 xxx_defconfig,不影响 Makefile。
5、makefile中文件逻辑
好的,现在我们清楚了这个配置文件的走向,我们再来看一下makefile中在载入最终配置文件.config后做了什么。
在makefile中是充斥着大量的变量,这些变量很多将是有.config中的配置来进行对应的赋值的,而这些变量便是为什么uboot可以适配多种型号的部分原因(另一个重要原因便是设备树的引入,一个负责内部,一个负责外设)。
(1)顶层的makefile
好了,在我们执行make是,顶层的makefile做了一个很关键的事情include .config,这一步的意义非常重要,它会将.config 中的所有 CONFIG_xxx 选项被转化为 Makefile 可直接使用的变量,这一步就是将变量变成其可以使用的符号了。
之后Makefile 做的第一件“分支决策”:先编 SPL 还是先编 U-Boot,在变量加载完成后,Makefile 会做 第一个也是最重要的判断:CONFIG_SPL 是否为 y ,如果:CONFIG_SPL = y,说明该平台需要 SPL(Secondary Program Loader)构建系统会 先进入 SPL 子构建流程,如果:CONFIG_SPL = n则 直接构建完整 U-Boot。这一步,决定了 工程是从 SPL 开始,还是直接从 U-Boot 开始。
CONFIG_SPL=y
↓
先编 spl/u-boot-spl
↓
再编完整 U-Boot
(2)SPL
那么这里说的SPL是什么呢。U-Boot 是一个多阶段 Bootloader,SPL 是其中负责最早期初始化的阶段,其一般在片内的SRAM运行,目的是加载 U-Boot,而U-Boot目的是加载Kernel,所有一句话说就是:
SPL(Secondary Program Loader)是一个“极简版的 U-Boot”,它的唯一使命是:把硬件环境准备好,然后把完整 U-Boot 拉起来。
所有,SPL 决定了“在 DRAM 尚不可用之前,系统如何完成最小初始化并加载下一阶段”,其实现逻辑便是在在这里被编译的。
(3)子目录 Makefile
在确定编译路径后,Makefile 会根据 .config 中的变量值:选择架构目录(arch/arm/ / arch/arm64/)、选择 CPU / SoC / board 相关路径、决定哪些子目录参与构建(drivers / cmd / net / fs 等)
接下来,Makefile 并不会“亲自编译源码”,而是:
递归进入各个子目录 Makefile,由它们决定具体的源码参与情况
U-Boot 的每一个功能模块,几乎都对应一个子 Makefile,例如:arch/arm/Makefile、common/Makefile、drivers/mmc/Makefile、cmd/Makefile
这些子 Makefile 中,最核心、最常见的规则形式是:
obj-$(CONFIG_MMC) += mmc.o #只有当
CONFIG_MMC=y时,mmc.c才会被编译成mmc.o
在加载变量值后,便会去找对应的文件地址找对应的文件,并通过子makefile文件中依赖关系进行编译所需的*.C/*.S->*.O等文件。
6、.lds链接阶段
链接阶段,.lds 决定 U-Boot 是否“能跑起来”。当所有需要参与编译的 .c / .S 文件都被编译为 .o 后,构建流程进入 链接阶段。
所有
.o文件会被统一链接,生成一个名为:u-boot
它是一个 ELF 格式文件,具有以下特点:是第一次“完整链接”的产物、包含全部代码段 / 数据段 / BSS、可用于 GDB 调试、不能直接烧录到 Flash
链接过程并不是随意拼接 .o,而是严格受 链接脚本(Linker Script) 控制。
常见位置:arch/arm/cpu/u-boot.lds
输入文件(object files) → 链接器脚本(linker script) → 输出文件(executable)
.text, .data, .bss ↓
section 布局规则
/* 注释风格与C类似 */
OUTPUT_FORMAT("elf32-littlearm") /* 输出文件格式 */
OUTPUT_ARCH(arm) /* 目标架构 */
ENTRY(_start) /* 入口点符号 */
/* 内存区域定义 */
MEMORY {
ROM (rx) : ORIGIN = 0x10000000, LENGTH = 1M
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 64M
}
/* 段(Section)布局定义 */
SECTIONS {
/* .text 段放在 ROM 区域 */
.text : {
*(.text.startup) /* 启动代码优先 */
*(.text) /* 所有其他代码 */
} > ROM
/* .data 段放在 RAM 区域 */
.data : {
*(.data)
} > RAM AT> ROM /* 加载地址在ROM,运行地址在RAM */
/* BSS 段(未初始化数据)*/
.bss : {
_bss_start = .; /* 定义符号 */
*(.bss)
_bss_end = .;
} > RAM
}
.lds 决定了:代码的起始地址(如 TEXT_BASE)、各段(TEXT / DATA / BSS)的内存布局、程序入口点
很多“U-Boot 能编出来但跑不起来”的问题,本质都出在链接脚本或早期地址布局上。
7、文件格式转化
这得到ELF文件后便是对这个文件进行转换了,将其转化成我们需要、可烧入的文件如u-boot.bin、u-boot.img、u-boot-dtb.bin。至此,一个完整、可启动的 U-Boot 镜像才算真正生成。


2639

被折叠的 条评论
为什么被折叠?



