U-Boot学习-U-Boot 编译过程解析

目录

1、deconfig

2、Kconfig

3、.config

4、  make menuconfig

5、makefile中文件逻辑

        (1)顶层的makefile

        (2)SPL

        (3)子目录 Makefile

6、.lds链接阶段

7、文件格式转化

我们先看一下整个流程逻辑:

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 镜像才算真正生成。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值