u-boot的编译过程及其makefile结构深入分析

概述

这篇文章会彻底分析uboot的编译过程,并对它的makefile的结构分布剖析,最终弄明白u-boot是怎么生成的。写这篇文章的目的是为了方便理解及日后查阅,如有错误的地方欢迎留言。

从makefile开始编译

Makefile(部分)内容如下

SRCTREE		:= $(CURDIR)
MKCONFIG	:= $(SRCTREE)/mkconfig
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
%_config::	unconfig
	@$(MKCONFIG) -A $(@:_config=)
sinclude .boards.depend
.boards.depend:	boards.cfg
	awk '(NF && $$1 !~ /^#/) { print $$1 ": " $$1 "_config; $$(MAKE)" }' $< > $@

命令 make tiny_4412;tiny_4412定义在.boards.depend文件中,定义如下:

tiny4412: tiny4412_config; $(MAKE)

在上面的执行命令 make tiny4412 时,发现tiny4412目标依赖 tiny4412_configtiny4412_config这个目标匹配上了下面这条规则

%_config::	unconfig
	@$(MKCONFIG) -A $(@:_config=)

$(@:_config=);$@表示目标,_config=表示用等号后面的内容替换掉_config。实际上就是执行下面这条命令

./mkconfig  -A tiny4412

mkconfig脚本配置编译环境

为减少篇幅,脚本中删除了一些没用的内容,如下面第二行所示

if [ \( $# -eq 2 \) -a \( "$1" = "-A" \) ] ; then
	# 过滤boards.cfg文件,此处为过滤出包含tiny4412字符串的行
	line=`egrep -i "^[[:space:]]*${2}[[:space:]]" boards.cfg` || {
        。。。错误信息
	}
    # 设置为环境变量,分别保存在 $1 $2 $3 $4 $5 $6中
	set ${line} 
	echo $1 $2 $3 $4 $5 $6 
        # 输出如下
        # tiny4412  arm           armv7  tiny4412   samsung  exynos            
        # Target    Architecture  CPU    Board      [VENDOR] [SOC]#对应含义      
	# add default board name if needed
	[ $# = 3 ] && set ${line} ${1}
fi
# 分析命令行传入的参数,前面代码显示传入的参数是 mkconfig -A tiny4412,修改为了boards.cfg中的参数
while [ $# -gt 0 ] ; do  # $#表示传递给脚本或函数的参数个数; -gt表示大于
	case "$1" in
	--) shift ; break ;;      # 命令行参数是否包含 --,没有
	-a) shift ; APPEND=yes ;; # 命令行参数是否包含 -a,没有
	-n) shift ; BOARD_NAME="${1%_config}" ; shift ;;  # 命令行参数是否包含 -n,没有
	-t) shift ; TARGETS="`echo $1 | sed 's:_: :g'` ${TARGETS}" ; shift ;;# 命令行参数是否包含 -t,没有
	*)  break ;;
	esac
done

