操作系统ucore lab1

操作系统ucore lab1

core是一个清华大学出品的教学用操作系统。其以MIT的xv6&jos,harvard的os161,和linux为蓝本。它循序渐进,适合操作系统的初学者,当然需要对c语言,计算机原理有一定的了解。

在开始做题之前,还需要一些准备工作:
由于整个lab依托于gccqemu,所以只能在linux环境下进行。而且在开始之前,还需要先安装gccqemu

练习一

理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
在此练习中,大家需要通过阅读代码来了解:

  1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

1、生成ucore.img需要kernelbootblock

$(UCOREIMG): $(kernel) $(bootblock)
( V ) d d i f = / d e v / z e r o o f = (V)dd if=/dev/zero of= (V)ddif=/dev/zeroof=@ count=10000
( V ) d d i f = (V)dd if= (V)ddif=(bootblock) of=$@ conv=notrun
( V ) d d i f = (V)dd if= (V)ddif=(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)123456

首先先创建一个大小为10000字节的块儿,然后再将bootblock拷贝过去。 生成ucore.img需要先生成kernel和bootblock。

2、生成kernel

而生成kernel的代码如下:

$(kernel): tools/kernel.ld
$(kernel): ( K O B J S ) @ e c h o " b b b b b b b b b b b b b b b b b b b b b b (KOBJS) @echo "bbbbbbbbbbbbbbbbbbbbbb (KOBJS)@echo"bbbbbbbbbbbbbbbbbbbbbb(KOBJS)"
@echo + ld $@
( V ) (V) (V)(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ ( K O B J S ) @ (KOBJS) @ (KOBJS)@(OBJDUMP) -S $@ > ( c a l l a s m f i l e , k e r n e l ) @ (call asmfile,kernel) @ (callasmfile,kernel)@(OBJDUMP) -t $@ | KaTeX parse error: Expected group after '^' at position 39: …d; s/ .* / /; /^̲$/d’ >
$(call symfile,kernel)1234567

通过make V=指令得到执行的具体命令如下:

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.o1

然后根据其中可以看到,要生成kernel,需要用GCC编译器将kern目录下所有的.c文件全部编译生成的.o文件的支持。具体如下:

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.o12345678910111213141516

3、生成bootblock

而生成bootblock的代码如下:

$(bootblock): ( c a l l t o o b j , (call toobj, (calltoobj,(bootfiles)) |
( c a l l t o t a r g e t , s i g n ) @ e c h o " = = = = = = = = = = = = = = = = = = = = = = = = (call totarget,sign) @echo "======================== (calltotarget,sign)@echo"========================(call toobj,$(bootfiles))"
@echo + ld $@
( V ) (V) (V)(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o ( c a l l t o o b j , b o o t b l o c k ) @ (call toobj,bootblock) @ (calltoobj,bootblock)@(OBJDUMP) -S $(call objfile,bootblock) > ( c a l l a s m f i l e , b o o t b l o c k ) @ (call asmfile,bootblock) @ (callasmfile,bootblock)@(OBJCOPY) -S -O binary $(call objfile,bootblock) ( c a l l o u t f i l e , b o o t b l o c k ) @ (call outfile,bootblock) @ (calloutfile,bootblock)@(call totarget,sign) $(call outfile,bootblock) $(bootblock)1234567

同样根据make V=指令打印的结果,得到要生成bootblock,首先需要生成bootasm.o、bootmain.o、sign。下列代码为生成bootasm.o、bootmain.o的代码,由宏定义批量实现了。

bootfiles = $(call listf_cc,boot)
( f o r e a c h f , (foreach f, (foreachf,(bootfiles), ( c a l l c c c o m p i l e , (call cc_compile, (callcccompile,(f), ( C C ) , (CC), (CC),(CFLAGS) -Os -nostdinc))12

而实际的命令在make V=指令结果里可以看到。 下述是由bootasm.S生成bootasm.o的具体命令:

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

下述是由bootmain.c生成bootmain.o的具体命令:

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

至于上述命令的具体参数,查阅资料罗列如下:

  • -ggdb 生成可供gdb使用的调试信息

  • -m32 生成适用于32位环境的代码

  • -gstabs 生成stabs格式的调试信息

  • -nostdinc 不使用标准库

  • -fno-stack-protector 不生成用于检测缓冲区溢出的代码

  • -Os 为减小代码大小而进行优化

  • -l 添加搜索头文件的路径

  • -fno-builtin 不进行builtin函数的优化

下列代码为生成sign的代码:

$(call
add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)12

下面是生成sign具体的命令:

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/sign12

有了上述的bootasm.o、bootmain.o、sign,接下来就可以生成bootblock了,实际命令如下:

ld -m
elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o
obj/boot/bootmain.o -o obj/bootblock.o1

参数解释如下:(不重复解释)

  • -m 模拟为i386上的连接器

  • -N 设置代码段和数据段均可读写

  • -e 指定入口

  • -Ttext 制定代码段开始位置

4、硬盘主引导扇区的特征

一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:

  • 磁盘主引导扇区只有512字节 ;

  • 磁盘最后两个字节为0x55AA ;

  • 由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符组成。

练习二

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

首先通过make qemu指令运行出等待调试的qemu虚拟机,然后再打开一个终端,通过下述命令连接到qemu虚拟机:

进入到调试界面:

输入si命令单步调试,

这是另一个终端会打印下一条命令的地址和内容:

然后输入b*0x7c00在初始化位置地址0x7c00设置上断点,如下:

然后输入continue使之继续运行:

这时成功在0x7c00处停止运行,然后我们查看此处的反汇编代码,如下:

对比此时bootasm.S中的起始代码,发现确实是一样的

在这里插入图片描述

练习三

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

首先先来分析一下bootloader:

1、关闭中断,将各个段寄存器重置

它先将各个寄存器置0

cli # Disable
interrupts
cld # String
operations increment
xorw %ax, %ax # Segment number
zero
movw %ax, %ds # -> Data
Segment
movw %ax, %es # -> Extra
Segment
movw %ax, %ss # -> Stack
Segment123456

2、开启A20

然后就是将A20置1,这里简单解释一下A20,当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。

seta20.1:
inb $0x64, %al # 读取状态寄存器,等待8042键盘控制器闲置
testb $0x2, %al # 判断输入缓存是否为空
jnz seta20.1
movb $0xd1, %al # 0xd1表示写输出端口命令,参数随后通过0x60端口写入
outb %al, $0x64
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 通过0x60写入数据11011111 即将A20置1
outb %al, $0x60
123456789101112131415

3、加载GDT表

lgdt gdtdesc1

4、将CR0的第0位置1

movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
1234

5、长跳转到32位代码段,重装CS和EIP

ljmp $PROT_MODE_CSEG, $protcseg1

6、重装DS、ES等段寄存器等

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 Segment123456

7、转到保护模式完成,进入boot主方法

movl $0x0, %ebp
movl $start, %esp
call bootmain123

实模式

在bootloader接手BIOS的工作后,当前的PC系统处于实模式(16位模式)运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。通过修改A20地址线可以完成从实模式到保护模式的转换。有关A20的进一步信息可参考附录“关于A20 Gate”。

保护模式

只有在保护模式下,80386的全部32根地址线有效,可寻址高达4G字节的线性地址空间和物理地址空间,可访问64TB(有2^ 14个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。这不仅为存储共享和保护提供了硬件支持,而且为实现虚拟存储提供了硬件支持。通过提供4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码数据的安全及任务的隔离。保护模式下,有两个段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一张段表可以包含8192 (2^13)个描述符,因而最多可以同时存在2 * 2^13 = 2 ^ 14个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。目前所谓的64TB(2^ (14+32)=2^ 46)逻辑地址空间是一个理论值,没有实际意义。在32位保护模式下,真正的物理空间仍然只有2^32字节那么大。

分段存储管理机制

只有在保护模式下才能使用分段存储管理机制。分段机制将内存划分成以起始地址和长度限制这两个二维参数表示的内存块,这些内存块就称之为段(Segment)。编译器把源程序编译成执行程序时用到的代码段、数据段、堆和栈等概念在这里可以与段联系起来,二者在含义上是一致的。分段机涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)。转换逻辑地址(Logical
Address,应用程序员看到的地址)到物理地址(Physical Address, 实际的物理内存地址)分以下两步:
分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址等于物理地址。分页地址转换,这一步中把线性地址转换为物理地址。(注意:这一步是可选的,由操作系统决定是否需要。在后续试验中会涉及。

上述转换过程对于应用程序员来说是不可见的。线性地址空间由一维的线性地址构成,线性地址空间和物理地址空间对等。线性地址32位长,线性地址空间容量为4G字节。分段地址转换的基本过程如下图所示。

段描述符

在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。在ucore中的kern/mm/mmu.h中的struct segdesc 数据结构中有具体的定义。
段基地址:规定线性地址空间中段的起始地址。在80386保护模式下,段基地址长32位。因为基地址长度与寻址地址的长度相同,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,而不象实方式下规定的边界必须被16整除。
段界限:规定段的大小。在80386保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。
段属性:确定段的各种性质。
段属性中的粒度位(Granularity),用符号G标记。G=0表示段界限以字节位位单位,20位的界限可表示的范围是1字节至1M字节,增量为1字节;G=1表示段界限以4K字节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节。
类型(TYPE):用于区别不同类型的描述符。可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等。
描述符特权级(Descriptor Privilege Level)(DPL):用来实现保护机制。
段存在位(Segment-Present bit):如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。图5-4显示了当存在位为0时,描述符的格式。操作系统可以任意的使用被标识为可用(AVAILABLE)的位。
已访问位(Accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位。操作系统可清除该位。

练习4

分析bootloader加载ELF格式的OS的过程
1.bootloader如何读取硬盘扇区的?
2.bootloader是如何加载 ELF格式的 OS?
这里主要分析是bootmain函数,

bootmain(void) {
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
ph = (struct proghdr )((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset):
}
((void (
)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}1234567891011121314151617

bootloader读取硬盘扇区

根据上述bootmain函数分析,首先是由readseg函数读取硬盘扇区,而readseg函数则循环调用了真正读取硬盘扇区的函数readsect来每次读出一个扇区 ,如下,详细的解释看代码中的注释:

readsect(void *dst, uint32_t secno) {
waitdisk(); // 等待硬盘就绪
// 写地址0x1f2~0x1f5,0x1f7,发出读取磁盘的命令
outb(0x1F2, 1);
outb(0x1F3, secno & 0xFF);
outb(0x1F4,
(secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20);
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4);//读取一个扇区
}123456789101112

bootloader加载 ELF格式的 OS,读取完磁盘之后,开始加载ELF格式的文件。详细的解释看代码中的注释。

bootmain(void) {
//首先判断是不是ELF
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
//ELF头部有描述ELF文件应加载到内存什么位置的描述表,这里读取出来将之存入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);
}
//根据ELF头表中的入口信息,找到内核的入口并开始运行
((void (
)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad
}123456789101112131415161718192021

练习5

完成kdebug.c中函数print_stackframe的实现,可以通过函数
print_stackframe来跟踪函数调用堆栈中记录的返回地址。

1、函数堆栈的原理

理解函数堆栈最重要的两点是:栈的结构,以及EBP寄存器的作用。

一个函数调用动作可分解为零到多个 PUSH指令(用于参数入栈)和一个 CALL 指令。CALL 指令内部其实还暗含了一个将返回地址压栈的动作,这是由硬件完成的。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl %ebp
movl %esp,%ebp12

这两条汇编指令的含义是:首先将ebp 寄存器入栈,然后将栈顶指针 esp 赋值给 ebp。
movl %esp %ebp这条指令表面上看是用esp覆盖 ebp原来的值,其实不然。因为给 ebp赋值之前,原ebp 值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原 ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。
大概就如下:

static inline uint32_t
read_ebp(void) {
uint32_t ebp;
asm volatile (“movl %%ebp, %0” : “=r” (ebp)); //内联汇编,读取edp寄存器的值到变量ebp
return ebp; //返回ebp的值
}

函数调用大概包括以下几个步骤:

  • 1、参数入栈:将参数从右向左(或从右向左)依次压入系统栈中。

  • 2、返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

  • 3、代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

  • 4、栈帧调整

4.1保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
4.2将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
4.3给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。

而函数返回大概包括以下几个步骤:

  • 1、保存返回值,通常将函数的返回值保存在寄存器EAX中。
  • 2、弹出当前帧,恢复上一个栈帧。
  • 2.1在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间
  • 2.2将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
  • 2.3将函数返回地址弹给EIP寄存器。

  • 3、跳转:按照函数返回地址跳回母函数中继续执行。

而由此我们可以直接根据ebp就能读取到各个栈帧的地址和值,一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用 4 字节内存,对应32位系统),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层 ebp 值。

2、print_stackframe函数的实现

执行make qume后,如下:

ebp:0x00007b38 eip:0x00100bf2 args:0x00010094 0x0010e950 0x00007b68 0x001000a2
kern/debug/kdebug.c:297: print_stackframe+48
ebp:0x00007b48 eip:0x00100f40 args:0x00000000 0x00000000 0x00000000 0x0010008d
kern/debug/kmonitor.c:125: mon_backtrace+23
ebp:0x00007b68 eip:0x001000a2 args:0x00000000 0x00007b90 0xffff0000 0x00007b94
kern/init/init.c:48: grade_backtrace2+32
ebp:0x00007b88 eip:0x001000d1 args:0x00000000 0xffff0000 0x00007bb4 0x001000e5
kern/init/init.c:53: grade_backtrace1+37
ebp:0x00007ba8 eip:0x001000f8 args:0x00000000 0x00100000 0xffff0000 0x00100109
kern/init/init.c:58: grade_backtrace0+29
ebp:0x00007bc8 eip:0x00100124 args:0x00000000 0x00000000 0x00000000 0x0010379c
kern/init/init.c:63: grade_backtrace+37
ebp:0x00007be8 eip:0x00100066 args:0x00000000 0x00000000 0x00000000 0x00007c4f
kern/init/init.c:28: kern_init+101
ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8

练习6

1.中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。注意除了系统调用中断(T_SYSCALL)以外,其它中断均使用中断门描述符,权限为内核态权限;而系统调用中断使用异常,权限为陷阱门描述符。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3.请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用 print_ticks子程序,向屏幕上打印一行文字100 ticks。

中断与异常定义

操作系统需要对计算机系统中的各种外设进行管理,这就需要CPU和外设能够相互通信才行。一般外设的速度远慢于CPU的速度。如果让操作系统通过CPU“主动关心”外设的事件,即采用通常的轮询(polling)机制,则太浪费CPU资源了。所以需要操作系统和CPU能够一起提供某种机制,让外设在需要操作系统处理外设相关事件的时候,能够“主动通知”操作系统,即打断操作系统和应用的正常执行,让操作系统完成外设的相关处理,然后在恢复操作系统和应用的正常执行。在操作系统中,这种机制称为中断机制。中断机制给操作系统提供了处理意外情况的能力,同时它也是实现进程/线程抢占式调度的一个重要基石。但中断的引入导致了对操作系统的理解更加困难。

中断分类

在操作系统中,有三种特殊的中断事件。
由CPU外部设备引起的外部事件如I/O中断、时钟中断、控制台中断等是异步产生的(即产生的时刻不确定),与CPU的执行无关,我们称之为异步中断(asynchronous interrupt)也称外部中断,简称中断(interrupt)。
而把在CPU执行指令期间检测到不正常的或非法的条件(如除零错、地址访问越界)所引起的内部事件称作同步中断(synchronous interrupt),也称内部中断,简称异常(exception)。
把在程序中使用请求系统服务的系统调用而引发的事件,称作陷入中断(trap interrupt),也称软中断(soft interrupt),系统调用(system call)简称trap。
当CPU收到中断或者异常的事件时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理例程中,在完成对这个事件的处理后再跳回到刚才被打断的程序或任务中。中断向量和中断服务例程的对应关系主要是由IDT(中断描述符表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符,留待CPU在产生中断后查询对应中断服务例程的起始地址。而IDT本身的起始地址保存在idtr寄存器中。

IDT

中断描述符表把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8字节的描述符数组,但IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8做为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。指令LIDT和SIDT用来操作IDTR。两条指令都有一个显示的操作数:一个6字节表示的内存地址。指令的含义如下:
LIDT(Load IDT Register)指令:使用一个包含线性地址基址和界限的内存操作数来加载IDT。操作系统创建IDT时需要执行它来设定IDT的起始地址。这条指令只能在特权级0执行。
SIDT(Store IDT Register)指令:拷贝IDTR的基址和界限部分到一个内存地址。这条指令可以在任意特权级执行。
IDT和IDTR寄存器的结构和关系如下图所示:
在这里插入图片描述

练习6.1

/* 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!
*/
}

练习6.2

这里这里主要就是实现对中断向量表的初始化。

注释如下:

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.
ow you should setup the entries of ISR in Interrupt Description
Table (IDT).
an you see idt[256] in this
file? Yes, it’s IDT! you can use SETGATE macro to setup each item of ter 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!
}1234567891011121314

重点就是两步

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

第二步,填充中断描述符表IDT。

第三部,加载中断描述符表。

对应到代码中如下所示:

id idt_init(void) {
extern uintptr_t __vectors[];//声明__vertors[]
int i;
for(i=0;i<256;i++) {
SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITCH_TOK],DPL_USER);
lidt(&idt_pd);//使用lidt指令加载中断描述符表
}123456789

这里的SETGATE在mmu.h中有定义,

define SETGATE(gate, istrap, sel, off,

简单解释一下参数

gate:为相应的idt[]数组内容,处理函数的入口地址

istrap:系统段设置为1,中断门设置为0

sel:段选择子

off:为__vectors[]数组内容

dpl:设置特权级。这里中断都设置为内核级,即第0级

练习6.3

这里根据指导书查看函数trap_dispatch,发现print_ticks()子程序已经被实现了,所以我们直接进行判断输出即可,如下(见注释):

RQ_OFFSET + IRQ_TIMER:
ticks ++; //每一次时钟信号会使变量ticks加1
if (ticks==TICK_NUM) {//TICK_NUM已经被预定义成了100,每到100便调用print_ticks()函数打印
ticks-=TICK_NUM;
print_ticks();
}
break;

实现之后,然后摁下了字母a,屏幕予以回显,试验成功。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值