OS课程 ucore_lab1实验报告
练习一:理解通过make生成执行文件的过程。
列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。
在此练习中,大家需要通过静态分析代码来了解:
- 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地>解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致>的结果)
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
1.1
1. 生成ucore.img需要kernel和bootblock
生成ucore.img的代码如下:
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)
首先先创建一个大小为1000字节的块,然后再将bootblock 复制过去。
生成ucore.img需要先生成kernel和bootblock
2. 生成kernel的代码如下:
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo "bbbbbbbbbbbbbbbbbbbbbb$(KOBJS)"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
通过make V=指令得到执行的具体命令如下:
+ cc kern/init/init.c //编译init.c
gcc -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c //编译readline.c
gcc -c kern/libs/readline.c -o
obj/kern/libs/readline.o
+ cc kern/libs/stdio.c //编译stdlio.c
gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c //编译kdebug.c
gcc -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c //编译komnitor.c
gcc -c kern/debug/kmonitor.c -o
obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c //编译panic.c
gcc -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c //编译clock.c
gcc -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c //编译console.c
gcc -c kern/driver/console.c -o
obj/kern/driver/console.o
+ cc kern/driver/intr.c //编译intr.c
gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c //编译prcirq.c
gcc -c kern/driver/picirq.c -o
obj/kern/driver/picirq.o
+ cc kern/trap/trap.c //编译trap.c
gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S //编译trapentry.S
gcc -c kern/trap/trapentry.S -o
obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S //编译vectors.S
gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c //编译pmm.c
gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c //编译printfmt.c
gcc -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c //编译string.c
gcc -c libs/string.c -o obj/libs/string.o
+ ld bin/kernel //链接成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 //编译bootasm.c
gcc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c //编译bootmain.c
gcc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c //编译sign.c
gcc -c tools/sign.c -o obj/sign/tools/sign.o
gcc -O2 obj/sign/tools/sign.o -o bin/sign
+ ld bin/bootblock //根据sign规范生成bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o
-o obj/bootblock.o
//创建大小为10000个块的ucore.img,初始化为0,每个块为512字节
dd if=/dev/zero of=bin/ucore.img count=10000
//把bootblock中的内容写到第一个块
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
//从第二个块开始写kernel中的内容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
根据其中可以看到,要生成 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.o
3.生成 bootblock:
代码如下:
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo "========================$(call toobj,$(bootfiles))"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
同样根据make V=指令打印的结果,得到要生成的bootblock,首先要生成bootasm.o、bootmain.o、sign,
代码如下:
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
由宏定义批量实现了。
而实际的命令在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.o
下面是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.o
查阅资料:
--ggdb 生成可供fdb使用的调试信息
--m32 生成适用于32位环境的代码
--gstabs 生成stabs格式的调试信息
– nostdinc 不是有标准库
--fno-stack-protector 不生成用于检测缓冲区溢出的代码
--Os 为减少代码大小而进行优化
添加搜索头文件的路径
--fno-builtin 不进行builtin函数的优化
下列代码为sign生成的代码:
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
下列是生成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/sign
有了上述的bootasm.o、bootmain.o、sign。接下来就可以生成block了,实际命令如下:
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
参数解释:
--m 模拟为i386上的连接器
--N 设置代码段和数据段均为可读写
--e 指定入口
--Ttext 制定代码段开始位置
总结:
编译所有生成bin/kernel所需的文件 链接生成bin/kernel 编译bootasm.S bootmain.c sign.c 根据sign规范生成obj/bootblock.o 生成ucore.img
1.2
截取sign.c文件中的部分源码:
char buf[512]; //定义buf数组
memset(buf, 0, sizeof(buf));
// 把buf数组的最后两位置为 0x55, 0xAA
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
//大小为512字节
fprintf(stderr, "write '%s' error,
size is %d.\n", argv[2], size);
return -1;
}
可知一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:
--磁盘主引导扇区只有512字节
--磁盘最后两个字节为0x55AA
--由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符构成。
练习二 使用qemu执行并调试lab1中的软件
为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:
- 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
- 在初始化位置0x7c00设置实地址断点,测试断点正常。
- 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
- 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
首先在CPU加电之后,CPU里面的ROM存储器会将其里面保存的初始值传给各个寄存器,其中CS:IP = 0Xf000 : fff0(CS:代码段寄存器;IP:指令寄存器),这个值决定了我们从内存中读数据的位置,PC = 16*CS + IP。
此时系统处于实模式,并且截止到目前为止系统的总线还不是我们平常的32位,这时的地址总线只有20位,所以地址空间的总大小只有1M,而我们的BIOS启动固件就在这个1M的空间里面。
BIOS启动固件需要提供以下的一些功能:
☆基本输入输出的程序
☆系统设置信息
☆开机后自检程序
☆系统自启动程序
在此我们需要找到CPU加电之后的第一条指令的位置,然后在这里break,单步跟踪BIOS的执行,根据PC = 16*CS + IP,我们可以得到PC = 0xffff0,所以BIOS的第一条指令的位置为0xffff0(在这里因为此时我们的地址空间只有20位,所以是0xffff0)。
在这里我们利用make debug来观察BIOS的单步执行:
2.1
修改lab1/tools/gdbinit,内容为:
set architecture i8086
target remote :1234
然后在lab1执行:
make debug
在gdb的调试界面,执行如下命令:
si
来单步跟踪,在gdb的调试界面,执行如下命令来查看BIOS代码:
x /2i$pc
得到如下截图:
2.2
修改gdbinit文件:
set architecture i8086
target remote :1234
b *0x7c00
c
x/2i $pc
得到如下结果,正常:
2.3
改写makefile文件:
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
然后执行 make debug:
得到q.log文件:
查看bootasm.S文件:
并与bootlock.asm文件对比:
从上面的结果可以看到:
bootasm.S文件中的代码和bootlock.asm是一样的,对于q.log文件,断点之后的代码和bootasm.S、bootlock.asm是一样的。
2.4
修改gdbinit文件,在0x7c4a处设置断点 (调用bootmain函数处):
set architecture i8086
target remote :1234
break *0x7c4a
输入make debug 得到结果:
断点设置正常!
练习三 分析bootloader进入保护模式的过程。
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
1.何开启A20,以及如何开启A20
2.如何初始化GDT表
3.如何使能和进入保护模式
关中断和清除段寄存器
.globl start
start:
.code16
cli //关中断
cld //清除方向标志
xorw %ax, %ax //ax清0
movw %ax, %ds //ds清0
movw %ax, %es //es清0
movw %ax, %ss //ss清0
3.1
初始时A20为0,访问超过1MB的空间时就会从.循环计数,将A20的地址线置为1后才可以访问4G内存。A20地址由8042控制,8042由2个I/O端口:0x60和0x64
打开A20流程:
1. 等待8042 Input buffer为空
2. 发送Write 8042 Output Port(P2)命令到Input buffer;
3. 等待8042 Input buffer为空
4. .将8042Output Port(P2)得到字节的第2位置1,然后哦写入8042 Input buffer;
seta20.1: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.1 //al的第2位为0,则跳出循环
movb $0xd1, %al //将0xd1写入al中
outb %al, $0x64 //将0xd1写入到0x64端口中
seta20.2: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.2 //al的第2位为0,则跳出循环
movb $0xdf, %al //将0xdf入al中
outb %al, $0x60 //将0xdf入到0x64端口中,打开A20
3.2
1. 载入GDT表
lgdt gdtdesc //载入GDT表
2:进入保护模式
通过将cr0寄存器PE位置1便开启了保护模式
cr0的第0位为1表示处于保护模式。
movl %cr0, %eax //加载cro到eax
orl $CR0_PE_ON, %eax //将eax的第0位置为1
movl %eax, %cr0 //将cr0的第0位置为1
3 通过长跳转更新cs的基地址:
以上已经打开了保护模式,所以这里需要用到逻辑地址。$PROT_MODE_CSEG的值为0x80。
ljmp $PROT_MODE_CSEG, $protcseg//长跳转进入保护模式
.code32
protcseg:
4: 设置段寄存器 并建立堆栈。
movw $PROT_MODE_DSEG, %ax //
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $0x0, %ebp //设置帧指针
movl $start, %esp //设置栈指针
####### 5:转到保护模式完成,进入boot主方法。
call bootmain //调用bootmain函数
3.3
将cr0寄存器置1
首先将cr0寄存器里面的内容取出来,然后进行一个或操作,最后将得到的结果再写入cr0中,由上文我们知道,在这里需要将cr0的最低位设置为1,所以我们的或操作是用来使得cr0的最低位为1的操作,也就是说我们的CR0_PE_ON的值必须为1,这样才可以达成目的,然后通过查询CR0_PE_ON的定义我们发现的确为1,所以顺利开启PE位。
练习四 分析bootloader加载ELF格式的OS的过程。
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
1. bootloader如何读取硬盘扇区的?
2. bootloader是如何加载ELF格式的OS?
4.1
分析bootloader读取硬盘扇区的代码:
BootLoader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,BootLoader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成的。
上一个练习中BootLoader已经成功进入了保护模式,接下来我们要做的是从硬盘读取并运行OS。对于硬盘来说,我们知道是分成许多扇区的,其中每个扇区大小为512字节。读取扇区的流程可从指导书查阅得到:
1. 等待磁盘准备好
利用waitdisk()函数进行检查
2. 发出读取扇区的命令
写地址0x1f2~0x1f7,第一条设置读取扇区的数目为1,然后四条是设置LBA的参数,最后一条是发出读取磁盘的命令.
以下是地址查询功能:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mAxpTqEF-1571535762286)(https://i.loli.net/2019/10/19/5azerf1XCcjI2qt.png)]
3. 等待磁盘准备好
利用waitdisk()函数进行检查
4. 把磁盘扇区数据读到指定内存
接下来我们了解一下如何具体的从硬盘读取数据。
因为我们所要读取的操作系统文件是存在0号硬盘上的,所以我们来看一下观念与0号硬盘的I/O端口:
outb(0x1F2, 1); //读取一个扇区
outb(0x1F3, secno & 0xFF); //要读取的扇区编号
outb(0x1F4, (secno >> 8)&0xFF);//用来存放读写柱面的低8位字节
outb(0x1F5, (secno >> 16)&0xFF);//用来存放读写柱面的高2位字节
// 用来存放要读/写的磁盘号及磁头号
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); //获取数据
}
  一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的,具体参数见上表,一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
  readseg简单包装了readsect,可以从设备读取任意长度的内容。
