Linux系统开发——U-Boot篇:搞懂U-Boot 是什么及顶层Makefile分析
本文最后更新于32 天前,其中的信息可能已经过时,如有错误请发送邮件到527388734@qq.com

一、何为U-Boot

初次接触U-Boot概念,想必大家都和博主一样好奇:它到底是个什么东西?它又在嵌入式Linux系统中起到什么作用?

其实用一句话可以概括——U-Boot是硬件上电后的第一个运行程序,主要负责初始化各种硬件接口(如网卡、串口、I2C 等),并将 Linux 内核加载到内存中运行。打个比方,这就像电脑开机时,我们看到的品牌 Logo 动画背后,BIOS 正在进行硬件自检(POST)和初始化。我们直观感受到的是那一番画面,实际上它在背后进行了各种硬件初始化的工作

在想到这里的时候,不知道大家会不会产生一个疑问。Linux内核难道不能自己启动自己吗?为什么还要个U-Boot做一个引导?

其实我们可以这么理解:市面上支持Linux系统的硬件千差万别,不同板子的 CPU、内存、外设接口各不相同。如果这些硬件差异都由内核来处理,那么每一款硬件都需要一个单独编译的内核版本,这会让linux内核生态极其臃肿,并且难以维护。

而有了U-Boot,关于这些硬件上的差异就可以直接交给U-Boot去管理,内核本身只做一些进程管理以及内存管理的系统性工作,并且内核本身只需要知道自己应该加载到内存的什么位子、根文件系统在哪就可以了。

所以学习U-Boot的意义可以帮助我们理解嵌入式系统的完整启动流程,具备系统移植和调试能力。

最后在来总结一下,U-Boot 是嵌入式 Linux 系统的“引导人”——它唤醒硬件、搬运内核、传递控制权,让 Linux 内核能够在千差万别的硬件上统一运行。没有 U-Boot,嵌入式 Linux 的开发和部署将变得极其困难。

了解到了U-Boot是什么以及学习它有什么意义,那么接下来,来跟着博主的节奏,一起进入U-Boot的世界吧!

二、U-Boot源码目录

在了解一个大型工程的时候,我们一般都会从他的源码目录入手,看看它的每一层目录都对应存放着什么文件,这有助于帮我们理清他的一个工程结构,也便于后续我们对于源码进行分析以及添加我们自己的硬件支持。下面这种图是U-Boot顶层目录的文件夹:

下面来讲解一下重点的目录:

  • arch:CPU架构相关代码,如 ARMv7、ARMv8、MIPS、RISC-V 等。这是 U-Boot 支持多平台的基础。
  • board:具体开发板支持代码。比如这个文件里存放着具体的板级初始化代码、编译规则以及板级的配置选项等等。移植 U-Boot 到新板子时,主要就是在这里添加或修改文件。
  • configs:存放各开发板的 defconfig 文件,定义了哪些功能被编译进 U-Boot。
  • include:存放全局头文件,特别是板级配置宏定义。

除目录以外,顶层还有一些重要的文件:

  • Makefile:顶层构建文件,定义编译规则、目标、架构和工具链配置
  • .config文件:内核配置结果文件
  • Kconfig定义Menuconfig菜单结构
  • u-boot.lds链接脚本:定义内存布局

掌握了这些目录结构,就能在庞大的 U-Boot 源码中快速定位需要的代码,无论是学习分析还是移植开发,都会事半功倍。

三、顶层Makefile分析

3.1 为什么要从Makefile开始分析?

了解到了他的源码结构,我们先从他的Makefile开始源码学习的第一步,因为Makefile规定了U-Boot的一个编译规则,了解到了它我们就可以知道哪些文件参与了编译。同时如果编译过程出现报错,Makefile也是我们排查问题的第一步,是工具链没有配置对,还是CPU架构没有设置好…

以正点原子的IM6ULL开发板为例,我们编译U-Boot会执行到以下两条重要指令:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig //编译配置文件,生成目标.config
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12 //整体编译,生成最终产物

//ARCH=xxx CROSS_COMPILE=xxx 是指定CPU的架构以及编译工具链,缩短下来其实就为:

make mx6ull_14x14_ddr512_emmc_defconfig
make V=1 -j12

3.2 Makefile 基本语法

所谓磨刀不误砍柴功,在分析Makefile之前,我们还是有必要先来了解(或者说复习一下)他的一个语法规则,这样可以极大的提高我们阅读的效率。

3.2.1 核心规则