[ $# -lt 4 ] && exit 1  # $# = 6
[ $# -gt 7 ] && exit 1

# Strip all options and/or _config suffixes
CONFIG_NAME="${1%_config}" # $1 = tiny4412,从右边开始,截取_config之后的内容,tiny4412


[ "${BOARD_NAME}" ] || BOARD_NAME="${1%_config}"

echo $CONFIG_NAME   # CONFIG_NAME = tiny4412
echo $BOARD_NAME    # BOARD_NAME  = tiny4412

arch="$2"           # arch = arm; line中第2个参数
cpu="$3"            # cpu  = armv7; line中第3个参数
if [ "$4" = "-" ] ; then
	board=${BOARD_NAME}
else
	board="$4"      # board = tiny4412; ; line中第4个参数
fi
[ $# -gt 4 ] && [ "$5" != "-" ] && vendor="$5"  # $5 = samsung   
[ $# -gt 5 ] && [ "$6" != "-" ] && soc="$6"     # $6 = exynos
[ $# -gt 6 ] && [ "$7" != "-" ] && {            # false,下面的语句不执行
	tmp="${7%:*}"
	if [ "$tmp" ] ; then
		CONFIG_NAME="$tmp"
	fi
	# Check if we only have a colon...
	if [ "${tmp}" != "$7" ] ; then
		options=${7#*:}
		TARGETS="`echo ${options} | sed 's:,: :g'` ${TARGETS}"
	fi
}

if [ "${ARCH}" -a "${ARCH}" != "${arch}" ]; then  # ARCH = arch = arm
	echo "Failed: \$ARCH=${ARCH}, should be '${arch}' for ${BOARD_NAME}" 1>&2
	exit 1
fi

echo "Configuring for ${BOARD_NAME} board..."

#
# 创建指向架构特定头文件的软链接
# SRCTREE = OBJTREE = /home/gm/uboot/u-boot-tiny4412
if [ "$SRCTREE" != "$OBJTREE" ] ; then
    # 不会执行的分支语句。。。
else  # 进入此分支,设置 include/asm -> arch/arm/include/asm
	cd ./include
	rm -f asm
	ln -s ../arch/${arch}/include/asm asm
fi

rm -f asm/arch

if [ -z "${soc}" ] ; then #判断 soc 是否为空,非空,条件为false
	ln -s ${LNPREFIX}arch-${cpu} asm/arch
else
	ln -s ${LNPREFIX}arch-${soc} asm/arch  #设置include/asm/arch -> include/asm/arch-exynos
fi

if [ "${arch}" = "arm" ] ; then
	rm -f asm/proc
	ln -s ${LNPREFIX}proc-armv asm/proc    # 设置 proc -> proc-armv
fi

# 创建make 需要include 的文件config.mk,内容如下:
############################################
  ARCH   = arm
  CPU    = armv7
  BOARD  = tiny4412
  VENDOR = samsung
  SOC    = exynos
############################################
echo "ARCH   = ${arch}"  >  config.mk
echo "CPU    = ${cpu}"   >> config.mk
echo "BOARD  = ${board}" >> config.mk
[ "${vendor}" ] && echo "VENDOR = ${vendor}" >> config.mk
[ "${soc}"    ] && echo "SOC    = ${soc}"    >> config.mk

# Assign board directory to BOARDIR variable
if [ -z "${vendor}" ] ; then
    BOARDDIR=${board}
else
    BOARDDIR=${vendor}/${board}   # BOARDDIR = samsung/tiny4412
fi
# 创建板卡特定的头文件
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   # TARGET为空,此处没有往文件输入内容
	i="`echo ${i} | sed '/=/ {s/=/\t/;q } ; { s/$/\t1/ }'`"
	echo "#define CONFIG_${i}" >>config.h ;
done

cat << EOF >> config.h  # 将以下内容重定向到config.h中
#define CONFIG_BOARDDIR board/$BOARDDIR
#include <config_defaults.h>
#include <configs/${CONFIG_NAME}.h>
#include <asm/config.h>
EOF

exit 0

mkconfig脚本执行完成后,与特定架构相关的编译环境就配置好了。接着执行的就是下面这条命令的$(MAKE),开始正式编译:

tiny4412: tiny4412_config; $(MAKE)

实际上相当于执行了两次makefile,第一次是 make tiny4412_config ; 第二次是直接 make

 Makefile正式编译

在继续之前,先来看下 make 的工作方式,解析 Makefile 是一个循序渐进的过程。

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

第一步可以直接略过,make命令开始时,会找寻include所指出的其它Makefile,并把其内容安置在当前的位置,从第三步初始化变量开始分析,碰到include时在进行替换。

//版本号
VERSION = 2010
PATCHLEVEL = 12
SUBLEVEL =
EXTRAVERSION =
ifneq "$(SUBLEVEL)" ""
U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)
else
U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL)$(EXTRAVERSION)
endif
TIMESTAMP_FILE = $(obj)include/timestamp_autogenerated.h
VERSION_FILE = $(obj)include/version_autogenerated.h
。。。
// 设置生成文件目录、源码目录、根目录
OBJTREE		:= $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))
SRCTREE		:= $(CURDIR)
TOPDIR		:= $(SRCTREE)
LNDIR		:= $(OBJTREE)
export	TOPDIR SRCTREE OBJTREE
MKCONFIG	:= $(SRCTREE)/mkconfig    // 设置MKCONFIG命令
export MKCONFIG
。。。
# $(obj) and (src) are defined in config.mk but here in main Makefile
# we also need them before config.mk is included which is the case for
# some targets like unconfig, clean, clobber, distclean, etc.
ifneq ($(OBJTREE),$(SRCTREE))
obj := $(OBJTREE)/
src := $(SRCTREE)/
else
obj :=
src :=
endif
export obj src
#########################################################################
// 这里是一些工具和一些测试代码,作为一个伪目标文件,下面被其他目标依赖,后面有SUBDIRS的编译规则
SUBDIRS	= tools \
	  examples/standalone \
	  examples/api
.PHONY : $(SUBDIRS)
# 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     // 编译配置文件,记录了编译那些功能

# 加载 ARCH, BOARD, and CPU 配置,这写变量在include/config.mk中定义,由mkconfig生成
include $(obj)include/config.mk
export	ARCH CPU BOARD VENDOR SOC
# 设置交叉编译工具链,交叉编译时这条语句不会执行
# 交叉编译时真正使用的交叉编译工具链在arch/arm/config.mk中设置
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif
# 加载一些配置,包括 配置编译器,注意配置编译器的时候使用的延迟变量,文件后面sinclude $(TOPDIR)/arch/$(ARCH)/config.mk,实际的编译器就是在这个文件中配置的。
# $(TOPDIR)/config.mk中还配置了编译是用的参数包括ARFLAGS、RELFLAGS、DBGFLAGS、OBJFLAGS、CFLAGS、LDFLAGS等,其中uboot的代码段地址也是在这里配置的,在LDFLAGS里, -Ttext $(CONFIG_SYS_TEXT_BASE)
include $(TOPDIR)/config.mk

# U-Boot objects....order is important (i.e. start must be first)
# OBJS是编译uboot时用到的目标文件,作为库的形式提供
# start.o是uboot启动入口,分析u-boot的启动过程时从这个文件开始
# 这里只列出了部分obj文件
OBJS  = $(CPUDIR)/start.o
ifeq ($(CPU),i386)
OBJS += $(CPUDIR)/start16.o
OBJS += $(CPUDIR)/resetvec.o
endif

OBJS := $(addprefix $(obj),$(OBJS))

LIBS  = lib/libgeneric.o
LIBS += lib/lzma/liblzma.o
LIBS += lib/lzo/liblzo.o
LIBS += $(shell if [ -f board/$(VENDOR)/common/Makefile ]; then echo \
	"board/$(VENDOR)/common/lib$(VENDOR).o"; fi)
LIBS += $(CPUDIR)/lib$(CPU).o
ifdef SOC
LIBS += $(CPUDIR)/$(SOC)/lib$(SOC).o
endif
LIBS += drivers/hwmon/libhwmon.o
LIBS += drivers/i2c/libi2c.o
LIBS += drivers/input/libinput.o
LIBS += drivers/misc/libmisc.o
LIBS += drivers/mmc/libmmc.o
LIBS += drivers/mtd/libmtd.o
LIBS += drivers/mtd/nand/libnand.o
LIBS += drivers/mtd/onenand/libonenand.o
LIBS += drivers/mtd/ubi/libubi.o
LIBS += drivers/mtd/spi/libspi_flash.o
LIBS += drivers/net/libnet.o
LIBS += drivers/net/phy/libphy.o
LIBS += drivers/pci/libpci.o
LIBS += drivers/pcmcia/libpcmcia.o
LIBS += drivers/power/libpower.o
LIBS += drivers/spi/libspi.o
。。。。。。。。。
LIBS := $(addprefix $(obj),$(sort $(LIBS)))
.PHONY : $(LIBS) $(TIMESTAMP_FILE) $(VERSION_FILE)

LIBBOARD = board/$(BOARDDIR)/lib$(BOARD).o  // 与特定芯片板卡相关的文件,这里指/board/samsung/tiny4412
LIBBOARD := $(addprefix $(obj),$(LIBBOARD))
# Add GCC lib
。。。。。。。。。。。。。。。。。。。。。

上面都是编译的准备配置工作,接下来makefile中列出了编译目标开始正式生成目标文件,现在来看看直接 make 命令是怎么编译的。

make 的时候它会找文件中的第一个目标文件(target),这里是all,ALL变量中,u-boot.bin是我们最终需要的文件,看看是怎么生成的。makefile规则如下:

# 这是需要生成的目标文件
ALL += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND) $(U_BOOT_ONENAND)
all:		$(ALL)
$(obj)u-boot.bin:	$(obj)u-boot
		$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@
		$(BOARD_SIZE_CHECK)
