课程实验 OS OS-Lab1 Kahvia 2022-11-13 2023-01-05 练习1
练习1:理解通过make生成执行文件的过程。(要求在报告中写出对下述问题的回答)
列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。
在此练习中,大家需要通过静态分析代码来了解:
1.操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
2.一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
1
过程
输入make “V=” 得到详细的make过程如下。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 [~/moocos/ucore_lab/labcodes/lab1] moocos-> make "V=" + cc kern/init/init.c gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o kern/init/init.c:95:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused-function ] lab1_switch_test(void) { ^ + cc kern/libs/readline.c gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o + cc kern/libs/stdio.c gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o + cc kern/debug/kdebug.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o kern/debug/kdebug.c:251:1: warning: ‘read_eip’ defined but not used [-Wunused-function ] read_eip(void) { ^ + cc kern/debug/kmonitor.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o + cc kern/debug/panic.c gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o + cc kern/driver/clock.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o + cc kern/driver/console.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o + cc kern/driver/intr.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o + cc kern/driver/picirq.c gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o + cc kern/trap/trap.c gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o kern/trap/trap.c:14:13: warning: ‘print_ticks’ defined but not used [-Wunused-function ] static void print_ticks () { ^ kern/trap/trap.c:30:26: warning: ‘idt_pd’ defined but not used [-Wunused-variable] static struct pseudodesc idt_pd = { ^ + cc kern/trap/trapentry.S gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o + cc kern/trap/vectors.S gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o + cc kern/mm/pmm.c gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o + cc libs/printfmt.c gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o + cc libs/string.c gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o + ld bin/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 + cc boot/bootasm.S gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o + cc boot/bootmain.c gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o + cc tools/sign.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 + ld bin/bootblock ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o 'obj/bootblock.out' size: 472 bytesbuild 512 bytes boot sector: 'bin/bootblock' success! dd if =/dev/zero of=bin/ucore.img count=1000010000+0 records in 10000+0 records out 5120000 bytes (5.1 MB) copied, 0.0484045 s, 106 MB/s dd if =bin/bootblock of=bin/ucore.img conv=notrunc1+0 records in 1+0 records out 512 bytes (512 B) copied, 0.000105295 s, 4.9 MB/s dd if =bin/kernel of=bin/ucore.img seek=1 conv=notrunc138+1 records in 138+1 records out 70775 bytes (71 kB) copied, 0.00033329 s, 212 MB/s [~/moocos/ucore_lab/labcodes/lab1] moocos->
其中我们可以看到一些与gcc有关的命令。
这个命令的作用是将某个 c 文件经过 预处理(-E)、汇编(-S)、编译(-c) 后,将得到的结果输出到目标文件,即.o后缀的文件。
还有ld命令
将 source 对应的源文件作连接处理,得到最终的文件。
make记录中对应的目标命令如下,将之前gcc编译出来的,一些以.o后缀的对象文件,作连接处理后,得到最终文件 kernel.ld,存放在tools目录下。
1 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
第二部分的编译和连接过程更短,可以看得更直观。编译得到bootasm.o和bootmain.o,再通过ld命令连接,得到obj目录下的bootblock.o文件。
1 2 3 4 5 6 7 8 9 10 + cc boot/bootasm.S gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o + cc boot/bootmain.c gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o + cc tools/sign.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 + ld bin/bootblock ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o 'obj/bootblock.out' size: 472 bytes
最后则是 dd 命令。
1 dd if =xxx of=yyy count=number
类似于拷贝,if 指定输入文件, of 指定输出文件, count 指定拷贝的的块数。
make过程中对应的代码块如下。conv=notrunc指的是“不截短输出文件”。
1 2 3 4 5 6 7 8 9 10 11 12 dd if =/dev/zero of=bin/ucore.img count=1000010000+0 records in 10000+0 records out 5120000 bytes (5.1 MB) copied, 0.0484045 s, 106 MB/s dd if =bin/bootblock of=bin/ucore.img conv=notrunc1+0 records in 1+0 records out 512 bytes (512 B) copied, 0.000105295 s, 4.9 MB/s dd if =bin/kernel of=bin/ucore.img seek=1 conv=notrunc138+1 records in 138+1 records out 70775 bytes (71 kB) copied, 0.00033329 s, 212 MB/s
具体拷贝的流程我结合指令详解也不是很明白。
结论
make生成 ucore.img 大概有三步,分别依赖指令 gcc, ld, dd。
gcc命令负责编译源文件,ld命令负责连接gcc得到的目标文件,dd命令负责拷贝目标文件到指定的位置。而dd 命令完成后得到的最终文件,就是 ucore.img
2
引导扇区的大小为512字节。
练习2
练习2:使用qemu执行并调试lab1中的软件。(要求在报告中简要写出练习过程)
为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
2.在初始化位置0x7c00设置实地址断点,测试断点正常。
3.从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
4.自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
1
在lab1目录下,执行 make -debug,进入调试界面,输入si进行单步跟踪,如下图所示
2
设置实地址断点
1 2 3 4 5 set architecture i8086 target remote :1234 b *0x7c00 c x/2i $pc
再次使用make -debug,测试断点正常
3
将makefile 的第218行起的代码,修改为如下代码
1 2 3 4 debug: $(UCOREIMG) $(V) $(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR) /q.log -parallel stdio -hda $< -serial null" $(V) sleep 2 $(V) $(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
make -debug 后产生 q.log 文件。通过查找断点地址得如下信息。
1 2 3 4 5 6 7 IN: 0x00007c00: cli 0x00007c01: cld 0x00007c02: xor %ax,%ax 0x00007c04: mov %ax,%ds 0x00007c06: mov %ax,%es 0x00007c08: mov %ax,%ss
而 bootasm.S 文件中,如此显示。
bootblock.asm 文件中如此显示。
通过比较得知,各文件中,指令差别不大。
4
设置新断点为 0x7c16
make -debug 如图所示。
此时正在执行的指令为 test 和 jne
练习3
练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
1.开启A20门
在 bootasm.S 文件中,有这样的代码
其中seta20.1是往端口0x64写数据0xd1,告诉CPU我要往8042芯片的P2端口写数据;seta20.2是往端口0x60写数据0xdf,从而将8042芯片的P2端口设置为1. 两段代码都需要先读0x64端口的第2位,确保输入缓冲区为空后再进行后续写操作。
2.初始化GDT表
同样在这个文件中。
3.进入保护模式
将cr0寄存器的PE位(cr0寄存器的最低位)设置为1。
1 2 3 movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0
练习4
练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
1.bootloader如何读取硬盘扇区的?
2.bootloader是如何加载ELF格式的OS?
1
在boot目录下的 bootmain.c 文件中,读取扇区的核心代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void readsect (void *dst, uint32_t secno) { waitdisk(); outb(0x1F2 , 1 ); outb(0x1F3 , secno & 0xFF ); outb(0x1F4 , (secno >> 8 ) & 0xFF ); outb(0x1F5 , (secno >> 16 ) & 0xFF ); outb(0x1F6 , ((secno >> 24 ) & 0xF ) | 0xE0 ); outb(0x1F7 , 0x20 ); waitdisk(); insl(0x1F0 , dst, SECTSIZE / 4 ); }
2
加载ELF格式的OS的核心代码如下所示
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 void bootmain (void ) { readseg((uintptr_t )ELFHDR, SECTSIZE * 8 , 0 ); if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph , *eph ; ph = (struct proghdr *)((uintptr_t )ELFHDR + ELFHDR->e_phoff); eph = ph + ELFHDR->e_phnum; for (; ph < eph; ph ++) { readseg(ph->p_va & 0xFFFFFF , ph->p_memsz, ph->p_offset); } ((void (*)(void ))(ELFHDR->e_entry & 0xFFFFFF ))(); bad: outw(0x8A00 , 0x8A00 ); outw(0x8A00 , 0x8E00 ); while (1 ); }
从磁盘读取第一页
判断是否是合法的ELF
加载每一个扇区
从ELF头部调用入口函数
练习5
练习5:实现函数调用堆栈跟踪函数 (需要编程)
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:
…
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096
kern/debug/kdebug.c:305: print_stackframe+22
ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53
kern/init/init.c:28: kern_init+88
ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
: – 0x00007d72 –
…
请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。
实现函数如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 print_stackframe(void ) { uint32_t *ebp = (uint32_t *)read_ebp(); uint32_t eip = read_eip(); while (ebp) { cprintf("ebp:0x%08x eip:0x%08x args:" , (uint32_t )ebp, eip); cprintf("0x%08x 0x%08x 0x%08x 0x%08x\n" , ebp[2 ], ebp[3 ], ebp[4 ], ebp[5 ]); print_debuginfo(eip - 1 ); eip = ebp[1 ]; ebp = (uint32_t *)*ebp; } }
执行make qemu,结果如下所示
ebp:ebp的值是kern_init函数的栈顶地址。
eip:eip的值是kern_init函数的返回地址,也就是bootmain函数调用kern_init对应的指令的下一条指令的地址。
args:存放的是boot loader指令的前16个字节。
练习6
练习6:完善中断初始化和处理 (需要编程)
请完成编码工作和回答如下问题:
1.中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3.请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
1
中断描述符表一个表项占8个字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成偏移量,即第16~ 32位是段选择子,第0~ 15、48~63位构成的偏移地址,通过段选择子去GDT中找到对应的基地址,然后基地址加上偏移量就是中断处理程序的地址。
2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void idt_init (void ) { extern uintptr_t __vectors[]; int i; for (i = 0 ; i < sizeof (idt) / sizeof (struct gatedesc); i ++) { SETGATE(idt[i], 0 , GD_KTEXT, __vectors[i], DPL_KERNEL); } SETGATE(idt[T_SWITCH_TOK], 0 , GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER); lidt(&idt_pd); }
3
1 2 3 4 5 6 7 8 9 10 11 case IRQ_OFFSET + IRQ_TIMER: ticks ++; if (ticks % TICK_NUM == 0 ) { print_ticks(); }
运行结果如下所示。