LV05-01-uboot-06-顶层Makefile-01-基础分析
本文主要是学习uboot顶层Makefile的相关笔记。若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows版本 | windows11 |
Ubuntu版本 | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) |
Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
Win32DiskImager | Win32DiskImager v1.0 |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
https://elixir.bootlin.com/linux/latest/source | 在线阅读linux kernel源码 | |
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) |
我们先来分析一下顶层的Makefile文件。
一、版本号与MAKEFLAGS 变量
1. 版本号
VERSION 是主版本号, PATCHLEVEL 是补丁版本号, SUBLEVEL 是次版本号,这三个一起构成了 uboot 的版本号,比如当前的 uboot 版本号是“2016.03”。EXTRAVERSION 是附加版本信息, NAME 是和名字有关的,一般不使用这两个。
2. MAKEFLAGS 变量
make 是支持递归调用的,也就是在 Makefile 中使用“make”命令来执行其他的 Makefile文件,一般都是子目录中的 Makefile 文件。假如在当前目录下存在一个“subdir”子目录,这个子目录中又有其对应的 Makefile 文件,那么这个工程在编译的时候其主目录中的 Makefile 就可以调用子目录中的 Makefile,以此来完成所有子目录的编译。主目录的 Makefile 可以使用如下代码来编译这个子目录:
1 | $(MAKE) -C subdir |
$(MAKE)就是调用“make”命令, -C 指定子目录。有时候我们需要向子 make 传递变量,这个时候使用“export”来导出要传递给子 make 的变量即可,如果不希望哪个变量传递给子make 的话就使用“unexport”来声明不导出:
1 | export VARIABLE # 导出变量给子 make 。 |
有两个特殊的变量:“SHELL”和“MAKEFLAGS”,这两个变量除非使用“unexport”声明,否则的话在整个make的执行过程中,它们的值始终自动的传递给子make。在uboot的主Makefile中有如下代码:
上述代码使用“+=”来给变量 MAKEFLAGS 追加了一些值,“-rR”表示禁止使用内置的隐含规则和变量定义,“--include-dir”指明搜索路径, ”$(CURDIR)”表示当前目录。
二、输出?
1. 命令输出
uboot 默认编译是不会在终端中显示完整的命令,都是短命令 :
在终端中输出短命令虽然看起来很清爽,但是不利于分析 uboot 的编译过程。可以通过设置变量“V=1“来实现完整的命令输出,这个在调试 uboot 的时候很有用:
顶层 Makefile 中控制命令输出的代码如下 :
上述代码中先使用 ifeq 来判断”$(origin V)”和”command line”是否相等。这里用到了 Makefile中的函数 origin, origin 和其他的函数不一样,它不操作变量的值, origin 用于告诉你变量是哪来的,语法为:
1 | $(origin <variable>) |
variable 是变量名, origin 函数的返回值就是变量来源,因此$(origin V)就是变量 V 的来源。如果变量 V 是在命令行定义的那么它的来源就是”command line”,这样”$(origin V)”和”commandline”就相等了。当这两个相等的时候变量 KBUILD_VERBOSE 就等于 V 的值,比如在命令行中输 入 “ V=1 “ 的话那么KBUILD_VERBOSE=1 。 如果没有在命令行输入 V 的 话KBUILD_VERBOSE=0。
第 80 行判断 KBUILD_VERBOSE 是否为 1,如果 KBUILD_VERBOSE 为 1 的话变量 quiet 和 Q 都为空,如果 KBUILD_VERBOSE=0 的话变量 quiet为 “quiet、_“,变量 Q 为“@” ,综上所述:
- V=1时
1 | KBUILD_VERBOSE=1 |
- V=0 或者命令行不定义 V 的话:
1 | KBUILD_VERBOSE=0 |
Makefile 中会用到变量 quiet 和 Q 来控制编译的时候是否在终端输出完整的命令,在顶层Makefile 中有很多如下所示的命令:
1 | $(Q)$(MAKE) $(build)=tools |
如果 V=0 的话上述命令展开就是“@ make $(build)=tools”, make 在执行的时候默认会在终端输出命令,但是在命令前面加上“@”就不会在终端输出命令了。当 V=1 的时候 Q 就为空,上述命令就是“make $(build)=tools”,因此在 make 执行的过程,命令会被完整的输出在终端上。有些命令会有两个版本,比如
sym 命令分为“quiet_cmd_sym”和“cmd_sym”两个版本,这两个命令的功能都是一样的,区别在于 make 执行的时候输出的命令不同。 quiet_cmd_xxx 命令输出信息少,也就是短命令,而 cmd_xxx 命令输出信息多,也就是完整的命令。
如果变量 quiet 为空的话,整个命令都会输出。
如果变量 quiet 为“quiet_”的话,仅输出短版本。
如果变量 quiet 为“silent_”的话,整个命令都不会输出。
2. 静默输出
设置 V=0 或者在命令行中不定义 V 的话,编译 uboot 的时候终端中显示的短命令,但是还是会有命令输出,有时候我们在编译 uboot 的时候不需要输出命令,这个时候就可以使用 uboot 的静默输出功能。编译的时候使用“make -s”即可实现静默输出,顶层 Makefile中相应的代码如下:
第 91 行判断当前正在使用的编译器版本号是否为 4.x,判断$(filter 4.%,$(MAKE_VERSION))和“ ” (空)是否相等,如果不相等的话就成立,执行里面的语句。也就是说 $(filter4.%,$(MAKE_VERSION))不为空的话条件就成立,这里用到了 Makefile 中的 filter 函数,这是个过滤函数,函数格式如下:
1 | $(filter <pattern...>,<text>) |
filter 函数表示以 pattern 模式过滤 text 字符串中的单词,仅保留符合模式 pattern 的单词,可以有多个模式。函数返回值就是符合 pattern 的字符串。因此$(filter 4.%,$(MAKE_VERSION))的含义就是在字符串“ MAKE_VERSION”中找出符合“ 4.%”的字符(%为通配符),MAKE_VERSION 是make工具的版本号,ubuntu22.04里面默认自带的make工具版本号为GNU Make 4.3,我们可以输入“make -v”查看。因此$(filter 4.%,$(MAKE_VERSION))不为空,条件成立,执行92~94 行的语句。
第 92 行也是一个判断语句,如果$(filter %s ,$(firstword x$(MAKEFLAGS)))不为空的话条件成立,变量 quiet 等于“silent_”。这里也用到了函数 filter,在$(firstword x$(MAKEFLAGS)))中过滤出符合“%s”的单词。到了函数 firstword,函数 firstword 是获取首单词,函数格式如下:
1 | $(firstword <text>) |
firstword 函数用于取出 text 字符串中的第一个单词,函数的返回值就是获取到的单词。当使用“make -s”编译的时候,“-s”会作为 MAKEFLAGS 变量的一部分传递给 Makefile。
3. 设置编译结果输出目录
uboot 可以将编译出来的目标文件输出到单独的目录中,在 make 的时候使用“O”来指定输出目录,比如“make O=out”就是设置目标文件输出到 out 目录中。这么做是为了将源文件和编译产生的文件分开,当然也可以不指定 O 参数,不指定的话源文件和编译产生的文件都在同一个目录内,一般我们不指定 O 参数。顶层 Makefile 中相关的代码如下:
第 124 行判断“O”是否来自于命令行,如果来自命令行的话条件成立, KBUILD_OUTPUT就为$(O),因此变量 KBUILD_OUTPUT 就是输出目录。
第 135 行判断 KBUILD_OUTPUT 是否为空。
第 139 行调用 mkdir 命令,创建 KBUILD_OUTPUT 目录,并且将创建成功以后的绝对路径赋值给 KBUILD_OUTPUT。至此,通过 O 指定的输出目录就存在了。
三、代码检查和模块编译
1. 代码检查
uboot 支持代码检查,使用命令“make C=1”使能代码检查,检查那些需要重新编译的文件。“make C=2”用于检查所有的源码文件,顶层 Makefile 中的代码如下:
第 176 行判断 C 是否来源于命令行,如果 C 来源于命令行,那就将 C 赋值给变量KBUILD_CHECKSRC,如果命令行没有 C 的话 KBUILD_CHECKSRC 就为 0。
2. 模块编译
在 uboot 中允许单独编译某个模块,使用命令 “ make M=dir ” 即可,旧语法“ make SUBDIRS=dir ”也是支持的。顶层 Makefile 中的代码如下:
第 186 行判断是否定义了 SUBDIRS ,如果定义了 SUBDIRS ,变量 KBUILD_EXTMOD=SUBDIRS,这里是为了支持老语法“make SUBIDRS=dir”
第 190 行判断是否在命令行定义了 M,如果定义了的话 KBUILD_EXTMOD=$(M)。
第 197 行判断 KBUILD_EXTMOD 时为空,如果为空的话目标_all 依赖 all,因此要先编译出 all。否则的话默认目标_all 依赖 modules,要先编译出modules,也就是编译模块。一般情况下我们不会在 uboot 中编译模块,所以此处会编译 all 这个目标。
第 203 行判断 KBUILD_SRC 是否为空,如果为空的话就设置变量 srctree 为当前目录,即srctree 为“.”,一般不设置 KBUILD_SRC。
第 214 行设置变量 objtree 为当前目录。
第 215 和 216 行分别设置变量 src 和 obj,都为当前目录。
第 218 行设置 VPATH。
第 220 行导出变量 scrtree、 objtree 和 VPATH。
四、架构相关
1. 获取主机架构和系统
接下来顶层 Makefile 会获取主机架构和系统,也就是我们电脑的架构和系统:
第 227 行定义了一个变量 HOSTARCH,用于保存主机架构,这里调用 shell 命令“uname -m”获取架构名称。
从图中可以看出当前电脑主机架构为“x86_64”, shell 中的“|”表示管道,意思是将左边的输出作为右边的输入, sed -e 是替换命令,“sed -e s/i.86/x86/”表示将管道输入的字符串中的“i.86”替换为“x86”,其他的“sed -e s”命令同理。对于我的电脑而言, HOSTARCH=x86_64。
第 237 行定义了变量 HOSTOS,此变量用于保存主机 OS 的值,先使用 shell 命令“uname -s”来获取主机 OS,结果如图所示:
可以看出此时的主机 OS 为“Linux”,使用管道将“Linux”作为后面“ tr ‘[:upper:]’ ‘[:lower:]’ ”的输入,“ tr ‘[:upper:]’ ‘[:lower:]’ ”表示将所有的大写字母替换为小写字母,因此得到“linux”。最后同样使用管道,将“ linux ”作为“ sed -e ‘s/\(cygwin).*/cygwin/‘ ”的输入,用于将 cygwin.* 替换为 cygwin 。因此HOSTOS = linux。
第 240 行导出 HOSTARCH = x86_64, HOSTOS = linux。
2. 设置目标架构、交叉编译器和配置文件
编 译 uboot 的 时 候 需 要 设 置 目 标 板 架 构 和 交 叉 编 译 器 ,“ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-”就是用于设置 ARCH 和CROSS_COMPILE,在顶层 Makefile 中代码如下:
第 245 行判断 HOSTARCH 和 ARCH 这两个变量是否相等,主机架构(变量 HOSTARCH)是x86_64,而我们编译的是 ARM 版本 uboot,肯定不相等,所以CROSS_COMPILE= arm-linuxgnueabihf-。从示例代码可以看出,每次编译 uboot 的时候都要在 make 命令后面设置ARCH 和 CROSS_COMPILE,使用起来很麻烦,可以直接修改顶层 Makefile,在里面加入 ARCH和 CROSS_COMPILE 的定义:
直接在顶层 Makefile 里面定义 ARCH 和 CROSS_COMPILE,这样就不用每次编译的时候都要在 make 命令后面定义 ARCH 和 CROSS_COMPILE。
第 249 行定义变量 KCONFIG_CONFIG, uboot 是可以配置的,这里设置配置文件为.config,.config 默认是没有的,需要使用命令“make xxx_defconfig”对 uboot 进行配置,配置完成以后就会在 uboot 根目录下生成.config。默认情况下.config 和xxx_defconfig 内容是一样的,因为.config 就是从 xxx_defconfig 复制过来的。如果后续自行调整了 uboot 的一些配置参数,那么这些新的配置参数就添加到了.config 中,而不是 xxx_defconfig。相当于 xxx_defconfig 只是一些初始配置,而.config 里面的才是实时有效的配置。
五、scripts/Kbuild.include
主 Makefile 会调用文件 scripts/Kbuild.include 这个文件,顶层 Makefile 中代码如下:
make 使用“include”包含了文件 scripts/Kbuild.include,此文件里面定义了很多变量:
在 uboot 的编译过程中会用到 scripts/Kbuild.include 中的这些变量。
六、变量相关
1. 交叉编译工具变量设置
上面我们只是设置了 CROSS_COMPILE 的名字,但是交叉编译器其他的工具还没有设置,顶层 Makefile 中相关代码如下:
2. 导出其他变量
接下来在顶层 Makefile 会导出很多变量:
后边还有很多,这里只是截取了一部分。这些变量中大部分都已经在前面定义了,我们重点来看一下下面这几个变量:
1 | ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR |
这 7 个变量在顶层 Makefile 是找不到的,说明这 7 个变量是在其他文件里面定义的。先来看一下这 7 个变量都是什么内容,在顶层 Makefile 中输入如图所示的内容:
然后执行以下命令:
1 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- my_test |
然后便会看到如下打印:
可以看到这 7 个变量的值,这 7 个变量是从哪里来的呢?在 uboot 根目录下有个文件叫做 config.mk,这 7 个变量就是在 config.mk 里面定义的,打开 config.mk 内容如下(只截取了一小部分):
接下来需要找到 CONFIG_SYS_ARCH、 CONFIG_SYS_CPU、 CONFIG_SYS_BOARD、CONFIG_SYS_VENDOR 和 CONFIG_SYS_SOC 这 5 个变量的值。这 5 个变量在 uboot 根目录下的.config 文件中有定义,定义如下:
所以就有:
1 | ARCH = arm |
再来分析config.mk,就会发现在 config.mk 中读取的文件有 :
1 | arch/arm/config.mk |