实验一:系统启动及中断
# 一、x86 启动顺序
# 1.1 第一条指令
如下图所示,寄存器在初始状态下都有一个缺省的值,其中 CS 和 EIP 结合在一起,决定了启动之后的第一条地址。
CS = F000H,EIP = 0000FFF0H
实际地址是:Base + EIP = FFFF0000H + 0000FFF0H = FFFFFFF0H,这是 BIOS 的 EPROM(Erasable Programmable Read Only Memory) 所在地。
通常第一条指令是一条长跳转指令(这样 CS 和 EIP 都会更新),跳转到 BIOS 代码中进行初始化工作。
处于实模式的段
在实模式中,16 位的段寄存器在左移 4 位之后叠加上 16 位的 Offset,形成 20 位的地址,这就是在实模式下的寻址方式,具体如下图所示。
- 段选择子(Segment Selector):CS、DS、SS ...
- 偏移量(Offset):EIP
# 1.2 从 BIOS 到 Bootloader
BIOS 加载存储设备(比如软盘、硬盘、光盘、USB 盘)上的第一个扇区(主引导扇区,Master Boot,or MBR)的 512 字节到内存的 0x7c00,然后跳转到 0x7c00 的第一条指令开始执行。
Q: 为什么 BIOS 不直接加载操作系统?
A: 这取决于 BIOS 的能力问题,因为刚开始设计的时候,它完成的功能就只是加载一个扇区,而一个 OS 的代码容量是大于 512 个字节的,如果依靠 BIOS 来加载一个复杂的很大容量的 OS 的话会增加 BIOS 的工作难度,因此 BIOS 只负责加载一个扇区,而这个扇区里面的代码会完成后续的加载工作,这个扇区我们称之为 Bootloader。
# 1.3 从 Bootloader 到 OS
- 使能保护模式(protection mode)& 段机制(segment-level protection),为后续操作系统的执行做准备;
- 从硬盘上读取 ELF 格式的 uCore kernel 代码(跟在 MBR 后面的扇区)并放到内存中固定位置;
- 跳转到 uCore OS 的入口点(entry point)执行,这时控制权到了 uCore OS 中。
下面对上述过程的细节进行说明。
使能保护模式 & 段机制
由下图可知,这里段寄存器起了一个指针的作用,用来指向段描述符,在段描述符中描述了一个段的起始地址和它的大小。这样我们可以根据 CS 里面 Index 的值来找到 uCore 代码段的起始地址和大小,同理,数据段也可以用一些其他特定的段寄存器,比如 ES、DS 等等来表述,堆栈段也可以用 SS 来表述。
但是,由于后面还存在页机制,所以段机制这部分的映射关系就尽量简单,具体做法是让段的大小是 4G,段的起始地址从零开始,这也意味着它的空间顶满 4G,从而将分段功能弱化。
这也意味着想通过段机制来实现代码段、数据段等的分割在 uCore 中是做不到的,这里面也有一个原因是在于,这份功能的实现和后续页机制的实现在功能上有一定的重叠,相对而言,我们更倾向于用页机制来完成分段,这是 Lab 2 中涉及到的内容。
但是段模式不能取消,因为在 x86 中,只要开启了保护模式,段模式就会自动 Enable,而且页机制还是建立在段机制的基础上实现的。因此,虽然它完成的功能是一个近似于对等的映射关系,但我们还是要把这个关系建立好。
在一个段寄存器里面,会保存一块区域叫做段选择子,它的值就代表 Index,一个 Index 就对应段描述符表里面的一项,即段描述符,通过段描述符中的起始地址加上 Offset(EIP)就得到了线性地址。由于还没有启用页机制,此时的线性地址就等同于物理地址,具体如下图所示。
那么要把段机制建立好,一个很重要的一点就是需要有一个数组来存储各个段描述符,我们称之为全局描述符表,简称 GDT,也叫段表,GDT 是由 Bootloader 建立的。
Bootloader 会描述好 GDT 的一个大致的空间,给出它的位置和大小,然后通过一个特殊的指令(如 lgdt
),就可以让 CPU 找到 GDT 的起始地址,然后通过内部的 GDTR 这个寄存器保存该地址信息,使得 CS、DS、SS 等可以和 GDT 建立对应关系。
下图是 GDT 中段描述符的一个详细表示,其中我们最关注的有两项:
- 基址(BASE)在什么地方
- 段的长度(LIMIT)是多大
上文中讲到,在 uCore 中把这个功能弱化了:基址全部是 0,段的长度都是 4G。
接下来看段寄存器要如何产生 Index(GDT 中的一个索引值):段寄存器一共有 16 位,其中高 13 位就是 GDT 的 Index,低 2 位表示当前段的特权级别(一般操作系统的特权级别是 0,应用程序的特权级别是 3),TI 用来标记是否是本地描述符表,在 uCore 中没有使用,值为 0,具体如下图所示。
经过上面的过程,映射关系就建立好了,最后还需要一步使能操作,使能保护模式就是将控制寄存器 CR0 的第 0 位(PE,见下图)置为 1,而段机制在保护模式下是自动使能的。
加载 ELF 格式的 uCore OS kernel
uCore OS 编译之后会生成一个 ELF 格式的执行程序,Bootloader 需要通过解析这个文件把 uCore 相应的代码、数据放到内存中相应地址。
ELF 里面有一个头,叫做 elfhdr(ELF Header),可以通过 elfhdr 中的 e_phoff
和 e_phnum
来进一步查找 proghdr(Program Header)这个结构,通过 proghdr 中的 va
、memsz
和 offset
就可以让内存中的一段区域用于存放 uCore 的代码段或数据段。
ELF Header 和 Program Header 的定义代码如下所示。
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12];
uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image
uint16_t e_machine; // 3=x86, 4=68K, etc.
uint32_t e_version; // file version, always 1
uint32_t e_entry; // entry point if executable
uint32_t e_phoff; // file position of program header or 0
uint32_t e_shoff; // file position of section header or 0
uint32_t e_flags; // architecture-specific flags, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // size of an entry in program header
uint16_t e_phnum; // number of entries in program header or 0
uint16_t e_shentsize; // size of an entry in section header
uint16_t e_shnum; // number of entries in section header or 0
uint16_t e_shstrndx; // section number that contains section name strings
};
/* program section header */
struct proghdr {
uint32_t p_type; // loadable code or data, dynamic linking info,etc.
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
uint32_t p_pa; // physical address, not used
uint32_t p_filesz; // size of segment in file
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
uint32_t p_flags; // read/write/execute bits
uint32_t p_align; // required alignment, invariably hardware page size
};
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
# 二、GCC 内联汇编
# 2.1 什么是内联汇编?
- 这是 GCC 对 C 语言的扩展;
- 可直接在 C 语句中插入汇编指令。
# 2.2 有何用处?
- 调用 C 语言不支持的指令;
- 用汇编在 C 语言中手动优化。
# 2.3 如何工作?
- 用给定的模板和约束来生成汇编指令;
- 在 C 函数内形成汇编代码。
Syntax
asm (assembler template
:output operands (optional)
:input operands (optional)
:clobbers (optional)
)
2
3
4
5
Example 1
汇编代码:
movl $0xffff, %eax
内联汇编:
asm ("movl $0xffff, %%eax\n")
Example 2
功能:将 CR0 寄存器的指定位置为 1。
内联汇编:
uint32_t cr0;
asm volatile ("movl %%cr0, %0\n" :"=r"(cr0));
cr0 |= 0x80000000;
asm volatile ("movl %0, %%cr0\n" ::"r"(cr0));
2
3
4
volatile
:不需要做进一步的优化,调整顺序;%0
:第一个用到的寄存器;r
:任意寄存器。
汇编代码:
movl %cr0, %ebx
movl %ebx, 12(%esp)
orl $-2147483648, 12(%esp)
movl 12(%esp), %eax
movl %eax, %cr0
2
3
4
5
# 三、x86 中断处理过程
每个中断或异常都会与一个中断服务例程(Interrupt Service Routine,简称 ISR)关联,其关联关系存储在中断描述符表(Interrupt Descriptor Table,简称 IDT)中。
在 x86 环境中有一系列硬件机制来支持上述关系的建立,具体如下图所示。
IDT 的起始地址和大小保存在中断描述符表寄存器 IDTR 中。
IDT 和 GDT 很类似,只是它是专门用来描述中断的,里面的每一项我们称之为中断门或者陷阱门,具体格式如下图所示,其中最主要的两部分就是段选择子(Segment Selector)和偏移量(Offset),依此我们就可以知道中断服务例程的起始地址。
下面来梳理一下整个过程(见下图):
- 产生了一个中断之后,我们可以知道它的中断号,CPU 会根据中断号来查找 IDT,找到相应的 中断门 或者 陷阱门;
- 然后从中断门或者陷阱门中取出段选择子,依此来查找 GDT,找到相应的 段描述符;
- 最后通过段描述符中的基地址和中断门或者陷阱门中的偏移量,相加获得对应的线性地址,指向中断服务例程。
# 四、练习
# 4.1 练习一
理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
首先来看一下 Makefile 中关于生成 ucore.img 的代码:
# create ucore.img
# UCOREIMG = bin/ucore.img
UCOREIMG := $(call totarget,ucore.img)
# {1}: {2} 中,{1} 是目标,{2} 是前置条件
# bin/ucore.img 需要依赖 bin/kernel 和 bin/bootblock
$(UCOREIMG): $(kernel) $(bootblock)
# 为 bin/ucore.img 分配 10000 个 block 的内存空间,并全部初始化为 0。
# 由于未指定 block 的大小,因此为默认值 512 字节,则总大小为 5000M,约 5G。
$(V)dd if=/dev/zero of=$@ count=10000
# 将 bin/bootblock 复制到 bin/ucore.img
$(V)dd if=$(bootblock) of=$@ conv=notrunc
# 将 bin/kernel 复制到 bin/ucore.img
# seek=1 表示复制时跳过 bin/ucore.img 的第一个 block(bootblock 的内容)
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
# 由于只有一个参数,这里直接返回
$(call create_target,ucore.img)
# -------------------- tools function begin --------------------
# 作用:添加 `bin/` 前缀
totarget = $(addprefix $(BINDIR)$(SLASH),$(1))
# add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
$(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef
# add packets and objs to target (target, #packes, #objs, cc, [, flags])
create_target = $(eval $(call do_create_target,$(1),$(2),$(3),$(4),$(5)))
# -------------------- tools function end --------------------
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
下面来分析 ucore.img 所依赖的 kernel 和 bootblock 的 Makefile 代码,首先来看 kernel 的代码:
# include kernel/user
# 引入 libs 目录下的文件
INCLUDE += libs/
# 为 include 内容增加前缀
CFLAGS += $(addprefix -I,$(INCLUDE))
# 将 libs 追加到变量
LIBDIR += libs
# 设置 obj 文件名:__objs_libs = obj/libs/**/*.o
$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)
# --------------------------------------------------------------
# $(1)目录下,满足`%.c %.S`的文件
listf_cc = $(call listf,$(1),$(CTYPE))
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
# -------------------- tools function begin --------------------
# list all files in some directories: (#directories, #types)
# $(2) = c S
# $(addprefix %.,$(2)) = %.c %.S
# $(if $(2),%.c %.S,%) = %.c %.S
# listf = $(filter %.c %.S, $(wildcard $(1)/*))
# $(1) is empty, so the listf is empty
# `wildcard`找到满足pattern的所有文件列表。
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
$(wildcard $(addsuffix $(SLASH)*,$(1))))
# add files to packet: (#files, cc[, flags, packet, dir])
add_files = $(eval $(call do_add_files_to_packet,$(1),$(2),$(3),$(4),$(5)))
# add files to packet: (#files, cc[, flags, packet, dir])
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
# -------------------- tools function end --------------------
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
# kernel
# 引入 kern 目录下的内核文件
KINCLUDE += kern/debug/ \
kern/driver/ \
kern/trap/ \
kern/mm/
KSRCDIR += kern/init \
kern/libs \
kern/debug \
kern/driver \
kern/trap \
kern/mm
KCFLAGS += $(addprefix -I,$(KINCLUDE))
# 设置 obj 文件名:__objs_kernel = obj/kern/**/*.o
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
# 指出 kernel 目标文件依赖的 obj 文件
# 最终效果为 KOBJS = obj/libs/*.o obj/kern/**/*.o
KOBJS = $(call read_packet,kernel libs)
# create kernel target
kernel = $(call totarget,kernel)
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
# 打印:`+ ld bin/kernel`
@echo + ld $@
# 链接所有生成的 obj 文件得到 kernel 文件
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
# 使用 objdump 工具对 kernel 目标文件反汇编,以便后续调试
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
# 使用 objdump 工具来解析 kernel 目标文件得到符号表
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
# 直接返回
$(call create_target,kernel)
# -------------------- tools function begin --------------------
read_packet = $(foreach p,$(call packetname,$(1)),$($(p)))
# change $(name) to $(OBJPREFIX)$(name): (#names)
# 结果为:$(OBJPREFIX)$(1)
packetname = $(if $(1),$(addprefix $(OBJPREFIX),$(1)),$(OBJPREFIX))
OBJPREFIX := __objs_
# add packets and objs to target (target, #packes, #objs, cc, [, flags])
create_target = $(eval $(call do_create_target,$(1),$(2),$(3),$(4),$(5)))
# add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
$(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef
# -------------------- tools function end --------------------
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
接下来继续分析另一个重要的依赖文件 bootblock:
# create bootblock
# bootfiles = boot/*.c boot/*.S
bootfiles = $(call listf_cc,boot)
# 编译 bootfiles 生成 .o 文件
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
# bootblock = bin/bootblock
bootblock = $(call totarget,bootblock)
# 声明 bin/bootblock 依赖于 obj/boot/*.o 和 bin/sign 文件
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
# 打印:`+ ld bin/bootblock`
@echo + ld $@
# 链接所有 .o 文件以生成 obj/bootblock.o
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
# 反汇编 obj/bootblock.o 文件得到 obj/bootblock.asm 文件
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
# 使用 objcopy 将 obj/bootblock.o 转换生成 obj/bootblock.out 文件
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
# 使用 bin/sign 工具将 obj/bootblock.out 转换生成 bin/bootblock 目标文件
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
# 直接返回
$(call create_target,bootblock)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后来分析生成 bootblock 过程中所用到的 sign 工具:
# create 'sign' tools
# __objs_sign = obj/sign/tools/sign.o
$(call add_files_host,tools/sign.c,sign,sign)
# 生成 obj/sign/tools/sign.o
$(call create_target_host,sign,sign)
# --------------------------------------------------------------
add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))
create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))
# -------------------- tools function begin --------------------
# add files to packet: (#files, cc[, flags, packet, dir])
add_files = $(eval $(call do_add_files_to_packet,$(1),$(2),$(3),$(4),$(5)))
# add files to packet: (#files, cc[, flags, packet, dir])
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
# add packets and objs to target (target, #packes, #objs, cc, [, flags])
create_target = $(eval $(call do_create_target,$(1),$(2),$(3),$(4),$(5)))
# add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
$(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef
# -------------------- tools function end --------------------
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
总结下 ucore.img 的生成过程:
- 编译 libs 和 kern 目录下所有的 .c 和 .S 文件,生成 .o 文件,并链接得到 bin/kernel 文件;
- 编译 boot 目录下所有的 .c 和 .S 文件,生成 .o 文件,并链接得到 bin/bootblock.out 文件;
- 编译 tools/sign.c 文件,得到 bin/sign 文件;
- 利用 bin/sign 工具将 bin/bootblock.out 文件转化为 512 字节的 bin/bootblock 文件,并将 bin/bootblock 的最后两个字节设置为 0x55 和 0xAA;
- 最后为 bin/ucore.img 分配 5000MB 的内存空间,并将 bin/bootblock 复制到 bin/ucore.img 的第一个 block,紧接着将 bin/kernel 复制到 bin/ucore.img 第二个 block 开始的位置。
最后通过 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
+ 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
+ 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
+ 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: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
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
2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
根据下面 tools/sign.c 的代码可知,一个被系统认为是符合规范的硬盘主引导扇区具有以下两个特征:
- 大小为 512 字节;
- 最后两个字节为 0x55 和 0xAA。
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, "Usage: <input filename> <output filename>\n");
return -1;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
return -1;
}
printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
if (st.st_size > 510) {
fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
return -1;
}
char buf[512];
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], "rb");
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
return -1;
}
fclose(ifp);
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
return -1;
}
fclose(ofp);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
return 0;
}
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
# 4.2 练习二
使用 qemu 执行并调试 lab1 中的软件。(要求在报告中简要写出练习过程)
为了熟悉使用 qemu 和 gdb 进行的调试工作,我们进行如下的小练习:
- 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
- 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
- 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。
1. 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
将 tools/gdbinit 中内容修改为:
set architecture i8086
target remote :1234
x /2i $pc
2
3
运行 make debug
后,可在 gdb 界面看到加电后第一条指令位置为 0xfff0,对应的指令如下:
=> 0xfff0: add %al,(%bx,%si)
0xfff0: add %al,(%bx,%si)
2
后续可通过 si 命令单步跟踪 BIOS 执行,并查看对应命令。
2. 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
在原 Makefile 文件中增加如下部分:
lab1-mon: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -monitor stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -x tools/lab1init"
2
3
4
并新建 tools/lab1init 文件,内容如下:
file bin/kernel // 调试 bin/kernel
target remote:1234 // 连接 qemu
set architecture i8086 // 设置当前 CPU 为 8086,对应 16 位实模式
b *0x7c00 // 在 0x7c00 处设置断点
continue // 继续执行
x /2i $pc // 显示当前 eip 处的汇编指令
2
3
4
5
6
运行 make lab1-mon
即可看到断点正常,输出如下:
Breakpoint 1, 0x00007c00 in ?? ()
=> 0x7c00: cli
0x7c01: cld
2
3
3. 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。
在问题 2 中已在 lab1-mon 中添加相关参数,打开 q.log 即可查到从 0x7c00 开始的汇编指令。可以看到,反汇编的代码与 bootblock.asm 基本相同,而与 bootasm.S 的差别在于:
- 反汇编的代码中的指令不带指示长度的后缀,而 bootasm.S 的指令则有,比如:反汇编的代码是
xor %eax, %eax
,而 bootasm.S 的代码为xorw %ax, %ax
; - 反汇编的代码中的通用寄存器是 32 位(带有 e 前缀),而 bootasm.S 的代码中的通用寄存器是 16 位(不带 e 前缀)。
# 4.3 练习三
BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 Bootloader。
请分析 Bootloader 是如何完成从实模式进入保护模式的。
提示:需要阅读小节 “保护模式和分段机制” 和 lab1/boot/bootasm.S 源码,了解如何从实模式切换到保护模式,需要了解:
- 为何开启 A20,以及如何开启 A20;
- 如何初始化 GDT 表;
- 如何使能和进入保护模式。
boot/bootasm.S 的完整代码如下:
点击查看代码
#include <asm.h>
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
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
75
76
77
78
79
80
81
82
83
84
85
86
下面结合代码针对上述每个问题进行详细说明。
1. 为何开启 A20,以及如何开启 A20
一开始时 A20 地址线控制是被屏蔽的(总为 0),直到系统软件通过一定的 IO 操作去打开它(参看 boot/bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用 32 位地址线,如果 A20 恒等于 0,那么系统只能访问奇数兆的内存,即只能访问 0-1M、2-3M、4-5M ...,这样无法有效访问所有可用内存。所以在保护模式下,这个开关也必须打开。
开启 A20 的具体步骤大致如下:
- 等待 8042 Input buffer 为空;
- 发送 Write 8042 Output Port (P2) 命令到 8042 Input buffer;
- 等待 8042 Input buffer 为空;
- 将 8042 Output Port (P2) 对应字节的第 2 位置为 1,然后写入 8042 Input buffer。
对应 boot/bootasm.S 的代码:涉及到 seta20.1 和 seta20.2 两部分,其中 seta20.1 是往端口 0x64 写数据 0xd1,告诉 CPU 我要往 8042 芯片的 P2 端口写数据;seta20.2 是往端口 0x60 写数据 0xdf,从而将 8042 芯片的 P2 端口设置为 1。两段代码都需要先读 0x64 端口的第 2 位,确保输入缓冲区为空后再进行后续写操作。
2. 如何初始化 GDT 表
boot/bootasm.S 中的 lgdt gdtdesc
把全局描述符表的大小和起始地址加载到全局描述符表寄存器 GDTR 中。
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt
2
3
下面的代码给出了全局描述符表的具体内容。共有 3 项,每项 8 字节。第 1 项是空白项,内容为全 0。后面 2 项分别是代码段和数据段的描述符,它们的 base 都设置为 0,limit 都设置为 0xffffff,也就是长度均为 4G。代码段设置了可读和可执行权限,数据段设置了可写权限。
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
2
3
4
SEG_NULLASM
和 SEG_ASM
的定义如下:
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
2
3
4
5
6
7
8
3. 如何使能和进入保护模式
将 cr0 寄存器的 PE 位(cr0 寄存器的最低位)设置为 1,便使能和进入保护模式了。代码如下所示:
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
2
3
# 4.4 练习四
分析 Bootloader 加载 ELF 格式的 OS 的过程。(要求在报告中写出分析)
通过阅读 bootmain.c,了解 Bootloader 如何加载 ELF 文件。通过分析源代码和通过 qemu 来运行并调试 Bootloader & OS,理解:
- Bootloader 是如何读取硬盘扇区的?
- Bootloader 是如何加载 ELF 格式的 OS 的?
1. Bootloader 是如何读取硬盘扇区的?
阅读材料给出了读一个扇区的大致流程:
- 等待磁盘准备好;
- 发出读取扇区的命令;
- 发出命令后,再次等待磁盘准备好;
- 把磁盘扇区数据读到指定内存。
实际操作中,需要知道怎样与硬盘交互。阅读材料中同样给出了答案:所有的 IO 操作是通过 CPU 访问硬盘的 IO 地址寄存器完成。一个硬盘有 8 个 IO 地址寄存器,其中第 1 个存储数据,第 8 个存储状态和命令,第 3 个存储要读写的扇区数,第 4~7 个存储要读写的起始扇区的编号(共 28 位)。
Bootloader 读取扇区的功能是在 boot/bootmain.c 的 readsect 函数中实现的,具体代码如下:
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// 1. 等待磁盘准备好
// wait for disk to be ready
waitdisk();
// 2. 发出读取扇区的命令
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// 3. 发出命令后,再次等待磁盘准备好
// wait for disk to be ready
waitdisk();
// 4. 把磁盘扇区数据读到指定内存
// insl 是以 dword 即 4 字节为单位读取的,因此这里 SECTIZE 需要除以 4
// read a sector
insl(0x1F0, dst, SECTSIZE / 4);
}
/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
// 不断查询读 0x1F7 寄存器的最高两位
// 直到最高位为 0、次高位为 1(这个状态应该意味着磁盘空闲)才返回
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
static inline void
insl(uint32_t port, void *addr, int cnt) {
asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
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
2. Bootloader 是如何加载 ELF 格式的 OS 的?
Bootloader 加载 OS 的功能是在 bootmain.c 的 bootmain 函数中实现的,具体代码如下:
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// 读取 ELF 头
// read the 1st page off disk
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 判断是否为合法 ELF 文件
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// 依据 ELF 头设置相关程序头指针,依次读取段数据至内存
// load each program segment (ignores ph flags)
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);
}
// ELF 格式的 OS 加载完成,根据 ELF 头的相关信息,转入 kernel 入口
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
/* *
* 调用读取单扇区函数,实现读取段数据函数
* readseg - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
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
# 4.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
<unknow>: -- 0x00007d72 –
……
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。
修改 kdebug.c 中的 print_stackframe 函数代码如下:
void
print_stackframe(void) {
/* LAB1 YOUR CODE : STEP 1 */
/* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
* (2) call read_eip() to get the value of eip. the type is (uint32_t);
* (3) from 0 .. STACKFRAME_DEPTH
* (3.1) printf value of ebp, eip
* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
* (3.3) cprintf("\n");
* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
* (3.5) popup a calling stackframe
* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]
* the calling funciton's ebp = ss:[ebp]
*/
// 首先定义两个局部变量,存放 ebp 和 eip 寄存器的值
uint32_t ebp = read_ebp();
uint32_t eip = read_eip();
int i, j;
// 回溯调用栈,同时不超过最大栈深度
for(i = 0; i < STACKFRAME_DEPTH && ebp != 0; i++) {
// 打印 ebp 和 eip 的值
cprintf("ebp:0x%08x eip:0x%08x", ebp, eip);
// 打印从 ebp+2[0..4]
uint32_t *arg = (uint32_t *)ebp + 2;
cprintf(" arg:");
for(j = 0; j < 4; j++) {
cprintf("0x%08x ", arg[j]);
}
cprintf("\n");
// 由于变量 eip 存放的是下一条指令的地址,因此将变量 eip 的值减去 1,
// 得到的指令地址就属于当前指令的范围了.
// 由于只要输入的地址属于当前指令的起始和结束位置之间,
// print_debuginfo 都能搜索到当前指令,因此这里减去 1 即可。
print_debuginfo(eip - 1);
// 以后变量 eip 的值就不能再调用 read_eip 来获取了(每次调用获取的值都是相同的)
// 而应该从 ebp 寄存器指向栈中的位置再往上一个单位中获取。
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
}
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
执行 make qemu 命令后对应输出如下:
ebp:0x00007b28 eip:0x001009b6 args:0x00010094 0x00010094 0x00007b58 0x00100097
kern/debug/kdebug.c:305: print_stackframe+21
ebp:0x00007b38 eip:0x00100ca5 args:0x00000000 0x00000000 0x00000000 0x00007ba8
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b58 eip:0x00100097 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b78 eip:0x001000c0 args:0x00000000 0xffff0000 0x00007ba4 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b98 eip:0x001000de args:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007bb8 eip:0x00100103 args:0x0010359c 0x00103580 0x0000136a 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007be8 eip:0x00100055 args:0x00000000 0x00000000 0x00000000 0x00007c4f
kern/init/init.c:28: kern_init+84
ebp:0x00007bf8 eip:0x00007d64 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d63 --
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与实验要求基本一致,其中最后一行为:
ebp:0x00007bf8 eip:0x00007d64 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
共有 ebp、eip 和 args 三类参数,下面分别给出解释:
ebp:0x00007bf8
:此时 ebp 的值是 kern_init 函数的栈顶地址,从 obj/bootblock.asm 文件中知道整个栈的栈顶地址为 0x00007c00,ebp 指向的栈位置存放调用者的 ebp 寄存器的值,ebp + 4 指向的栈位置存放返回地址的值,这意味着 kern_init 函数的调用者(也就是 bootmain 函数)没有传递任何输入参数给它!因为单是存放旧的 ebp、返回地址已经占用 8 字节了。eip:0x00007d64
:eip 的值是 kern_init 函数的返回地址,也就是 bootmain 函数调用 kern_init 对应的指令的下一条指令的地址。这与 obj/bootblock.asm 是相符合的。args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
:一般来说,args 存放的 4 个 dword 是对应 4 个输入参数的值。但这里比较特殊,由于 bootmain 函数调用 kern_init 并没传递任何输入参数,并且栈顶的位置恰好在 Bootloader 第一条指令存放的地址的上面,而 args 恰好是 kern_int 的 ebp 寄存器指向的栈顶往上第 2~5 个单元,因此 args 存放的就是 Bootloader 指令的前 16 个字节!可以对比 obj/bootblock.asm 文件来验证(验证时要注意系统是小端字节序)。
# 4.6 练习六
完善中断初始化和处理(需要编程)
1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断描述符表一个表项占 8 个字节,如下图(注意是两行),分别对应 IDT 表中的中断门和陷阱门描述符。
其中第 16~31 位是段选择子,用于索引全局描述符表 GDT 来获取中断处理代码对应的段地址,再加上第 0~15、48~63 位构成的偏移地址,即可得到中断处理代码的入口。
2. 请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init。在 idt_init 函数中,依次对所有中断入口进行初始化。使用 mmu.h 中的 SETGATE 宏,填充 idt 数组内容。每个中断的入口由 tools/vectors.c 生成,使用 trap.c 中声明的 vectors 数组即可。
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
// 取得 vecotrs 数组
extern uintptr_t __vectors[];
int i;
// 设置 IDT 表
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
// 中断描述符,设置为 interrupt gate,段选择子,偏移量,内核态
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
// 系统调用:设置为 trap gate,用户态
// set for switch from user to kernel
SETGATE(idt[T_SWITCH_TOK], 1, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
// 使用 lidt 命令将 IDT 表的起始地址加载到 IDTR 寄存器中
// load the IDT
lidt(&idt_pd);
}
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
3. 请编程完善 trap.c 中的中断处理函数 trap,在对时钟中断进行处理的部分填写 trap 函数中处理时钟中断的部分,使操作系统每遇到 100 次时钟中断后,调用 print_ticks 子程序,向屏幕上打印一行文字 “100 ticks”。
按照题目要求完成即可,当遇到中断号为 IRQ_OFFSET + IRQ_TIMER
时自增 tricks,同时每 100 次调用一下 print_ticks 方法。
/* trap_dispatch - dispatch based on what type of trap occurred */
static void
trap_dispatch(struct trapframe *tf) {
char c;
switch (tf->tf_trapno) {
case IRQ_OFFSET + IRQ_TIMER:
/* LAB1 YOUR CODE : STEP 3 */
/* handle the timer interrupt */
/* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
* (3) Too Simple? Yes, I think so!
*/
ticks++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
case IRQ_OFFSET + IRQ_COM1:
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
//LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
// set eflags, make sure ucore can use io under user mode.
// if CPL > IOPL, then cpu will generate a general protection.
switchk2u.tf_eflags |= FL_IOPL_MASK;
// set temporary stack
// then iret will jump to the right stack
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}
break;
case IRQ_OFFSET + IRQ_IDE1:
case IRQ_OFFSET + IRQ_IDE2:
/* do nothing */
break;
default:
// in kernel, it must be a mistake
if ((tf->tf_cs & 3) == 0) {
print_trapframe(tf);
panic("unexpected trap in kernel.\n");
}
}
}
/* *
* trap - handles or dispatches an exception/interrupt. if and when trap() returns,
* the code in kern/trap/trapentry.S restores the old CPU state saved in the
* trapframe and then uses the iret instruction to return from the exception.
* */
void
trap(struct trapframe *tf) {
// dispatch based on what type of trap occurred
trap_dispatch(tf);
}
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
75
76
# 五、参考资料
- 操作系统(RISC-V) - 清华大学 (opens new window)
- uCore OS 实验指导书 (opens new window)
- IA-32 Intel® Architecture Software Developer’s Manual (opens new window)
- 《ucore lab1 exercise1》实验报告 (opens new window)
- 《ucore lab1 exercise3》实验报告 (opens new window)
- 《ucore lab1 exercise4》实验报告 (opens new window)
- 《ucore lab1 exercise5》实验报告 (opens new window)
- 《ucore lab1 exercise6》实验报告 (opens new window)
- lab1 练习6 中断向量表的初始化 (opens new window)
- 《“操作系统实验:基于 uCore OS”实验报告(lab1)》yaocanweiyao (opens new window)
- 《“操作系统实验:基于 uCore OS”实验报告(lab1)》lanqiao7713340614 (opens new window)
- 函数堆栈 (opens new window)