30天自制操作系统——第6天实验总结

实验日期实验项目
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初始化,实现一个简单的中断处理程序,有一定的功能。实验创新使用中断对之前学习的打印字符,字符串的内容进行了回顾,不足之处就是每次只能按下键盘上的一个键执行相应的操作,相信之后的学习过程可以去解决这个问题,以此实现按下不同键,有不同的功能(显示字符或者绘制图案),这样就似乎和电脑上的快捷键有点像了。
  • 在看书的时候,也遇到了很多之前学过的知识点(有的已经忘记了),这个时候需要对前面的知识点进行回顾,以便更好地理解新的知识点。对于内容繁多的实验,最好是每看一点,做上相应的记录(大致的思路以及疑问),一是方便以后的复习,二是对前后内容的联系也有一定的帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值