实验一:系统启动及中断

2022-12-03 操作系统 uCore

# 一、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

  1. 使能保护模式(protection mode)& 段机制(segment-level protection),为后续操作系统的执行做准备;
  2. 从硬盘上读取 ELF 格式的 uCore kernel 代码(跟在 MBR 后面的扇区)并放到内存中固定位置;
  3. 跳转到 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 中段描述符的一个详细表示,其中我们最关注的有两项:

  1. 基址(BASE)在什么地方
  2. 段的长度(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_phoffe_phnum 来进一步查找 proghdr(Program Header)这个结构,通过 proghdr 中的 vamemszoffset 就可以让内存中的一段区域用于存放 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
};
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

# 二、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)
)
1
2
3
4
5

Example 1

汇编代码:

movl $0xffff, %eax
1

内联汇编:

asm ("movl $0xffff, %%eax\n")
1

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));
1
2
3
4
  • volatile:不需要做进一步的优化,调整顺序;
  • %0:第一个用到的寄存器;
  • r:任意寄存器。

汇编代码:

movl %cr0, %ebx
movl %ebx, 12(%esp)
orl $-2147483648, 12(%esp)
movl 12(%esp), %eax
movl %eax, %cr0
1
2
3
4
5

# 三、x86 中断处理过程

每个中断或异常都会与一个中断服务例程(Interrupt Service Routine,简称 ISR)关联,其关联关系存储在中断描述符表(Interrupt Descriptor Table,简称 IDT)中。

在 x86 环境中有一系列硬件机制来支持上述关系的建立,具体如下图所示。

IDT 的起始地址和大小保存在中断描述符表寄存器 IDTR 中。

IDT 和 GDT 很类似,只是它是专门用来描述中断的,里面的每一项我们称之为中断门或者陷阱门,具体格式如下图所示,其中最主要的两部分就是段选择子(Segment Selector)和偏移量(Offset),依此我们就可以知道中断服务例程的起始地址。

下面来梳理一下整个过程(见下图):

  1. 产生了一个中断之后,我们可以知道它的中断号,CPU 会根据中断号来查找 IDT,找到相应的 中断门 或者 陷阱门
  2. 然后从中断门或者陷阱门中取出段选择子,依此来查找 GDT,找到相应的 段描述符
  3. 最后通过段描述符中的基地址和中断门或者陷阱门中的偏移量,相加获得对应的线性地址,指向中断服务例程。

# 四、练习

# 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  --------------------
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

下面来分析 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  --------------------
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
# 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  --------------------
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

接下来继续分析另一个重要的依赖文件 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)
1
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  --------------------
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

总结下 ucore.img 的生成过程:

  1. 编译 libs 和 kern 目录下所有的 .c 和 .S 文件,生成 .o 文件,并链接得到 bin/kernel 文件;
  2. 编译 boot 目录下所有的 .c 和 .S 文件,生成 .o 文件,并链接得到 bin/bootblock.out 文件;
  3. 编译 tools/sign.c 文件,得到 bin/sign 文件;
  4. 利用 bin/sign 工具将 bin/bootblock.out 文件转化为 512 字节的 bin/bootblock 文件,并将 bin/bootblock 的最后两个字节设置为 0x55 和 0xAA;
  5. 最后为 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
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

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

根据下面 tools/sign.c 的代码可知,一个被系统认为是符合规范的硬盘主引导扇区具有以下两个特征:

  1. 大小为 512 字节;
  2. 最后两个字节为 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;
}
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

# 4.2 练习二

使用 qemu 执行并调试 lab1 中的软件。(要求在报告中简要写出练习过程)

为了熟悉使用 qemu 和 gdb 进行的调试工作,我们进行如下的小练习:

  1. 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
  2. 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
  3. 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。

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

将 tools/gdbinit 中内容修改为:

set architecture i8086
target remote :1234
x /2i $pc
1
2
3

运行 make debug 后,可在 gdb 界面看到加电后第一条指令位置为 0xfff0,对应的指令如下:

=> 0xfff0:  add %al,(%bx,%si)
   0xfff0:  add %al,(%bx,%si)
1
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"
1
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 处的汇编指令
1
2
3
4
5
6

运行 make lab1-mon 即可看到断点正常,输出如下:

Breakpoint 1, 0x00007c00 in ?? ()
=> 0x7c00:    cli    
   0x7c01:    cld
1
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
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
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 的具体步骤大致如下:

  1. 等待 8042 Input buffer 为空;
  2. 发送 Write 8042 Output Port (P2) 命令到 8042 Input buffer;
  3. 等待 8042 Input buffer 为空;
  4. 将 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 
1
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
1
2
3
4

SEG_NULLASMSEG_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)
1
2
3
4
5
6
7
8

3. 如何使能和进入保护模式

将 cr0 寄存器的 PE 位(cr0 寄存器的最低位)设置为 1,便使能和进入保护模式了。代码如下所示:

    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0
1
2
3

# 4.4 练习四

分析 Bootloader 加载 ELF 格式的 OS 的过程。(要求在报告中写出分析)

通过阅读 bootmain.c,了解 Bootloader 如何加载 ELF 文件。通过分析源代码和通过 qemu 来运行并调试 Bootloader & OS,理解:

  1. Bootloader 是如何读取硬盘扇区的?
  2. Bootloader 是如何加载 ELF 格式的 OS 的?

1. Bootloader 是如何读取硬盘扇区的?

阅读材料给出了读一个扇区的大致流程:

  1. 等待磁盘准备好;
  2. 发出读取扇区的命令;
  3. 发出命令后,再次等待磁盘准备好;
  4. 把磁盘扇区数据读到指定内存。

实际操作中,需要知道怎样与硬盘交互。阅读材料中同样给出了答案:所有的 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");
}
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

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);
    }
}
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

# 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 –
……
1
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];
	}
}
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

执行 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 --
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

与实验要求基本一致,其中最后一行为:

ebp:0x00007bf8 eip:0x00007d64 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 
1

共有 ebp、eip 和 args 三类参数,下面分别给出解释:

  1. ebp:0x00007bf8:此时 ebp 的值是 kern_init 函数的栈顶地址,从 obj/bootblock.asm 文件中知道整个栈的栈顶地址为 0x00007c00,ebp 指向的栈位置存放调用者的 ebp 寄存器的值,ebp + 4 指向的栈位置存放返回地址的值,这意味着 kern_init 函数的调用者(也就是 bootmain 函数)没有传递任何输入参数给它!因为单是存放旧的 ebp、返回地址已经占用 8 字节了。
  2. eip:0x00007d64:eip 的值是 kern_init 函数的返回地址,也就是 bootmain 函数调用 kern_init 对应的指令的下一条指令的地址。这与 obj/bootblock.asm 是相符合的。
  3. 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);
}
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

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);
}
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
75
76

# 五、参考资料

Last Updated: 2023-01-28 4:31:25