练习1 report
问题1:OS镜像文件ucore.img是如何一步一步生成的(需要比较详细地解释Makefile中的每一条相关命令和命令参数的含义,以及说明命令导致的结果)?
- GNU make是一种代码维护工具,在大中型项目中,它将根据程序各个模块的更target新情况,自动地维护和生成目标代码。make命令执行时,需要一个Makefile文件,以告诉make命令需要怎样去编译和链接程序。使用Makefile文件时,有如下规则:(1)如果这个工程没有编译过,那么所有的c文件都要编译并链接;(2)如果这个工程的某几个c文件被修改,那么只编译被修改的c文件,并链接目标程序;(3)如果这个工程的头文件被改变了,那么需要编译引用这几个头文件的c文件,并链接目标程序。
Makefile文件大致由target、prerequisites、command组成,target表示要生成的目标文件,prerequisites指要生成target所需要的文件或是目标,command是make需要执行的命令(shell命令),必须以[tab]开头。只要prerequisites中有一个以上的文件比文件要新,command的指令就会被执行,这就是Makefile的规则。
2. 编译所有生成bin/kernel所需的文件:
生成这些.o文件的相关makefile代码为
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
1) init.c是OS的初始化启动代码,把init.c编译为名为init.o的中间目标文件(-c参数表示生成init.o的目标文件,-o表示生成可执行文件。gcc -c a.c -o a.o表示把源文件a.c编译成指定文件名a.o的中间目标文件。):
gcc -c kern/init/init.c -o obj/kern/init/init.o
2) 把readline.c编译为名为readline的中间目标文件:
gcc -c kern/libs/readline.c -o obj/kern/libs/readline.o
3) 编译stdio.c生成stdio.o:
gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
4) kdebug.c提供源码和二进制对应关系的查询功能,用于显示调用栈关系。编译kdebug.c生成kdebug.o:
gcc -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
5) panic.c提供了panic函数,便于在发现错误后,调用kernel monitor。编译panic.c生成panic.o:
gcc -c kern/debug/panic.c -o obj/kern/debug/panic.o
6) clock.c实现了对时钟控制器8253的初始化操作。编译clock.c生成panic.o:
gcc -c kern/driver/clock.c -o obj/kern/driver/clock.o
7) kmonitor.c实现提供动态分析命令的kernel monitor,便于在ucore出现bug或问题之后,能够进入kernel monitor中,查看当前调用关系。编译kmonitor.c生成kmonitor.o:
gcc -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
8) console.c实现了对串口和键盘的中断方式的处理操作。将console.c编译生成console.o:
gcc -c kern/driver/console.c -o obj/kern/driver/console.o
9) intr.c实现了通过设置CPU的Eflags来屏蔽和使能中断的函数。编译intr.c生成intr.o:
gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o
10) picirq.c实现了对控制中断器8259A的初始化和使能操作。编译picriq.c生成picriq.o:
gcc -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
11) vectors.S包括256个中断服务例程的入口地址和第一步初步处理时先。此文件是由tools/vector.c在编译ucore期间动态生成的。编译voctor.S生成vector.o:
gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
12) trapentry.S紧接着第一步初步处理后,进一步完成第二步初步处理;并且又恢复中断上下文的处理,即中断处理完毕后的返回准备操作。编译trapentry.S生成trapentry.o:
gcc -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
13) trap.c紧接着第二步初步处理后,继续完成具体的各种中断处理操作。编译trap.c生成trap.o:
gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o
14) pmm.c设定ucore操作系统在段机制中要用到的全局变量:任务状态栏ts,全局描述符表gdt[],加载全局描述符表寄存器的函数lgdt,临时的内核栈stack(),以及对全局描述符表和任务状态段的初始化函数gdt_init。编译pmm.c生成pmm.o:
gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
15) 编译printfmt.c生成printfmt.o:
gcc -c libs/printfmt.c -o obj/libs/printfmt.o
16) 编译string.c生成string.o:
gcc -c libs/string.c -o obj/libs/string.o
3.链接生成bin/kernel:
生成kernel需要第一步所生成的那些.o文件和kernel.ld,而kernel.ld已经存在。生成kernel的Makefile相关代码为
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo + 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)
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel
obj/kern/init/init.o obj/kern/libs/readline.o
obj/kern/libs/stdio.o obj/kern/debug/kdebug.o
obj/kern/debug/kmonitor.o obj/kern/debug/panic.o
obj/kern/driver/clock.o obj/kern/driver/console.o
obj/kern/driver/intr.o obj/kern/driver/picirq.o
obj/kern/trap/trap.o obj/kern/trap/trapentry.o
obj/kern/trap/vectors.o obj/kern/mm/pmm.o
obj/libs/printfmt.o obj/libs/string.o
其中ld命令是关键,用于把目标代码文件连接为可执行文件或者库文件:
-m模拟指定的连接器elf_i386;
-T指定命令文件为tools/kernel.ld,即让连接器使用指定的该脚本;
-o指定输出文件名字为kernel。
4.生成bootblock:
在生成bootblock之前需要生成bootasm.o、bootmain.o、sign:
1) 生成bootasm.o需要bootasm.S,bootasm.S定义并实现bootloader最先执行的函数start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用了bootmain.c中的bootmain函数。实际命令为:
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs
-nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc
-c boot/bootasm.S -o obj/boot/bootasm.o
关键的参数:
-ggdb:生成可供gdb使用的调试信息。这样才能用qemu+gdb来调试bootloader or ucore;
-m32:生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386,所以ucore也要是32位的软件;
-gstabs:生成stabs格式的调试信息。这样要ucore的monitor可以显示出便于开发者阅读的函数调用栈信息
-nostdinc:不使用标准库。标准库是给应用程序用的,我们是编译ucore内核,OS内核是提供服务的,所以所有的服务要自给自足。
-fno-stack-protector:不生成用于检测缓冲区溢出的代码。
-Os:为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节。
-I<dir>:添加搜索头文件的路径。
-Wall:产生尽可能多的警告信息。
-fno-builtin:除非用__builtin_前缀,否则不进行builtin函数的优化
2) 生成bootmain.o需要bootmain.c,bootmain.c定义并实现了bootmain函数,实现了通过屏幕、串口和并口显示字符串,bootmain函数加载ucore操作系统到内存,然后跳到ucore的入口处执行。实际命令为:
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc
-fno-stack-protector -Ilibs/ -Os -nostdinc
-c boot/bootmain.c -o obj/boot/bootmain.o
其参数与前面提到的一致。
3) 生成sign需要sign.c,sign.c是一个C语言小程序,用于生成一个规范的硬盘主引导扇区。实际命令为:
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
关键的参数:
-I<dir>:添加搜索头文件的路径。
-Wall:产生尽可能多的警告信息。
-O2:优化程序。
-g:生成可供gdb使用的调试信息。
4) 由bootasm.o,bootmain.o,sign生成bootblock.o:
实际命令为:
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
关键的参数:
-m:模拟指定的连接器为elf_i386;
-N:指定读取/写入文本和数据段;
-e:使用指定的符号start作为程序的初始执行点;
-Ttext:使用指定的地址0x7C00作为文本段的起始点;
-nonstdlib:不使用标准库。
5) 拷贝二进制代码bootblock.o到bootblock.out
objcopy -S -O binary obj/bootblock.o obj/bootblock.out
关键的参数:
-S:移除所有符号和重定位信息
-O <bfdname>:指定输出格式
6) 使用sign工具处理bootblock.out,生成bootblock
bin/sign obj/bootblock.out bin/bootblock
5.生成ucore.img:
1) 生成一个有10000个块的文件,每个块默认512字节,用0填充
dd if=/dev/zero of=bin/ucore.img count=10000
2) 把bootblock中的内容写到第一个块
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
3) 从第二个块开始写kernel中的内容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
其中dd命令的作用是用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的 转换。第一条命令即将/dev/zero全盘数据,这里用0填充,备份到bin/下的 ucore.img文件。
关键参数:
if=文件名:输入文件名,缺省即标准输入;
of=文件名:输出文件名,缺省即标准输出;
seek=blocks:从输出文件开头跳过blocks个块后开始复制;
conv:用指定的参数转换文件,此处的参数notrunc为不截短输出文件。
问题2:一个被认为是符合规范的硬盘主引导扇区的特征是什么?
通常,我们将包含MBR引导代码的扇区称为主引导扇区。通常由3部分组成:
主引导程序(MBR,占446字节)、磁盘分区表项(占4×16个字节,负责说明磁盘上的分区情况)、结束标志(占2个字节,其值为AA55)。
因为sign.c是用于生成一个符合规范的硬盘主引导扇区,所以我截取了sign.c中的部分代码并加以注释来更清晰地分析主引导分区的特征:
char buf[512]; //定义buff数组
memset(buf, 0, sizeof(buf)); //将buff数组初始化为0
buf[510] = 0x55;
buf[511] = 0xAA; //将buff数组最后两位初始化为0x55,0xAA
得出主引导扇区的特征:
- 大小为512个字节;
- 第510个字节为0x55;
- 第511个字节为0xAA;
- 其余字节为0。