在Makefile中有一个核心的机制就是:目标——依赖。格式如下:

目标: 依赖1 依赖2 ...
      命令1
      命令2

举一个简单的例子,便于大家理解:

u-boot.bin: u-boot.elf
      arm-linux-gnueabihf-objcopy -O binary u-boot.elf u-boot.bin

这里的u-boot.bin就是对应的目标,我们在终端上输入make u-boot.bin之后我们的Makefile就会匹配执行到这里。而u-boot.elf是执行u-boot.bin目标的一个依赖,意思就是如果想要生成u-boot.bin那么就先要有u-boot.elf(这时候Makefile会自动的去查找u-boot.elf对应的目标,有点像C语言当中的函数调用机制),然后再去执行objcopy 命令进行格式转换。这就是Makefile的一个核心目标匹配机制。

3.2.2 变量

在Makefile中还有很多关于变量赋值的操作,这样的最主要的目的就是为了简化代码,例如:

# 定义变量
CROSS_COMPILE = arm-linux-gnueabihf- //指定交叉编译工具
#使用变量
CC = $(CROSS_COMPILE)gcc //$(xxx)符号就是展开变量。所以CC的值就为:arm-linux-gnueabihf-gcc,在后续使用$(CC)就可以代替arm-linux-gnueabihf-gcc这么一大长串内容,从而起到了简化代码的作用

除了这种我们用户自己的定义的变量以外,Makefile还内置了一些变量,同样也为了简化书写。比如:

  • $@:目标文件名
  • $<:第一个依赖文件名
  • $^:所有依赖文件列表

我们在看到这些不认识的符号不要慌就可以了,遇到什么就去查什么就可以了。

3.2.3 条件判断以及函数

Makefile还存在一些基本的流程控制语法以及内置函数,这样让他很像一门编程语言。例如:

条件判断:ifeq (a, b)
循环:$(foreach v,list,text)
替换函数:$(patsubst pattern,replacement,text)
去空格函数:$(strip string)

在这里也不给大家依依列举了,我们同样遇到什么就去查什么就好了。

3.3 U-Boot 流程分析

接下来,我们就开始正式的U-Boot的Makefile分析了!下面我将以关键的两个编译指令来展开分析。

3.3.1 配置阶段——make xxx_defconfig

我们如果想要知道它做了什么,那么第一步就是需要找到xxx_defconfig对应的一个目标。如下所示:

//%是通配符,匹配所有以 config 结尾的目标
%config: scripts_basic outputmakefile FORCE
	$(Q)$(MAKE) $(build)=scripts/kconfig $@

我们可以看到它依赖了scripts_basic outputmakefile FORCE。而FORCE是Makefile当中强制执行的一条语法规则,就是保证每次在make xxx_deconfig的时候都会去执行,如果不加这个的话Makefile可会检查到存在的一些依赖的文件,就会跳过执行。理解到它的一个含义之后,那么他的关键依赖就只剩scripts_basic outputmakefile。接下来我们重点看下这分别对应的是什么东西。

3.3.2.1 outputmakefile

outputmakefile对应的如下依赖如下:

outputmakefile:
ifneq ($(KBUILD_SRC),)
	$(Q)ln -fsn $(srctree) source
	$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile \
	    $(srctree) $(objtree) $(VERSION) $(PATCHLEVEL)
endif

这里我们就看到了Makefile当中的一个判断语法——ifneq。意思就是当KBUILD_SRC不为空的时候才会进行进行来处理。

如果我们按照常理去分析这个变量是否为空的话也是很麻烦的,因为想要判断变量的值还会牵扯到很多逻辑,甚至可能这个变量就不在本Makefile内。

所以在这里介绍一下高效判断变量值的方法——直接打印出来。我们可以自己写一个目标,而这个目标就是打印变量的值:

mytest:
	@echo KBUILD_SRC = $(KBUILD_SRC)
//执行:make mytest即可看到mytest 的值

可以看到KBUILD_SRC 的值确实为空,那么outputmakefile其实就是相当于什么也没有执行,同样为空。

3.3.2.2 scripts_basic

我们根据上面的分析得知outputmakefile是无效的依赖,那么现在最关键的就是要分析scripts_basic到底是个什么了。我们同样需要先找到他的目标位置:

scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic
	$(Q)rm -f .tmp_quiet_recordmcount

我们看到了scripts_basic当中有三个变量,分别是Q、MAKE、build,我们同样把这些变量打印出来。

mytest:
	@echo Q = $(Q)
	@echo MAKE = $(MAKE)
	@echo build = $(build)

