操作系统实验--30天自制操作系统第6天实验日志

一、实验主要内容

1、内容1:分割源文件
经过前几次的程序编写现在我们的bookpack.c程序已经很长了,所以我们需要就将其进行切割成几个部分以便于后续代码管理,分别是graphic.c、dsctbl.c和bootpack.c三个部分。这三个部分依次是关于画图的处理、以及关于GDT、IDT的处理以及其他部分的处理。
分割源文件的优点和缺点如下:
在这里插入图片描述

分割之后如果graphic.c也想使用naskfunc.nas函数的话,就必须加上void io_out8(int port,int data)函数声明。因为分割之后,编译时程序是不知道有bootpack.c的存在的,虽然这些都已经写在了bootpack.c函数中了。在修改了bootpack.c文件后,也要对Makefile文件进行相应的修改,相应的流程为:
在这里插入图片描述

其中makefile文件的内容与前几天的编译方法一致,增加了如下部分:
在这里插入图片描述

2、内容2:整理Makefile
虽然之前bootpack.c文件已经分割好了,变得简洁了许多,但是相比较而言,Makefile文件却变得十分繁琐亢长。因为有些指令都做的是相同的事情,但如果每次添加源文件都需要增加很多类似的编译规则就会变得很麻烦,因此需要利用一般规则,将雷同的独立的文件生成规则,归纳为一般规则。
在这里插入图片描述
在这里插入图片描述

将以上6个独立的文件生成规则,归纳为两个一般的规则。
在这里插入图片描述

因为Make.exe首先会寻找普通的生成规则,如果没找到,那么就尝试使用一般规则。即使一般规则和普通的生成规则发生冲突,也不会有什么问题。因为这时候,普通生成规则的优先级是要大于一般规则的优先级的。
3、内容3:整理头文件
由于各个源文件都需要重复声明类似于void io_out8这种函数,导致我们的总代码行数增加,如果想要更加精简的话,那么首先就是要将重复的部分删去。然后可以将之前大部分的重复部分全部归纳到bootpack.h文件。
在这里插入图片描述

.h文件大概如图所示。在这个文件中,不仅罗列了函数的定义,还在注释中写明了函数的定义是在哪一个源文件中,这样的话,如果想要修改函数定义的话,只需要看一下该文件就知道函数文件在哪个源文件中。
想要在编译的时候,每个源文件都能够使用相应的函数和变量,只需要在开头加上# include “bootpack.h"即可。当编译器读到这一行时,,就会将这一行替换成所指定文件的内容,然后再进行编译。这样bootpack.h的所有内容就会间接的写到graphic.c中。同理可以在其他文件中增加上这一行。
4、内容4:意犹未尽
在这里插入图片描述

Load_gdtr这个函数是用来将指定的段上限(limit)和地址值赋值给名为GDTR的48位寄存器。因为这个寄存器不能直接用MOV指令来赋值,要给他赋值的话,唯一的一个方法就是通过LGDT指令来指定一个内存地址,从指定的地址读取6个字节,然后赋值给GDTR寄存器。该寄存器的低16位(即内存的最初2个字节)是段上限,它等于GDT的有效字节数-1。剩下的高32位(即剩余的4个字节),代表GDT的开始地址。
在一开始执行这个函数的时候,DWORD[ESP+4]中存放的是段上限,DWORD[ESP+8]里存放的是地址,其中段上限和地址分别为0x0000ffff和0x00270000,这里看起来是8个字节了,但寄存器只有6个字节,这就意味着我们需要将其中2个字节舍弃,这里应该舍弃段上线的高两个字节,对于0x0000ffff和0x00270000,在内存中由于小端法的缘故实际存储是[FF FF 00 00 00 00 27 00]我们需要将这里后6为变成[FF FF 00 00 27 00]这样才能符合GDTR寄存器的大小
所以这个函数使用MOV AX,[ESP+4],MOV [ESP+6],AX将FF FF这两个字节数据移动到后面两位,使得现在内存中的数据为[FF FF FF FF 00 00 27 00],然后将ESP+6开始的6个字节存入GDTR寄存器就完成了。
Naskfunc.nas的_load_idtr是设置IDTR的值,IDTR与GDTR结构体基本上是一样的,所以函数的实际操作也同理。
关于dsctbl.c中的set_segmdesc函数意义
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

这个函数是按照CPU的规格要求,将段的信息归结成8个字节写入内存。其中段的信息包含段的大小、段的起始地址和段的管理属性(禁止写入、禁止执行和系统专用等)。为了能够写入这些信息,设计了一个struct SEGMENT_DESCRIPTOR结构体。
其中,段的地址是用32位来表示的,这个地址也被称为基址。在这里使用了一个base的变量名,其中,base又分为low(2字节)、mid(1字节)和high(1字节)三段,合起来刚好32位,这样一来设置了一个标志位G表示Gbit,标志位为1解释成页,相当于4KB。段上限20位最大为1MB,4KB×1MB=4GB。Limit_low和limit_high里面保存段上限其中limit_high的高四位写入段属性。最后剩下的12位是剩下的段属性-“段的访问权属性”用access_right或ar表示,合并高四位中的4位就变成16位:xxxx0000xxxxxxxx,高四位称为“扩展访问权”到386时代以后才能使用,一般是“GD00”,G是上面说到的G,D为1表示32位,为0表示16位,除了运行80286程序之外都用D=1的模式。
但这里对base的赋值分为了3段,而为何不使用一段呢。
这里主要是为了与80286时代的程序兼容。
而段上限又是为何要分为多段赋值呢
因为段上限表示一个段有多少个字节,但段上限最多也就4GB,也就是32位的数值,如果直接放入到结构体中,再加上基址,一共要8个字节,这样就将结构体占满了这样就没有地方保存段的管理信息了。
因此段上限只能使用20位,但在段的属性中设了一个标志位Gbit,当这个标志位为1时,上限的单位被解释为页,而1页指的是4KB,因此4KBX1M=4GB,这样就可以指定4GB的段了。
段属性也就是段的访问权属性,其是12位的,在程序中用变量名access_right或ar来表示。而12位段属性中那个的高4位放在limit_high的高4位中,相当于limit_high变量中有一半的数据是属于ar的,所以为了能够实现这点,段上限的赋值就也是分段的了,所以程序将ar当作如下16位来构成处理(其中的0是ar将其看作是0,实际的数据还是limit的高4位数据):
在这里插入图片描述

ar的高4位被称为扩展访问权,低8位如下所示:
在这里插入图片描述

总得来说,CPU到底是处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权为0x9a的段还是位于访问权为0xfa的段,如果在应用模式执行LGDT是被阻止的,如果应用程序段是0x9a则为系统模式,0xfa则为应用模式。
5、内容5:初始化PIC
如果想要实现鼠标指针的移动,那么就必须使用中断,而要使用中断,那么就必须将GDT和IDT正确无误的初始化。
什么是PIC?
PIC是可编程中断控制器,其实将8个中断信号集合成一个中断信号的装置。PIC监视着输入管教的8个中断信号,只要有一个中断信号进来,就将唯一的输出管教信号变成ON,并通知给CPU。其线路连接为:
在这里插入图片描述

与CPU直接相连的PIC成为主PIC,与主PIC相连的PIC称为从PIC,主PIC负责处理第0到7号中断信号,从PIC负责处理第8到第15号中断信号。如果住PIC不通知CPU,那么从PIC的意思便不能传达给CPU。从PIC通过第2号IRQ与主PIC相连。CPU只能接受主PIC传送的信号,只有主PIC向CPU传送从PIC数据的时候,从PIC的中断信号才有用。
PIC的初始化程序:
在这里插入图片描述

