← 首页

疯狂的 Make:xv6 的 Makefile

Makefile

Makefile 的基本规则结构如下:

目标: 依赖
	命令

需要注意的是,命令前面必须是 Tab 而非空格。Makefile 中可以提供多个目标,在终端输入 make <目标名> 即执行相关的命令。

在执行没有参数的 make 时,make 程序会尝试构建 Makefile 中第一个目标。

.PHONY

make 在尝试构建目标时,会根据文件时间戳判断目标是否需要更新:如果目标不存在,或者依赖文件比目标更新,就会调用相关命令生成或更新目标。

但很多时候,目标并不是一个文件,而只是一个任务名,比如 make clean。这种情况下,可以在 Makefile 中写

.PHONY: 目标1 目标2 目标3 # 也可以分多行写

这样,make 在构建这类目标的时候就退化为一个命令执行器。

变量

xv6 的 Makefile 中用到的变量大致可分为这几类:立即展开变量、递归展开变量、条件变量。他们的语法是:

立即展开变量 := 变量值
递归变量 = 变量值
 
ifndef 条件变量
条件变量 := 默认值
endif
 
ifneq ($(变量或表达式),)
变量名 += 追加内容
endif # 若不相等,执行其中的语句

立即展开变量会在定义时立刻计算出来,并把结果保存下来。递归展开变量不会在定义时立刻计算右侧内容,而是在使用变量时再展开。

通过调用 shell 命令生成变量

语法为 $(shell 命令),这里会调用系统shell,把命令的输出结果作为变量值。

通配符

$(wildcard ...) 这种写法允许使用通配符,比如

clang-format -i $(wildcard kernel/*.[ch] user/*.[ch] mkfs/*.c)`

自动变量:$@$<$^$*

自动变量含义
$@当前目标
$<第一个依赖
$^所有依赖,去重
$*模式规则中 % 匹配到的部分

Xv6 的 Makefile

常量折叠

xv6Makefile 开头定义了这样一坨东西

# Try to infer the correct TOOLPREFIX if not set
ifndef TOOLPREFIX
TOOLPREFIX := $(shell if riscv64-unknown-elf-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; \
	then echo 'riscv64-unknown-elf-'; \
	elif riscv64-elf-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; \
	then echo 'riscv64-elf-'; \
	elif riscv64-none-elf-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; \
	then echo 'riscv64-none-elf-'; \
	elif riscv64-linux-gnu-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; \
	then echo 'riscv64-linux-gnu-'; \
	elif riscv64-unknown-linux-gnu-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; \
	then echo 'riscv64-unknown-linux-gnu-'; \
	else echo "***" 1>&2; \
	echo "*** Error: Couldn't find a riscv64 version of GCC/binutils." 1>&2; \
	echo "*** To turn off this error, run 'gmake TOOLPREFIX= ...'." 1>&2; \
	echo "***" 1>&2; exit 1; fi)
endif

我确信这是一个常量,因为我并没有跨平台修改代码的需求,可以把这段改为:

TOOLPREFIX = riscv64-unknown-elf-

检查各个目标

对于 xv6Makefile,第一个目标是这样的

$K/kernel: $(OBJS) $K/kernel.ld
	$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) 
	$(OBJDUMP) -S $K/kernel > $K/kernel.asm
	$(OBJDUMP) -t $K/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $K/kernel.sym

上面这一段 Makefile 的大意为:如果 $(OBJS)$K/kernel.ld 有变化,就重新生成 $K/kernel,如果要生成$K/kernel,依次执行以下几步:

  1. 用链接器把所有目标文件 $(OBJS) 按照 $K/kernel.ld 的布局规则链接成 $K/kernel
  2. $K/kernel 反汇编,生成一个方便调试和查看的 $K/kernel.asm 文件。
  3. $K/kernel 中提取符号表,整理成简洁格式,保存为 $K/kernel.sym

鉴于我根本看不懂 .asm.sym 文件,所以这后两行其实可以先删了。


第二个目标是这样的

$K/%.o: $K/%.S
	$(CC) -march=rv64gc -g -c -o $@ $<

这句可以理解为,目标是所有的 .o 文件,每一个 .o 文件都可以通过同名的 .S 文件编译得到。


第三个目标是一个 tags 文件,但这个文件是给 Emacs 用的,作为 Vim 用户,我无法容忍这个目标的存在,将其删除。

tags: $(OBJS)
	etags kernel/*.S kernel/*.c

第四个目标,与第一个目标 kernel 类似

_%: %.o $(ULIB) $U/user.ld
	$(LD) $(LDFLAGS) -T $U/user.ld -o $@ $< $(ULIB)
	$(OBJDUMP) -S $@ > $*.asm
	$(OBJDUMP) -t $@ | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $*.sym
← 首页