上一节U-Boot学习(1):简介及命令行指令详解中,介绍了如何使用U-Boot。我们知道一个U-Boot可能要适配不同的硬件,所以不同的硬件就有不同的配置,配置后就可以编译U-Boot,最终生成镜像。U-Boot如何编译,以什么规则编译,编译后下载到内存中的哪里呢,这一切都在Makefile中。所以本节课就来分析一下U-Boot的Makefile的结构。
文章目录
1 U-Boot源码1.1 下载1.2 目录结构
2 U-Boot编译2.1 源码2.2 配置2.3 编译
3 .config配置文件生成分析3.1 Makefile展开3.2 头文件依赖生成之fixdep工具3.3 .config文件生成
1 U-Boot源码
1.1 下载
1、官方源码
U-Boot官方的源码在Github上有对应的镜像:https://github.com/u-boot/u-boot,大家可以在这里下载源码。
2、半导体产商适配源码
前面说了,U-Boot主要是一个引导的操作,但是对于不同半导体产商的芯片来说,它们的接口类型和初始化方式都有差异,虽然U-Boot源码中做了一定的适配,但是不如半导体产商自身做的适配完善,比如NXP就提供了I.MX系列Cortex-A核芯片的U-Boot适配,可以在下面的地址中下载:https://github.com/nxp-imx/uboot-imx。
这里面提供的U-Boot基本上就是基于官方的开发板进行了适配,也就是说我们使用同一款芯片的话,假设Flash可以接到多个Flash接口上,可能官方开发板接在接口1,而你的产品接到了接口2,这时候你就可以在官方源码的基础上更改一下这个接口。如果需要网络启动,那对于以太网接口的初始化也是一样的,要适配你自己的开发板的硬件连接。
1.2 目录结构
解压U-Boot源码后,常见的文件夹的作用如下:
目录描述/arch体系结构特定的文件/arch/arm适用于ARM体系结构的通用文件(该目录下除了ARM架构外还有其他架构的,这里省略)/api面向外部应用的机器/体系结构无关的API/board依赖于板级别的文件/boot支持镜像和引导/cmdU-Boot命令函数/common各体系结构通用的杂项函数/configs板默认配置文件/disk处理磁盘驱动分区的代码/doc文档(一组ReST和README文件的混合)/drivers设备驱动程序/dts用于构建内部U-Boot fdt的Makefile/env环境支持/examples独立应用程序示例代码等/fs文件系统代码(cramfs,ext2,jffs2等)/include头文件/lib所有体系结构通用的库例程/Licenses各种许可文件/net网络代码/post自检/scripts各种构建脚本和Makefile/test各种单元测试文件/tools用于构建和签名FIT镜像等的工具
2 U-Boot编译
2.1 源码
这里我们以NXP官方提供的U-Boot源码为例进行分析,我这就下载了当前这个仓库的默认分支if_v2022.04。下载解压后,目录结构如下:
2.2 配置
(1)不同开发板的配置
我们现在想要编译U-Boot,首先我们来看看README中怎么说: 也就是说在configs目录下有对应不同的配置文件,不同的配置文件对应不同的开发板,我们根据自己的需求选择一个配置文件进行make,这里我就选择目录下的imx6ul_isiot_emmc_defconfig。在执行指令之前,需要安装以下四个库
sudo apt install make
sudo apt install gcc
sudo apt install bison
sudo apt install flex
现在我们执行make imx6ul_isiot_emmc_defconfig,看一下执行的结果: 此时目录下会生成一个.config文件: 实际上就是一些宏定义,一会编译U-Boot的时候肯定会用到这些宏定义。
(2)交叉编译器的配置 继续看README: 也就是说我们要指定用什么交叉编译器来编译U-Boot。
交叉编译器下载地址:https://releases.linaro.org/components/toolchain/binaries/
这里我下载了gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf.tar.xz,把交叉编译器解压到了/usr/local/arm/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf,然后需要在/etc/profile的最后添加环境变量:export PATH=$PATH:/usr/local/arm/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin,最后输入source /etc/profile让环境变量立即生效(每次打开终端都要输入一次,要一直生效需要重启系统)。
选择交叉编译器的时候,需要注意交叉编译器所支持的处理器架构要与芯片的处理器架构一样
现在我们就直接在U-Boot的Makefile中修改CROSS_COMPILE变量为我们交叉编译器的执行名前缀:
2.3 编译
现在就可以直接在U-Boot目录下输入make进行编译,由于编译过程比较久,我们可以指定-j12,表示使用12个线程编译以加快编译速度。输入make -j12,但是会报错: 这是因为U-Boot的部分文件使用了OpenSSL协议,我们需要安装相关库:
sudo apt install libssl-dev
然后我们还会遇到问题: 这里我们不使用LDO旁路检测,直接在.config文件中注释掉CONFIG_LDO_BYPASS_CHECK即可。 现在就编译成功了,目录下多了很多的文件
3 .config配置文件生成分析
前面我们虽然编译出来了u-boot.bin,但是这个文件的链接地址是否和我们开发板的RAM相对应呢,还有刚刚的make XXXconfig完成了什么操作,.config中的内容代表什么呢。这些都与Makefile有关,所以现在我们就来分析一下Makefile。
对于Makefile相关知识及常用函数可以参考我的另一篇文章:Makefile基础、常用函数及通用Makefile
3.1 Makefile展开
由前面的介绍我们知道,在configs目录下有对应不同的配置文件,前面我们选择了make imx6ul_isiot_emmc_defconfig这个配置,我们来看一下Makefile中对应的生成规则:
%为通配符,即匹配了所有make xxxdeconfig
展开来实际上是:
make -f ./scripts/Makefile.build obj=scripts/kconfig imx6ul_isiot_emmc_defconfig
这句make指令使用了-f选项指定了要使用的Makefile文件的路径,即./scripts/Makefile.build。然后,通过obj=scripts/basic传递了一个变量obj和我们的目标名。
1、outputmakefile
PHONY += outputmakefile
outputmakefile:
ifneq ($(KBUILD_SRC),)
$(Q)ln -fsn $(srctree) source
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
endif
可以看到如果KBUILD_SRC为空的话,才会执行下面的指令,但是实际上KBUILD_SRC是为空的,文档中有以下两句:
# KBUILD_SRC is set on invocation of make in OBJ directory
# KBUILD_SRC is not intended to be used by the regular user (for now)
其中第一句指出KBUILD_SRC是在调用make时设置的(如果不设置的话就使用当前目录)。
2、FORCE
PHONY += FORCE
FORCE:
这里FORCE被声明为.PHONY(伪目标),所以如果FORCE是其他目标的依赖,那么无论FORCE是否实际上被修改,这些目标都将始终被认为是需要重新生成的。因此,make将忽略文件时间戳,总是认为.PHONY目标需要重新生成。
3、scripts_basic
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount
# To avoid any implicit rule to kick in, define an empty command.
scripts/basic/%: scripts_basic ;
我们在Makefile中搜索Q、MAKE、build等变量,展开后就是:
make -f ./scripts/Makefile.build obj=scripts/basic
rm -f .tmp_quiet_recordmcount
这里的make和前面的%config的语句很像,最终还是执行./scripts/Makefile.build这个Makefile。
3.2 头文件依赖生成之fixdep工具
这里就不继续往下分析了,Makefile.build和Linux内核的Makefile.build很像,可以参考前面分享的Makefile介绍的文章。这里就直接说结论:scripts_basic会生成fixdep应用。 这个应用是用来生成头文件依赖的,下面来介绍一下相关的知识。
在编译过程中,使用-MD选项可以生成依赖关系文件。这些文件包含了源代码文件和它们之间的依赖关系,通常以 Makefile 的规则格式保存,这样可以在后续的编译过程中更好地管理文件的依赖关系。这在大型项目中是很有用的,因为它可以确保在修改一个源文件后,只重新编译与之相关的文件,而不是整个项目。
下面是一个简单的例子,假设有一个C语言项目,包含三个源文件:main.c,utils.c,和header.h。main.c 包含了main函数,utils.c包含了一些工具函数,而header.h包含了函数的声明。
假设我们使用GCC编译器,可以通过以下命令使用-MD选项生成依赖关系文件:
gcc -MD -o main main.c utils.c
这个命令会生成main.d文件,内容可能如下:
main.o: main.c header.h
utils.o: utils.c header.h
这表示在编译main.c时,依赖于main.c和header.h;在编译utils.c时,依赖于utils.c和header.h。这样,如果修改了header.h,只有依赖于它的文件会重新编译,而不是整个项目。
这对于Makefile来说非常有用,因为它们可以根据这些依赖关系文件来判断哪些文件需要重新编译,从而提高编译效率。
fixdep是一个用于生成Makefile依赖关系的工具。它主要用于处理头文件之间的依赖关系,以确保在构建U-Boot时,当某个头文件发生变化时,只重新编译与之相关的文件,而不是整个项目。具体来说,fixdep主要完成以下任务:
解析源代码文件中的#include指令:fixdep分析源代码文件,找到其中包含的头文件。生成Makefile规则: 通过分析源代码文件中的 #include 指令,fixdep 生成一个包含依赖关系的 Makefile 规则。这些规则通常包含了源文件和其所依赖的头文件,以及它们之间的关系。输出依赖关系: fixdep将生成的 Makefile 规则输出到标准输出或指定的文件中,以便后续的构建工具(如Make)使用。
例如,在U-Boot的Makefile中可能包含类似如下的使用fixdep的命令:
depend:
$(SRCTREE)/scripts/fixdep $(CFLAGS) $(CPPFLAGS) $(SRCARCH) $(SRC) > .depend
这个命令使用fixdep生成头文件依赖关系,并将结果保存到.depend文件中。在Makefile的其他地方可以包含这个文件,以便Make工具能够使用这些依赖关系。
3.3 .config文件生成
前面的%config目标最终会执行:
scripts/kconfig/conf --defconfig=arch/../configs/imx6ul_isiot_emmc_defconfig Kconfig
我们可以通过执行make imx6ul_isiot_emmc_defconfig V=1看到: 在U-Boot中,scripts/kconfig/conf是Kconfig工具的配置脚本,用于处理配置文件和生成配置信息。Kconfig是Linux内核中使用的配置系统,也被U-Boot采用,用于管理项目的配置选项。conf脚本会根据imx6ul_isiot_emmc_defconfig中的配置生成一个配置文件.config ,其中包含了用户所做的配置选项。这个文件会被后续的构建过程使用,以根据用户的配置生成相应的代码。