(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
首先看Makefile文件,109行之前都是一些编译选项,暂且跳过,用到的时候再分析。直接来看生成ucore镜像的部分:
$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)
这个call函数用到了add_files_cc和listf_cc两个表达式,定义如下:
listf_cc = $(call listf,$(1),$(CTYPE))
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
先看listf_cc,看到listf_cc中用到了另一个表达式listf,其定义在function.mk中:
# in tools/function.mk
# list all files in some directories: (#directories, #types)
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
$(wildcard $(addsuffix $(SLASH)*,$(1))))
可见listf的作用是列出目录下所有符合某种type的文件,如果规定了文件类型,那么用addprefix构造一个pattern,之后用wildcard得到该目录下所有的文件。所以,listf_cc可以被替换为如下表示:
listf_cc = $(call listf, libs, c S)
listf = $(filter %.c %.S, $(wildcard libs/*))
所以listf_cc的作用是把所有后缀名为.c和.S的文件列出来。
再来看add_files_cc,把参数带入即可认为是如下的代码:
add_files_cc = $(call add_files, all_cS_files, $(CC), $(CFLAGS), $(3), libs,)
之后再看tools/function.mk中add_files的定义,发现调用了do_add_files_to_packet这个宏,一并给出定义:
# in tools/function.mk
# add files to packet: (#files, cc[, flags, packet, dir])
add_files = $(eval $(call do_add_files_to_packet,$(1),$(2),$(3),$(4),$(5)))
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__)
endef
do_add_files_to_packet这个宏首先看__temp_packet__是否定义过,如果未定义过则初始化为空;之后初始化所有libs目录下.c.S文件的.o文件,之后分别用cc_template编译生成每个.c.S的.o和.d文件。.d文件中的依赖关系也在cc_template中得到,下面是string.d的内容,与我们的分析符合:
obj/libs/string.o obj/libs/string.d: libs/string.c libs/string.h \
libs/defs.h libs/x86.h
综上,$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)
的作用是:
接下来,KINCLUDE存放kernel代码的引用目录,KSRCDIR存放kernel的源码目录,KCFLAGS存放kernel的编译选项;
接下来与上面类似的$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
表示生成kernel代码的.o和.d文件。同时生成了一个名为__objs_kernel的packet,我觉得这是在eval函数的二次求值过程中实现的,这个packet中存放着kernel相关的.o文件的名字。类似的之前也生成了一个__objs_libs的packet,存放libs相关的.o文件名。这些packet不是显示的出现在Makefile中的,但是如果我们用echo $(origin __objs_libs)
来测试,会发现返回值是“file”。
接下来的KOBJS=$(call read_packet,kernel libs)
就是把上文提到的__objs_kernel和__objs_libs这两个packet中的文件名合起来;
之后kernel = $(call totarget,kernel)
表示在bin目录下生成kernel文件;
$(kernel): tools/kernel.ld
和$(kernel): $(KOBJS)
表示kernel依赖于kernel.ld和$(KOBJS)中的.o文件,之后用GNU的ld链接器把这些目标文件全部链接起来,-T表示用kernel.ld代替缺省的链接脚本,-S表示把这些目标文件的源代码和汇编代码输出到kernel.asm中,-t表示输出目标文件的符号表,之后通过管道把反汇编结果作为sed的输入,最后将符号表输出到kernel.sym中。
在$(call create_target,kernel)
中使用了function.mk中的do_create_target宏,但是这个宏没咋看懂,觉得没完成注释给出的功能,推测可能是用了makefile的某种特性或者是什么隐含规则,希望看懂的大佬教教我hhh。
至此这个toy OS的kernel就生成好了,我并不准备再去阅读kernel的代码,kernel为每个程序安排运行的时间,防止程序长时间独占CPU,目前知道这么多就够了吧(大概
bootblock在百度百科中的解释是:bootblock是BIOS中一段特定的区域,包含有用于引导的最小指令集,正常的BIOS升级操作不能消除这段信息。如果BIOS升级失败,可以利用bootblock来重新恢复。
了解了bootblock的作用,我们接下来继续看代码。
bootfiles = $(call listf_cc,boot)
列出boot/下的所有.c.S文件;
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
对所有的boot/下的.c.S文件用cc_compile进行编译,同时新增'-Os -nostdinc'两个编译选项,分别是编译优化和不让gcc到标准编译目录下寻找头文件,这一步结束后,obj/boot/下应出现bootmain.o(.d)和bootasm.o(.d);
bootblock = $(call totarget,bootblock)
生成bin/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)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
$(call create_target,bootblock)
可见,bootblock文件的前提是bootmain.o和bootasm.o和sign程序,同样使用ld链接器将bootmain.o和bootasm.o链接为bootblock.o,-N
表示把代码段和数据段都设置为可读而且可写的,-e
表示自行设置程序入口,-Ttext
一开始没找到含义,在这里找到是指定入口地址的意思,这里设定入口地址为0x7C00,为什么是这个值呢?暂且卖个关子,后面再看。接下来把obj/bootblock.o的源码和反汇编代码混合输出到bootblock.asm中,把bootblock.o的源码copy并且以二进制的形式(-O binary
)translate到bootblock.out中,-S
表示不copy重定向和符号信息。之后利用bin/sign程序依靠bootblock.out生成bootblock程序。
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
这两行代码用来生成bin/下的sign程序,类似的先生成在obj/sign/tools/下编译生成sign.o和sign.d,之后用sign.o生成sign。
至此,sign工具和bootblock也生成完毕。
最后生成ucore.img的部分比较短,在这里一并给出。
# 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)
首先,生成bin/ucore.img文件,之后看到ucore.img的生成需要kernel和bootblock文件为依赖,之后使用dd命令从/dev/zero中向ucore.img写入10000个512bytes的空字符,可以看作是初始化过程;之后从bootblock文件中向ucore.img写入数据,conv=notrunc
表示不截断输出文件,接下来从kernel中向ucore.img写入,此时跳过一个obs大小的块(缺省为512bytes)。
至此,整个ucore.img就生成完了。
因为sign.c用来生成bootblock,所以可以观察sign.c来确定主引导扇区特征:
if (st.st_size > 510) {
fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
return -1;
}
所以扇区大小为512byte,且bootblock的大小小于等于510bytes。
char buf[512];
......
buf[510] = 0x55;
buf[511] = 0xAA;
最后两个字节必须是0x55和0xaa。
查阅了一些资料,发现bootblock所在的这个块就是我们常听到的主引导记录,也叫MBR(Master Boot Record),最后两个字节0x55和0xaa是MBR的结束标志,就产生了这样的疑问:为什么是0x55和0xaa?怎么就不能是0x66和0xbb?个人猜测可能有点网络里的同步码的意思?
在知乎上瞎逛的时候发现了这样一篇回答,介绍了看门狗电路的作用,意思是说如果一段时间得不到0x55、0xaa这样的数据,看门狗就会认为程序出了异常,好像可以稍微解释一下,如果收到0x55和0xaa,那么认为主引导分区没问题,继续进行下面的操作。那这是否也意味着用户程序也会时不时发送这两个字节保证程序的正常进行?
关于gcc的编译参数,可以使用man gcc
来查看帮助信息
清华大学操作系统课程 ucore Lab 1
GNU GCC使用ld链接器进行链接的完整过程是怎样的?
makefile eval函数详解
跟我一起写Makefile:使用函数
bootblock解释
计算机是如何启动的?
Linux 下的两个特殊的文件 -- /dev/null 和 /dev/zero 简介及对比