操作系统实验报告:ucore-lab1

时间:2021-04-22 00:53:58

系统软件启动过程


这一次实验用的是清华的操作系统实验ucore的lab1

清华的ucore就是在MIT的oslab基础上做的那个东西。做下来感觉确实比jyy写的oslab要好一些。

本次实验需要参考一些资料才能做。

官网:

http://os.cs.tsinghua.edu.cn/oscourse/OS2015

实验指导书:

https://www.gitbook.com/book/objectkuan/ucore-docs/details

另外,搜索“ucore lab1”可以找到网上的很多实验报告参考

问题1:通过静态分析代码来理解 make 生成执行文件的过程。

1)操作系统镜像文件 minios.img 是如何一步一步生成的?(需要比较详细地解释 Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

我本来打算详细地解释一遍Makefile,然而能力有限,前面的部分还能勉强读下去,但是在遇到复杂的函数调用之后,推进的速度大大下降,而本次实验时间有限,只好暂时放弃。

 注意:这道题并不需要详细解释makefile,只需要解释执行的命令就可以了。我一开始是理解错了题目的意思,然后既然开始做了就尽量写下去了。

下面是我对makefile的100行之前和200行以后的理解。

 

#定义变量PROJ为challenge,在后面handin中使用了这个变量,将其插入生成的压缩包名字中。

#可能是用来让同学改为学号等信息对提交的作业进行区分

PROJ:= challenge

EMPTY:=

SPACE:= $(EMPTY) $(EMPTY)

SLASH:= /

#没有使用的3个变量

V := @

#变量V=@,后面大量使用了V

#@的作用是不输出后面的命令,只输出结果

#在这里修改V即可调整输出的内容

#也可以 make "V=" 来完整输出



W:=

#W:= -Wall

#为了不输出warning,我自己加的



#need llvm/cang-3.5+

#USELLVM := 1

#若要使用LLVM则去掉前面一行的#即可



# try to infer the correct GCCPREFIX

#这里是在选择交叉编译器。

#检查环境变量GCCPREFIX是否被设置(通常是没有的)

#如果没有被设置,那么判断运行环境,自行定义变量GCCPREFIX

#如果你想使用这里不能判断出的交叉编译器,可以自行设置GCCPREFIX环境变量

ifndef GCCPREFIX

GCCPREFIX := $(shell if i386-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; \

then echo 'i386-elf-'; \

elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; \

then echo ''; \

else echo "***" 1>&2; \

echo "*** Error: Couldn't find an i386-elf version of GCC/binutils." 1>&2; \

echo "*** Is the directory with i386-elf-gcc in your PATH?" 1>&2; \

echo "*** If your i386-elf toolchain is installed with a command" 1>&2; \

echo "*** prefix other than 'i386-elf-', set your GCCPREFIX" 1>&2; \

echo "*** environment variable to that prefix and run 'make' again." 1>&2; \

echo "*** To turn off this error, run 'gmake GCCPREFIX= ...'." 1>&2; \

echo "***" 1>&2; exit 1; fi)

endif



# try to infer the correct QEMU

#与上面的类似,这里在设置QEMU。

#QEMU是一款优秀的模拟处理器,使用方便,比virtualbox更适合进行实验。

ifndef QEMU

QEMU := $(shell if which qemu-system-i386 > /dev/null; \

then echo 'qemu-system-i386'; exit; \

elif which i386-elf-qemu > /dev/null; \

then echo 'i386-elf-qemu'; exit; \

elif which qemu > /dev/null; \

then echo 'qemu'; exit; \

else \

echo "***" 1>&2; \

echo "*** Error: Couldn't find a working QEMU executable." 1>&2; \

echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; \

echo "***" 1>&2; exit 1; fi)

endif



#使用伪目标.SUFFIXES定义自己的后缀列表

# eliminate default suffix rules

.SUFFIXES: .c .S .h



#遇到error就删除所有目标文件

# delete target files if there is an error (or make is interrupted)

.DELETE_ON_ERROR:



#设置编译器选项

# define compiler and flags

ifndef USELLVM



# hostcc是给主机用的编译器,按照主机格式。cc是i386,elf32格式的编译器。

# 我用的是ubuntu,这两者是一样的,在别的环境中可能有所区别。

HOSTCC:= gcc



# -g 是为了gdb能够对程序进行调试

# -Wall 生成警告信息

# -O2 优化处理(0,1,2,3表示不同的优化程度,0为不优化)

HOSTCFLAGS:= -g $(W) -O2



CC:= $(GCCPREFIX)gcc



# -fno-builtin 不接受非“__”开头的内建函数

# -ggdb让gcc 为gdb生成比较丰富的调试信息

# -m32 编译32位程序

# -gstabs 此选项以stabs格式声称调试信息,但是不包括gdb调试信息

# -nostdinc 不在标准系统目录中搜索头文件,只在-I指定的目录中搜索

#DEFS是未定义量。可用来对CFLAGS进行扩展。

CFLAGS:= -fno-builtin $(W) -ggdb -m32 -gstabs -nostdinc $(DEFS)





#这句话的意思是,如果-fno-stack-protector选项存在,就添加它。过程蛮复杂的。

# -fstack-protector-all 启用堆栈保护,为所有函数插入保护代码

# -E 仅作预处理,不进行编译、汇编和链接

# -x c 指明使用的语言为c语言

# 前一个/dev/null用来指定目标文件

# >/dev/null 2>&1 将标准输出与错误输出重定向到/dev/null

# /dev/null是一个垃圾桶一样的东西

# ‘&&’之前的半句表示,试着对一个垃圾跑一下这个命令,所有的输出都作为垃圾,为了快一点,开了-E。

# 如果不能运行,那么&&前面的条件不成立,后面的就被忽视。

# 如果可以运行,那么&&后面的句子得到执行,于是CFLAGS += -fno-stack-protector

CFLAGS+= $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)



else

HOSTCC:= clang

HOSTCFLAGS:= -g $(W) -O2

CC:= clang

CFLAGS:= -fno-builtin $(W) -g -m32 -mno-sse -nostdinc $(DEFS)

CFLAGS+= $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)

endif



#源文件类型为c和S

CTYPE:= c S



#一些链接选项

LD := $(GCCPREFIX)ld



#ld -V命令会输出连接器的版本与支持的模拟器。在其中搜索elf_i386。

#若支持,则LDFLAGS := -m elf_i386

LDFLAGS:= -m $(shell $(LD) -V | grep elf_i386 2>/dev/null)



# -nostdlib 不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器

LDFLAGS+= -nostdlib



OBJCOPY := $(GCCPREFIX)objcopy

OBJDUMP := $(GCCPREFIX)objdump



#定义一些命令

COPY:= cp

MKDIR := mkdir -p

MV:= mv

RM:= rm -f

AWK:= awk

SED:= sed

SH:= sh

TR:= tr

TOUCH:= touch -c



OBJDIR:= obj

BINDIR:= bin



ALLOBJS:=

ALLDEPS:=

TARGETS:=



#function.mk中定义了大量的函数。

#.mk中每个函数都有注释。

#时间原因,不打算看.mk中函数的具体实现了,都很厉害的样子

include tools/function.mk



#call函数:call func,变量1,变量2,...

#listf函数在function.mk中定义,列出某地址(变量1)下某些类型(变量2)文件

#listf_cc函数即列出某地址(变量1)下.c与.S文件

listf_cc = $(call listf,$(1),$(CTYPE))



# for cc



# add files to packet

# add_files:(#files, cc[, flags, packet, dir])

# add_files_cc:(#files, packet, flags, dir)flags已添加,这个变量仅用以扩展

add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))



# add packets and objs to target

# create_target(target, #packets, #objs, cc, [, flags])

# create_target_cc(target, #packets, #objs)

create_target_cc = $(call create_target,$(1),$(2),$(3),$(CC),$(CFLAGS))



# for hostcc

# 下面这段与for cc的是一样的功能

add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))

create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))



#patsubst替换通配符

#cgtype(filenames,type1,type2)

#把文件名中后缀是type1的改为type2,如*.c改为*.o

cgtype = $(patsubst %.$(2),%.$(3),$(1))



#toobj : get .o obj files: (#files[, packet])

#列出所有.o文件

objfile = $(call toobj,$(1))

#.o改为.asm

asmfile = $(call cgtype,$(call toobj,$(1)),o,asm)

#.o改为.out

outfile = $(call cgtype,$(call toobj,$(1)),o,out)

#.o改为.sym

symfile = $(call cgtype,$(call toobj,$(1)),o,sym)
#######################################################
#这里是原makefile的104行
#
#
#
#
#100行~200行太复杂,不进行具体注释,下面有简单介绍
#
#
#
#
#
#这里是原makefile的203行
#######################################################
#下面的很多命令是qemu的参数,其中一些网上不太好查,有兴趣的话可以去 http://wiki.qemu.org/Manual 查阅

#终端模式打开qemu

qemu-mon: $(UCOREIMG)

$(V)$(QEMU) -no-reboot -monitor stdio -hda $< -serial null

#新窗口下打开qemu

qemu: $(UCOREIMG)

$(V)$(QEMU) -no-reboot -parallel stdio -hda $< -serial null

#运行并生成log文件

log: $(UCOREIMG)

$(V)$(QEMU) -no-reboot -d int,cpu_reset -D q.log -parallel stdio -hda $< -serial null

#禁止图形界面,转到终端

qemu-nox: $(UCOREIMG)

$(V)$(QEMU) -no-reboot -serial mon:stdio -hda $< -nographic

TERMINAL :=gnome-terminal

#调试

debug: $(UCOREIMG)

$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &

$(V)sleep 2

$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

#在终端打开qemu进行调试,现在终端会陷入死循环QAQ

debug-nox: $(UCOREIMG)

$(V)$(QEMU) -S -s -serial mon:stdio -hda $< -nographic &

$(V)sleep 2

$(V)$(TERMINAL) -e "gdb -q -x tools/gdbinit"



.PHONY: grade touch



GRADE_GDB_IN:= .gdb.in

GRADE_QEMU_OUT:= .qemu.out

HANDIN:= proj$(PROJ)-handin.tar.gz



TOUCH_FILES:= kern/trap/trap.c



MAKEOPTS:= --quiet --no-print-directory



#clean并check output

grade:

$(V)$(MAKE) $(MAKEOPTS) clean

$(V)$(SH) tools/grade.sh

#unknown

touch:

$(V)$(foreach f,$(TOUCH_FILES),$(TOUCH) $(f))

#unknown

print-%:

@echo $($(shell echo $(patsubst print-%,%,$@) | $(TR) [a-z] [A-Z]))



.PHONY: clean dist-clean handin packall tags

#删除

clean:

$(V)$(RM) $(GRADE_GDB_IN) $(GRADE_QEMU_OUT) cscope* tags

-$(RM) -r $(OBJDIR) $(BINDIR)

#把压缩包也删除

dist-clean: clean

-$(RM) $(HANDIN)

#打包并输出一句话

handin: packall

@echo Please visit http://www.nju.edu.cn and upload $(HANDIN). Thanks!

#打包

packall: clean

@$(RM) -f $(HANDIN)

@tar -czf $(HANDIN) `find . -type f -o -type d | grep -v '^\.*$$' | grep -vF '$(HANDIN)'`

#可能是输出所有tags,要cscope工具

tags:

@echo TAGS ALL

$(V)rm -f cscope.files cscope.in.out cscope.out cscope.po.out tags

$(V)find . -type f -name "*.[chS]" >cscope.files

$(V)cscope -bq

$(V)ctags -L cscope.files

 

 

makefile的结构:

1~138行:定义各种变量/函数,设置参数,进行准备工作。

140~153行:生成bin/kernel

155~170行:生成bin/bootblock

172~176行:生成bin/sign

178~188行:生成bin/ucore.img

189~201行:收尾工作/定义变量

203~269行:定义各种make目标

 

下面来看一下makefile的执行过程。

为了显示所有执行的命令,我一开始是把Line6的“V:=@”改为“V:=”,然后make。不过后来在网上看到,可以通过执行make "V=" 来达到目的。

 

javan@javan-PC:/media/javan/文档/OS/Lab7/lab1$ make "V="

//Part1:生成bin/kernel

//生成init.o

+ cc kern/init/init.c

gcc -Ikern/init/ -fno-builtin  -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

//生成stdio.o

+ cc kern/libs/stdio.c

gcc -Ikern/libs/ -fno-builtin  -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

//生成readline.o

+ cc kern/libs/readline.c

gcc -Ikern/libs/ -fno-builtin  -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

//生成panic.o

+ cc kern/debug/panic.c

gcc -Ikern/debug/ -fno-builtin  -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

//生成kdebug.o

+ cc kern/debug/kdebug.c

gcc -Ikern/debug/ -fno-builtin  -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

//生成kmonitor.o

+ cc kern/debug/kmonitor.c

gcc -Ikern/debug/ -fno-builtin  -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

//生成clock.o

+ cc kern/driver/clock.c

gcc -Ikern/driver/ -fno-builtin  -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

//生成console.o

+ cc kern/driver/console.c

gcc -Ikern/driver/ -fno-builtin  -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

//生成picirq.o

+ cc kern/driver/picirq.c

gcc -Ikern/driver/ -fno-builtin  -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

//生成intr.o

+ cc kern/driver/intr.c

gcc -Ikern/driver/ -fno-builtin  -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

//生成trap.o

+ cc kern/trap/trap.c

gcc -Ikern/trap/ -fno-builtin  -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

//生成vectors.o

+ cc kern/trap/vectors.S

gcc -Ikern/trap/ -fno-builtin  -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

//生成trapentry.o

+ cc kern/trap/trapentry.S

gcc -Ikern/trap/ -fno-builtin  -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

//生成pmm.o

+ cc kern/mm/pmm.c

gcc -Ikern/mm/ -fno-builtin  -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

//生成string.o

+ cc libs/string.c

gcc -Ilibs/ -fno-builtin  -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o

//生成printfmt.o

+ cc libs/printfmt.c

gcc -Ilibs/ -fno-builtin  -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o

//连接.o文件生成bin/kernel

+ ld bin/kernel

ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o  obj/libs/string.o obj/libs/printfmt.o

 

//Part2:生成bin/bootblock

//生成bootasm.o

+ cc boot/bootasm.S

gcc -Iboot/ -fno-builtin  -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

//生成bootmain.o

+ cc boot/bootmain.c

gcc -Iboot/ -fno-builtin  -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

 

//Part3:生成bin/sign

+ cc tools/sign.c

gcc -Itools/ -g  -O2 -c tools/sign.c -o obj/sign/tools/sign.o

gcc -g  -O2 obj/sign/tools/sign.o -o bin/sign

 

//连接,生成bootblock

+ 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: 468 bytes

build 512 bytes boot sector: 'bin/bootblock' success!

//Part4:生成ucore.img

dd if=/dev/zero of=bin/ucore.img count=10000

记录了10000+0 的读入

记录了10000+0 的写出

5120000字节(5.1 MB)已复制,0.219116 秒,23.4 MB/秒

dd if=bin/bootblock of=bin/ucore.img conv=notrunc

记录了1+0 的读入

记录了1+0 的写出

512字节(512 B)已复制,0.000170474 秒,3.0 MB/秒

dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

记录了138+1 的读入

记录了138+1 的写出

70824字节(71 kB)已复制,0.00297832 秒,23.8 MB/秒

 

一些出现的参数

 

GCCFLAGS

# -g 是为了gdb能够对程序进行调试

# -Wall 生成警告信息

# -O2 优化处理(0,1,2,3表示不同的优化程度,0为不优化)

# -fno-builtin 不接受非“__”开头的内建函数

# -ggdb让gcc 为gdb生成比较丰富的调试信息

# -m32 编译32位程序

# -gstabs 此选项以stabs格式声称调试信息,但是不包括gdb调试信息

# -nostdinc 不在标准系统目录中搜索头文件,只在-I指定的目录中搜索

# -fstack-protector-all 启用堆栈保护,为所有函数插入保护代码

# -E 仅作预处理,不进行编译、汇编和链接

# -x c 指明使用的语言为c语言

 

LDFLAGS

# -nostdlib 不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器

#-m elf_i386使用elf_i386模拟器

#-N把 text 和 data 节设置为可读写.同时,取消数据节的页对齐,同时,取消对共享库的连接.

#-e func以符号func的位置作为程序开始运行的位置

#-Ttext addr指定节在文件中的绝对地址为addr

 

2)一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

vim tools/sign.c

 操作系统实验报告:ucore-lab1

可以看出,主引导区大小为512字节且最后两个字节为0x55和0xAA,只要达到这两个条件即符合规范。

 

题目 2:熟悉使用 qemu 和 gdb 进行的调试工作。

(要求在报告中简要写出练习过程)

1)从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。

<1>修改tools/gdbinit

(注意保存一下原来的gdbinit)

set  architecture    i8086

target     remote    :1234

<2>make debug

 操作系统实验报告:ucore-lab1

以上实验步骤来自官方实验指导书,但是遇到了问题

warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration of GDB.  Attempting to continue with the default i8086 settings.

本该显示的汇编代码显示为??

初步估计是gdb与qemu不兼容。

我使用版本为gdb 7.9,qemu 2.2,ubuntu 15.04

查阅了一些资料,没有找到准确的解决办法。

考虑到时间问题,这题暂且搁置。

 

更新:

在gdbinit中添加

define hook-stop

x/i $pc

end

可以强制反汇编当前指令。

修改后:

 操作系统实验报告:ucore-lab1

可以看到,现在多出一行显示出了汇编指令。

使用命令si可以单步执行一条汇编指令调试。

操作系统实验报告:ucore-lab1

2) 在初始化位置 0x7c00 设置实地址断点,测试断点正常。

 操作系统实验报告:ucore-lab1

b *0x7c00设置断点

执行

遇到断点,暂停。

 

3从 0x7c00 开 始 跟 踪 代 码 运 行 将 单 步 跟 踪 反 汇 编 得 到 的 代 码 与 bootasm.S bootblock.asm 进行比较。

单步执行,将gdb结果与bootblock.asm比较,直到0x7ccf,除一些表达上的差异,仍没有发现明显区别。

 

4在 bootloader 或内核代码中任意找一处代码,设置断点并进行测试。

 操作系统实验报告:ucore-lab1

gdbinit文件改回原来的样子。

make debug

程序进入init.c中的kern_init()

b pmm_init设置断点

这是初始化gdt的函数。

c执行

遇到断点,在pmm.c中的pmm_init处停止

 

题目 3:分析 bootloader 进入保护模式的过程。BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析 bootloader 是如何完成从实模式进入保护模式的。

这段功能由bootasm.S来实现,只需要阅读这个文件即可,里面有很详细的注释,并不难读懂。

bootbloader首先屏蔽所有中断, 

之后将段寄存器清零, 

打开A20地址线, 

加载GDT的基地址, 

切换到保护模式, 

跳转到32位代码. 在32位代码中, bootloader重新设置保护模式下的段寄存器, 然后设置栈顶指针, 之后跳转到C代码。

 

题目 4:分析 bootloader 加载 ELF 格式的 OS 的过程。

1) bootloader 如何读取硬盘扇区的?

2) bootloader 是如何加载 ELF 格式的 OS?

注意到bootasm.S中line71: call bootmain

于是阅读bootmain.c中的bootmain函数。

void

bootmain(void) {

// read the 1st page off disk

/*

首先,调用readseg()函数读取8个扇区,获得ELF头表。

而readseg()循环调用readsec()每次读出一个扇区

readsec()的过程即bootloader读取硬盘扇区的过程:

1.等待磁盘准备好

2.写地址0x1f2~0x1f5,0x1f7,发出读取磁盘的命令

3.等待磁盘准备好

4.调用函数insl把扇区数据读到内存

*/

readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);



// is this a valid ELF?

//判断是否是elf文件

//在判断MBR是否合法时,我们使用了魔数0x55和0xaa,

//这里可能是相同的思想

if (ELFHDR->e_magic != ELF_MAGIC) {

goto bad;

}



struct proghdr *ph, *eph;



// load each program segment (ignores ph flags)

/*

pragram header,程序头表,描述一个段在文件中的位置、大小

以及它被放到内存后所在的位置和大小

将程序头表的地址存在ph

*/

ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);

eph = ph + ELFHDR->e_phnum;



//按照程序头表的描述,将ELF文件中的数据载入内存

for (; ph < eph; ph ++) {

readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);

}



// call the entry point from the ELF header

// note: does not return

//根据ELF头表中的入口信息,找到内核入口开始运行,不再返回

((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();



bad:

outw(0x8A00, 0x8A00);

outw(0x8A00, 0x8E00);



/* do nothing */

while (1);

}


 

原第五题:实现函数调用堆栈跟踪函数

在ucore的lab1中,第五题为实现函数调用堆栈跟踪函数。

需补全kern/debug/kdebug.c中的print_stackframe()函数。

我们的实验中把这题删掉了。试着做了一下。

这题跟上学期的PA中的打印栈帧链类似,不过这题简单一些,因为注释很丰富,而且有实现好的函数可供调用。

代码如下:

 操作系统实验报告:ucore-lab1

make qemu运行效果如下:

 操作系统实验报告:ucore-lab1

最后一行,ebp与eip的值对应着bootmain函数的栈帧与调用kern_init后的指令地址。由于kern_init()并没有参数传入,因此这里输出的是bootloader的二进制代码。

 

第五题:完善中断初始化与处理

1)中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

其实问的是中断描述符表。实验指导书中有交代。

中断描述符表一个表项占8字节。其中0~15位和48~63位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。

图示如下。

 操作系统实验报告:ucore-lab1

2请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init

来到idt_init(),阅读注释。

根据注释,

第一步,声明__vertors[],其中存放着中断服务程序的入口地址。这个数组生成于vertor.S中。

第二步,填充中断描述符表IDT。可以看到idt[]声明于trap.cline28

根据题目的提示,这里应当用到mmu.h中的SETGATE宏。

找到SETGATE宏的声明,

SETGATE(gate, istrap, sel, off, dpl)

在声明前有注释:

/* *

 * Set up a normal interrupt/trap gate descriptor

 *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate

 *   - sel: Code segment selector for interrupt/trap handler

 *   - off: Offset in code segment for interrupt/trap handler

 *   - dpl: Descriptor Privilege Level - the privilege level required

 *          for software to invoke this interrupt/trap gate explicitly

 *          using an int instruction.

 * */

阅读宏定义内容,容易看出gateidt[]中的某一项。

istrap:陷阱门设为1,中断门设为0.

 

sel:段选择子。

       查阅资料可以知道,16位的段描述符,高13位为段在描述符表中的索引号,低3    位为特权级。

   易知,中断描述符表应当在内核代码段中。

   所以我们现在要找到内核代码段的索引号。

   于是跳转到描述符表gdt定义的位置阅读代码。

static struct segdesc gdt[] = {

    SEG_NULL,

    [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_KERNEL),

    [SEG_KDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_KERNEL),

    [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_USER),

    [SEG_UDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_USER),

    [SEG_TSS]    = SEG_NULL,

}

而从gdt的定义中可以看出,内核代码段的索引号为SEG_KTEXT

因此段描述符应当为(SEG_KTEXT<<3)|dpldpl为特权级。

不过参考答案中给出的是GD_KTEXT,跳转到GD_KTEXT定义的地方阅读代码, 发现GD_KTEXT=SEG_KTEXT<<3,所以前面的理解好像有点小问题,中断描述符 表中的段选择子后三位应该是空的?

 

off:处理函数的入口地址,即__vectors[]中的内容。

dpl:特权级.从实验指导书中可知,ucore中的应用程序处于特权级3,内核态特权级为0.

而除了T_SYSCALL之外的所有中断都是内核态权限。

 

T_SYSCALL是一个特例,单独修改dit

这里似乎有点问题。

T_SYSCALL是一个宏定义,被定义为0x80,这应当是它的调用号吧,它的中断描述 符不该是idt[128]么。。。

参考答案中代码写的是idt[T_SWITCH_TOK],查看其定义,为121

根据注释,T_SWITCH_TOK是从用户态跳转到内核态的意思。而实验指导书中说,这 一过程是通过T_SYSCALL实现的。

这两个数字怎么可以不一致。。。

 

第三步:加载中断描述符表。

注释中说,使用lidt指令,其参数为ldt_pd

查看这两者的定义。查看手册寻找lidt的用法。

可知应当写成lidt(&ldt_pd)

 操作系统实验报告:ucore-lab1

 

3请编程完善 trap.c 中的中断处理函数 trap,在对时钟中断进行处理的部分填写 trap 函数中处理时钟中断的部分,

使操作系统每遇到 100 次时钟中断后,调用 print_ticks 子程序,向屏幕上打印一行文字”100 ticks”。

查看函数trap,发现它调用中断处理函数trap_dispatch来处理。

查看函数trap_dispatch,发现在 “case IRQ_OFFSET + IRQ_TIMER:”下方有一段注释,指导我们写这题。

使用变量ticks计数,每达到TICK_NUM次,就调用print_ticks来输出点东西。而这三者都是已经定义好的。这题确实蛮简单的。

操作系统实验报告:ucore-lab1