在操作系统原理的实验二中有一个课后题是简单汇编编程。工程目录下主要.cpp文件和.asm文件(还有一些头文件),编写完汇编代码之后只需要一个”make run”命令就可以完成目录下两种源码的编译,链接还有运行。

运行指令之后我用ls查看了文件目录,发现多了.cpp和.asm对应的.o,还有一个名为”test”的可执行文件。当时觉得非常的神奇! 为什么只需要一句”make run”就可以把所有事情一步到位都做完呢

panda@panda:~/lab2/assignment$ ls
end.include  head.include  makefile  student.asm
student.o  test  test.cpp  test.o

于是再仔细看了一下目录,发现目录中有一个名为”makefile”的文件。打开后发现是一个内容有点像脚本的文本:

TARTGET = test

AS = nasm
CXX = g++

ASM_FILE = $(wildcard *.asm)
CXX_FILE = $(wildcard *.cpp)

ALL_OBJ += $(ASM_FILE:%.asm=%.o)
ALL_OBJ += $(CXX_FILE:%.cpp=%.o)

run:
	@rm -rf *.o
	@nasm -f elf32 $(ASM_FILE)
	@g++ -m32 -g -c $(CXX_FILE)
	@g++ -m32 -o $(TARTGET) $(ALL_OBJ)
	@./$(TARTGET)
clean:
	@rm -rf *.o

粗略看了一下文本中的命令语句之后就大概知道了是怎么回事,”make run”命令依赖于这一个”makefile”文件来完成一系列的工作。想到这里突然眼前一亮,这可是好东西啊!makefile把项目目录下所有的源码预先指定好依赖关系和对应的编译命令,然后每次对源码进行修改后都只需要一个语句就可以完成整个项目的可执行文件生成,简直不要太方便了好吗。

接下来便是在搜索引擎上找到makefile的参考文档以及更加仔细地解析这个makefile中各行的意思。

这里我分别找到了原版和中文版的docs:GNU makeGNU make CN

通过文档可以得知,第一行的 “TARTGET = test” 实际上是指定了最终生成的目标可执行文件的名称。这里的值是test,所以ls可以看到有一个名为”test”的可执行文件,输入”./xxx”即可直接运行。

panda@panda:~/lab2/assignment$ ./test
>>> begin test
>>> if test pass!
>>> while test pass!
Mr.Chen, students and TAs are the best!

之后是一些变量的定义,变量可以为后续的依赖关系建立和命令运行提供便利。

  • 定义变量的语法是”var_name = files”,使用变量的时候则是”$(var_name)”

在 “ run: “ 之后是一系列命令,在这里他指定了目录下源码按顺序编译,还有生成可执行文件并执行。

  • 值得注意的是,每一个命令行必须有一个缩进,缩进告诉make此行是一个命令行。有了缩进之后make才能按照命令完成相应的动作。

事实上在我看到 “ run: “ 之后第一想法是这是下方这一系列指令组成的集合的名称。而我们在终端中输入的”make run”则是指定make指令针对makefile中的run之后的指令块来运行。

于是我又进一步尝试了”make clean”和”make”指令。预期的效果是前者只运行”clean:”之后的命令,而后者运行整个makefile文件中的命令。看下方的执行结果,前者运行之后确实执行了”clean:”之后的”rm -rf *.o”指令,可以看到目录下所有的.o文件都被删除了。

panda@panda:~/lab2/assignment$ ls
end.include  head.include  makefile  student.asm  test  test.cpp

但是后者的运行结构跟”make run”完全一致!也就是说当我们没有指定运行目标时,它只运行了run(第一个)

  • 文档中提到:在默认的情况下,make执行的是makefile中的第一个规则及其依赖文件的规则,此规则的第一个目标称之为“终极目标”。

“第一个规则”很好理解,make确实只执行了run之下的内容,没有执行的clean的内容。但是什么叫做“依赖文件的规则”呢? 参考文档内给出了一个例子:

# sample Makefile
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o
edit : $(objects)  # edit是终极目标
    cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean  # 这里是把clean指定为伪目标 具体可以参考文档
clean :
    rm edit $(objects)

这个文档中第一个规则是”edit”,而edit还依赖于目录下所有的.o文件,由于在make之前目录下没有.o文件,所以make会在makefile中寻找合适的依赖关系并执行,所以之后的8个”.o:”规则全都会被执行。然后实际上每一个”.o”规则也声明了其依赖关系,即依赖于冒号之后的.h文件。