```cpp
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
va -= offset % SECTSIZE;
uint32_t secno = (offset / SECTSIZE) + 1;
// 加1因为0扇区被引导占用
// ELF文件从1扇区开始
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
4.2
接下来我们需要读取ELF格式的OS,在读取ELF格式的OS之前我们需要了解ELF格式的文件在UCore里面是如何进行存储的,首先我们来观察一下用来读取ELF的结构体elfhdr。
ELF定义:
在这里我们只需要关注其中的几个参数:
e_magic:是用来判断读出来的ELF格式的文件是否为正确的格式;
e_phoff:是program header表的位置偏移;
e_phnum:是program header表中的入口数目;
e_entry:是程序入口所对应的虚拟地址。
由于我们需要把ELF格式的OS加载到内存中的程序块中,所以我们需要了解下在内存中进程块是如何存储的:
在这里我们需要了解一些参数:
p_va:一个对应当前段的虚拟地址;
p_memsz:当前段的内存大小;
p_offset:段相对于文件头的偏移。
了解了程序在磁盘和内存中分别的存储方式之后我们就需要开始从内存中读取数据加载到内存中来。由于上问的操作,我们将一些OS的ELF文件读到ELFHDR里面,所以在加载操作开始之前我们需要对ELFHDR进行判断,观察是否是一个合法的ELF头。
以下是bootmain函数代码:
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过储存在头部的幻数判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,
// 先将描述表的头地址存在ph
//ph表示ELF段表首地址,eph表示ELF段表末地址
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
//接下来通过循环读取每个段,并且将每个段读入相应的虚存p_va中。
// 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// 根据ELF头部储存的入口信息,找到内核的入口,调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值。
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
总结:
1. 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用。
2. 校验e_magic 子段
3. 根据偏移量分别把程序短的数据读取到内存中
练习五 实现函数调用堆栈跟踪函数
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q13w018A-1571535762290)(https://i.loli.net/2019/10/20/UwSDHp7YJIoP9nr.png)]
请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。
函数堆栈:
理解函数堆栈最重要的两点是栈的结构和EBP寄存器的作用。一个函数调用可分解为零到多个push指令(用于参数入栈)和一个CALL指令。CALL指令内部还暗含了一个将返回地址压栈的动作,这是由硬件完成的。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
pushl %ebp
movl %esp,%ebp
这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。movl %esp,%ebp这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着占中的一个地址(原ebp入栈后的栈顶),从改地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而改地址出又存储着上一层函数调用时的ebp值。
一般而言,ss:[ebp+4]处为返回地址(即调用时的 eip),ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。
打开 labcodes/lab1/kern/debug/kdebug.c,找到 print_stackframe函数:
实现:
通过一个for循环来循环输出栈内的相关参数,首先获取栈传入的参数,根据上面的分析我们可以知道第一个参数存在ebp+8的位置,在这里是通过ebp+2来实现的,因为在这里2是int型,所以可以得到第一个参数,然后我们需要得到原ebp以及返回地址的值,根据分析我们知道原ebp的值就存在ebp的位置,eip的值存在ebp+4的位置,所以在这里通过数组的操作实现具体功能。
执行make qemu得到:
最后一行的解释:
其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。(因为此时ebp对应地址的值为0)
bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。
练习六 完善中断初始化和处理
请完成编码工作和回答如下问题:
1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而ucore的应用程序处于特权级3,需要采用`int 0x80`指令操作(这种方式称为软中断,软件中断,Tra中断,在lab5会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。
要求完成问题2和问题3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题1的回答。完成这问题2和3要求的部分代码后,运行整个系统,可以看到大约每1秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。
6.1
一个表项的结构如下:
/*lab1/kern/mm/mmu.h*/
/* Gate descriptors for interrupts and traps */
struct gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_ss : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
中断描述符表一个表项占8字节。其中015位和4863位分别为offset偏移量的低16位和高6位,16~31位为段选择子。通过段选择子获得段基址,加上偏移量即可得到中断处理代码的入口。如下图:
6.2
打开kern/trap/trap.c找到idt_init函数,完成代码:
第一步,声明__vertors[],其中存放着中断服务程序的入口地址。这个数组生成于vertor.S中。
第二步,填充中断描述符表IDT。
第三步,加载中断描述符表。
其中SETGATE在mmu.h中有定义:
#define SETGATE(gate, istrap, sel, off, dpl){ \ (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \ (gate).gd_ss = (sel); \ (gate).gd_args = 0; \ (gate).gd_rsv1 = 0; \ (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \ (gate).gd_s = 0; \ (gate).gd_dpl = (dpl); \ (gate).gd_p = 1; \ (gate).gd_off_31_16 = (uint32_t)(off) >> 16; \ }
- gate:为相应的idt[]数组内容,处理函数的入口地址
- istrap:系统段设置为1,中断门设置为0
- sel:段选择子
- off:为__vectors[]数组内容
- dpl:设置特权级。这里中断都设置为内核级,即第0级
6.3
根据指导书查看函数trap_dispatch,发现print_ticks()子程序已经被实现了,所以我们直接进行判断输出即可,如下(见注释):
........
........
case IRQ_OFFSET + IRQ_TIMER:
ticks ++; //每一次时钟信号会使变量ticks加1
if (ticks==TICK_NUM) {
//TICK_NUM已经被预定义成了100,每到100便调用print_ticks()函数打印
ticks-=TICK_NUM;
print_ticks();
}
break;
.........
.........
根据提示补充:
运行结果:
收获:
本次实验花费了大量的时间与精力,但收获也同样不少:学习了如何基本的运行qemu,如何单步调试动态调试,了解到bootloader启动过程,分段机制,ELF文件格式等等相关知识,懂得如何中断,堆栈的利用,学会一些基本的编程知识。
参考链接:
[1].https://blog.csdn.net/Ni9htMar3/article/details/62422984
[2].https://blog.csdn.net/tiu2014/article/details/53998595
[3].https://blog.csdn.net/tangyuanzong/article/details/78595854
[4].https://blog.csdn.net/qq_19876131/article/details/51706973
[5].http://qiaoin.github.io/ucore-ex1-notes.html