如何阅读makefile
作为使用者,makefile读起来真的是又干又硬。我读的第一篇makefile——清华大学ucore的makefile由两部分组成:提供一套生成rule的API并维护生成文件列表的function.mk,和Makefile。这篇makefile我读了三次。
第一遍读的时候秉承着自顶向下的原则,直接开始读Makefile,好像大概了解了他在干什么,但不知道他具体怎么做,在探究期间get到查阅了GNU文档的熟练度,总算了解到了rule,prerequsite,recipe,variable这些概念,顺着读下来也读了个七七八八,把语法给弄懂了(虽然现在还不知道多重dollar的意义是什么)
第二遍读的时候是对照着输出来看的。这个时候做了些后边的题,对需要生成的文件有了大致概念,再对照着Makefile看,发现了当时看被忽略掉的function.mk,饶有兴趣地看了会,但是Makefile的语法实在是。。。。不堪入目,函数形参名直接用数字表示,由实际调用去一层一层向上找被调用的函数试图理解,没多久就被绕晕了。。。
第三遍读是ddl前一晚,由于我发现把“ucore.img是如何一步步生成的”这一题给答成“如何写Makefile”了,赶紧重新看了一遍。虽然最终我满意的这个版本没有即时完成,但我重新学到了很多看Makefile的方法。
由于前面提到的Makefile自定义函数参数名很乱,于是我干脆从function.mk开始看起。还有一个原义是因为function.mk是作者自定义的函数库,它的每一个部分都会被用到,不存在白看的情况。由于Makefile的语法本身很复杂,所以我直接按照之前看的经验,将函数分成两类看,分为名字转换,编译规则生成两大类,并直接记下他们的名字和效果,省去了一个个分析语法的麻烦。然后很快就能发现,再Makefile中使用时,这些函数只不过是提前被填充了一些字段,或者直接是套娃,加个foreach而已。
对于生成的文件从何而来,我找到了生成相应路径名的函数,使用Vim的搜索功能很方便的就查找出了所有使用这些函数的Rule,那些Prerequisite大概率就是他们的编译源文件。
所以总结下来,看Makefile只需要:
- 熟悉Makefile语法
- 明确目标:了解最终有什么文件被生成,文件名从何处来,生成这些文件的Rule在哪里,这些文件的源文件是什么,这些文件的源文件就是原始文件吗?不是的话他们又是怎么被生成和记录的?
- 简化函数,提取核心函数:这些函数叫什么名字,他们的作用是什么,记录下来
对于写报告,一定要抓住核心,不要写任何无关问题仅仅是为了展示努力程度的知识,这样只会显得报告冗长甚至走题。
以下就是我对ucore.img如何一步步生成的分析
ucore.img 如何一步步生成
编译参数解释
GCC
$gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs\
-nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc \
-c boot/bootasm.S -o obj/boot/bootasm.o
# [-Idir...]: 将dir添加到头文件搜索路径。
# -fno-builtin: 屏蔽不以__builtin_开头的内置函数
# -Wall: 打开所有warning选项,范围包括宏定义的conjunction
# -ggdb: 生成GDB使用的debug信息
# -m32: 以32bit为单位对齐
# -gstabs: 生成stabs格式的debug信息(不包含GDB扩展)
# -nostdinc: 改变system directories的搜索顺序
# -fno-stack-protector: 关闭stack-protector,避免编译器增加guard代码检查堆栈分配函数
# -Os: 以最小代码量为目标优化,具体是使用不会增加代码量的-O2优化
# -c: 编译或汇编原文件,但不链接。(输出.o结尾的object file)
# -o: 将输出放到指定的file中。
预处理器
define cc_template
$$(call todep,$(1),$(4)): $(1) | $$$$(dir $$$$@)
@$(2) -I$$(dir $(1)) $(3) -MM $$< -MT "$$(patsubst %.d,%.o,$$@) $$@">$$@
# Option that control the preprocessor
# -MM: 使用make描述依赖的rule替代preprocessor的输出 e.g.: filename.o : file1.c file2.c
# -MT "<string>": target就是你指定的string
ld
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel $^
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 -o obj/bootblock.o $^
# -m elf_i386: 仿真elf_i386 linker
# -nostdlib:
# -T: 用指定的linker script(tools/kernel.ld) 替换默认linker script
# -N --OMAGIC: 将数据和代码区设为可读写,取消数据段的页对齐,禁止链接共享库,允许Unix magic number。
# -e : 指明程序入口为start
# -Ttext: 指明text段的地址为 0x7C00
# $^: makefile 自动变量,整个prerequisite列表
objdump
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
# -S --source: 反汇编并显示源码(-d)
objcopy
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
# -S --strip-all: 不复制调试信息
# -O binary: output a binary file
Makefile解读
function.mk
名字转换
toobj: obj/[dir/]filebasename.o
todep: obj/[dir/]filebasename.d
totarget: bin/filename
packetname: __obj_[prefix]
包(文件列表)
listf: 将dir/*[.<type>]列出
do_add_files_to_packet -> cc_template: 编译files并将生成的objs并加入packet中
do_add_objs_to_packet: 将objs加入packet中
do_create_target: 使用taget,packet里的objs和指定的objs(可以指定是否链接)创建rule
do_finish_all: 创建ALLOBJS的文件夹, obj/,bin/
函数
do_cc_compile -> cc_template:
1. 预处理为make生成文件obj/[dir/]filename.d, 内含target为.o .d的rule
2. 编译生成目标文件.o
makefile_定制function
名字
listf_cc: 文件夹dir下.c文件列表
objfile: filebasename.o
asmfile: filebasename.asm
outfile: filebasename.out
symfile: filebasename.sym
生成rule
add_files_cc/hostcc -> add_files = do_add_files_to_packet: 用files生成编译型rule,并将目标文件加入到到指定的packet里,还可以选定obj下的子文件夹(这里并没有用到)
create_target_cc/hostcc -> create_target = 使用target,packet,和obj生成编译型rule
makefile_kernel
-
将所有lib文件夹下的文件都生成编译rule,并添加到packet lib中
-
将所有kernel依赖文件都生成编译rule,并将目标文件添加到packet kernel中
-
自定义
bin/kernel
的生成rule,prerequisite为lib包和kernel包中的目标文件 -
将
bin/kernel
加入TARGET中。
makefile_bootloader
-
对每个boot文件夹下的文件都创建编译rule
-
自定义
bin/bootblock
的生成rule -
将
bin/bootblock
加入TARGET中
makefile_sign
使用本机选项编译tools/sign.c并将sign添加到TARGET中
makefile_ucore.img
-
使用字节拷贝工具dd将bootblock和kernel拷贝到一片空白文件中,制作磁盘映像
-
将
bin/ucore.img
加入到TARGET中
各文件的关系
理清了各个函数的作用和在makefile中的使用,我们应该已经能知道生成的文件是什么,由那些操作来,源文件是什么。
总共生成了两个文件夹:obj和bin。前缀bin从totarget中来,前缀obj从toobj中来。
只需查找toobj就能发现:使用toobj的rule是在cc_template中定义的,而cc_template只在cc_compile, add_files_to_packet中出现,调用cc_compile的只在编译各个boot文件夹下文件时出现过,因此确定:boot/*.c --> obj/boot*.o
;而add_files_to_packet在Makefile中被重新封装成add_files_cc和add_files_host,使用搜索工具,发现在创建packet lib、kernel、sign, 编译lib、kernel子文件夹下的.c文件和tools/sign.c时,使用了add_files_cc和add_files_host,因此确定:lib/*.c --> obj/lib/*.o
, kernel/*/*.c --> lib/kernel/*/*.o
,tools/sign.c --> lib/tools/sign.c
再来查找totarget:在function.mk中只有create_target使用了totarget创建rule,该函数在Makefile中被定制成了create_target_host 和 create_target_cc,查找这些函数,发现创建了以下target:
- bin/kernel:由obj/libs obj/kernel 的.o文件链接而成
- bin/bootblock:由obj/boot 的 .o 文件链接而成
- bin/sign:由bin/tools/sign/的.o文件链接而成
- bin/ucore.img: 由dd工具将bin/bootblock和bin/kernel拼接生成
但是obj文件夹中,除了boot kernel libs sign文件夹下的文件,还有一些文件是从何而来呢?
其中bootblock.asm bootblock.o bootblock.out 来自于 bin/bootblock的生成rule:
- bootblock.asm:由objdump反汇编bootblock而来
- bootblock.o:由objcopy将调试信息剔除而来
- bootblock.out:在bootblock.o的基础上,用bin/sign工具处理后的结果
对应的,kernel.asm和kernel.sym来自于 bin/kernel的生成rule:
- kernel.asm:objdump工具打印的kernel的反汇编结果
- kernel.sym:objdump工具打印的kernel的符号表入口,使用sed过滤后的结果