上文讲到,如果需求仅略微修改,整个从编译到仅保留二进制文件到添加HeaderInfo到烧写到SD卡的一系列命令都需要重新再输入一遍,这很繁琐。
如何解决这个问题呢?
制作一个bash脚本文件
制作一个bash脚本文件,也就是制作一个批处理文件:
#!/bin/bash arm-linux-gcc -c mystart.s arm-linux-gcc -c mylowlevel_init.s //.s文件生成.o文件 arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o //.o文件生成可执行文件 arm-linux-objcopy -O binary myboot myboot.bin //只保留二进制文件 ./mkv210 u-boot.bin u-boot.16k //添加HeaderInfo sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1 //烧写到SD卡
然后运行这个文件:
bash genmyboot.sh
虽然,这样也能够实现功能,但是某种程度上可以看出并不智能。什么意思呢?可能某次修改,只修改了其中的某几个文件,其他的很多文件都没有修改。但如果运行这个bash,所有的程序都要走一遍,也就是无论文件是否被修改,都会被处理,会浪费很多的时间。
Makefile
无需畏惧Makefile
Makefile是一个能够让初学者很挫败的文件,初看会让人头昏眼花,感觉在看“天书”。比如下面是U-Boot的Makefile中很少的一部分:
$(obj)u-boot.img: $(obj)u-boot.bin $(obj)tools/mkimage -A $(ARCH) -T firmware -C none \ -O u-boot -a $(CONFIG_SYS_TEXT_BASE) -e 0 \ -n $(shell sed -n -e 's/.*U_BOOT_VERSION//p' $(VERSION_FILE) | \ sed -e 's/"[ ]*$$/ for $(BOARD) board"/') \ -d $< $@ $(obj)u-boot.ubl: $(obj)spl/u-boot-spl.bin $(obj)u-boot.bin $(OBJCOPY) ${OBJCFLAGS} --pad-to=$(PAD_TO) -O binary $(obj)spl/u-boot-spl $(obj)spl/u-boot-spl-pad.bin cat $(obj)spl/u-boot-spl-pad.bin $(obj)u-boot.bin > $(obj)u-boot-ubl.bin $(obj)tools/mkimage -n $(UBL_CONFIG) -T ublimage \ -e $(CONFIG_SYS_TEXT_BASE) -d $(obj)u-boot-ubl.bin $(obj)u-boot.ubl rm $(obj)u-boot-ubl.bin rm $(obj)spl/u-boot-spl-pad.bin $(obj)u-boot.ais: $(obj)spl/u-boot-spl.bin $(obj)u-boot.img $(obj)tools/mkimage -s -n $(if $(CONFIG_AIS_CONFIG_FILE),$(CONFIG_AIS_CONFIG_FILE),"/dev/null") \ -T aisimage \ -e $(CONFIG_SPL_TEXT_BASE) \ -d $(obj)spl/u-boot-spl.bin \ $(obj)spl/u-boot-spl.ais $(OBJCOPY) ${OBJCFLAGS} -I binary \ --pad-to=$(CONFIG_SPL_MAX_SIZE) -O binary \ $(obj)spl/u-boot-spl.ais $(obj)spl/u-boot-spl-pad.ais cat $(obj)spl/u-boot-spl-pad.ais $(obj)u-boot.img > \ $(obj)u-boot.ais
是不是很费解?Makefile其实语法比较简单,但复杂就复杂在其中运用了许多shell脚本和各种正则表达式等,这些混杂在一起,就会让人摸不着头脑。
如果需要详细了解Makefile的内容,可以参考链接:跟我一起写Makefile。
Makefile的基本语法
target ... : prerequisites ... command ... ...
具体含义为:
- target:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label);
- prerequisites:生成该target所依赖的文件和/或target;
- command:该target要执行的命令(任意的shell命令)。
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件, 其生成规则定义在command中。说白一点就是说:prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
这就是Makefile的规则,也就是Makefile中最核心的内容。
Makefile案例
既然大致了解了Makefile的规则,那么如何才能完成制作描述U-Boot烧写镜像到SD卡的过程的Makefile呢?
.s文件生成.o文件
先根据这个简单的规则,写一个简单的Makefile。这里首先注明一下make命令的寻找优先级,make会在当前目录下寻找以下的Makefile文件,优先级由高到低为:
GUNMakefile > makefile > Makefile
一般,文件命名为Makefile。
制作一个Makefile文件,用于将mystart.s汇编成mystart.o文件:
mystart.o: mystart.s arm-linux-gcc -c mystart.s
这很简单,如果同时还需要将mylowlevel_init.s汇编成mylowlevel_init.o文件:
mystart.o: mystart.s arm-linux-gcc -c mystart.s mylowlevel_init.o: mylowlevel_init.s arm-linux-gcc -c mylowlevel_init.s
如果此时,还是运行Makefile,会发现只运行了前一句指令,但后一句的指令并没有被运行。这是因为:Makefile只运行第一个目标target,其余的目标target都不会被运行。
怎么解决呢?利用Makefile生成一个伪目标target,再利用伪目标target去生成接下来的两个目标target。方式如下:
.PHONY:all //注明all为伪目标(可写可不写) all: mystart.o mylowlevel_init.o mystart.o: mystart.s arm-linux-gcc -c mystart.s mylowlevel_init.o: mylowlevel_init.s arm-linux-gcc -c mylowlevel_init.s
当make的时候,目标all本身不运行任何命令,但是它依赖于mystart.o和mylowlevel_init.o。但当前目录下并没有这两个文件,因此它就会自动在当前Makefile中寻找是否存在目标target,如果存在,就自动执行该目标的命令。
其余步骤
.PHONY:all //注明all为伪目标(可写可不写) all: mystart.o mylowlevel_init.o arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o arm-linux-objcopy -O binary myboot myboot.bin ./mkv210 u-boot.bin u-boot.16k mystart.o: mystart.s arm-linux-gcc -c mystart.s mylowlevel_init.o: mylowlevel_init.s arm-linux-gcc -c mylowlevel_init.s
由于烧写到SD卡的命令,需要SD卡已经插入的状态,一般而言,不会将它和这些命令都写在一起。但是,每次都输入这么麻烦的代码,也是很麻烦的事情。于是,可以用另一种方式:
.PHONY:all //注明all为伪目标(可写可不写) all: mystart.o mylowlevel_init.o arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o arm-linux-objcopy -O binary myboot myboot.bin ./mkv210 u-boot.bin u-boot.16k mystart.o: mystart.s arm-linux-gcc -c mystart.s mylowlevel_init.o: mylowlevel_init.s arm-linux-gcc -c mylowlevel_init.s .PHONY:mksd mksd: sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1
创造一个伪目标,专门用于烧写镜像到SD卡中。但是,一般情况下,这句伪目标是运行不到的。那怎么样才能只运行这句伪目标呢?
只需要在make的时候,人为地指定伪目标即可:
make mksd
Makefile的改进
尽管此时Makefile的运行没有问题,但是还是会发现一个问题。如果修改了mylowlevel_init.s文件,只会将mylowlevel_init.s文件会变成mylowlevel_init.o文件,mystart.s并不会重新汇编,这很不错;但是如果两个都不修改,此时两个.s文件都不会会变成.o文件,但是后面链接、只保留二进制文件、添加HeaderInfo三句依然还是会运行!
这是为什么呢?
由于all是一个伪目标,没有办法进行目标与依赖之间的新旧关系,因此就会一直都会运行后面的三句。
改进后的代码为:
.PHONY:all //注明all为伪目标(可写可不写) all: myboot mystart.o: mystart.s arm-linux-gcc -c mystart.s mylowlevel_init.o: mylowlevel_init.s arm-linux-gcc -c mylowlevel_init.s myboot: mystart.o mylowlevel_init.o myboot.lds arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o arm-linux-objcopy -O binary myboot myboot.bin ./mkv210 u-boot.bin u-boot.16k .PHONY:mksd mksd: sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1
如此便好,也就是说,最好让伪目标没有命令可以执行。
Makefile的自动化变量
尽管上文的Makefile看起来比较“优雅”了,但是还是存在问题的:如果存在100个.s文件需要汇编成.o文件,那么需要写100条类似于如下的代码。
mystart.o: mystart.s arm-linux-gcc -c mystart.s
这想一想,也是非常繁琐。于是,Makefile就引进了:
- 自动化变量:$@(所有目标target集合)、$^(所有依赖集合)、$<(所有依赖集合中的第一个)
- 模式匹配:%.x(当前目录下所有.x结尾的文件)
有了自动化变量和模式匹配,就可以写出更加简洁的Makefile了:
.PHONY:all //注明all为伪目标(可写可不写) all: myboot %.o: %.s arm-linux-gcc -c $< myboot: mystart.o mylowlevel_init.o myboot.lds arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o arm-linux-objcopy -O binary myboot myboot.bin ./mkv210 u-boot.bin u-boot.16k .PHONY:mksd mksd: sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1
同样,可以使用变量来代替某些内容,有点类似于宏定义的样子。一般采用:=来赋值,引用的时候需要用$()来引用:
CC := arm-linux-gcc .PHONY:all //注明all为伪目标(可写可不写) all: myboot %.o: %.s $(CC) -c $< myboot: mystart.o mylowlevel_init.o myboot.lds arm-linux-ld -T myboot.lds -o myboot mystart.o mylowlevel_init.o arm-linux-objcopy -O binary myboot myboot.bin ./mkv210 u-boot.bin u-boot.16k .PHONY:mksd mksd: sudo dd iflag=dsync oflag=dsync if=u-boot.16k of=/dev/sdb seek=1
当然,U-Boot的Makefile肯定比本文的要复杂得多的多,之后的博文会对此进行详细分析。