然后.o文件实际上是由源码编译得到的,这个例子中源码是.c文件,所以理论上每一个”.o:”规则之后应该需要有单独且缩进开头的编译指令” cc -c xxx.c”。但是这个例子中并没有这样的指令。

  • 这是因为makefile中存在一些隐含规则,make本身可以自动完成对.c文件的编译并生成对应的.o文件,所以上述的编译指令是可以省略的,这会让makefile变得更加清晰简洁。

了解到这里之后,再重新看回到原来的makefile文件,会看到它并没有很好体现出makefile中的依赖关系,它更像一个普普通通的脚本文件:

TARTGET = test

AS = nasm
CXX = g++

ASM_FILE = $(wildcard *.asm)
CXX_FILE = $(wildcard *.cpp)

ALL_OBJ += $(ASM_FILE:%.asm=%.o)
ALL_OBJ += $(CXX_FILE:%.cpp=%.o)

run:
    @rm -rf *.o
    @nasm -f elf32 $(ASM_FILE)
    @g++ -m32 -g -c $(CXX_FILE)
    @g++ -m32 -o $(TARTGET) $(ALL_OBJ)
    @./$(TARTGET)
clean:
    @rm -rf *.o  
# 指令之前有没有@的区别是执行make时会不会显示指令内容

所以我尝试模仿参考文档中的例子对这个makefile进行重构:

TARTGET = test

AS = nasm
CXX = g++

ASM_FILE = $(wildcard *.asm)
CXX_FILE = $(wildcard *.cpp)

ALL_OBJ += $(ASM_FILE:%.asm=%.o)
ALL_OBJ += $(CXX_FILE:%.cpp=%.o)

test : $(ALL_OBJ)
    @g++ -m32 -o $(TARTGET) $(ALL_OBJ)
test.o : $(CXX_FILE)
    @g++ -m32 -g -c $(CXX_FILE)
student.o : $(ASM_FILE)
    @nasm -f elf32 $(ASM_FILE)
else:
    @ls
clean:
    @-rm -rf test *.o

这样就可以很明显看出test依赖于所有的.o文件,通过g++ -o编译得到;而test.o依赖于test.cpp文件且通过g++ -g指令编译得到;student.o则依赖于student.asm文件并通过nasm指令编译得到。

在这个例子中” test: “是makefile中的终极目标,下方的两个规则则因为与edit的依赖关系所以都会被执行。而”else:”和”clean:”下的指令则不会被执行。

# 重构后的makefile测试

可以看到这与原先项目目录中存在的makefile的功能是基本一致的(只不过我没有写入运行指令,采用了手动执行的方式)

panda@panda:~/lab2/assignment$ make
/usr/bin/ld: student.o: warning: relocation against `while_flag' in read-only section `.text'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE

panda@panda:~/lab2/assignment$ ls
end.include  head.include  makefile  student.asm  student.o  test  test.cpp  test.o

panda@panda:~/lab2/assignment$ ./test
>>> begin test
>>> if test pass!
>>> while test pass!
Mr.Chen, students and TAs are the best!

到这里对make指令和makefile就有了初步的认识了~ 然后我就突然想起操作系统原理实验一的”编译内核”中也用到了”make -j8”指令,当时只知道”-j8”参数是指定编译线程数,但没有细究make这个指令是怎么对一个复杂的内核源码按照一定的关系去编译。

现在再回到那个linux内核的目录下查看文件,果然找到了名为”Makefile”的文件(表格中的倒数第二行第三个)

panda@panda:~/lab1/linux-5.10.169$ ls
arch           drivers   Kconfig      modules.builtin          samples     virt
block          fs        kernel       modules.builtin.modinfo  scripts     vmlinux
certs          include   lib          modules-only.symvers     security    vmlinux.o
COPYING        init      LICENSES     modules.order            sound       vmlinux.symvers
CREDITS        io_uring  MAINTAINERS  Module.symvers           System.map
crypto         ipc       Makefile     net                      tools
Documentation  Kbuild    mm           README                   usr
  • 注意,makefile的命名一般默认为 “GNUmakefile”, “makefile” 或者 “Makefile”。 而通常我们使用 “Makefile” 来命名,一是它可以直接被make指令识别到,二是首字母大写可以让它在文件目录中可以更快被维护者注意到。

还有一件事,在搜索makefile的过程中还意外发现了”cmake”,进行了一点小小的了解。在知乎上看到了一个很形象的解释:

图片

哈哈哈!真的很形象。

以上这就是本次探索的全部内容!更细节的makefile用法将会在之后使用到的时候再去深入学习。