从零开始搭建环境编写操作系统 AT&T GCC (六)中断设置和启用 IDT PIC

来源:互联网 发布:网站源码下载 编辑:程序博客网 时间:2024/05/27 21:50

  连界面都已经完工了,下一步要让鼠标动起来,键盘用起来,时钟跑起来,这只是最基本的中断,我们还要使用USB,串口等等有各种各样的中断,慢慢来吧
一、什么是中断
   中断是计算机体系中一个非常重要的概念,中断的产生是因为中央处理器CPU与外部设备的性能不匹配而采取的一种提高CPU利用率的改善机制。当多个外设同时请求CPU对其进行操作时,CPU如果采用线性任务处理方式时,执行一个任务对外设进行相应操作,其它任务和外设就必须等待,一直到当前正在执行的任务完毕,CPU才能执行下一个任务并进行相应的外设操作。由于CPU的运行速度远远大于其它的外设,所以在这样的线性工作机制下CPU的利用率非常的低。于是,人们就开始为提高CPU的利用率而改进CPU的工作方式。后来便诞生了中断机制。
  中断就是让CPU中断当前的工作,转而去执行另外的任务。当任务完成,或达到某个特殊条件时,CPU再回过头来继续执行原来未完成的工作。比如:我们正在读一本书,这时电话铃声响了,我们要暂时中断读书这个任务,接起电话,等接完电话之后再回到读书这个任务中去。
  异常是当CPU在执行当前任务时,出现了一些非正常的错误,CPU则暂停当前任务,转入错误处理程序,处理完毕后再回到原任务中继续执行。比如:我们正在读一本书,在边读边翻页的过程中,一不小心把一页纸撕坏了,那么只好先中断读书这个任务,用胶水把书页粘好,再回到中断前的位置继续读书。
二、x86中断的相关术语和知识
  1、中断向量表:用过51单片机,stm32,或者做过arm裸板的小伙伴们对中断向量表这种东西一定不陌生,顾名思义,就是发生某一种中断的时候,我要到哪个函数中去执行啊?CPU当然不知道,所以我们要规定这样一个对应关系,比如说发生了1号中断,我们要到0x8000内存地址处处理这个中断,当然还有二号三号四号中断,一系列的中断我们要给他一个对应的关系,在实模式下,BIOS已经给我们配置好了一套中断向量表,就是我们设置显示模式用的int 10等,这是软中断,就是我通过程序触发的中断,其实这里的int 非常类似与jmp,但是也有很大的不同,中断的时候会把所有的寄存器入栈,什么意思呢,就是我会把所有的寄存器存储起来,保证我处理完这个中断后,可以完全恢复到我中断前的状态。有软中断当然还有硬中断,比如我敲了一下键盘,这个时候就会触发一个硬中断,会跳到键盘的处理代码处去。
  
  2、IDT:然而我们启动了保护模式,BIOS给我们配置的中断就无法使用了,我们必须要建立自己的中断向量表,x86体系给我们提供了这样的功能,那就是IDT,interrupt descriptor table,跟GDT非常非常的类似。
  
  3、PIC (Programmable Interrupt Controller):可编程中断控制器,我们有了中断向量表并没有什么卵用,我们怎么才能知道触发了第几个中断呢,这时候PIC就派上了用场,几十年不衰的经典中断器就是8259A,可能学过计算机原理的都对这个东西深恶痛绝,考试不会丫!现在多核CPU使用的都是APIC,先进可编程中断控制器,基本所有的电脑都会配置这两款中断器来保证软件的兼容性,具体可以百度 intel编程手册(这是一本5000页+的书),当然手册是最详细最清晰最严谨的了。我们现在还是使用8259A,我们看看这个芯片是怎么跟CPU链接的。(现在的8259A都集成在南桥芯片上)
  这里写图片描述
  当然这是非常非常非常简化版本,要不然我的大学计算机也不会这么惨了。
  
  4、ICW 初始化命令字:既然PIC是可编程的,我们怎么给它编程呢,这个时候就需要ICW,这一节本来就很枯燥,大家耐心看哈,一遍看不懂多看几遍,不贴图了,贴个网址
  https://baike.baidu.com/item/8259A/11048399?fr=aladdin#1
  这是8259A的百度百科。
  继续正题,x86CPU串联有两个PIC控制器,为什么串联两个呢,因为一个的中断引脚数量太少了,只有八个,不够用,所以我们可以通过触发PIC2(从)的中断,将中断传给PIC1(主),然后由PIC1传给CPU,为什么不多串几个呢,那么多干嘛,两个已经够用了。所以ICW其实就是中断器的控制指令,8259A是通过向相应的端口写入特定的ICW来实现的,主8259A对应的端口地址为0x20和0x21,从8259A对应的端口地址是0xa0和0xa1。而ICW一共有4个,每一个都是具有特定格式的字节,下面看一下4个ICW的特定格式:
  这里写图片描述
  5、OCW 操作命令字:这个是用来屏蔽中断、设定优先级和读取寄存器的命令字,同样有严格的格式规定,我们在这里只使用其屏蔽中断的功能,使用方法为修改OCW1,OCW1的每一个位对应了相应的引脚是否可以中断,0位可以中断,1位屏蔽中断
  由于8259A只有一条地址线A0,所以它只能有两个端口地址,而8259A有7个命令字,每个命令字要写入相应的寄存器。为此,采取以下几点措施:
  第一,以端口地址区分;
  第二,把命令字中的某些位作为特征码来区分;
  第三,以命令字的写入顺序来区分。
  推荐博文:http://blog.csdn.net/yxc135/article/details/8763435

  6、中断的处理过程:说了这么多,就是位为这一步打下基础的,最权威的中断处理过程在INTEL编程指南中有,但是写的略有些复杂,找到了一个简练的版本。
  一个外部中断请求信号通过中断请求线IRQ(中断管脚,见上上图),传输到IMR(中断屏蔽寄存器),IMR根据所设定的中断屏蔽字(OCW1),决定是将其丢弃还是接受。如果可以接受,则8259A将IRR(中断请求暂存寄存器)中代表此IRQ的位置位,以表示此IRQ有中断请求信号,并同时向CPU的INTR(中断请求)管脚发送一个信号。但CPU这时可能正在执行一条指令,因此CPU不会立即响应。而当这CPU正忙着执行某条指令时,还有可能有其余的IRQ线送来中断请求,这些请求都会接受IMR的挑选。如果没有被屏蔽,那么这些请求也会被放到IRR中,也即IRR中代表它们的IRQ的相应位会被置1。
  当CPU执行完一条指令时后,会检查一下INTR管脚是否有信号。如果发现有信号,就会转到中断服务,此时,CPU会立即向8259A芯片的INTA(中断应答)管脚发送一个信号。当芯片收到此信号后,判优部件开始工作,它在IRR中,挑选优先级最高的中断,将中断请求送到ISR(中断服务寄存器),也即将ISR中代表此IRQ的位置一,并将IRR中相应位置零,表明此中断正在接受CPU的处理。同时,将它的编号写入中断向量寄存器IVR的低三位(IVR正是由ICW2所指定的,不知你是否还记得ICW2的最低三位在指定时都是0,而在这里,它们被利用了!)这时,CPU还会送来第二个INTA信号,当收到此信号后,芯片将IVR中的内容,也就是此中断的中断号送上通向CPU的数据线。
  这是搬的百度百科,咱们需要用的,其实就是初始化ICW,确定每一个中断管脚IRQ对应的中断号,比如说1号管脚有中断信号,这个引脚没有屏蔽,PIC就会向CPU输出设定的中断号,比如20号,CPU就会查一下20号中断对应的处理代码在什么位置,然后跳过去。
  码字码了这么多,就是希望大家对中断有一个清晰的认识,因为这是硬件最最最重要的内容了,所有涉及硬件的编程都不会离开中断的操作。