那么第一条指令$(Q)$(MAKE) $(build)=scripts/basic展开就为:

@ make -f ./scripts/Makefile.build obj=scripts/basic
//他是去编译了./scripts目录下的Makefile.build文件

U-Boot的Makefile在这里设计的就很巧妙,在传统设计框架下,如果顶层Makefile要想去编译子目录的Makefile需要:

    cd scripts/basic && make
    cd scripts/kconfig && make
    cd common && make
    // ... 几十个目录,每个子目录都要启动一个新的 make 进程,效率极其低下

而Makefile.build相当于就是一套编译模板,我们只需要给obj变量赋值,他就会通过这个模板规范去编译目标的Makefile,那么上面一层指令就是相当于去编译scripts/basic的Makefile。

所以分析到这里scripts_basic匹配的是什么内容就很清晰了,我们只需要看scripts/basic路径下的Makefile文件即可。如下所示:

hostprogs-y	:= fixdep
always		:= $(hostprogs-y)

# fixdep is needed to compile other host programs
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep

他的核心作用就是要编译出fixdep工具。而fixdep就是U-Boot构建的一个关键工具。

如果大家想要详细了解可以看https://blog.csdn.net/guyongqiangx/article/details/52588409这篇文章。

我在这里为大家做一下总结:fixdep 是一个依赖关系解析工具,核心作用是:跟踪头文件依赖关系,确保修改任何头文件或配置选项后,只重新编译真正受影响的源文件,避免不必要的全量编译,从而大幅提升构建效率。

分析到这里,想抛给大家一个问题:你还记得我们最终的分析目标是什么吗?哈哈哈其实这就是我认为分析Makefile的一个核心难点。一个目标会依赖很多依赖,而依赖又会依赖各种依赖,所以很容易忘记自己的核心目标是什么。

我们最终是需要去分析make xxx_defconfig而刚刚我们分析的成果——fixdep其实就是他的一个依赖项而以,知道这个依赖项之后我们就可以,它到底执行的是什么:

$(Q)$(MAKE) $(build)=scripts/kconfig $@
//还是这三个变量,$@为第一个目标文件名,也就是xxx_defconfig 
//拆开就为:
@ make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig
//同样还是按照Makefile.build这套编译模板去编译scripts/kconfig下的Makefile,而xxx_defconfig是传递给这个Makefile的目标,就会匹配到如下:

%_defconfig: $(obj)/conf
	$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)
//接下来我们就需要开始分析它展开后是什么内容
  • obj:我们出入的值——scripts/kconfig/conf
  • $@:目标文件
  • SRCARCH:通过打印可以得为 ..
  • Kconfig:通过打印可以得为 Kconfig
  • $<:第一个依赖文件名,也就是$(obj)/conf

既然都知道了这些变量名,那么我们就可以将这条指令展开:

scripts/kconfig/conf --defconfig=arch/../configs/xxx_defconfig Kconfig

scripts/kconfig/conf我们可以看到也是U-Boot自带的工具。

我们已知的是在执行make xxx_defconfig是会生成一个.config文件。而这个conf工具就是生成.config的关键,大家如果感兴趣他是如何生成.config文件的,可以去了解一下conf.c,这个是他的内部的实现源码。

其实看到这里不知道大家会不会有一个疑问:在我们make xxx_defconfig的时候他需要依赖fixdep,而这么分析下来fixdep和最终生成的产物.config没有直接关系,那为什么还构成依赖关系呢?原因就是因为fixdep是在编译阶段需要用到的工具,在配置阶段就把他编译出来是为了方便后续编译能够直接使用。

一句话总结:配置阶段编译 fixdep 是一种优化策略——虽然它和生成 .config 没有直接关系,但提前准备好编译阶段需要的工具,可以让后续的编译流程更流畅、更高效。这正是大型工程构建系统设计的精妙之处。

最后我们再来总结以下make xxx_defconfig的整个流程:

3.3.2 编译阶段——make V=1 -j12

我们先来分析以下make V=1 -j12的一个组成:

  • V=1:这个参数指定了是否把编译过程详细输出
  • -j12:使用 12 个线程并行编译(加快编译速度)

这么看下来这条指令其实没有指定编译目标。因此 Makefile 会执行默认目标。

PHONY := _all
_all:

PHONY += all
ifeq ($(KBUILD_EXTMOD),)
_all: all
else
_all: modules
endif
//通过打印可以得知KBUILD_EXTMOD变量为空,所以默认目标_all就依赖于all
all:		$(ALL-y)
ifneq ($(CONFIG_SYS_GENERIC_BOARD),y)
	@echo "===================== WARNING ======================"
	@echo "Please convert this board to generic board."
	@echo "Otherwise it will be removed by the end of 2014."
	@echo "See doc/README.generic-board for further information"
	@echo "===================================================="
endif
ifeq ($(CONFIG_DM_I2C_COMPAT),y)
	@echo "===================== WARNING ======================"
	@echo "This board uses CONFIG_DM_I2C_COMPAT. Please remove"
	@echo "(possibly in a subsequent patch in your series)"
	@echo "before sending patches to the mailing list."
	@echo "===================================================="
endif
//而all:又依赖于ALL-y,ALL-y可以得知他的变量的内容为:
ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check
//可以看到ALL-y这个变量就是产物的集合。默认产物是上面这些东西,同时他也可以通过配置生成的.config文件去动态配置还有哪些产物。例如:
CONFIG_OF_SEPARATE=y
ALL-$(CONFIG_OF_SEPARATE) += u-boot-dtb-tegra.bin //相当于产物内容又增加了u-boot-dtb-tegra.bin

我们的核心目标是探索u-boot.bin是怎么生成的,所以直接查找u-boot.bin相关依赖。

ifeq ($(CONFIG_OF_SEPARATE),y)
u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE
	$(call if_changed,cat)

u-boot.bin: u-boot-dtb.bin FORCE
	$(call if_changed,copy)
else
u-boot.bin: u-boot-nodtb.bin FORCE
	$(call if_changed,copy)
endif
//可以看到这部分的逻辑关键在于CONFIG_OF_SEPARATE的配置,它决定了u-boot.bin依赖于什么内容。我们可以到生成的.config里面去查找

可以看到CONFIG_OF_SEPARATE并没有包含,所以u-boot.bin实际依赖于u-boot-nodtb.bin

//u-boot-nodtb.bin又依赖于u-boot
u-boot-nodtb.bin: u-boot FORCE
	$(call if_changed,objcopy)
	$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
	$(BOARD_SIZE_CHECK)
//u-boot的规则为:
u-boot:	$(u-boot-init) $(u-boot-main) u-boot.lds FORCE
	$(call if_changed,u-boot__)

看到这里如果想要搞清楚u-boot是什么,则我们又需要知道u-boot-init 、u-boot-main变量是什么值,通过打印可以得知:

u-boot-init 记录的是 start.o(启动入口文件),u-boot-main 记录的是大量的 built-in.o(各个子目录的编译集合),u-boot.lds 是链接脚本。简单来说,就是通过 u-boot.lds 链接脚本,将 start.o 和所有 built-in.o 链接成一个完整的 ELF 可执行文件 u-boot

分析到这一步,目前的依赖深度有些深,让我带大家重新梳理一下依赖关系。

我们已经大体的知道u-boot是由大量的.o链接生成的ELF格式文件,这相当于是u-boot.bin的最底层的一个文件,也是编译生成的一个重要生成产物。而u-boot-nodtb.bin在此基础上做了objcopy处理,变成了纯二进制文件。在此基础上再添加设备树信息(在有配置的情况下添加,没有的话就基本上是一个复制操作),生成了最终的一个u-boot.bin的一个最终烧录文件。

一个简化的生成流程为:

//链接生成u-boot
arm-linux-gnueabihf-ld -T u-boot.lds \
    arch/arm/cpu/armv7/start.o \
    common/built-in.o \
    drivers/built-in.o \
    board/freescale/mx6ullevk/built-in.o \
    ... \
    -o u-boot
// 转换为纯二进制 u-boot-nodtb.bin
arm-linux-gnueabihf-objcopy -O binary u-boot u-boot-nodtb.bin
//生成最终产物u-boot.bin
cat u-boot-nodtb.bin u-boot.dtb > u-boot.bin //没有配置设备树:cp u-boot-nodtb.bin u-boot.bin

编译阶段完整流程如下图所示:

分析到这里我们已经了解到了U-Boot的一个编译流程,在接下来的篇章里我将对于U-Boot启动流程展开讲解,欢迎大家来和我讨论!以下是本篇文章用到的U-Boot源码,大家可以自行下载。

通过网盘分享的文件:uboot-imx-2016.03-2.1.0-g0ae7e33-v1.7.tar.bz2 链接: https://pan.baidu.com/s/1W8f-0U1bWzBnWtp10yj5JQ 提取码: mb7k

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