ucore makefile分析

不可视境界线最后变动于:2022年4月13日 中午

贴个出处: -> link

不妨对整个lab1所提供的Makefile进行解释如下:

不妨首先考虑Makefile中生成ucore.img相关的主要代码(暂时不考虑细节问题)来描述生成出ucore.img的每一个具体步骤:

  • 生成kernel:

    • 首先是$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)这一段代码,含义是寻找libs目录下的所有具有.c, .s后缀的文件,并生成相应的.o文件,放置在obj/libs/文件夹下,具体生成的文件是printfmt.o, string.o文件,与此同时,该文件夹下还生成了.d文件,这是Makefile自动生成的依赖文件列表所存放的位置,比如打开string.d文件可以发现,string.o文件的生成依赖于string.c, string.h, x86.h, defs.h四个文件,这与我们对于代码的观察是一致的;这部分编译所使用的编译选项保存在CFLAGS变量下,关于具体每一个使用到的gcc编译选项的含义,将在下文具体分析Makefile中定义CFLAGS变量的部分进行详细描述;
    • $(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS)这一段代码将用于生成kernel的所有子目录下包含的CTYPE文件(.s, .c文件)所对应的.o文件以及.d文件,这段代码与上述生成obj/libs/*.o文件的代码类似,区别仅在于其还新指定了若干gcc编译选项,存放在KCFLAGS变量中,具体为制定了若干存放在KINCLUDE变量下的头文件;具体而言,该命令最终生成的文件为obj/kern下子目录里的以stdio, readline, panic, kdebug, kmonitor, clock, console, picirq, intr, trap, vector, trapentry, pmm为前缀的.d, .o文件;
    • 接下来$(kernel): tools/kernel.ld表示/bin/kernel文件依赖于tools/kernel.ld文件,并且没有指定生成规则,也就是说如果没有预先准备好kernel.ld,就会在make的时候产生错误;之后的$(kernel): $(KOBJS)表示kernel文件的生成还依赖于上述生成的obj/libs, obj/kernels下的.o文件,并且生成规则为使用ld链接器将这些.o文件连接成kernel文件,其中ld的-T表示指定使用kernel.ld来替代默认的链接器脚本;关于LDFLAGS中的选项含义,将在下文中描述LDFLAGS变量定义的时候进行描述;之后还使用objdump反汇编出kernel的汇编代码,-S表示将源代码与汇编代码混合展示出来,这部分代码最终保存在kernel.asm文件中;-t表示打印出文件的符号表表项,然后通过管道将带有符号表的反汇编结果作为sed命令的标准输入进行处理,最终将符号表信息保存到kernel.sym文件中;
  • 生成bootblock文件:

    • 首先是

      1
      $(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc)

      这一段代码,表示将boot/文件夹下的bootasm.S, bootmain.c两个文件编译成相应的.o文件,并且生成依赖文件.d;其中涉及到的两个gcc编译选项含义如下所示:

      • -nostdinc: 不搜索默认路径头文件;
      • -0s: 针对生成代码的大小进行优化,这是因为bootloader的总大小被限制为不大于512-2=510字节;
    • 接下来由代码

      1
      $(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)

      可知,bootblock依赖于bootasm.o, bootmain.o文件与sign文件,其中两个.o文件由以下规则生成:

      1
      $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)

      : 使用ld链接器将依赖的.o文件链接成bootblock.o文件,该文件中除了$(LDFLAGS)之外的其他选项含义如下:

      • -N:将代码段和数据段设置为可读可写;
      • -e:设置入口;
      • -Ttext:设置起始地址为0X7C00;
    • @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock): 使用objdump将编译结果反汇编出来,保存在bootclock.asm中,-S表示将源代码与汇编代码混合表示;

    • @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock): 使用objcopy将bootblock.o二进制拷贝到bootblock.out,其中:

      • -S:表示移除符号和重定位信息;
      • -O:表示指定输出格式;
    • @$(call totarget,sign) $(call outfile,bootblock) $(bootblock): 使用sign程序, 利用bootblock.out生成bootblock;

    • $(call add_files_host,tools/sign.c,sign,sign: 利用tools/sing.c生成sign.o, $(call create_target_host,sign,sign)则利用sign.o生成sign,至此bootblock所依赖的文件均生成完毕;

  • 最后一个部分是利用dd命令使用bootblock, kernel文件来生成ucore.img文件:

    • $(V)dd if=/dev/zero of=$@ count=10000 命令表示从/dev/zero文件中获取10000个block,每一个block为512字节,并且均为空字符,并且输出到目标文件ucore.img中;
    • $(V)dd if=$(bootblock) of=$@ conv=notrunc 命令表示从bootblock文件中获取数据,并且输出到目标文件ucore.img中,-notruct选项表示不要对数据进行删减;
    • $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc 命令表示从kernel文件中获取数据,并且输出到目标文件ucore.img中, 并且seek = 1表示跳过第一个block,输出到第二个块;
  • 至此,关于生成ucore.img文件的主要的Makefile命令分析完成;

  • 接下来将就整个Makefile文件中的其他每个部分进行分析:首先在Makefile的最开始是对各种常量的初始化:

1
2
3
4
5
6
PROJ    := challenge
EMPTY :=
SPACE := $(EMPTY) $(EMPTY)
SLASH := /

V :=
  • 接下来部分则用于推断环境中调用所安装的gcc应当使用的命令:
    在本部分,如果为定义GCCPREFIX变量,则利用了linux bash中的技巧来推断所使用的gcc命令是什么, 在本部分首先猜测gcc命令的前缀是i386-elf-,因此执行i386-elf-objdump -i命令,2>&1表示将错误输出一起输出到标准输出里,然后通过管道的方式传递给下一条bash命令grep ‘^elf32-i386$$’ >/dev/null 2>&1;,>/dev/null这部分表示将标准输出输出到一个空设备里,而输入上一条命令发送给grep的标准输出(作为grep的输入)中可以匹配到’^elf32-i386$$’的话,则说明i386-elf-objdump这一命令是存在的,那么条件满足,由echo输出’i386-elf-‘,由于是在$()里的bash命令,这个输出会作为值被赋给GCCPREFIX变量;如果i386-elf-objdump命令不存在,则猜测使用的gcc命令不包含其他前缀,则继续按照上述方法,测试objdump这条命令是否存在,如果存在则GCCPREFIX为空串,否则之间报错,要求显示地提供gcc的前缀作为GCCPREFIX变量的数值(可以在环境变量中指定);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
endi
  • 接下来部分与上述方法一致,利用bash命令来推断qemu的命令,因此具体细节不再赘述;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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; \
elif which qemu > /dev/null; \
then echo '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)
endi
  • 接下来的部分定义了各种编译命令以及编译选项,其中-fno-stack-protector编译选项的确定也使用了与上文确定GCCPREFIX相似的技巧,巧妙地利用了linux bash中 && 连接起来的两条指令,如果第一条指令出错,则第二条指令不会执行的特点,来确认当前的gcc是否允许使用 -fno-stack-protector这一编译选项;
  • 该段Makefile代码中所设计的所有gcc编译选项和链接器ld选项的作用分别如下:
    • -g:在编译中加入调试信息,便于之后使用gdb进行调试;
    • -Wall:使能所有编译警告,便于发现潜在的错误;
    • -O2: 开启O2编译优化;
    • -fno-builtin: 不承认所有不是以builtin为开头的内建函数;
    • -ggdb 产生gdb所需要的调试信息(与-g的区别是ggdb的调试信息是专门为gdb而生成的);
    • -m32: 32位模式;
    • -gstabs:以stabs格式输出调试信息,不包括gdb拓展;
    • -nostdinc: 不搜索默认路径头文件;
    • -fno-stack-protector: 禁用堆栈保护;
    • -nostdlib: 该链接器选项表示不链接任何系统标准启动文件和标准库文件,这是因为编译操作系统内核和bootloader是不需要这些启动文件和库就应该能够执行的;
  • 其他涉及到的bash命令选项为:
    • mkdir -p: 允许创建嵌套子目录;
    • touch -c: 不创建已经存在的文件;
    • rm -f: 无视任何确认提示;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# eliminate default suffix rules
.SUFFIXES: .c .S .h

# delete target files if there is an error (or make is interrupted)
.DELETE_ON_ERROR:

# define compiler and flags
ifndef USELLVM
HOSTCC := gcc
HOSTCFLAGS := -g -Wall -O2
CC := $(GCCPREFIX)gcc
CFLAGS := -fno-builtin -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)
else
HOSTCC := clang
HOSTCFLAGS := -g -Wall -O2
CC := clang
CFLAGS := -fno-builtin -Wall -g -m32 -mno-sse -nostdinc $(DEFS)
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
endif

CTYPE := c S

LD := $(GCCPREFIX)ld
LDFLAGS := -m $(shell $(LD) -V | grep elf_i386 2>/dev/null)
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 :=
  • 接下来的部分引用了tools/function.mk文件,因此不仿分析该文件的内容如下:
1
2
3
# list all files in some directories: (#directories, #types)
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
$(wildcard $(addsuffix $(SLASH)*,$(1)))

上述定义了一个获取某一个目录下的所有某类型文件的表达式,该表达式可以使用call函数调用来使用,其中$(if $(2),$(addprefix %.,$(2)),%)部分是用于构造一个%.某后缀形式的pattern,$(wildcard $(addsuffix $(SLASH)*,$(1))部分则是被是用来获取当前目录下的而所有文件,并且使用filter函数过滤出这所有文件中具有.$(2) (即call传入的第二个参数)后缀的文件;

1
2
3
# get .o obj files: (#files[, packet])
toobj = $(addprefix $(OBJDIR)$(SLASH)$(if $(2),$(2)$(SLASH)),\
$(addsuffix .o,$(basename $(1)))

该表达式表示将传入的文件名列表中的所有后缀修改为.o,并且将其添加上这些.o文件的目录,获取到这些.o文件最终应该存放的位置;

1
2
# get .d dependency files: (#files[, packet])
todep = $(patsubst %.o,%.d,$(call toobj,$(1),$(2))

将所有.o文件的后缀名修改为.d;

1
totarget = $(addprefix $(BINDIR)$(SLASH),$(1))

获取由第一个参数传入的binary文件最终应当存放的位置;

1
2
# change $(name) to $(OBJPREFIX)$(name): (#names)
packetname = $(if $(1),$(addprefix $(OBJPREFIX),$(1)),$(OBJPREFIX)

给第一个参数传入的所有文件名加上$(OBJPREFIX)前缀;

1
2
3
4
5
6
7
8
9
# cc compile template, generate rule for dep, obj: (file, cc[, flags, dir])
define cc_template
$$(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 += $$(call toobj,$(1),$(4))
ende

这部分使用define多行定义了一个编译的模板(对单个文件进行编译成object文件),其中若干处$$表示原本的字符$,这是因为后文中将对这个部分执行eval,而$$<即原本的$<表示了依赖目标的值,$@表示了目标的值, 在本部分中,将最终生成出目标文件的依赖文件,以及定义了生成目标文件的规则;

更具体一点,该模板的前半部分是用于生成Makefile .d依赖文件(利用gcc的-MM选项),后半部分则是使用gcc编译出.o文件, 并且将所有.o文件加入到ALLOBJS变量中;

关于上述代码中的$(V)的使用,发现原本V变量定义的是@, 也就是不在shell中输出的符号, 如果在make命令行中overide the definition of V, 也就是使用V=赋值为空, 那么$(V)后面的命令就会被输出.

1
2
3
define do_cc_compile
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(4))))
ende

表示将传入的文件列表中的每一个文件都使用cc_template进行生成编译模板;

1
2
3
4
5
6
7
8
9
10
# add files to packet: (#files, cc[, flags, packet, dir])
define do_add_files_to_packet
__temp_packet__ := $(call packetname,$(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))))
$$(__temp_packet__) += $$(__temp_objs__)
ende

上述代码中,首先使用call packetname生成出某一个packetname对应的makefile中变量的名字,然后使用origin查询这个变量是否已经定义过,如果为定义,则初始化该变量为空;之后使用toobj生成出该packet中所需要的生成的.o文件的文件名列表,然后将其添加到以__temp_packet__这个变量中所存的值作为名字的变量中去,并且使用cc_template生成出该packet生成.d文件和.o文件的代码;

1
2
3
4
5
6
7
8
# 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)
ende

上述代码表示将某一个.o文件添加到某一个packet对应的makefile中的变量中的文件列表中去;举例,如果要添加a.o到pack这一个packet中,则结果就是__objs_这个变量会执行__objs_pack += a.o这个一个操作;

1
2
3
4
5
6
7
8
9
10
11
12
# add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__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
ende

上述代码表示将第一个参数传入的binary targets和第三个参数传入的object文件均添加到TARGETS变量中去,之后根据第4个参数是否传入gcc编译命令来确定是否生成编译的规则;

1
2
3
4
5
6
# finish all
define do_finish_all
ALLDEPS = $$(ALLOBJS:.o=.d)
$$(sort $$(dir $$(ALLOBJS)) $(BINDIR)$(SLASH) $(OBJDIR)$(SLASH)):
@$(MKDIR) $$@
ende

创建编译过程中所需要的子目录;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# --------------------  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)))

read_packet = $(foreach p,$(call packetname,$(1)),$($(p)))

add_dependency = $(eval $(1): $(2))

finish_all = $(eval $(call do_finish_all)

接下来这部分则是使用eval来进一步将原先设计好的编译代码的表达式中的变量替换为变量的数值,从而方便后面生成编译的规则,接下来不妨以cc_compile这个表达式的求值为例,说明Makefile中是如何生成编译规则的:

为了方便起见,不妨假设传入cc_compile这个表达式的四个参数分别为main.c, gcc, -Wall, bin, 则不妨首先计算$(call do_cc_compile,$(1),$(2),$(3),$(4))表达式的数值如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
cc_compile  
=$(eval $(call do_cc_compile,$(1),$(2),$(3),$(4))
=$(eval $$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(4))))
=$(eval $(foreach f, $(1), $(eval $(call cc_template, $(f), $(2), $(3), $(4)))))
=$(eval $(eval $(call cc_template, $(1), $(2), $(3), $(4)))) (since $(1)=main.c)
=$(eval $(eval
$$(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 += $$(call toobj,$(1),$(4)
))
= $(eval
$(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 += $(call toobj,$(1),$(4))
)
= $(eval
obj/main.d: main.c | $$$(dir $$@)
@gcc -I./ -Wall -MM main.c -MT "main.o main.d"> main.d
obj/main.o: main.c | $$$(dir $$@)
@echo + cc main.c
$(V)gcc -I./ -Wall -c main.c -o main.o
)
# 注意到还有一次expansion, 是因为 .SECONDEXPANSION: 这个built-in target
=
obj/main.d: main.c | obj
@gcc -I./ -Wall -MM main.c -MT "main.o main.d"> main.d
obj/main.o: main.c | obj
@echo + cc main.c
$(V)gcc -I./ -Wall -c main.c -o main.o

至此通过例子演示了如果使用Makefile来生成一系列编译规则,如果使用make --trace(所有输出)或者make -n(Don’t actually run any recipe; just print them.),可以发现生成obj/boot/bootasm.d, obj/boot/bootasm.o的实际执行的命令为

1
2
3
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -MM boot/bootmain.c -MT "obj/boot/bootmain.o obj/boot/bootmain.d"> obj/boot/bootmain.d
和:
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

,与上述例子中展开的结果进行对比,可以确认该分析过程的正确性;

  • 其余Makefile命令均用于生成.PHONY目标来完成clean,grade等一系列功能,与具体生成ucore.img过程无关,因此在本报告中将不太对其进行分析;