三、具体实现方法
  1、初始化PIC芯片
  前边讲了这么多,其实就是很简单的几句代码。打开main.c,增加如下函数,别忘了把FunctionOut8这个函数声明进来,这个函数在functions.s中。

void InitPIC(){//PIC configuration://设置主8259A和从8259A    FunctionOut8(0x20,0x11);    FunctionOut8(0xa0,0x11);//设置IRQ0-IRQ7的中断向量为0x20-0x27    FunctionOut8(0x21,0x20);//设置IRQ8-IRQ15的中断向量为0x28-0x2f    FunctionOut8(0xa1,0x28);//使从片PIC2连接到主片上    FunctionOut8(0x21,0x04);    FunctionOut8(0xa1,0x02);//打开8086模式    FunctionOut8(0x21,0x01);    FunctionOut8(0xa1,0x01);//关闭IRQ0-IRQ7的0x20-0x27中断    FunctionOut8(0x21,0xff);//关闭IRQ8-IRQ15的0x28-0x2f中断    FunctionOut8(0xa1,0xff);}

  2、初始化IDT
  先看一下IDT每一项的格式
这里写图片描述
  IDT与前面所学习的GDT类似,但是格式简单多了。
     Offset 0-15:中断服务程序ISR的偏移地址0-15位。
     Selector:中断服务程序ISR的选择子。
     DPL:中断服务程序运行等级。
     P:存在标志(segment-present flag)。
     Offset 16-31:中断服务程序ISR的偏移地址16-31位。
  同样存在IDTR寄存器来保存IDT表在内存中的位置,寄存器格式同GDTR,前32位为基址,后16为表限,表示表的大小。
  怎么实现呢,我们首先创建一个函数InitIDT,然后在函数里声明一个静态结构idt_struct,结构的内容为:
  

    static struct idt_struct{    short   offset1;    short   selector;    short   no_use;    short   offset2;    } idt[0x30];//初始化0~0x30的中断

  我打算先初始化0x00~0x30的中断,所以就把这个结构体实例化了,这个结构体在内存中是线性排布的,所以我们只需要把结构体的每个参数设定好,然后把结构体的大小和首地值给IDTR工作就完成了,说起来很简单,做起来也很简单,看一下InitIDT是怎么实现的:
  

void InitIDT(){    static struct idt_struct{    short   offset1;    short   selector;    short   no_use;    short   offset2;    } idt[0x30];//初始化0~0x30的中断    int i;    ////////////#0,必须有0项    idt[0].offset1 = 0x00;    idt[0].selector = 0x00;    idt[0].no_use = 0x00;    idt[0].offset2 = 0x00;    for (i=1;i<0x30;i++)    {        idt[i].offset1 = (short)((int)(void*)DefaultIntCallBack-0x8200);        idt[i].selector = 0x0008;        idt[i].no_use = 0x8e00;        idt[i].offset2 = (short)(((int)(void*)DefaultIntCallBack-0x8200)>>16);      }    FunctionLidt(0x30*8-1,idt);}

  简单解释一下,我们首先把idt-struct的第一项完全置为零,这是芯片要求我们这么做的,然后我们进行了一个循环,把#1~#0x30全都设置为同样的值,这个值是什么呢,offset1是前16位基址,我们定义了一个回调函数,当发生中断的时候就触发我们的函数DefaultIntCallBack,我这么写是什么意思呢,在C语言中,函数名跟汇编一样同样代表了地址,但由于在lds文件中规定了代码偏移0x8200,但是我们又进入了保护模式,所以使用绝对地址的时候要减去偏移量0x8200,先把它转换成指针类型,然后等效转换位int型,最后舍掉高16位,转换成short型,赋给了offset1,offset2也是同理的,只不过向右移动16位。选择子我们暂时定为系统代码段,即GDT#1。no_use是设置p位和DPL位,我们用最高权限,有效段来进行设定。
  这样我们的表就做完了,然后我们要告诉系统我们的表在哪儿,我们自己都不知道表在哪,但是我们有指针啊,所以我们写了一个汇编函数FunctionLidt,用这个函数调用lidt命令来传入我们表的大小和首地值,看一下FunctionLidt又是怎么写的。首先我要给出FunctionLidt的参数结构
  extern void FunctionLidt(short, void *);
  void *是什么就不赘述了,不知道的可以去百第一下。
  这里的参数安排是很讲究的,GCC编译器函数传参方式前边也已经说过了,从右向左依次push入栈,最后push指针入栈,所以我要把short放在void *的后边,这样与IDTR的排列就非常的相似了,但是不要忘记push是入栈了四个字节,所以short类型前边会补齐四字节导致段限与基址并不连续,是这样子的 (从右到左地址增加)
   32位基址 0000000000000000 16位段限,所以我们只要把16位段限与16个零换过来,然后将esp+6的内存地址赋给lidt就可以了,不知道这样说能不能懂。看一下怎么实现的:
  

FunctionLidt: #void FunctionLidt(short,void *)    movw    4(%esp),    %ax    movw    %ax,        6(%esp)     lidt    6(%esp) #巧妙使用    ret

  不要忘记global一下,我们的工作就完成了。
  运行一下,切换到qemu的控制台,输入命令info registers,查看寄存器,IDT基址是0x9360,段限是0x17f
  你的基址当然可能跟我的不同,我们的程序大小是不同的
  这里写图片描述
  
  再输入 x/200xb 0x9360 看一下0x9360存的什么,太好了,我们的表格打印出来了!信息完全没有错误(注意x86是小端模式,低位放在低地址)
  这里写图片描述
  我们看一下表格给的回调函数,就是发生中断时调用的函数地址是0x04d9 但应该加上0x8200,
  输入命令x/200i 0x86c9 反汇编一下看看这里是什么
  先看看我的回调函数里写了什么:
  

void DefaultIntCallBack()//回调函数{    PutString(100, 100,"There Is An INT!!!!\0",0xffffff);}

  输入一段话,编译的话应该可以看到push0xffffff的传参过程,看看0x86c1+0x8200处有没有呢,成功!
  这里写图片描述
  
  所有的理论都通过了,剩下的就可以享受结果了。
  FunctionLidt后边加个sti允许中断,然后在SysMain中写一个5/0,会触发int00除零中断(我会把系统中断表放到下边)

FunctionLidt: #void FunctionLidt(short,void *)    movw    4(%esp),    %ax    movw    %ax,        6(%esp)     lidt    6(%esp) #巧妙使用    sti    ret

 这里写图片描述
 
  中断函数被调用,输出了一段文字 This Is An INT!!!
  这节总算结束了,下节整理一下代码,然后继续让鼠标和键盘动起来
  系统中断表:
  这里写图片描述

阅读全文
1 0
原创粉丝点击