ucor_os实验报告(lab1)_makefile文件详解!
练习一 理解通过make生成执行文件的过程
为完成这个练习,需要准备的知识比较多:
- linux常用命令
特别是要理解grep、awk、sed这三个命令,以及linux万物兼文件的思想
>文件 | 含义 |
---|---|
&0 | stdin,标准输入文件 |
&1 | stdout,标准输出文件 |
&2 | stderr,标准错误文件 |
/dev/null | 空设备,是一个特殊的设备文件,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。 |
- makefile基本使用方法
在这个实验中,大量使用了函数call和函数eval来调用一些在tools/function.mk中的一些自定义语句,必须把这两个函数理解清楚
可以参考Makefile教程(绝对经典,所有问题看这一篇足够了)- 函数名:dir
格式:KaTeX parse error: Expected 'EOF', got '\<' at position 6: (dir \̲<̲names>)。 ***作…(dir src/main.c foo.c) 则返回src/ 和 ./ - 函数名:call
格式:$(call <expression>,<parm1>,<parm2>,<parm3>…)
作用:call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。
示例:reverse = $(1)$(2) foo = $(call reverse,a,b),则foo=ab. - 函数名:eval
格式:$(eval text)
作用:函数“eval”对它的参数进行展开,展开的结果作为Makefile的一部分,make可以对展开内容进行语法解析。
示例:eval函数详解 - 函数名:patsubst
格式:$(patsubst <pattern>,<replacement>,<text> )
作用:模式字符串替换函数,查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。
示例: - 函数名:foreach
格式:$(foreach <var>,<list>,<text> )
作用:把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。每一次 会返回一个字符串,循环过程中, 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
示例:names := a b c d
files := ( f o r e a c h n , (foreach n, (foreachn,(names),$(n).o).返回:a.o b.o c.o d.o
- 函数名:dir
- GCC命令一些特殊选项
>选项 | 作用 |
---|---|
-fno-builtin | 不承认所有不是以builtin为开头的内建函数 |
-fno-PIC | 在编译时产生的代码全部使用绝对地址(-fPIC则是全部使用相对地址) |
-m32 | 生成32位程序 |
-gstabs | 生成stabs格式的调试信息,没有GDB extenstion |
-nostdinc | 不搜索系统include目录,只在-I参数指定的目录中搜索头文件 |
-fno-stack-protector | 不在关键函数的堆栈中设置保护值(若设置为-f,则在返回地址和返回值之前都将验证这个保护值,如果缓冲区溢出,保护值不再匹配,程序就会退出) |
-x language filename | 将filename文件类型当做指定的语言language进行编译 |
-E | 预编译 |
一、操作系统镜像文件ucore.img是如何一步一步生成的?
要理解该项目的makefile,首先要看懂其tools/function.mk中各条语句的功能
工具函数tools/function.mk
OBJPREFIX := __objs_
.SECONDEXPANSION:
# -------------------- function begin --------------------
# list all files in some directories: (#directories, #types)
# 返回返回相应directories目录下所有 类型为(types)的文件
# example:输入为listf(libs, c s),输出为libs/a.c libs/a.s
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
$(wildcard $(addsuffix $(SLASH)*,$(1))))
# get .o obj files: (#files[, packet])
# 给出文件名列表files,和软件包名称packet,返回相应文件的目标文件名称:/obj/packet/file.o
# example $(call toobj,libs/a.c libs/b.c,__obj_),生成相应的输出makefile代码为 obj/__obj_/libs/a.o obj/__obj_/libs/b.o
toobj = $(addprefix $(OBJDIR)$(SLASH)$(if $(2),$(2)$(SLASH)),\
$(addsuffix .o,$(basename $(1))))
# get .d dependency files: (#files[, packet])
# 输入为文件名列表files和软件包名称packet,输出为相应代码文件的依赖文件名列表:obj/packet/file.d
# example $(call todep,libs/a.c libs/b.c,__obj__),对应相应的makefile代码为 __obj_/libs/a.d __obj_/libs/b.d
todep = $(patsubst %.o,%.d,$(call toobj,$(1),$(2)))
# 输出最终的目标文件完整路径名
# example $(call totarget,kernel),则对应于makefile代码为输出的最总内核目标文件为bin/kernel
totarget = $(addprefix $(BINDIR)$(SLASH),$(1))
# change $(name) to $(OBJPREFIX)$(name): (#names)
# 给定名字加上前缀$(OBJPREFIX)
# example:$(call packetname,a.o b.o),则输出为__objs_a.o __objs_b.o,若没有a.o等参数,则输出__objs_
packetname = $(if $(1),$(addprefix $(OBJPREFIX),$(1)),$(OBJPREFIX))
# cc compile template, generate rule for dep, obj: (file, cc[, flags, dir])
# 内核各个模块编译的C代码模板,用来为每一个.c或者.s文件生成编译后的目标文件
define cc_template
# 生成依目标文件的依赖文件。4个$$$$符号是因为该代码要被eval两次,并且最终生成的makefile文件继续保留对规则目标文件名的引用。
# $<:第一个依赖文件。-MT "$$(patsubst %.d,%.o,$$@) $$@":在规则中的目标文件添加%.o (这样的话,目标文件由%o %d组成)
# > $$@:将依赖规则信息输出到目标文件(%.d)中。
# "|"号表示 后面的依赖目标是order-only Prerequisites,(执行某个或某些规则,但不会引起生成目标被重新生成)
# $$$$(dir $$$$@):是一个order-only Prerequisites,其内容是是%.d文件的路径
$$(call todep,$(1),$(4)): $(1) | $$$$(dir $$$$@)
@$(2) -I$$(dir $(1)) $(3) -MM $$< -MT "$$(patsubst %.d,%.o,$$@) $$@"> $$@
# 生成目标文件的规则。
$$(call toobj,$(1),$(4)): $(1) | $$$$(dir $$$$@)
@echo + cc $$<
$(V)$(2) -I$$(dir $(1)) $(3) -c $$< -o $$@
# 将所有的目标文件序列添加至ALLOBJS变量
ALLOBJS += $$(call toobj,$(1),$(4))
endef
# compile file: (#files, cc[, flags, dir])
# 用来生成最终的makefile中的所有目标文件的规则。
# 对$(1)指定的文件序列,逐个利用cc_template处理,
define do_cc_compile
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(4))))
endef
# add files to packet: (#files, cc[, flags, packet, dir])
# packet和dir有区别,前者是作为__objs_系列变量的后缀,后者是目标文件的目录
# 此模板,就是真正在makefile中用来编译所有的目表文件,并生成makefile规则的模板。
define do_add_files_to_packet
# __temp_packet__用来记录所有的临时目标文件。执行后,它的值=__objs_$(4)
__temp_packet__ := $(call packetname,$(4))
# 如果__objs_$(4)这个变量没有定义,就定义一个,并让它的值=空
ifeq ($$(origin $$(__temp_packet__)),undefined)
$$(__temp_packet__) :=
endif
__temp_objs__ := $(call toobj,$(1),$(5))
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(5))))
# 在__objs_$(4)这个变量的值的后面添加上$$(__temp_objs__),实际就是obj/file.o等
$$(__temp_packet__) += $$(__temp_objs__)
endef
# add objs to packet: (#objs, packet)
define do_add_objs_to_packet
__temp_packet__ := $(call packetname,$(2))
ifeq ($$(origin $$(__temp_packet__)),undefined)
$$(__temp_packet__) :=
endif
$$(__temp_packet__) += $(1)
endef
# add packets and objs to target (target, #packes, #objs[, cc, flags])
# 用来生成最终的target,在内核代码中,也就是最终的kernel和bootloader的makefile规则,$$(__temp_objs__) | $$$$(dir $$$$@) 该语句表示依赖规则的目标文件,还需要有目录的支持,如果目录不存在则应该创建,见后面规则。
define do_create_target
__temp_target__ = $(call totarget,$(1))
# 读取__objs_$(2)中的文件序列,再在最后加上$(3)返回给__temp_objs__(为什么要加$(3)???没弄明白)
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
$(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef
# finish all
define do_finish_all
ALLDEPS = $$(ALLOBJS:.o=.d)
$$(sort $$(dir $$(ALLOBJS)) $(BINDIR)$(SLASH) $(OBJDIR)$(SLASH)):
# 如果相应目录不存在则执行makedir -p 命令
@$(MKDIR) $$@
endef
# 函数功能:函数“eval”是一个比较特殊的函数。使用它可以在Makefile中构造一个可变的规则结构关系(依赖关系链),其中可以使用其它变量和函数。
# 函数“eval”对它的参数进行展开,展开的结果作为Makefile的一部分,make可以对展开内容进行语法解析。
# 展开的结果可以包含一个新变量、目标、隐含规则或者是明确规则等。也就是说此函数的功能主要是:根据其参数的关系、结构,对它们进行替换展开。
# 返回值:函数“eval”的返回值时空,也可以说没有返回值。
# -------------------- function end --------------------
# compile file: (#files, cc[, flags, dir])
cc_compile = $(eval $(call do_cc_compile,$(1),$(2),$(3),$(4)))
# add files to packet: (#files, cc[, flags, packet, dir])
add_files = $(eval $(call do_add_files_to_packet,$(1),$(2),$(3),$(4),$(5)))
# add objs to packet: (#objs, packet)
add_objs = $(eval $(call do_add_objs_to_packet,$(1),$(2)))
# add packets and objs to target (target, #packes, #objs, cc, [, flags])
create_target = $(eval $(call do_create_target,$(1),$(2),$(3),$(4),$(5)))
# 诸葛选取取变量__objs_$(1)中的文件序列赋值给p,然后返回以某个文件为变量名的值
read_packet = $(foreach p,$(call packetname,$(1)),$($(p)))
add_dependency = $(eval $(1): $(2))
finish_all = $(eval $(call do_finish_all))
makefile主文件
PROJ := challenge
EMPTY :=
SPACE := $(EMPTY) $(EMPTY)
SLASH := /
V := @
# try to infer the correct GCCPREFX
ifndef GCCPREFIX
GCCPREFIX := $(shell if i386-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; \
then echo 'i386-elf-'; \
elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; \
then echo ''; \
else echo "***" 1>&2; \
echo "*** Error: Couldn't find an i386-elf version of GCC/binutils." 1>&2; \
echo "*** Is the directory with i386-elf-gcc in your PATH?" 1>&2; \
echo "*** If your i386-elf toolchain is installed with a command" 1>&2; \
echo "*** prefix other than 'i386-elf-', set your GCCPREFIX" 1>&2; \
echo "*** environment variable to that prefix and run 'make' again." 1>&2; \
echo "*** To turn off this error, run 'gmake GCCPREFIX= ...'." 1>&2; \
echo "***" 1>&2; exit 1; fi)
# 该语句根据名字判断是GCC命令的前缀
endif
# try to infer the correct QEMU
ifndef QEMU
QEMU := $(shell if which qemu-system-i386 > /dev/null; \
then echo 'qemu-system-i386'; exit; \
elif which i386-elf-qemu > /dev/null; \
then echo 'i386-elf-qemu'; exit; \
else \
echo "***" 1>&2; \
echo "*** Error: Couldn't find a working QEMU executable." 1>&2; \
echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; \
echo "***" 1>&2; exit 1; fi)
endif
# eliminate default suffix rules,声明后缀依赖
.SUFFIXES: .c .S .h
# delete target files if there is an error (or make is interrupted),当make发生错误时,要删除的文件
.DELETE_ON_ERROR:
# define compiler and flags
HOSTCC := gcc
HOSTCFLAGS := -g -Wall -O2
CC := $(GCCPREFIX)gcc
# -fno-builtin:不使用C内建函数
# -fno-PIC:在编译时产生的代码全部使用绝对地址(-fPIC则是全部使用相对地址)
# -m32:生成32位程序
# -gstabs:生成stabs格式的调试信息,没有GDB extenstion
# -nostdinc:不搜索系统include目录,只在-I参数指定的目录中搜索头文件
# -fno-stack-protector:不在关键函数的堆栈中设置保护值(若设置为-f,则在返回地址和返回值之前都将验证这个保护值,如果缓冲区溢出,保护值不再匹配,程序就会退出)
# -x language filename:将filename文件类型当做指定的语言language进行编译
# -E :预编译
# gcc -fno-stack-protector -E -x -c /dev/null >/dev/null && echo -fno-stack-protecotr,先执行第一条命令(对dev/null用编译c语言的方式进行预编译),若成功,就输出-fno-stack-protecotr
CFLAGS := -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc $(DEFS)
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
CTYPE := c S
# head -n 1 filename:输出文件的第一行内容
LD := $(GCCPREFIX)ld
# -m 指定链接时使用的模拟链接器(elf_i386)
# $(shell $(LD) -V | grep elf_i386 2>/dev/null | head -n 1)是为了获取环境支持的模拟连接器
LDFLAGS := -m $(shell $(LD) -V | grep elf_i386 2>/dev/null | head -n 1)
# -nostdlib:不使用标准库
LDFLAGS += -nostdlib
OBJCOPY := $(GCCPREFIX)objcopy
OBJDUMP := $(GCCPREFIX)objdump
COPY := cp
MKDIR := mkdir -p
MV := mv
RM := rm -f
AWK := awk
SED := sed
SH := sh
TR := tr
TOUCH := touch -c
OBJDIR := obj
BINDIR := bin
ALLOBJS :=
ALLDEPS :=
TARGETS :=
# function.mk中定义了很多有用的表达式,可以用call 来调用
include tools/function.mk
# 这里利用function.mk中定义的表达式listf(列出参数1指定目录中类型为type的文件),将其参数2固化为$(CTYPE),c和S。
listf_cc = $(call listf,$(1),$(CTYPE))
# for cc
# 参数: $(1):#files。源代码文件名
# packet,附加编译选项,目标在obj目录下的子目录名
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
create_target_cc = $(call create_target,$(1),$(2),$(3),$(CC),$(CFLAGS))
# for hostcc
add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))
create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))
# cgtype参数:1、文件序列;2、模式串;3、替换串
cgtype = $(patsubst %.$(2),%.$(3),$(1))
objfile = $(call toobj,$(1))
# 将目标文件%.o改为%.asm
asmfile = $(call cgtype,$(call toobj,$(1)),o,asm)
# 将目标文件%.o改为%.out
outfile = $(call cgtype,$(call toobj,$(1)),o,out)
# 将目标文件%.o改为%.sym
symfile = $(call cgtype,$(call toobj,$(1)),o,sym)
# for match pattern
match = $(shell echo $(2) | $(AWK) '{for(i=1;i<=NF;i++){if(match("$(1)","^"$$(i)"$$")){exit 1;}}}'; echo $$?)
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# include kernel/user
INCLUDE += libs/
CFLAGS += $(addprefix -I,$(INCLUDE))
LIBDIR += libs
# 对LIBDIR目录中每一个C或S文件,生成目标文件。
# 这里参数libs是add_files_cc的第2个,add_files的第4个,do_add_files_to_packet的第4个变量
# 实际上libs最终和__objs_组成一个变量:__objs_libs,用来记录libs下的目标文件序列
# 在这里,没有传$(3),所以flag没有附加选项
# 在这里,没有传$(4),这个参数是add_files_cc的第4个,add_files的第5个,do_add_files_to_packet的第5个
# 最终$(4)会传给toobj的$(2),因为这里没有传这个参数,因此生成obj文件路径直接是obj/file.o
$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)
# -------------------------------------------------------------------
# kernel
KINCLUDE += kern/debug/ \
kern/driver/ \
kern/trap/ \
kern/mm/
KSRCDIR += kern/init \
kern/libs \
kern/debug \
kern/driver \
kern/trap \
kern/mm
KCFLAGS += $(addprefix -I,$(KINCLUDE))
# 编译kern文件夹下的源代码,编译号的obj文件会放在obj目录下
# 同LIBDIR,kernel会和__objs_组成一个变量。而KCFLAGS是CFLAGS的附加选项
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
# 依次读取__objs_kernel和__objs_libs这两个变量中的文件序列,并返回给KOBJS
KOBJS = $(call read_packet,kernel libs)
# create kernel target
kernel = $(call totarget,kernel)
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo + ld $@
# 使用tools/kernel.ld脚本链接所有目标文件
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
$(call create_target,kernel)
# -------------------------------------------------------------------
# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
bootblock = $(call totarget,bootblock)
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJDUMP) -t $(call objfile,bootblock) | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
$(call create_target,bootblock)
# -------------------------------------------------------------------
# create 'sign' tools
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
# -------------------------------------------------------------------
# create ucore.img
UCOREIMG := $(call totarget,ucore.img)
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
$(call finish_all)
IGNORE_ALLDEPS = clean \
dist-clean \
grade \
touch \
print-.+ \
handin
ifeq ($(call match,$(MAKECMDGOALS),$(IGNORE_ALLDEPS)),0)
-include $(ALLDEPS)
endif
# files for grade script
TARGETS: $(TARGETS)
all: $(TARGETS)
.DEFAULT_GOAL := TARGETS
.PHONY: qemu qemu-nox debug debug-nox
lab1-mon: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -monitor stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -x tools/lab1init"
debug-mon: $(UCOREIMG)
# $(V)$(QEMU) -S -s -monitor stdio -hda $< -serial null &
$(V)$(TERMINAL) -e "$(QEMU) -S -s -monitor stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -x tools/moninit"
qemu-mon: $(UCOREIMG)
$(V)$(QEMU) -monitor stdio -hda $< -serial null
qemu: $(UCOREIMG)
$(V)$(QEMU) -parallel stdio -hda $< -serial null
qemu-nox: $(UCOREIMG)
$(V)$(QEMU) -serial mon:stdio -hda $< -nographic
TERMINAL :=gnome-terminal
gdb: $(UCOREIMG)
$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null
debug: $(UCOREIMG)
$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &
$(V)sleep 2
$(V)$(TERMINAL) -e "cgdb -q -x tools/gdbinit"
debug-nox: $(UCOREIMG)
$(V)$(QEMU) -S -s -serial mon:stdio -hda $< -nographic &
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -x tools/gdbinit"
.PHONY: grade touch
GRADE_GDB_IN := .gdb.in
GRADE_QEMU_OUT := .qemu.out
HANDIN := proj$(PROJ)-handin.tar.gz
TOUCH_FILES := kern/trap/trap.c
MAKEOPTS := --quiet --no-print-directory
grade:
$(V)$(MAKE) $(MAKEOPTS) clean
$(V)$(SH) tools/grade.sh
touch:
$(V)$(foreach f,$(TOUCH_FILES),$(TOUCH) $(f))
print-%:
@echo $($(shell echo $(patsubst print-%,%,$@) | $(TR) [a-z] [A-Z]))
.PHONY: clean dist-clean handin packall
clean:
$(V)$(RM) $(GRADE_GDB_IN) $(GRADE_QEMU_OUT)
-$(RM) -r $(OBJDIR) $(BINDIR)
dist-clean: clean
-$(RM) $(HANDIN)
handin: packall
@echo Please visit http://learn.tsinghua.edu.cn and upload $(HANDIN). Thanks!
packall: clean
@$(RM) -f $(HANDIN)
@tar -czf $(HANDIN) `find . -type f -o -type d | grep -v '^\.*$$' | grep -vF '$(HANDIN)'`