Makefile能有效且灵活地管理汇编程序和C程序的编译链接行为。它是一个基于shell但又不同于shell的脚本语言。它的强大和灵活也注定它语法的复杂性。 Makefile对于嵌入式Linux的软件工程来说有很悠久的历史。不同于单片机程序开发有专门的IDE,免去这方面的麻烦。一个通用而强大的Makefile对于从业人员来说是很重要的。当然业内也是有python scons之类的替代方案。但这不妨碍我们学习它的执着。 而解析u-boot的Makefile的目的,是因为要实现一个自己的Makefile必须有一个参考别人优秀作品的过程,而u-boot Makefile就是一个很合适的优秀例子。 相关硬件:JZ2440 相关软件:u-boot-1.1.6 + 100ask补丁
在此我首先要强调的是Makefile不只是一个文件,它也可以是一个工程,分管是工程的一个重要特征,因此需要首先阐明Makefile工程的文件架构。
上面列表转载自u-boot Makefile整体解析
单凭上面比较单调的文字描述,其实是不足以形象地描绘文件所分管的作用。想进一步了解必须结合接下来章节的内容。
单在u-boot 进行make编译链接前,必须有一个配置的步骤. 在此案例中,命令行指令为:Make 100ask24x0_config 在这里,我必须要说在u-boot中有这么几个概念: ARCH 代表 mpu的内核类型 CPU 代表 mpu的内核型号 BOARD 代表硬件板子(mpu+外围IC电路)的型号 SOC 代表 mpu的型号 这几个设置项对u-boot来说,就是对硬件平台的概括与认识。无论遇上任何板子,都需要为这四个设置项找到对应值。如果u-boot的源码中,并没有对应的硬件设置项的值,那么就需要自己去写相关的硬件代码,去实现相关的硬件配置。 就拿JZ2440为例子,对应的配置项为: ARCH = arm CPU = arm920t BOARD = 100ask24x0 SOC = s3c24x0 而其中的100ask24x0就是u-boot本来是不支持的,需要自己实现的板级硬件代码 正是因为u-boot可能匹配的硬件是具有多样性的,所以在软件编译链接前,一定要在u-boot的代码中找出正确的硬件代码进行编译。而这个就是make xxx_config实现的功能。 接下来,让我们看看make 100ask24x0在Makefile中是怎样处理的。
MKCONFIG := $(SRCTREE)/mkconfig # 2. 运行 命令 ./mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0 100ask24x0_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0 #1.删除include/config.h include/config.mk 和 board目录下的所有子目录的config.tmp文件 unconfig: @rm -f $(obj)include/config.h $(obj)include/config.mk \ $(obj)board/*/config.tmp $(obj)board/*/*/config.tmp如上所示,在顶层makefile直接把参数100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0丢给mkconfig执行。
# 1. BOARD_NAME = 100ask24x0 [ "${BOARD_NAME}" ] || BOARD_NAME="$1" # # Create link to architecture specific headers # if [ "$SRCTREE" != "$OBJTREE" ] ; then ........ else # 2. 删除 asm文件 # 3. 创建 asm 文件,并链接到 asm-arm 目录 cd ./include rm -f asm ln -s asm-$2 asm fi # 3. 删除asm-arm/arch目录 rm -f asm-$2/arch if [ -z "$6" -o "$6" = "NULL" ] ; then ...... else # 4. 创建asm-arm/arch 文件,并链接到 arch-s3c24x0 ln -s ${LNPREFIX}arch-$6 asm-$2/arch fi if [ "$2" = "arm" ] ; then # 5. 删除asm-arm/proc 文件 # 6. 创建asm-arm/proc 文件,并链接到 proc-armv目录 rm -f asm-$2/proc ln -s ${LNPREFIX}proc-armv asm-$2/proc fi # # Create include file for Make # # 7. 把 ARCH = arm # CPU = arm920t # BOARD = 100ask24x0 # SOC = s3c24x0 # 输入到 include/config.mk文件中 echo "ARCH = $2" > config.mk echo "CPU = $3" >> config.mk echo "BOARD = $4" >> config.mk [ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk [ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk # # Create board specific header file # if [ "$APPEND" = "yes" ] # Append to existing config file then ...... else # 8. 创建include/config.h > config.h # Create new config file fi # 8. 把文本"#include <configs/100ask24x0.h>"写入到include/config.h echo "/* Automatically generated - do not edit */" >>config.h echo "#include <configs/$1.h>" >>config.h从mkconfig脚本来看,整个操作都集中在include目录,创建并链接文件 asm,arch,proc,用于能准确找到正确的硬件代码头文件。再有就是创建config.mk 把硬件设置信息保存起来,创建config.h 让 100ask24x0.h作用于全局。
我们都知道所有makefile都必然有一个总目标,而顶层makefile的总目标就是要生成u-boot的镜像文件。不过我们首先要说的是,顶层makefile的几个关键的变量和包含的文件。
# 1.包含 include/config.mk(里面有硬件配置信息),如果包含失败,则报错退出 include $(OBJTREE)/include/config.mk # 2. 包含顶层config.mk文件,该文件其实被所有的makefile所需要,里面有*.c,*.s编译成*.o的规则,链接文件的路径,头文件的路径,和编译,链接命令所需的参数项,还有的就是它会去arch cpu board soc代码目录下的config.mk include $(TOPDIR)/config.mk # 3. 创建LIBS变量,把所有需要链接的库都写进去 LIBS = lib_generic/libgeneric.a LIBS += board/$(BOARDDIR)/lib$(BOARD).a LIBS += cpu/$(CPU)/lib$(CPU).a ifdef SOC LIBS += cpu/$(CPU)/$(SOC)/lib$(SOC).a endif LIBS += lib_$(ARCH)/lib$(ARCH).a LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \ fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a LIBS += net/libnet.a LIBS += disk/libdisk.a LIBS += rtc/librtc.a LIBS += dtt/libdtt.a LIBS += drivers/libdrivers.a LIBS += drivers/nand/libnand.a LIBS += drivers/nand_legacy/libnand_legacy.a LIBS += drivers/usb/libusb.a LIBS += drivers/sk98lin/libsk98lin.a LIBS += common/libcommon.a LIBS += $(BOARDLIBS) LIBS := $(addprefix $(obj),$(LIBS)) # 创建SUBDIRS变量 SUBDIRS = tools \ examples \ post \ post/cpu从上述的脚本可以看出include/config.mk是在早期 就被包含进来,意味着里面的ARCH CPU BOARD等变量都是作用全局的,至关重要的变量。 而顶层的config.mk是贯穿整个makefile文件体系的脚本,内容相当的多,所以后面再展开说明。 变量LIBS里面所记录的库,都是在最终链接成u-boot镜像所需的库,而这里面的一个库往往代表是某个目录下编译所有*.o的集合。 变量SUBDIRS里面的目录是需要首先被生成依赖关系文件(.depend)的目录,大家知道有这么一回事就可以。 接下来我们开始分析顶层makefile的总目标
ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND) # 0. 总目标就是要生成u-boot.srec u-boot.bin System.map。至于 $(U_BOOT_NAND)是空的,不予理会 all: $(ALL) # 7. 把u-boot文件转换成u-boot.srec文件 $(obj)u-boot.srec: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O srec $< $@ # 6. 把start.o 和 $(LIBS)中所有的库 都链接成 u-boot(elf文件格式),此处的u-boot可转换成u-boot.srec 和 # u-boot.bin文件 $(obj)u-boot: depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT) UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) |sed -n -e 's/.*\(__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 # 1. 逐步进入目录tools examples post post/cpu,执行make _depend,生成依赖关系 depend dep: for dir in $(SUBDIRS) ; do $(MAKE) -C $$dir _depend ; done VERSION_FILE = $(obj)include/version_autogenerated.h # 2. 把文本"#define U_BOOT_VERSION "U-Boot 1.1.6""写入到include/version_autogenerated.h version: @echo -n "#define U_BOOT_VERSION \"U-Boot " > $(VERSION_FILE); \ echo -n "$(U_BOOT_VERSION)" >> $(VERSION_FILE); \ echo -n $(shell $(CONFIG_SHELL) $(TOPDIR)/tools/setlocalversion \ $(TOPDIR)) >> $(VERSION_FILE); \ echo "\"" >> $(VERSION_FILE) # 3. 逐步进入目录tools examples post post/cpu,执行make all $(SUBDIRS): $(MAKE) -C $@ all OBJS = cpu/$(CPU)/start.o # 4. 进入cpu/arm920t 生成start.o $(OBJS): $(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@)) # 5. 从$(LIBS)中提取目录路径,并执行make(一般都是生成依赖关系,和 库文件) $(LIBS): $(MAKE) -C $(dir $(subst $(obj),,$@)) ifeq ($(CONFIG_NAND_U_BOOT),y) ...... else LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds endif由于顶层makefile生成总目标的依赖关系较多,我只筛选其中必要的一部分进行讲解。如上面的顶层Makefile脚本所示,生成u-boot镜像的过程无非就是进入对应的目录执行对应makefile,以达到生成依赖关系和库的目的。最终把所有的库都链接成u-boot。
从顶层makefile的分析看来,子目录下的makefile的功能是生成依赖关系和库文件。所以接下来,选board/100ask24x0/makefile作为例子讲解:
# 1. 包含顶层config.mk文件,以满足*.c *.s生成*.o的需求 include $(TOPDIR)/config.mk # 2. 定义需要生成的库文件名字,此处你会发现$(BOARD)并没定义,同时也没有包含include/config.mk # 那是因为顶层makefille在调用make -C时,就已经把已有的变量传递子目录的makefile LIB = $(obj)lib$(BOARD).a # 3. 定义需要参与库文件的*.o文件 COBJS := 100ask24x0.o boot_init.o SOBJS := lowlevel_init.o SRCS := $(SOBJS:.o=.S) $(COBJS:.o=.c) OBJS := $(addprefix $(obj),$(COBJS)) SOBJS := $(addprefix $(obj),$(SOBJS)) # 6. 在生成依赖关系后,则把.o文件 合成为 库文件(.a文件) $(LIB): $(obj).depend $(OBJS) $(SOBJS) $(AR) $(ARFLAGS) $@ $(OBJS) $(SOBJS) clean: rm -f $(SOBJS) $(OBJS) distclean: clean rm -f $(LIB) core *.bak .depend # 4. 通过包含 rules.mk 文件来生成依赖关系 include $(SRCTREE)/rules.mk # 5. 包含已有的依赖关系 sinclude $(obj).depend由上面可以看出,我们子目录makefile是如何生成库文件的。同时这里有两个地方需要展开说明的是,.o文件是从何而来的。我们看看 顶层 config.mk的一段脚本。
OPTFLAGS= -Os CROSS_COMPILE = arm-linux- CPP = $(CC) -E CC = $(CROSS_COMPILE)gcc AFLAGS := $(AFLAGS_DEBUG) -D__ASSEMBLY__ $(CPPFLAGS) CPPFLAGS := $(DBGFLAGS) $(OPTFLAGS) $(RELFLAGS) \ -D__KERNEL__ -DTEXT_BASE=$(TEXT_BASE) \ CPPFLAGS += -I$(TOPDIR)/include CPPFLAGS += -fno-builtin -ffreestanding -nostdinc \ -isystem $(gccincdir) -pipe $(PLATFORM_CPPFLAGS) CFLAGS := $(CPPFLAGS) -Wall -Wstrict-prototypes ifndef REMOTE_BUILD ...... else $(obj)%.s: %.S $(CPP) $(AFLAGS) -o $@ $< # 相当于arm-linux-gcc -D__ASSEMBLY__ -Os -D__KERNEL__ -I$(TOPDIR)/include -DTEXT_BASE=0x33F80000 -c -o # 太长了没办法完全展开 *.o *.S $(obj)%.o: %.S $(CC) $(AFLAGS) -c -o $@ $ # 相当于arm-linux-gcc -D__ASSEMBLY__ -Os -D__KERNEL__ -I$(TOPDIR)/include -DTEXT_BASE=0x33F80000 -c -o # 太长了没办法完全展开 *.o *.S $(obj)%.o: %.c $(CC) $(CFLAGS) -c -o $@ $< endif真是有了上述的脚本,在子makefile下所有的.c ,.s 文件都能编译成.o文件
之前说过顶层rules.mk主要是用于生成依赖关系。那让我们来看看脚本。
# 1. 此处阐述的是_depend的目标依赖于.depend _depend: $(obj).depend # 2. 此处阐述.depend依赖于Makefile文件,顶层config.mk文件,还有子目录下需要参与编译的.c文件集合$(SRCS) $(obj).depend: $(src)Makefile $(TOPDIR)/config.mk $(SRCS) @echo $(SRCS) @echo “This is depend here\n\r” # 3. 删除子目录下的.depend文件 @rm -f $@ # 4. 针对每个参与编译的 .c文件有如下操作 @for f in $(SRCS); do \ # basename $f 意思是获取去掉路径后的文件名 # sed -e 's/\(.*\)\.\w/\1.o/'; 意思是把文件名中*.c 替换成 *.o 然后复制给 变量g # 最后执行arm-linux-gcc -M $(HOST_CFLAGS) $(CPPFLAGS) -MQ *.o *.c >> .depend ; g=`basename $$f | sed -e 's/\(.*\)\.\w/\1.o/'`; \ $(CC) -M $(HOST_CFLAGS) $(CPPFLAGS) -MQ $(obj)$$g $$f >> $@ ; \ donerulues.mk就是通过上述脚本来生成依赖关系.depend的。有人会问,依赖关系有什么用? 依赖关系指的是一个.c文件所依赖的.h 和 其他的.c文件的集合,如果一个已经被编译过的工程,仅修改了一个.h 或 .c文件,那么通过依赖关系就可以找到有多少关联的.c文件需要重新编译,避免整个工程重新编译的时间浪费。所以在任何的子目录makefile中,都会有以下的一句脚本。它是用来包含已经存在的子目录下依赖关系文件。
sinclude $(obj).depend关于顶层config.mk,我在子目录makefile就已经讲过一些,也是最关键,因此我就不在此展开。因为熟悉makefile语法的,一般都比较容易理解。而熟练度不够高的,或许需要找一些基础的培训教程看看。
通篇开来,我讲的东西,可能未必能让所有人都能读懂。但是我用最通俗的说法总结的就是: 1. make xxx_config 是为了跟指定的硬件代码找到对应关系,include/config.mk就是它输出的硬件配置信息 2. 顶层Makefile是为了把所有参与编译的子目录的库文件链接成u-boot镜像文件 3. 子目录Makefile里面包含了顶层config.mk 和 rules.mk文件,最终生成库文件和依赖关系文件 4. 顶层config.mk 是 .c .s文件编译成.o文件的编译规则 5. 顶层rules.mk是生成依赖关系文件.depend的规则