第6天:分割编译与中断处理
1、内容1:分割源文件
因为之前的bookpack.c程序已经很长了,因此就将其进行切割成几个部分,分别是graphic.c、dsctbl.c和bootpack.c三个部分。这三个部分依次是关于画图的处理、关于GDT、IDT等descriptor table的处理以及其他部分的处理。
但是如果graphic.c也想使用naskfunc.nas函数的话,必须加上void io_out8(int port,int data)函数声明。虽然这些都已经写在了bootpack.c函数中了,但是在编译器编译graphic.c时,程序是不知道有bootpack.c的存在的。
既然修改了bootpack.c文件,那么也需要根据其来修改我们的Makefile文件,相应的流程为:
修改之后的文件夹内容为:
其中makefile文件的内容增加了如下部分:
2、内容2:整理Makefile
虽然之前bootpack.c文件已经分割好了,变得简洁了许多,但是相比较而言,Makefile文件却变得十分繁琐亢长。因为有些指令都做的是相同的事情,但如果每次添加源文件都需要增加很多类似的编译规则就会变得很麻烦,因此需要利用一般规则,将雷同的独立的文件生成规则,归纳为一般规则。
将以上6个独立的文件生成规则,归纳为两个一般的规则。
那么make.exe的新流程又是怎样的呢?
Make.exe首先会寻找普通的生成规则,如果没找到,那么就尝试使用一般规则。即使一般规则和普通的生成规则发生冲突,也不会有什么问题。因为这时候,普通生成规则的优先级是要大于一般规则的优先级的。
3、内容3:整理头文件
虽然之前将bootpack.c文件进行了切割,但是总的行数反而比切割之前的行数还要多,这是因为各个源文件都需要重复声明void io_out8这个函数。如果想要更加精简的话,那么首先就是要将重复的部分删去。可以将之前大部分的重复部分全部归纳到bootpack.h文件。
.h文件大概如图所示。在这个文件中,不仅罗列了函数的定义,还在注释中写明了函数的定义是在哪一个源文件中,这样的话,如果想要修改函数定义的话,只需要看一下该文件就知道函数文件在哪个源文件中。
如果想要编译graphic.c文件时,只需要让编译器去读bootpack.h文件即可。
只需要在开头加上# include “bootpack.h"即可。当编译器读到这一行时,,就会将这一行替换成所指定文件的内容,然后再进行编译。这样bootpack.h的所有内容就会间接的写到graphic.c中。同理可以在其他文件中增加上这一行。
4、内容4:意犹未尽
这个函数是用来将指定的段上限(limit)和地址值赋值给名为GDTR的48位寄存器。但这个寄存器不能直接用MOV指令来赋值,要给他赋值的话,唯一的一个方法就是通过LGDT指令来指定一个内存地址,从指定的地址读取6个字节(也就是48位),然后赋值给GDTR寄存器。该寄存器的低16位(即内存的最初2个字节)是段上限,它等于GDT的有效字节数-1。剩下的高32位(即剩余的4个字节),代表GDT的开始地址。
在一开始执行这个函数的时候,DWORD[ESP+4]中存放的是段上限,DWORD[ESP+8]里存放的是地址,其中段上限和地址分别为0x0000ffff和0x00270000。
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的模式。
为什么要分成3段呢?
主要是为了与80286时代的程序兼容。
段上限表示一个段有多少个字节,但段上限最多也就4GB,也就是32位的数值,如果直接放入到结构体中,再加上基址,一共要8个字节,这样就将结构体占满了这样就没有地方保存段的管理信息了。
因此段上限只能使用20位,但在段的属性中设了一个标志位Gbit,当这个标志位为1时,上限的单位被解释为页,而1页指的是4KB,因此4KBX1M=4GB,这样就可以指定4GB的段了。
段属性也就是段的访问权属性,其是12位的,在程序中用变量名access_right或ar来表示。因为12位段属性中那个的高4位放在limit_high的高4位中,所以程序将ar当作如下16位来构成处理:
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执行中断处理程序,输出信息。
实验结果: