从零开始搭建环境编写操作系统 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!!!
这节总算结束了,下节整理一下代码,然后继续让鼠标和键盘动起来
系统中断表:
- 从零开始搭建环境编写操作系统 AT&T GCC (六)中断设置和启用 IDT PIC
- 从零开始搭建环境编写操作系统 AT&T GCC (一)搭建环境和测试环境
- 从零开始搭建环境编写操作系统 AT&T GCC (五)显示鼠标和字符
- 从零开始搭建环境编写操作系统 AT&T GCC (七)GDB调试和-monitor
- 从零开始搭建环境编写操作系统 AT&T GCC (八)使用键盘和滚轮鼠标
- 从零开始搭建环境编写操作系统 AT&T GCC (二)从实模式到保护模式
- 从零开始搭建环境编写操作系统 AT&T GCC (三)引入C语言
- 从零开始搭建环境编写操作系统 AT&T GCC (四)绘制界面
- 从零开始搭建环境编写操作系统 AT&T GCC (十)多任务
- 从零开始搭建环境编写操作系统 AT&T GCC (九)内存管理
- 操作系统之GDT和IDT(三)
- 【Linux操作系统分析】中断和异常(1)——中断描述符表IDT,I/O中断处理,中断向量
- PIC单片机开发环境搭建
- AT&T 汇编和 GCC 内联汇编简介
- GCC编译at&t汇编
- 中断向量表和中断描述符表IDT
- 第8章 设置IDT(中断描述符表)
- Robotframework环境搭建六:设置日志目录
- 函数指针和指针函数
- 博弈论模型(威佐夫博弈)
- Android SurfaceView相关概念及原理
- ip地址掩码和位数对应关系表、子网掩码、网络地址、主机地址-yellowcong
- 求带环链表的入口的多种解法
- 从零开始搭建环境编写操作系统 AT&T GCC (六)中断设置和启用 IDT PIC
- Linux系统目录结构以及简单说明
- spring-boot
- Matlab线性规划实例
- 【博弈+找规律】HDU_4642_Fliping game
- 管道(pipe、mkfifo)
- PhpStorm中如何使用Xdebug工具
- Ant 使用教程
- Python,Pycharm,Anaconda等的关系与安装过程~为初学者跳过各种坑