PIC的寄存器都是8位寄存器,对应8路IRQ信号,如果某一位值为1,则该位对应的IRQ信号被屏蔽。IMR是中断屏蔽寄存器,8位分别对应8路IRQ信号,如果某一位的值是1,那么该位所对应的IRQ信号被屏蔽,PIC就忽视该路信号。
ICW是初始化控制数据,其有4个,分别编号为1-4,共有4个字节的数据。ICW1和ICW4与PIC主板配线方式,中断信号的电气特性有关,一般都是固定值。ICW3是有关主从连接的设定,硬件上设定IRQ2与从PIC相连,所以设定为0000010。最后的ICW2才是操作系统能独立设定的,决定IRQ以哪一号通知CPU,中断信号IRQ 0-15对应INT 0x20-0x2f,之所以不用INT0x00-0x0f是因为当应用程序影响操作系统时,CPU内部会自动产生INT0x00-0x1f,如果IRQ与这些号码重复了,那么CPU就分不清其是IRQ还是CPU系统保护通知。
6、内容6:中断处理程序的制作
因为鼠标是IRQ12,键盘是IRQ1,因此编写用于INT 0x2c和INT 0x21的中断处理程序,即中断发生时所要调用的程序。
注意,中断处理之后不能执行“return”只能通过执行IRETD指令,用汇编语言修改naskfunc.nas。
在这里插入图片描述

CALL _inthandler21就是键盘的中断,调用这个函数,前面都是做中断前的“现场保护”工作,把寄存器的值压入栈中保存,三个段寄存器相等(DS/ES/SS)。中断处理之后再把栈中的值放出来,做“恢复现场”的工作。最后就是将这个函数_inthandler21注册到IDT中。该函数只是显示一条信息,然后保持在待机状态。
在这里插入图片描述

其中之所以要用汇编语言来编写,是因为中断处理完成之后,不能执行return,必须执行IRETD指令,因此只能使用汇编程序来编写代码。
最后加入的信息最后取出,这种缓冲区是先进后出,简称FILO。
这里要说明的栈是FILO型的缓冲区,PUSH将数据压入栈顶,POP将数据从栈顶取出。
PUSH EAX相当于ESP的值减去4,将所得结果作为地址值,将寄存器中的值保存到该地址所对应的内存中,POP EAX则相反。
想要注册_asm_inthandler21,在init_gdtidt中使用以下方法即可:
在这里插入图片描述

这里注册的是在idt的第0x21号,如果发生了中断,CPU会自动调用_asm_inthandler21,这里的2*8表示asm_inthandler21属于哪一个段,段号是2乘以8是因为低三位必须是0(段寄存器第三位不用)。
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);这个段正好涵盖了整个bootpack.hrb,最后的AR_CODE32_ER将IDT属性设定为0x008e,表示用于中断处理的有效设定。在HariMain的最后,修改了PIC的IMR,以便接受来自键盘和鼠标的中断。
补充指令“io_sti();”仅仅是执行STI指令,是CLI的逆指令,执行STI指令之后,IF(中断许可标志位)变为1,CPU接受外部中断。下面是按下键盘之后的状态。
实验结果便是只要按下键盘上某个键或者动一动鼠标,中断信号便会传到CPU,然后COU执行中断处理程序,输出信息。
实验结果:
在这里插入图片描述

二、遇到的问题及解决方法
1,从在键盘上按下一个字母到屏幕上显示出这个字母,计算机内部都经历了那些过程(主要描述跟键盘中断相关的过程)。
回答:当键盘按下一个键后,与CPU相连的PIC接收到IRQ1传来的中断请求,如果PIC的设置中没有屏蔽掉IRQ1的信号,那么PIC就根据在ICW2寄存器中设置的中断号的偏移来告诉CPU是几号中断(按照书中的设置这里是0x21号中断),CPU收到中断信息后,根据GDTR寄存器中保存的中断记录表地址和中断号来找到相应的IDT的信息,根据IDT中ar值(权限)来确定该中断是否处理,在中断处理有效设定的情况下,CPU根据IDT中的段信息找到中断处理方法所处的段,然后进行跳转,跳转到IDT中设定的处理函数中开始运行,至此,键盘按下后的中断处理函数开始执行。
2,在运行程序时,按下键盘后,再次按下时系统无反应
描述:根据书中第7天的实验,得到获得键盘编码和设置缓存区优化获取键盘编码的方法
但在实验中发现只有第一按下键盘后,屏幕上会打印相应的键盘编码,按下其余非控制键盘时无响应。但其在主函数中的代码中可以看到该代码有将原结果覆盖然后再显示的一个步骤,但运行时却没有反应
在这里插入图片描述

