目录
U-Boot顶层Makefile分析
在阅读uboot源码之前,肯定是要先看一下顶层Makefile,分析gcc版本代码的时候一定是先从顶层Makefile开始的,然后再是子Makefile,这样通过层层分析Makefile即可了解整个工程的组织结构。顶层Makefile也就是uboot根目录下的Makefile文件,由于顶层Makefile文件内容比较多,所以我们将其分开来看。
版本号
顶层Makefile一开始是版本号,内容如下(为了方便分析,顶层Makefile代码段前段行号采用 Makefile中的行号,因为uboot会更新,因此行号可能会与你所看的顶层Makefile有所不同):
VERSION是主版本号, PATCHLEVEL是补丁版本号, SUBLEVEL是次版本号,这三个一,起构成了uboot的版本号,比如当前的uboot版本号就是"2016.03"。EXTRAVERSION是附加版本信息,NAME是和名字有关的,一般不使用这两个。
MAKEFLAGS变量
make是支持递归调用的,也就是在Makefile中使用"make”命令来执行其他的Makefile文件,一般都是子目录中的Makefile文件。假如在当前目录下存在一个"subdir”子目录,这个子目录中又有其对应的Makefile文件,那么这个工程在编译的时候其主目录中的Makefile就可以调用子目录中的Makefile,以此来完成所有子目录的编译。主目录的Makefile可以使用如下代码来编译这个子目录:
$(MAKE) -C subdir
$(MAKE)就是调用"make”命令, -C指定子目录。有时候我们需要向子make传递变量,这个时候使用"export”来导出要传递给子make的变量即可,如果不希望哪个变量传递给子make的话就使用“unexport”来声明不导出:
export VARIABLE ...... //导出变量给子make。
unexport VARIABLE ....... //不导出变量给子make。
有两个特殊的变量:“SHELL”和"MAKEFLAGS”,这两个变量除非使用“unexport”声明,否则的话在整个make的执行过程中,它们的值始终自动的传递给子make。在uboot的主Makefile中有如下代码:
上述代码使用“+=”来给变量MAKEFLAGS追加了一些值,“-rR”表示禁止使用内置的隐含规则和变量定义,“--include-dir”指明搜索路径,”$(CURDIR)”表示当前目录。
命令输出
uboot默认编译是不会在终端中显示完整的命令,都是短命令,如图所示:
在终端中输出短命令虽然看起来很清爽,但是不利于分uboot的编译过程。可以通过设置变量"V=1"来实现完整的命令输出,这个在调试uboot的时候很有用,结果如图所示:
顶层Makefile中控制命令输出的代码如下:
上述代码中先使用ifeq来判断"$(origin V)"和"command line"是否相等。这里用到了Makefile中的函数origin,origin和其他的函数不一样,它不操作变量的值,origin用于告诉你变量是哪来的,语法为:
$(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的话:
V=0或者命令行不定义V的话:
Makefile中会用到变量quiet和Q来控制编译的时候是否在终端输出完整的命令,在顶层Makefile中有很多如下所示的命令:
$(Q)$(MAKE) $(build)=tools
如果V=0的话上述命令展开就是"@make $(build)-tools", make在执行的时候默认会在终端输出命令,但是在命令前面加上“@”就不会在终端输出命令了。当 V=1 的时候Q就为空,上述命令就是"make $(build)-tools”,因此在make执行的过程,命令会被完整的输出在终端上。
有些命令会有两个版本,比如:
quiet_cmd_sym ?= SYM $@
cmd_sym ?= $(OBJDUMP) -t $<> $@
sym命令分为"quiet_cmd_sym”和"cmd_sym”两个版本,这两个命令的功能都是一样的,区别在于make执行的时候输出的命令不同。quiet_cmd_xxx命令输出信息少,也就是短命令,而cmd_xxx命令输出信息多,也就是完整的命令。
如果变量quiet为空的话,整个命令都会输出。如果变量quiet为“quiet_”的话,仅输出短版本。如果变量quiet为"silent_”的话,整个命令都不会输出。
静默输出
设置V=0或者在命令行中不定义V的话,编译uboot的时候终端中显示的短命令,但是还是会有命令输出,有时候我们在编译uboot的时候不需要输出命令,这个时候就可以使用uboot的静默输出功能。编译的时候使用"make-s"即可实现静默输出,顶层Makefile中相应的代码如下:
第91行判断当前正在使用的编译器版本号是否为4.x,判断$(filter 4.%,$(MAKE_VERSION))和“”(空)是否相等,如果不相等的话就成立,执行里面的语句。也就是说$(filter4.%,$(MAKE_VERSION))不为空的话条件就成立,这里用到了Makefile中的filter函数,这是个过滤函数,函数格式如下:
$(filter <pattern...>,<text>)
filter函数表示以pattern模式过滤text字符串中的单词,仅保留符合模式pattern的单词,可以有多个模式。函数返回值就是符合pattern的字符串。因此$(filter 4.%,$(MAKE_VERSION))的含义就是在字符串"MAKE-VERSION”中找出符合"4.%”的字符(%为通配符),MAKE_VERSION是make工具的版本号,ubuntu16.04里面默认自带的make工具版本号为 4.1,大家可以输入“make-v”查看。因此$(filter 4.%,$(MAKE_VERSION))不为空,条件成立,执行92~94行的语句。
第92行也是一个判断语句,如果$(filter %s,$(firstword x$(MAKEFLAGS)))不为空的话条件,成立,变量quiet等于"silent”。这里也用到了函数filter,在$(firstword xS(MAKEFLAGS)))中过滤出符合"%s”的单词。到了函数firstword,函数firstword是获取首单词,函数格式如下:
$(firstword <text>)
firstword函数用于取出text字符串中的第一个单词,函数的返回值就是获取到的单词。当使用“make-s”编译的时候, “-s”会作为MAKEFLAGS变量的一部分传递给Makefile。在顶层Makfile中添加如图所示的代码:
图中的两行代码用于输出$(firstword x$(MAKEFLAGS))的结果,最后修改文件mx6ull alientek emmc.sh,在里面加入"-s”选项,结果如图所示:
修改完成以后执行mx6ull_alientek_emmc.sh,结果如图所示:
从图可以看出第一个单词是"xrRs",将$(filter %s ,$(firstword x$(MAKEFLAGS))展开就是$(filter %s, xrRs),而$(filter %s, xrRs)的返回值肯定不为空,条件成立, quiet-silent_第101行使用export导出变量quiet、Q和KBUILD_VERBOSE。
设置编译结果输出目录
uboot可以将编译出来的目标文件输出到单独的目录中,在make的时候使用“O”来指定输出目录,比如"make O-out”就是设置目标文件输出到out目录中。这么做是为了将源文件和编译产生的文件分开,当然也可以不指定0参数,不指定的话源文件和编译产生的文件都在同一个目录内,一般我们不指定0参数。顶层Makefile中相关的代码如下:
第124行判断"O”是否来自于命令行,如果来自命令行的话条件成立, KBUILD_OUTPUT就为$(O),因此变量KBUILD_OUTPUT就是输出目录。
第135行判断KBUILD_OUTPUT是否为空。
第139行调用mkdir命令,创建KBUILD_OUTPUT目录,并且将创建成功以后的绝对路径赋值给KBUILD_OUTPUT。至此,通过O指定的输出目录就存在了。
代码检查
uboot支持代码检查,使用命令"make C=1”使能代码检查,检查那些需要重新编译的文件。“make C-2”用于检查所有的源码文件,顶层Makefile中的代码如下:
第176行判断C是否来源于命令行,如果C来源于命令行,那就将C赋值给变量KBUILD_CHECKSRC,如果命令行没有C的话KBUILD_CHECKSRC就为0。
模块编译
在uboot中允许单独编译某个模块,使用命令"make M=dir”即可,旧语法"makeSUBDIRS=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是否为空,如果为空的话就设置变量sretree为当前目录,即srctree为“.”,一般不设置 KBUILD_SRC。
第214行设置变量objtree为当前目录。
第215和216行分别设置变量src和obj,都为当前目录。
第218行设置VPATH。
第220行导出变量scrtree、objtree和VPATH。
获取主机架构和系统
接下来顶层Makefile会获取主机架构和系统,也就是我们电脑的架构和系统,代码如下:
第227行定义了一个变量HOSTARCH,用于保存主机架构,这里调用shell命令“uname-m”获取架构名称,结果如图所示:
从图可以看出当前电脑主机架构为“x86_64", shell中的"1”表示管道,意思是将左边的输出作为右边的输入, sed-e是替换命令,"sed-e s/i.86/x86/”表示将管道输入的字符串中的"i.86"替换为"x86",其他的"sed-es"命令同理。对于我的电脑而言, HOSTARCH-x86_64。
第237行定义了变量HOSTOS,此变量用于保存主机OS的值,先使用shell命令"uname-s”来获取主机OS,结果如图所示:
从图可以看出此时的主机OS为“Linux”,使用管道将“Linux”作为后面“tr'[:upper:]"[:lower:]"的输入, "tr'[:upper:]'"[:lower:]'"表示将所有的大写字母替换为小写字母,因此得到“linux”。最后同样使用管道,将“linux”作为"sed-e'sA(cygwinl).*/cygwin/”的输入,用于将cygwin.*替换为 cygwin*因此,HOSTOS=linux。
第240行导出HOSTARCH=x86_64, HOSTOS=linux。
设置目标架构、交叉编译器和配置文件
编译uboot 的时候需要设置目标板架构和交叉编译器,
“make ARCH=armCROSS_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
示例代码中使用"include"包含了文件scripts/Kbuild.include,此文件里面定义了很多变量,如图所示:
在uboot的编译过程中会用到scripts/Kbuild.include中的这些变量。
交叉编译工具变量设置
上面只是设置了CROSS-COMPILE的名字,但是交叉编译器其他的工具还没有设置,顶层Makefile中相关代码如下:
导出其他变量
接下来在顶层Makefile会导出很多变量,代码如下:
这些变量中大部分都已经在前面定义了,我们重点来看一下下面这几个变量:
ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR
这7个变量在顶层Makefile是找不到的,说明这7个变量是在其他文件里面定义的,先来看一下这7个变量都是什么内容,在顶层Makefile中输入如图所示的内容:
修改好顶层Makefile以后执行如下命令:
make ARCH=arm CROSS COMPILE=arm-linux-gnueabihf- mytest
从图可以看到这7个变量的值,这7个变量是从哪里来的呢?在uboot根目录下有个文件叫做 config.mk,这7个变量就是在config.mk里面定义的,打开config.mk内容如下:
第25行定义变量ARCH,值为$(CONFIG_SYS_ARCH:"%"=%),也就是提取.CONFIG_SYS_ARCH里面双引号"”之间的内容。比如CONFIG_SYS_ARCH= "arm"的话,ARCH=arm。
第 26 行定义变量 CPU,值为$(CONFIG_SYS_CPU:"%"=%)。
第32行定义变量BOARD,值为(CONFIG_SYS_BOARD:"%"=%)。
第34行定义变量VENDOR,值为$(CONFIG_SYS_VENDOR:"%"=%)。
第37 行定义变量 SOC,值为$(CONFIG_SYS_SOC:"%"=%)。
第44行定义变量CPUDIR,值为arch/$(ARCH)/cpu$(if $(CPU),/$(CPU),)。
第46行sinclude和include的功能类似,在Makefile中都是读取指定文件内容,这里读取文件$(srctree)/arch/S(ARCH)/config.mk的内容。sinclude读取的文件如果不存在的话不会报错。
第47行读取文件$(srctree)/$(CPUDIR)/config.mk的内容。
第50行读取文件$(srctree)/$(CPUDIR)/S(SOC)/config.mk的内容。
第54行定义变量BOARDDIR,如果定义了VENDOR 那么BOARDDIR=$(VENDOR)/$(BOARD),否则的 BOARDDIR=$(BOARD)。
第60行读取文件$(srctree)/board/$(BOARDDIR)/config.mk。
接下来需要找到CONFIG_SYS_ARCH、CONFIG_SYS_CPU、CONFIG_SYS_BOARD、CONFIG_SYS_VENDOR和CONFIG_SYS_SOC这5个变量的值。这5个变量在uboot根目录下的.config文件中有定义.定义如下:
根据示例代码可知:
在config.mk中读取的文件有: