uboot makefile构建分析
前言
几年前分析过uboot的构建及启动过程,做了笔记,但最终没有转为文章。这次又有机会开发嵌入式产品了(之前一年多都是在搞x86 linux),看了下uboot的构建过程,觉得有必要写下整个分析过程,为了自己也为了分享,因此就有了这篇文章。
目标
通过分析uboot的整个构建过程,了解我们要开发的板子是通过哪些文件来配置的,这些配置是怎么对构建起作用的。我想只有清楚了这些,我们才能很轻松的对uboot进行修改、配置、裁剪等等来满足项目需求。而且,如果熟悉了uboot的构建过程,以后开发uboot支持的其他架构,其他板子就顺手多了。
构建过程分析
uboot的构建包含几个步骤,比如可选的清理、生成配置、编译。其实它是完全参考了linux内核的构建过程(最新的uboot就更像了,几乎是一个模子套出来的^_^
),后面我会再写一篇linux内核的构建分析,就更有体会了。分析的uboot版本采用xilinx zynq_zturn
的uboot,最新的uboot可以通过git clone git://git.denx.de/u-boot.git
拿到,我要分析的uboot版本信息在makefile显示为:
VERSION = 2013
PATCHLEVEL = 10
SUBLEVEL =
EXTRAVERSION =
最新的显示为
VERSION = 2016
PATCHLEVEL = 05
SUBLEVEL =
EXTRAVERSION =
可以看到年代差距还是非常大的,之所以不拿最新的uboot作为分析对象,其一,我需要验证我分析的每个过程,最新的uboot我没法马上验证,其二,原理是一样的,看懂了这篇文章,我相信最新的uboot也是可以轻而易举的看懂的_
下面开始对每个步骤进行详细的分析。
make ARCH=arm CROSS_COMPILE=arm-linux- distclean
过程
这一步是完全清理掉执行后面两个步骤生成的中间文件,一般在只在第一次构建的时候为了保险起见,执行下这个步骤。每当我们第一次编译uboot的时候,必须先弄清楚我们用什么编译器(由CROSS_COMPILE
指定),编译什么架构的(由ARCH指定),对应板子的默认配置(第二步生成配置的时候由xxx_config
指定)。这个太简单,不清楚也不会影响到我写这篇文章的目标,因此就不多说了。
make ARCH=arm CROSS_COMPILE=arm-linux- zynq_zturn_config
过程
这一步就是生成配置的过程,主要生成了include/config.h和include/config.mk以及建立对应的链接文件指向对应的链接目录。
unconfig:
@rm -f $(obj)include/config.h $(obj)include/config.mk \
$(obj)board/*/config.tmp $(obj)board/*/*/config.tmp \
$(obj)include/autoconf.mk $(obj)include/autoconf.mk.dep \
$(obj)include/spl-autoconf.mk \
$(obj)include/tpl-autoconf.mk
%_config:: unconfig
@$(MKCONFIG) -A $(@:_config=)
先处理unconfig依赖,清理掉之前生成的临时文件,然后通过@$(MKCONFIG) -A $(@:_config=)
完成配置。MKCONFIG在前面已经配置成MKCONFIG := $(SRCTREE)/mkconfig
,其实就是当前目录下的mkconfig shell脚本文件,而$(@:_config=)
是make的内建语法,其实就是去掉zynq_zturn_config
里的_config
,因为$(@)就是zynq_zturn_config
,所以最终就是执行mkconfig -A zynq_zturn
。这个脚本的功能也就是前面说的,生成include/config.h和include/config.mk以及建立对应的链接文件指向对应的链接目录。下面通过直接注释的方式说明该脚本的执行过程。
#!/bin/sh -e
# Script to create header files and links to configure
# U-Boot for a specific board.
#
# Parameters: Target Architecture CPU Board [VENDOR] [SOC]
#
# (C) 2002-2013 DENX Software Engineering, Wolfgang Denk <wd@denx.de>
# include/include/
# SPDX-License-Identifier: GPL-2.0+
#
APPEND=no # Default: Create new config file
BOARD_NAME="" # Name to print in make output
TARGETS=""
arch=""
cpu=""
board=""
vendor=""
soc=""
options=""
#如果输入参数是2个且第一个参数是-A(我当前就是这个情形),就直接从boards.cfg里面通过awk提取,放入到变量line中
if [ \( $# -eq 2 \) -a \( "$1" = "-A" \) ] ; then
# Automatic mode
#大致的意思是取某一行,这一行不是#开头,且它的第七个字段与第二个输入参数(也就是zynq_zturn)匹配,那么输出这行的每个字段
line=`awk '($0 !~ /^#/ && $7 ~ /^'"$2"'$/) { print $1, $2, $3, $4, $5, $6, $7, $8 }' boards.cfg`
if [ -z "$line" ] ; then
echo "make: *** No rule to make target \`$2_config'. Stop." >&2
exit 1
fi
#如果提取成功,将这些变量设置为输入参数
set ${line}
# add default board name if needed
[ $# = 3 ] && set ${line} ${1}
fi
#这里就是遍历刚才设置的每一个参数,我这里的第一个参数就不符合,也就是直接跳过这段
while [ $# -gt 0 ] ; do
case "$1" in
--) shift ; break ;;
-a) shift ; APPEND=yes ;;
-n) shift ; BOARD_NAME="${7%_config}" ; shift ;;
-t) shift ; TARGETS="`echo $1 | sed 's:_: :g'` ${TARGETS}" ; shift ;;
*) break ;;
esac
done
[ $# -lt 7 ] && exit 1
[ $# -gt 8 ] && exit 1
# Strip all options and/or _config suffixes
# 第七个输入参数根据boards.cfg描述,应该是target
CONFIG_NAME="${7%_config}"
# 如果BOARD_NAME为空,也将其设置为target
[ "${BOARD_NAME}" ] || BOARD_NAME="${7%_config}"
#根据boards.cfg里面的描述,每个字段意义如下:
#Status, Arch, CPU:SPLCPU, SoC, Vendor, Board name, Target, Options, Maintainers
#下面就是根据配置来设置变量arch cpu spl_cpu board vendor soc
arch="$2"
cpu=`echo $3 | awk 'BEGIN {FS = ":"} ; {print $1}'`
spl_cpu=`echo $3 | awk 'BEGIN {FS = ":"} ; {print $2}'`
if [ "$6" = "-" ] ; then
board=${BOARD_NAME}
else
board="$6"
fi
[ "$5" != "-" ] && vendor="$5"
[ "$4" != "-" ] && soc="$4"
[ $# -gt 7 ] && [ "$8" != "-" ] && {
# check if we have a board config name in the options field
# the options field mave have a board config name and a list
# of options, both separated by a colon (':'); the options are
# separated by commas (',').
#
# Check for board name
tmp="${8%:*}"
if [ "$tmp" ] ; then
CONFIG_NAME="$tmp"
fi
# Check if we only have a colon...
if [ "${tmp}" != "$8" ] ; then
options=${8#*:}
TARGETS="`echo ${options} | sed 's:,: :g'` ${TARGETS}"
fi
}
if [ "${ARCH}" -a "${ARCH}" != "${arch}" ]; then
echo "Failed: \$ARCH=${ARCH}, should be '${arch}' for ${BOARD_NAME}" 1>&2
exit 1
fi
if [ "$options" ] ; then
echo "Configuring for ${BOARD_NAME} - Board: ${CONFIG_NAME}, Options: ${options}"
else
echo "Configuring for ${BOARD_NAME} board..."
fi
#
# Create link to architecture specific headers
#
# 这里会根据是否是out-of-build,配置的arch来设置目录链接,这些目录链接在编译时需要用到
if [ "$SRCTREE" != "$OBJTREE" ] ; then
mkdir -p ${OBJTREE}/include
mkdir -p ${OBJTREE}/include2
cd ${OBJTREE}/include2
rm -f asm
ln -s ${SRCTREE}/arch/${arch}/include/asm asm
LNPREFIX=${SRCTREE}/arch/${arch}/include/asm/
cd ../include
mkdir -p asm
else
cd ./include
rm -f asm
ln -s ../arch/${arch}/include/asm asm
fi
rm -f asm/arch
if [ -z "${soc}" ] ; then
ln -s ${LNPREFIX}arch-${cpu} asm/arch
else
ln -s ${LNPREFIX}arch-${soc} asm/arch
fi
if [ "${arch}" = "arm" ] ; then
rm -f asm/proc
ln -s ${LNPREFIX}proc-armv asm/proc
fi
#
# Create include file for Make
#
#这里就是创建include/config.mk文件了,这个文件是给编译的时候include的,用来告诉它以下信息:
#ARCH = arm
#CPU = armv7
#BOARD = zynq
#VENDOR = xilinx
#SOC = zynq
( echo "ARCH = ${arch}"
if [ ! -z "$spl_cpu" ] ; then
echo 'ifeq ($(CONFIG_SPL_BUILD),y)'
echo "CPU = ${spl_cpu}"
echo "else"
echo "CPU = ${cpu}"
echo "endif"
else
echo "CPU = ${cpu}"
fi
echo "BOARD = ${board}"
[ "${vendor}" ] && echo "VENDOR = ${vendor}"
[ "${soc}" ] && echo "SOC = ${soc}"
exit 0 ) > config.mk
# Assign board directory to BOARDIR variable
if [ -z "${vendor}" ] ; then
BOARDDIR=${board}
else
BOARDDIR=${vendor}/${board}
fi
#
# Create board specific header file
#
#这里就是创建include/config.h文件了
if [ "$APPEND" = "yes" ] # Append to existing config file
then
echo >> config.h
else
> config.h # Create new config file
fi
echo "/* Automatically generated - do not edit */" >>config.h
for i in ${TARGETS} ; do
i="`echo ${i} | sed '/=/ {s/=/ /;q; } ; { s/$/ 1/; }'`"
echo "#define CONFIG_${i}" >>config.h ;
done
echo "#define CONFIG_SYS_ARCH \"${arch}\"" >> config.h
echo "#define CONFIG_SYS_CPU \"${cpu}\"" >> config.h
echo "#define CONFIG_SYS_BOARD \"${board}\"" >> config.h
[ "${vendor}" ] && echo "#define CONFIG_SYS_VENDOR \"${vendor}\"" >> config.h
[ "${soc}" ] && echo "#define CONFIG_SYS_SOC \"${soc}\"" >> config.h
cat << EOF >> config.h
#define CONFIG_BOARDDIR board/$BOARDDIR
#include <config_cmd_defaults.h>
#include <config_defaults.h>
#include <configs/${CONFIG_NAME}.h>
#include <asm/config.h>
#include <config_fallbacks.h>
#include <config_uncmd_spl.h>
EOF
exit 0
总的来说,就是根据zynq_zturn_config
从boards.cfg里面提取硬件信息,如架构名称、cpu名称、板子名称、厂商名称、soc名称,然后生成include/config.h和include/config.mk,其中include/config.mk给makefile用的,而include/config.h则是生成配置信息头文件所依赖的,也是很多其他头文件依赖的。
make ARCH=arm CROSS_COMPILE=arm-linux-
过程
这部分选几个重点进行说明。
第一点,支持的配置变量
O
ifdef O
ifeq ("$(origin O)", "command line")
BUILD_DIR := $(O)
endif
endif
通过O=xxx 来指定out-of-build,当然也就是通过export BUILD_DIR=xxx来指定,不过命令行的O=xxx优先级高
C
ifdef C
ifeq ("$(origin C)", "command line")
CHECKSRC := $(C)
endif
endif
通过C=1来开启静态代码检查,这个是sparse实现的,应该是linus本人写的一个工具
第二点,伪目标all
ifeq ($(obj)include/config.mk,$(wildcard $(obj)include/config.mk))
# Include autoconf.mk before config.mk so that the config options are available
# to all top level build files. We need the dummy all: target to prevent the
# dependency target in autoconf.mk.dep from being the default.
all:
sinclude $(obj)include/autoconf.mk.dep
sinclude $(obj)include/autoconf.mk
ifndef CONFIG_SANDBOX
SUBDIRS += $(SUBDIR_EXAMPLES)
endif
...
...
...
all: $(ALL-y) $(SUBDIR_EXAMPLES)
...
...
...
else # !config.mk
all $(obj)u-boot.hex $(obj)u-boot.srec $(obj)u-boot.bin \
$(obj)u-boot.img $(obj)u-boot.dis $(obj)u-boot \
$(filter-out tools,$(SUBDIRS)) \
updater depend dep tags ctags etags cscope $(obj)System.map:
@echo "System not configured - see README" >&2
@ exit 1
tools: $(VERSION_FILE) $(TIMESTAMP_FILE)
$(MAKE) -C $@ all
endif # config.mk
我们会发现有三个all目标,最后的那个all只在你还没执行第二步配置的时候会执行且提示你系统还没配置呢!第一个和第二个all在执行了第二步配置后生效,之所以要弄两个all,用它原话说:
# Include autoconf.mk before config.mk so that the config options are available
# to all top level build files. We need the dummy all: target to prevent the
# dependency target in autoconf.mk.dep from being the default.
其实就是防止你在第三步执行的时候,没有指定目标,那这时会采用第一个遇到的合适的作为目标,那么在第二个all前的include里所载入的makefile里面的目标就很有可能被当做目标,所以为了防止这一点,使用了两个目标,前面的目标就是告诉makefile这次构建的目标是all,当它又遇到后面的all时,make会将两次的依赖整合起来,这个语法是make所支持的。
第三点,include/autoconf.mk及include/autoconf.mk.dep生成过程
在前面有看到,第一个all后紧跟着就是sinclude 它俩,问题是在执行完第二步后,这两是还没有生成的。根据make的原则,文件不存在,那么跳过它,继续处理后面的内容,等到所有的都处理完后,再回来看能否通过规则找到这俩。下面给出make的大致流程:
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
往后找,我们能够看到它俩的规则:
$(obj)include/autoconf.mk.dep: $(obj)include/config.h include/common.h
@$(XECHO) Generating $@ ; \
set -e ; \
: Generate the dependancies ; \
$(CC) -x c -DDO_DEPS_ONLY -M $(CFLAGS) $(CPPFLAGS) \
-MQ $(obj)include/autoconf.mk include/common.h > $@
$(obj)include/autoconf.mk: $(obj)include/config.h
@$(XECHO) Generating $@ ; \
set -e ; \
: Extract the config macros ; \
$(CPP) $(CFLAGS) -DDO_DEPS_ONLY -dM include/common.h | \
sed -n -f tools/scripts/define2mk.sed > $@.tmp && \
mv $@.tmp $@
这里会从include/common.h里面提取并生成配置信息
CONFIG_SPI_FLASH_WINBOND=y
CONFIG_CMD_FAT=y
CONFIG_ARMV7=y
CONFIG_CMD_ITEST=y
CONFIG_CMD_EDITENV=y
CONFIG_SYS_ENET=y
CONFIG_ZYNQ_BOOT_FREEBSD=y
CONFIG_ZYNQ=y
...
...
...
从这里我们会发现,如果我们要配置uboot,这就是我们的入口点。这次就分析到这里,后面的部分其实就容易了,当然如果有必要,我会继续再写一篇完善这篇,包括uboot bin的构建过程、链接脚本的作用等等。
参考
- [U-BOOT顶层MAKEFILE详解](http://www. csdn123.com/html/blogs/20130710/34823.htm)
完!
2016年5月
毕业那两年在做嵌入式应用开发,主要是单片机和arm linux上的应用开发,后来又做了两年arm linux驱动开发,15年到现在在做pc端及嵌入式端开发,包括服务器系统裁剪、底层框架实现、硬件加速等。喜欢技术分享、交流!联系方式: 907882971@qq.com、rongpmcu@gmail.com