系统软件启动过程
这一次实验用的是清华的操作系统实验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
可以看出,主引导区大小为512字节且最后两个字节为0x55和0xAA,只要达到这两个条件即符合规范。
题目 2:熟悉使用 qemu 和 gdb 进行的调试工作。
(要求在报告中简要写出练习过程)
1)从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
<1>修改tools/gdbinit
(注意保存一下原来的gdbinit)
set architecture i8086
target remote :1234
<2>make debug
以上实验步骤来自官方实验指导书,但是遇到了问题
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
可以强制反汇编当前指令。
修改后:
可以看到,现在多出一行显示出了汇编指令。
使用命令si可以单步执行一条汇编指令调试。
2) 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
b *0x7c00设置断点
c 执行
遇到断点,暂停。
3)从 0x7c00 开 始 跟 踪 代 码 运 行 , 将 单 步 跟 踪 反 汇 编 得 到 的 代 码 与 bootasm.S 和bootblock.asm 进行比较。
单步执行,将gdb结果与bootblock.asm比较,直到0x7ccf,除一些表达上的差异,仍没有发现明显区别。
4)在 bootloader 或内核代码中任意找一处代码,设置断点并进行测试。
将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中的打印栈帧链类似,不过这题简单一些,因为注释很丰富,而且有实现好的函数可供调用。
代码如下:
make qemu运行效果如下:
最后一行,ebp与eip的值对应着bootmain函数的栈帧与调用kern_init后的指令地址。由于kern_init()并没有参数传入,因此这里输出的是bootloader的二进制代码。
第五题:完善中断初始化与处理
1)中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
其实问的是中断描述符表。实验指导书中有交代。
中断描述符表一个表项占8字节。其中0~15位和48~63位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。
图示如下。
2)请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init。
来到idt_init(),阅读注释。
根据注释,
第一步,声明__vertors[],其中存放着中断服务程序的入口地址。这个数组生成于vertor.S中。
第二步,填充中断描述符表IDT。可以看到idt[]声明于trap.c的line28
根据题目的提示,这里应当用到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.
* */
阅读宏定义内容,容易看出gate即idt[]中的某一项。
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)|dpl,dpl为特权级。
不过参考答案中给出的是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)
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来输出点东西。而这三者都是已经定义好的。这题确实蛮简单的。