那么这里应该就有两个可能,一个是中断只响应了一次,或者是接收到的数据没有发生变化
这里我们对代码进行一些修改来进行验证
在这里插入图片描述

如果只接收到一次中断,那么屏幕上只会打印出一个A(经实验键盘上A的编码是30),如果是是接收到的数据没有发生变化那么会输出一串A。
运行结果:
在这里插入图片描述

程序不断的在屏幕上打印,而且我只按下了一次键盘A,那么这里很明显,作者在这里的中断处理代码有点问题,这里因为我只按下了一次键盘,但却执行了很多次我怀疑可能是fifo8_status函数返回的是负数,也就是fifokey->free>fifokey->size,所以这里我们将条件改为小等于于0试试,结果任不让人乐观,且我发现在我切换进程后就不在打印字符了,但如果再次按下键盘任会不断打印。
在这里插入图片描述

而导致无限打印的原因一有可能是变量keyfifo的属性没有发生改变,二是中断无限生效。
这里我们用keyfifo中q变量来固定只要第一个数据(如果正常的话应该只有一个数据)
在这里插入图片描述

运行结果:
在这里插入图片描述

说明字符无限打印的原因正是由于同一中断信息的无限响应(换句话说被第一次按下的键就无限占用了信息输入端口导致其他的键被按下时无法响应)。导致keyfifo中buf全是A
而且此时其他键按下的信息根本无法到达缓冲区
这里我的思路是通过类似于进程切换的方式让中断停止或重新响应,这里我尝试了
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_0);
和io_out8(PIC0_IMR,0xff)来重置中断,不过都失败了,对于书中作者所描述的情况我还不确定是不是我的设备问题,但现在这里我也没有其他办法去解决这个问题,不过经过测试发现只有单键盘代码的才会无限占用,像控制按键或数字按键等双按键编码的不会无限抢占。
三、程序设计创新点
1、描述创新点1,改良键盘中断显示信息,按下键盘后屏幕能够打印相应的字符
对于想要打印相应的字符,我们需要得到能够区分键盘每个键的信息,这里是键盘编码,键盘编码我们可以在键盘中断触发后在端口0x0060读取数据(使用io_in8())可以得到每个按键的编码,我们通过测试可以得到每个按键的编码;
在这里插入图片描述

这里目前测出的是数字和字母的编码,我们将其导入到数组中,在每次中断处理时进行比对就可以得知我满按下的是哪一个按键了
运行结果如下:按下1后
在这里插入图片描述

按下A后:
在这里插入图片描述

1、描述创新点1,改良键盘中断显示信息,按下键盘后屏幕能够打印相应的字符
1、描述创新点1,改良键盘中断显示信息,按下键盘后屏幕能够打印相应的字符
2、描述创新点2,用键盘来操作图像在屏幕上的移动
在上一节中通过实验发现虽然单按键编码的按键会无限响应中断,但双按键编码的还是能够正常响应,这里我们就使用数字按键(双按键编码),来完成对屏幕中图像的移动
代码如下:
在这里插入图片描述

原理很简单识别到相应按键后进行相关的图像绘制即可,其中需要注意的是
因为是双按键编码所以一次会触发两个中断,所以我这里使用了一个flag变量来使两次响应只有一次生效,剩下的就是画图了
效果如下:
在这里插入图片描述
效果虽然简单,但这也是后续开发游戏的第一步了
四、实验心得体会
这次实验主要学习到的内容就是段记录表和中断记录表以及中断的设置和中断处理函数的编写,当然在做这次实验当中遇到了很多的麻烦,因为第6天的内容中没有按键编码的获取办法,而在第七天中作者给出的获取按键编码的方法对于单按键编码按键出现了很大的问题,这个问题目前都还没有解决,如果后续想做键盘的连续交互的话就只能使用双按键编码的按键了

猜你喜欢

转载自blog.csdn.net/qq_49327751/article/details/121255391