30天自制操作系统-第六天

分割编译与中断处理

       学习目标:1、分割源文件、整理Makefile、整理头文件;2、段的介绍;3、初始化PIC;4、中断处理程序的制作;

      1、分割源文件、整理Makefile、整理头文件

      优点:1、如果分得好的话,将来进行修改时,容易找到地方;

                  2、如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度;

                  3、多个小文件比一个大文件好处理。

      缺点:1、源文件数量增加;

                  2、分类分得不好的话,修改时不容易找到地方。

                  make.exe会首先寻找普通的生成规则,如果没有找到,就尝试用一般规则,即使一般规则与普通生成规则有冲突,也不会有问题。这时候,普通生成规则要优于一般生成规则。

           2、段的介绍

_load_gdtr:		; void load_gdtr(int limit, int addr); 用来指定段上限和地址值赋值给名为GDTR的48位寄存器
MOV		AX,[ESP+4]		; limit
MOV		[ESP+6],AX
LGDT	[ESP+6]  ;这是一个很特别的寄存器,并不能用MOV指令来赋值,,唯一的办法就是指定一个内存地址,从指定的地址读取6个字节(也就是48位),使用LGDT
RET
          naskfunc.nas的_load_idtr设置IDTR,DTR与GDTR结构体基本上是一样的,程序非常相似。

         这个函数是按照CPU的规格要求,将段的信息归结成8个字节写入内存。这8个字节包括以下3点:1、段的大小;2、段的起始地址;3、段的管理属性(禁止写入,禁止执行,系统专用等),为了写入以下信息,准备了struct SEGMENT_DESCRIPTOR这样一个结构体。

        在这个结构体里base又分为low(2个字节),mid(1个字节),high(1个字节)3段,合起来刚好是32位。所以,这里只要按顺序填入相应的数值就行了。

        至于为什么分为3段,主要是为了与80286时代的程序兼容。

       段上限最大是4GB,也就是一个32位的数值,如果直接放进去,这个数值本身就要占用4个字节,再加上基址(base),一共就要8个字节,就把整个结构体占满了,这样以来,就没有地方保存段的管理属性了,这可不行。因此,段上限只能使用20位(1MB)。

       段上限只有1MB,有种又回到了16位时代的错觉,太可悲了,因此,英特尔的叔叔们又想了一个办法,他们在段的属性里设了一个标志位,叫做Gbit。这个标志位是1的时候,limit的单位不解释成字节(byte),而解释成页(page)。1页是指4KB。这样一来,4KB*1M=4GB。

       这20位的段上限分别写到limit_low和limit_high里。看起来它们好像共有3字节,即24位,但实际上我们接着要把段属性写入limit_high的上4位里,所以最后段上限还是只有20,。

        段的访问属性:在程序中用变量名access_right或ar来表示。因为12位段属性中的高4位放在limit_high的高4位里,所以程序里有意把ar当作如下的16位构成来处理:

        xxxx0000xxxxxxxx(其中x是0或1)  高4位被称为“扩展访问权”,这4位由“GD00”构成,其中G是指刚才所说的G bit,D是指段的模式,1是指32位模式,0是指16位模式。

        ar的低8位从80286时代就已经有了:

         00000000(0x00):未使用的记录表

        10010010(0x92):系统专用,可读写的段。不可执行

        10011010(0x9a):系统专用,可执行的段。可读不可写。

        11110010(0xf2):应用程序用,可读写的段。不可执行。

        11111010(0xfa):应用程序用,可执行的段。可读不可写。

        CPU有系统模式(也成“ring0”)和应用模式(也称“ring3”)之分。

       比如,如果在应用程序模式下试图执行LGDT等指令的话,CPU则对该指令不予执行,并马上告诉操作系统说“那个应用程序居然想要执行LGDT,有问题!

        CPU到底是处于操作系统模式还是应用模式,取决于执行中的应用程序是位于访问权是0x9a的段,还是位于访问权为oxfa的段。

