实验日期 | 实验项目 |
---|---|
2020.11.5 | 第6天 分割编译和中断处理 |
文章目录
一、实验主要内容
1、 内容1 分割编译,整理Makefile, 整理头文件
(1).内容概要
- 实验内容:分割源文件,便于以后的设计和修改;整理Makefile文件;整理头文件。
源文件分割的利弊
优点:
a.按照处理内容进行分类,如果分得好的话,将来进行修改时,容易找到地方。
b.如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度。
c.单个源文件都不长。多个小文件比一个大文件好处理。
缺点:
a.增加了源文件的数量。
b.分类分得不好的话,不容易找到地方进行修改。
Makefile文件时用到了通配符,在这个规则中,依赖关系中的名字用%来代替,其通配符的使用和linux中相同,*代表任意字符,?代表任意一个字符,[…]指定某个字符,如果用通配符的写法这样文件变成了一个变量,前面要加上$。
C语言中“.h”文件可以用来存放对函数的声明,其他程序文件需要用到.h文件里面的声明时,可以直接加上头文件即可。需要注意的是,写头文件时双引号与尖括号的区别:双引号(“”)表示该头文件与源文件位于同一个文件夹里,而尖括号(<>)则表示该头文件位于编译器所提供的文件夹里。
(2).关键代码分析
代码基本上没有变化。分割后用dsctbl.c用存放GDT,IDT的定义和初始化,graphic.c用来存放绘制界面,鼠标等的函数,bootpack.c存放主函数部分,bootpack.h用来存放函数声明和宏定义。
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
这一句是将所有的.c文件编译为.gas文件。
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
这一句是将所有的.gas文件编译为.nas文件。
%.obj : %.nas Makefile
$(NASK) $*.nas $*.obj $*.lst
这一句是将所有的.nas文件编译为.obj文件。
2、 内容2 段的相关设置
(1).内容概要
- 实验内容:将段上限和地址值赋值给GDTR;初始化GDT。
- 实验重点:理解load_idar函数的设置;掌握将段的信息(8个字节)写入内存的方法,即set_segmdesc函数的代码分析。
GDTR是一个48位的段地址管理寄存器,其中高32位表示基地址,低16位表示界限。在汇编中,不能直接使用MOV指令去赋值。这里需要用LGDT指令:指定一个内存地址,从指定地址中读取6个字节,然后赋值给GDTR。
段的信息主要包括三个部分,即段的大小,段的起始地址,段的管理属性,总结如下:
段的大小 | 使用20位,在段属性中设置了标志位G_bit,当其有效时,段大小的单位为4kb,这样就可以表示4GB的大小了。存储时,分为low(2字节)和high(1字节)两个部分。 |
段的起始地址 | 使用32位,存储时,分为low(2个字节),mid(1个字节)和high(1个字节)三个部分。 |
段的管理属性 | 使用12位,其中高4位存放在段大小high中,作用是扩展访问权,由GD00构成,D用来确定段的模式,1表示32位模式,0表示16位模式。低8位存放在ar中,其中0xfa表示应用程序用,可执行的段,可读不可写;0x9a表示系统专用,可执行的段,可读不可写 |
CPU是处于系统模式还是应用程序,取决于执行中的应用程序是位于访问权为0x9a的段还是0xfa的段。
(2).关键代码分析
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
这部分代码是用来初始化GDTR寄存器的。汇编第1句将段上限存储在寄存器AX中,汇编第2句将AX再次放入地址[ESP+6]-[ESP+8]中,最后从[ESP+6]中取出6个字节的内容(包括段上限和基址)给GDTR。同理load_idtr函数对寄存器IDTR初始化。
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
//如果limit上限超过20位表示的范围,则使用G_bit位
ar |= 0x8000; // G_bit = 1
limit /= 0x1000;//换算成以4kb为单位
}
sd->limit_low = limit & 0xffff;//取段上限的低2个字节存入limit_low
sd->base_low = base & 0xffff; //取基址的低2个字节存入base_low
sd->base_mid = (base >> 16) & 0xff;//先将基址已经存放好的低2个字节移走,再取1个字节存入base_mid
sd->access_right = ar & 0xff;//取ar的一个字节存入access_right
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);//limit_high有8位,其中高4位存储ar的段属性高4位,对应((ar >> 8) & 0xf0)
//低4位存储limit段大小的高4位,对应((limit >> 16) & 0x0f)
sd->base_high = (base >> 24) & 0xff;//先将基址已经存放的3个字节移走,将最后一个字节存入base_high
return;
}
这部分代码是对GDT结构体的相关信息的设置,包括段大小,段基址和段的管理属性。段的结构体将地址分为3个部分,用base_low,base_mid,base_high表示;段大小分为2个部分,用limit_low,limit_high表示,段的管理属性用access_right表示。
3、 内容3 初始化PIC
(1).内容概要
- 实验内容: 知道主从PIC的连接方式;学会设定PIC。
- 实验重点:理解PIC的设置原理以及对应的硬件结构。
PIC是可编程中断控制器。其可以将8个中断信号集合成一个中断信号,一旦有一个中断信号进来,唯一的输出管脚信号就会变为ON,并通知CPU。我们涉及到的PIC有两个,主PIC和CPU直接相连,负责处理第0-7号中断信号,从PIC负责处理第8-15号信号。两者的连接结构如下:
PIC的寄存器都是8位寄存器,其中IMR称为中断屏蔽寄存器,对应8路IRQ信号,当该位为1时,说明对应的IRQ信号被屏蔽,PIC就忽略该路信号。ICW为初始化控制数据的寄存器,ICW一共有4个,ICW1,ICW4与PIC主板配线方式,中断信号的电气特性等有关,ICW3是用有关主从连接的设定,ICW2是决定IRQ哪一号中断通知CPU。
当应用程序对操作系统破坏时,CPU内部会自动产生中断信号0x00~0x1f,以此来保护系统。所以IPQ的中断号码只能从0x20开始,到0x2f。
(2).关键代码分析
void init_pic(void)//PIC的初始化程序
{
io_out8(PIC0_IMR, 0xff );//禁止所有中断
io_out8(PIC1_IMR, 0xff ); //禁止所有中断
//主PIC的相关设定
io_out8(PIC0_ICW1, 0x11 ); //边沿触发模式
io_out8(PIC0_ICW2, 0x20 ); //IRQ0-7由INT20-27接收
io_out8(PIC0_ICW3, 1 << 2); //PIC1由IRQ2连接,对应的ICW3为0000 0100,所以是1<<2
io_out8(PIC0_ICW4, 0x01 ); //无缓冲区模式
//从PIC的相关设定
io_out8(PIC1_ICW1, 0x11 ); //边沿触发模式
io_out8(PIC1_ICW2, 0x28 ); IRQ8-15由INT28-2f接收
io_out8(PIC1_ICW3, 2 ); PIC1由IRQ2连接,2为标识号,表示IRQ2
io_out8(PIC1_ICW4, 0x01 ); 无缓冲区模式
io_out8(PIC0_IMR, 0xfb );//11111011 PIC1以外全部禁止
io_out8(PIC1_IMR, 0xff );//11111111 禁止所有中断
return;
}
这部分代码是PIC的初始化程序。对PIC中的众多寄存器进行初始化,使用端口号码对其区分。PIC和CPU的通信是通过函数in_out8实现,CPU将要设置的值使用OUT指令传给相应的端口。
4、 内容4 中断处理程序的制作
(1).内容概要
- 实验内容:编写鼠标和键盘的中断处理程序;了解几种常见的缓冲区的信息进入和取出;实现按下键盘的中断设定。
- 实验重点:知道中断的整个处理过程,例如敲击键盘后中断的处理过程。
键盘中断的处理过程:按下键盘上任意键后,计算机捕获到键盘被敲下的动作,PIC0收到一个中断信号,并将唯一的输出管脚信号变为ON。PIC将中断信号告知CPU,CPU暂时停止正在处理的任务,命令PIC发送两个字节的数据(0xcd和中断号)给自己,CPU再根据IDT的设定,转而执行中断程序(这里的中断程序是实现将一行字打印到屏幕上的功能)。中断程序执行完后,调用之前设定好的函数,返回处理中的任务。
缓冲区:用于暂时存储,暂时记忆的存储区域。常见的缓冲区种类有:先进先出(FIFO),先进后出(FILO)。其中栈可以用来实现FILO型的缓冲区,PUSH将数据压入栈,POP将数据从栈顶取出。
(2).关键代码分析
io_sti(),这个函数表示中断许可标志变为1,允许CPU接受来自外部设备的中断。
void inthandler21(int *esp)//键盘中断处理程序
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;//定义一个结构体变量
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);//绘制背景
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");//打印字符
for (;;) {
io_hlt();
}
}
这部分代码是键盘中断处理程序,当产生键盘中断后,会执行中断处理程序,也就是以上代码的内容。代码第1句是定义结构体变量,第2句是绘制一个左上角坐标为(0,0),右下角坐标为(328-1,15),颜色为黑色的矩形,328-1表示的是容纳32个字符,每个字符的宽是用8位表示的。鼠标中断处理程序和键盘中断处理程序类似。
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
这两句代码表示对中断号为0x21,0x2c的IDT的设定。第1个参数表示注册的中断号,第2个参数表示调用的函数地址,第3个参数表示中断处理函数的段,*8是表示将其左移3位(低3位有其他信息,必须不能被覆盖),最后一个参数表示IDT的属性,设定位0x008e,表示这是用于中断处理的有效设定。
_asm_inthandler21:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21
POP EAX
POPAD
POP DS
POP ES
IRETD //中断返回
这部分代码的作用是在执行中断处理程序之前保存寄存器的值,在执行后恢复寄存器的值,以便可以正常返回到CPU原来被打断处理的任务。
代码实现效果:按下键盘后,显示一行字。
二、遇到的问题及解决方法
1、 描述问题1
- 问题描述
书上P114页中段的地址为什么需要分为3段?
- 解决方法
主要是为了与80286那个时候的程序兼容。做了这样的处理,80286用的操作系统也可以直接在386以后的CPU上运行了。查阅资料知,80286的地址线是24位,而386分段后low和mid刚好3个字节,有24位,可以适用于80286。
2、 描述问题2
- 问题描述
第5天中涉及到了GDT和IDT的结构体,在第6天解释了GDT结构体的相关组成,并未对IDT的结构体进行具体解释。这里对其进行解释说明。
- 解决方法
offset表示调用中断函数的地址,selector表示中断程序所在的段,access_right表示IDT的设置属性。其中将地址分为两段,分别为offset_low和offset_high, ar分为两段,分别为dw_count和access_right。
三、程序设计创新点
1、 描述创新点1,关键代码及结果截图
- 创新点1
本次实验的重点内容是中断前的准备工作以及实现键盘中断。那是否可以用中断处理程序去实现其他的功能呢。比如按下键盘上按键,将其显示出来,或者显示汉字。难点在于如何去区分不同按键按下。这里使用函数io_in8来实现,将敲下键盘的记录值使用汇编存入EAX中,作为io_in8函数的返回值。根据返回值的不同来确定按下哪个按键。
- 关键代码
#define PORT_KEYDAT 0x0060 //0x0060表示键盘设备
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
unsigned char data;
extern char hankaku[4128];
data = io_in8(PORT_KEYDAT);//从端口输出一个字节给EAX
if(data==0x1e)
putfont8(binfo->vram, binfo->scrnx, 0,0, COL8_FFFFFF, hankaku+'A'*16);
else if(data==0x30)
putfont8(binfo->vram, binfo->scrnx, 0,0, COL8_FFFFFF, hankaku+'B'*16);
else if(data==0x23)
putfont8(binfo->vram, binfo->scrnx, 0,0, COL8_FFFFFF, hankaku+0x100*16);
else if(data==0x13)
putfont8(binfo->vram, binfo->scrnx, 0,0, COL8_FFFFFF, hankaku+0x101*16);
else
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "ERROR");
for (;;) {
io_hlt();
}
}
这部分代码通过io_in8函数来得到按下按键后值,通过值,实现不同的操作。这里A对应输出字符A,B对应输出字符B,H对应输出汉字黄,R对应输出汉字R,其他键对应输出字符串“ERROR”。
- 结果截图
按下键盘上A,R,B,H,结果如下:
按下键盘上除A,B,H,R的其他键,结果如下:
四、实验心得体会
- 本次实验是自制操作系统的第6天,今天的内容主要是对第5天内容的进一步说明,对段结构体详细解释,中断前的准备工作——PIC初始化,实现一个简单的中断处理程序,有一定的功能。实验创新使用中断对之前学习的打印字符,字符串的内容进行了回顾,不足之处就是每次只能按下键盘上的一个键执行相应的操作,相信之后的学习过程可以去解决这个问题,以此实现按下不同键,有不同的功能(显示字符或者绘制图案),这样就似乎和电脑上的快捷键有点像了。
- 在看书的时候,也遇到了很多之前学过的知识点(有的已经忘记了),这个时候需要对前面的知识点进行回顾,以便更好地理解新的知识点。对于内容繁多的实验,最好是每看一点,做上相应的记录(大致的思路以及疑问),一是方便以后的复习,二是对前后内容的联系也有一定的帮助。