GEN_UBOOT = \
		UNDEF_SYM=`$(OBJDUMP) -x $(LIBBOARD) $(LIBS) | \
		sed  -n -e 's/.*\($(SYM_PREFIX)__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
		cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
			--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
			-Map u-boot.map -o u-boot
$(obj)u-boot:	depend \
		$(SUBDIRS) $(OBJS) $(LIBBOARD) $(LIBS) $(LDSCRIPT) $(obj)u-boot.lds
		@echo "GEN_UBOOT  "  $(GEN_UBOOT)
		$(GEN_UBOOT)
ifeq ($(CONFIG_KALLSYMS),y)
		smap=`$(call SYSTEM_MAP,u-boot) | \
			awk '$$2 ~ /[tTwW]/ {printf $$1 $$3 "\\\\000"}'` ; \
		$(CC) $(CFLAGS) -DSYSTEM_MAP="\"$${smap}\"" \
			-c common/system_map.c -o $(obj)common/system_map.o
		$(GEN_UBOOT) $(obj)common/system_map.o
endif
$(OBJS):	depend
		$(MAKE) -C $(CPUDIR) $(if $(REMOTE_BUILD),$@,$(notdir $@))
$(LIBS):	depend $(SUBDIRS)
		$(MAKE) -C $(dir $(subst $(obj),,$@))
$(LIBBOARD):	depend $(LIBS)
		$(MAKE) -C $(dir $(subst $(obj),,$@))
$(SUBDIRS):	depend
		$(MAKE) -C $@ all

all作为第一个目标文件,这是一个伪目标,依赖文件ALL才是最终生成的文件。在编译u-boot.bin时先生成依赖文件u-boot,这是一个elf文件,它还依赖几个目录SUBDIRS,讲目录定义为伪目标,编译的时候用 $(MAKE) -C $@ all 的-C参数指定编译路径。至此,u-boot的编译过程就结束了。

链接器脚本文件

最后一个要说的和编译相关的是链接器脚本。什么是链接脚本?链接脚本就是程序链接时的参考文件,其主要目的是描述如何把输入文件中的段(SECTION)映射到输出文件中,并控制输出文件的存储布局。链接脚本的基本命令是SECTIONS命令,一个SECTIONS命令内部包含一个或多个段,段(SECTION)是链接脚本的基本单元,它表示输入文件中的某个段是如何放置的。

连接器有个默认的内置链接脚本, 可用arm-linux-ld --verbose 查看,-T选项用以指定自己的链接脚本, 将代替默认的链接脚本。

查看elf文件的信息的工具 readelf -a、objdump -h、nm。

下面是u-boot下tiny4412的链接器脚本文件:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")/*指定输出可执行文件是elf格式,32位ARM指令,小端*/
OUTPUT_ARCH(arm)/*指定输出可执行文件的平台为ARM*/
ENTRY(_start)/*指定输出可执行文件的起始代码段为_start*/

在运行一个程序时,第一个被执行到的指令成为”入口点”,你可以使用”ENTRY链接脚本命令来设置入口点,参数是一个符号名,如下:  ENTRY(SYMBOL) ;有很多不同的方法来设置入口点,链接器会按顺序尝试以下方法来设置入口点,如果成功了,就会停止。

  1. ’-e’ 入口命令行选项 
  2. 链接脚本中的ENTRY(SYMBOL)命令 
  3. 如果定义了start,就使用start的值 
  4. 如果存在就使用’.text’段的首地址 
  5. 地址’0’
SECTIONS
{
	. = 0x00000000;  /* 设置起始地址,表示程序的链接地址 */
	. = ALIGN(4);    /* 代码以4字节对齐  */
	.text	:        /* 代码段 */
	{
		arch/arm/cpu/armv7/start.o	(.text)/*指明CPU上电后第一个执行的文件*/  
		board/samsung/tiny4412/libtiny4412.o (.text)
		arch/arm/cpu/armv7/exynos/libexynos.o	(.text)
		*(.text) /*下面依次为各个text段函数*/
	}
	. = ALIGN(4);
	.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } /*指定只读数据段*/

	. = ALIGN(4);
	.data : {
		*(.data)
	}
	. = ALIGN(4);
	. = .;
	__u_boot_cmd_start = .;/*把__u_boot_cmd_start赋值为当前位置, 即起始位置,在start.s中会用到*/
	.u_boot_cmd : { *(.u_boot_cmd) } /*指定u_boot_cmd段, uboot把所有的uboot命令放在该段.*/
	__u_boot_cmd_end = .;/*把__u_boot_cmd_end赋值为当前位置,即结束位置*/
	. = ALIGN(4);
	.rel.dyn : {
		__rel_dyn_start = .;
		*(.rel*)
		__rel_dyn_end = .;
	}
	.dynsym : {         /* 里面装载了符号信息 */
		__dynsym_start = .;
		*(.dynsym)
	}
	.bss __rel_dyn_start (OVERLAY) : {
		__bss_start = .; /*把__bss_start赋值为当前位置,即bss段的开始位置*/
		*(.bss)
		 . = ALIGN(4);
		_end = .;
	}
	/DISCARD/ : { *(.dynstr*) }
	/DISCARD/ : { *(.dynamic*) }
	/DISCARD/ : { *(.plt*) }
	/DISCARD/ : { *(.interp*) }
	/DISCARD/ : { *(.gnu*) }
}

上面用到了许多和段有关的知识,这是ELF文件的特性,这个地方不太理解,用到的工具也有好几个,后面专门用一片文章把这部分知识学透。 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值