struct SEGMENT_DESCRIPTOR {
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
	if (limit > 0xfffff) {
		ar |= 0x8000; /* G_bit = 1 */
		limit /= 0x1000;
	}
	sd->limit_low    = limit & 0xffff;
	sd->base_low     = base & 0xffff;
	sd->base_mid     = (base >> 16) & 0xff;
	sd->access_right = ar & 0xff;
	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
	sd->base_high    = (base >> 24) & 0xff;
	return;
}

            3、初始化PIC

            为了实现鼠标指针的移动,必须使用中断。要使用中断,则必须将GDT和IDT正确无误地初始化。

           还有一件事要做:就是初始化PIC(programmable interrupt controller),意思是“可编程中断控制器”。

            在设计上,CPU单独只能处理一个中断,这不够用,所以在设计电脑时,就在主板上增设了几个辅助芯片。PIC是将8个中断信号集合成一个中断信号的装置。PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。中断信号共设计了15个,因此增设了2个PIC。

            与CPU直接相连的PIC称为主PIC,与主PIC相连的PIC称为从PIC。主PIC负责处理第0到第7号中断信号,从PIC负责处理第8到第15号中断信号。

           另外,从PIC通过第2号IRQ与主PIC相连。主板上的配线是这样,无法用软件来改变。

           从CPU的角度来看,PIC是外设,CPU通过OUT指令进行操作。程序中的PIC0和PIC1,分别指主PIC和从PIC。PIC内部有很多寄存器,用端口号码对彼此进行区别,以决定是写入哪个寄存器。但是,端口号相同的东西有很多,可能让人觉得混乱,因为PIC有些很细微的规则,比如写入ICW1之后,紧跟着一定要写入ICW2等,所以端口号相同,也能够很好地区分开来。

        PIC的寄存器,首先,它们都是8位寄存器。IMR是中断屏蔽寄存器,8位分别对应8路IRQ信号。如果某一位的值是1,则该位所对用的IRQ信号被屏蔽,PIC就忽略该路信号。ICW是初始化控制数据,这里为32位,ICW有4个,分别编号为1-4,ICW1和ICW4与PIC主板配线方式、中断信号的电特性有关。ICW3是主-从连接的设定,对主PIC而言,第几号IRQ与PIC相连,是用8位来设定的。ICW2决定了IRQ以哪一号中断通知CPU。

      这次是以INT 0x20-0x2f接收中断信号IRQ0-15而设定的。

      可能大家会有疑问,为什么 INT 0x00-0x0f不行吗?是这样的,INT 0x00-0x1f不能用于IRQ,因为应用程序想要对操作系统干坏事的时候,CPU会自动产生INT 0x00-0x1f,如果IRQ与这些号码重复了,CPU就分不清它到底是IRQ,还是CPU的系统保护通知。

void init_pic(void)
/* PIC偺弶婜壔 */
{
	io_out8(PIC0_IMR,  0xff  ); 
	io_out8(PIC1_IMR,  0xff  ); 

	io_out8(PIC0_ICW1, 0x11  );
	io_out8(PIC0_ICW2, 0x20  ); 
	io_out8(PIC0_ICW3, 1 << 2); 
	io_out8(PIC0_ICW4, 0x01  ); 

	io_out8(PIC1_ICW1, 0x11  ); 
	io_out8(PIC1_ICW2, 0x28  ); 
	io_out8(PIC1_ICW3, 2     ); 
	io_out8(PIC1_ICW4, 0x01  ); 

	io_out8(PIC0_IMR,  0xfb  ); 
	io_out8(PIC1_IMR,  0xff  ); 

	return;
}
          4、中断处理程序的制作

         中断程序处理完成后,不能执行“return;”(=RET指令),而是必须执行IRETD指令,而且这个指令还不能用C语言写,所以,还得借助汇编语言的力量修改naskfunc.nas。

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();
	}
}

EXTERN	_inthandler21
_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_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
               EXTERN指令,在调用CALL的地方再进行说明。
               PUSH :将数据压入栈顶;POP:将数据从栈顶取出。

               PUSH EAX  相当于    ADD ESP,-4      MOV   [SS:ESP],EAX    也就是说,ESP的值减去4,以所得结果作为地址值,将寄存器中的值保存到该地址所对应的内存里。

               POP  EA      相当于    MOV EAX,[SS:ESP]     ADD ESP,4   

               PUSHAD    相当于:  PUSH EAX   PUSH   ECX   PUSH  EDX   PUSH   EBX   PUSH   ESP    PUSH   EBP   PUSH   ESI   PUSH   EDI

               反过来,POPAD指令相当于按以上相反的顺序,把它们全都POP出来。

               关于DS和ES中放入SS值的部分,因为C语言自以为是地认为“DS也好,ES也好,SS也好,它们都是指同一个段”,所以如果不按照它的想法设定的话,函数inthandler21就不能顺利执行。

               如果要将这个函数注册到IDT中去,在dsctbl.c的init_gdtidt里加入以下语句。

set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); //2*8表示的是asm_inthandler21属于哪一个段,即段号是2,乘以8是因为低3位有别的意思,必须为0
             不过,号码为2的段,究竟是什么样的段呢?

set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER); //说明这个段正好覆盖了整个bootpack.hrb,最后的AR_INTGATE32将IDT的属性设为0x008e,它表示这是用于中断处理的有效设定。

             io_sti();仅仅执行STI指令,执行后,IF变为1,CPU接受来自外部设备的中断。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值