第7章-中断
这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件
7.2操作系统是中断驱动的
没有中断,操作系统几乎什么都做不了,操作系统是中断驱动。
7.3中断分类
7.3.1外部中断
外部中断是指来自 CPU 外部的中断,而外部的中断源必须是某个硬件,所以外部中断又称为**硬件中断。**CPU 提供统一的接口作为中断信号的公共线路,所有来自外设的中断信号都共享公共线路连接到 CPU 。CPU 为大家提供了两条信号线。外部硬件的中断是通过两根信号线通知 CPU 的,这两根信号线就是INTR (INTeRrupt)和 NMI (Non Maskable Interrupt)。
可屏蔽中断是通过INTR 引脚进入 CPU 的,可以通过 eflags寄存器的 IF 位将所有这些外部设备的中断屏蔽。不可屏蔽中断是通过 NMI 引脚进入 CPU 的,它表示系统中发生了致命的错误。统统为导致看机的各种原因分配一个中断向量号就足够了,所以不可屏蔽中断的中断向量号为 2.
把中断处理程序分为上半部和下半部两部分,把中断处理程序中需要立即执行的部分(分分钟不能耽误的部分)划分到上半部,这部分是要限时执行的,所以通常情况下只完成中断应答或硬件复位等重要紧迫的工作。 而中断处理程序中那些不紧急的部分则被推迟到下半部中去完成。由于中断处理程序的上半部是刻不容缓要执行的,所以上半部是在关中断不被打扰的情况下执行的。 下半部也属于中断处理程序,所以中断处理程序下半部则是在开中断的情况下执行的,如果有新的中断发生,原来这个旧中断的下半部就会被换下 CPU,先执行新的中断处理程序的上半部 ,后面再调度其上 CPU 完成其下半部的执行。
7.3.2内部中断
内部中断分为:软中断和异常
软中断,就是由软件主动发起的中断,因为它来自于软件,所以称之为软中断;异常是另一种内部中断,是指令执行期间 CPU 内部产生的错误引起的。
7.4中断描述符
中断描述符表( Interrupt Descriptor Table, IDT)是保护模式下用于存储中断处理程序入口的表,当
CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的
起始地址,然后执行中断处理程序。 实模式下用于存储中断处理程序入口的表叫中断向量表( Interrupt Vector Table, IVT )
任务门和任务状态段( TSS )是 Intel 处理器在硬件一级提供的任务切换机制,所以任务门需要和 TSS 配合在一起使用,在任务门中记录的是 TSS 选择子,偏移量未使用。任务门可以存在于全局描述符表 GDT、局部描述符表 LDT、中断描述符表 IDT 中。描述符中任务门的 type 值为二进制 0101 。
**中断门包含了中断处理程序所在段的段选择子和段内偏移地址。**当通过此方式进入中断后,标志寄存器 eflags
中的的IF位自动置0 ,也就是在进入中断后,自动把中断关闭,避免中断嵌套。 Linux 就是利用中断门实现的系统调用,就是那个著名的 int 0x80
。中断门只允许存在于 IDT 中。描述符中中断门的 type 值 为二进制 1110。
陷阱门和中断门非常相似,区别是由陷阱门进入中断后,标志寄存器 eflags
中的IF位不会自动置 0。陷阱门只允许存在于 IDT 中。描述符中陆阱门的 type 值为二进制 1111 。
调用门是提供给用户进程进入特权0级的方式,其 DPL 为3。调用门中记录例程的地址,它不能用int 指令调用,只能用 call 和 jmp 指令。调用门可以安装在 GDT 和 LDT 中。描述符中调用门的 type 值为二进制 1100。
位于地址。0~0x3ff 的是中断向量表 IVT,它是实模式下用于存储中断处理程序入口的表 。
对比中断向量表,中断描述符表有两个区别 。
- 中断描述符表地址不限制,在哪里都可以 。
- 中断描述符表中的每个描述符用 8 字节描述。
在 CPU 内部有个中断描述符表寄存器( Interrupt Descriptor Table Register, IDTR),该寄存器分为两
部分:第 0~ 15 位是表界眼,即 IDT 大小减 1 ,第 16~47 位是 IDT 的基地址 。只有寄存器 IDTR指向了 IDT,当 CPU 接收到中断向量号时才能找到中断向量处理程序,这样中断系统才能正常运作 。
同加载GDT一样,加载IDTR也有一个专用指令—lidt
,lidt 48位内存数据
。
指令cli 使 IF 位为0 ,这称为关中断,指令 sti 使IF位为1 ,这称为开中断。
为什么 CPU 执行完旧任务后还能回到新任务呢?原因是在执行新任务之前, CPU 做了两件准备工作 。
- 将旧任务 TSS 选择子写到了新任务 TSS 中的“上一个任务 TSS 的指针”字段中 。
- 将新任务标志寄存器 eflags 中的 NT 位置 l ,表示新任务之所以能够执行,是因为有别的任务调
用了它。
CPU 把新任务执行完后还是要回去继续执行旧任务的,怎样回到旧任务呢?这也是通过iret指令。 iret 指令因此有了两个功能, 一是从中断返回,另外一个就是返回到调用自己执行的那个旧任务,这也相当于执行一个任务。 当 CPU 执行 iret 时,它会去检查 NT 位的值,如果 NT 位为 1 ,这说明当前任务是被嵌套执行的,因此会从自己 TSS 中“上一个任务 TSS 的指针”字段中获取旧任务,然后去执行该任务。如果 NT 位的值为 0,这表示当前是在中断处理环境下,于是就执行正常的中断退出流程。
7.4.2 中断发生时的压栈
1.有特权级变化
- 处理器根据中断向量号找到对应的中断描述符后,拿 CPL 和中断门描述符中选择子对应的目标代码段的 DPL 比对,若 CPL 权限比 DPL 低,即数值上 CPL>DPL,这表示要向高特权级转移,需要切换到高特权级的栈。 于是处理器先临时保存当前栈SS和 ESP 的值,记作 SS_old和 ESP_old,然后再TSS中找到目标代码的SS和ESP,然后把old存入栈中
- 在新棋中压入 EFLAGS 寄存器
- 由于要切换到目标代码段,对于这种段间转移,要将 cs 和 EIP 保存到 当前枝中备份,记作 CS_old和EIP_old,以便中断程序执行结束后能恢复到被中断的进程.
- 某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息, 一会介绍。错误码会紧跟在 EIP 之后入拢,记作 ERROR_CODE
2.没有特权级变化
7.5可编程中断控制器8259A
IC4:
表示是否要写入ICW4
SNGL:
若为1表示单片,否则为级联,当 SNGL 为 0 时,主片和从片也是需要 ICW3 的
ADI:
用来设置 8085 的调用时间间隔, x86 不需要设置
LTIM:
用来设置中断检测方式, LTIM 为 0 表示边沿触发, LTIM 为 1表示电平触发。
7.6编写中断处理程序
7.6.1从最简单的中断处理程序开始
- 用汇编语言实现中断处理程序
代码分析:
1、代码功能
创建33个中断处理函数
2、实现原理
中断信号进入中断控制器进行处理之后,会被分配中断号,通过特定的中断号码,可以调用特地的中断处理程序去处理。0—19中断号为处理器内部固定的异常类型,20-31是Intel保留的。同时为了演示中断机理,写一个时钟中断处理程序,所以共33个。
3、代码逻辑
定义33个中断处理程序,每个程序包含处理部分与本程序的地址
4、怎么写代码?
A、定义没有压入错误码但为了统一管理需要压入0的宏参数;定义要压入错误码所以我们什么都不做的宏参数
B、定义一个文本段,里面放着要打印的字符串信息,然后定义一个标号(就在文本段下方)。由于编译器的特性,会将同一类型的SECTION组合成一个大的SEGMENT,所以D中调用宏所形成的每个中断处理程序中的入口地址部分(这个入口地址会被定义成文本段)会统一聚集在这个要打印的字符串这里(因为它是被定义成了文本段),也就是字符串信息下面的标号处,于是这个标号便可以管理所有的中断处理程序地址
C、定义一个中断处理程序宏,宏中包含程序段:程序处理部分(打印字符串信息)、文本段:本程序的入口地址部分
D、调用C定义的宏实现33个中断处理程序的定义(传不同的参数),要理清楚哪些中断要压入错误码,哪些中断不会压入错误码。不压入错误码的我们压入一串0,这样能实现中断统一定义
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了;错误码,为了保存栈中格式统一,这里不做操作
%define ZERO push 0 ;若在相关的异常中CPU没有压入错误码;为了统一栈中的格式,就手动压入一个0
extern put_str ;声明外部函数
section .data
intr_str db "interrupt occur!", 0xa ,0
global inter_entry_table
inter_entry_table:%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理函数都要压入中断向量号;所以一个中断类型一个中断处理程序;自己知道自己的中断向量号是多少%2push intr_strcall put_stradd esp ,4 ;跳过参数;若果是从片上进入的中断,除了往片上发生EOI外,还要往住主片上发送EOImov al, 0x20 ;中断结束命令EOIout 0xa0, al ;向从片发送out 0x20, al ;向主片发送add esp, 4 ;跨越error_codeiret ;从中断返回,32位下等同指令iretdsection .datadd intr%1entry ;存储各个中断入口程序的地址;形成intr_entry_table数组
%endmacroVECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0A,ZERO
VECTOR 0x0B,ZERO
VECTOR 0x0C,ZERO
VECTOR 0x0D,ZERO
VECTOR 0x0E,ZERO
VECTOR 0x0F,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1A,ZERO
VECTOR 0x1B,ZERO
VECTOR 0x1C,ZERO
VECTOR 0x1D,ZERO
VECTOR 0x1E,ERROR_CODE
VECTOR 0x1F,ZERO
VECTOR 0x20,ZERO
每个中断处理程序都一样,就是调用字符串打印函数 put_str 来打印宇符串“ interrupt occur!”,之后直接退出中断。
2.创建中断描述符表 IDT ,安装中断处理程序
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"#define IDT_DESC_CNT 0x21 //目前总共支持的中断数
#define PIC_M_CTRL 0x20 //主片控制段口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1/*中断门描述符结构体*/
struct gate_desc {uint16_t func_offset_low_word; //目标代码段内的偏移量的15-0位uint16_t selector; //目标代码段描述符选择子uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节//此项固定值,不用考虑uint8_t attribute;uint16_t func_offset_high_word; //目标代码段内的偏移量的31-16位
};//j静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr, intr_handler function);
static struct gate_desc idtr[IDT_DESC_CNT]; //idt是中断描述符表//本质上就是一个中断门描述符数组
extern intr_handler inter_entry_table[IDT_DESC_CNT]; //声明引用定义在kernel.s中的终端处理函数入口数组/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr, intr_handler function){p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;p_gdesc->selector = SELECTOR_K_CODE;p_gdesc->dcount = 0;p_gdesc->attribute = attr;p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}/*初始化中断描述符表*/
static void idt_desc_init(void){int i;for(i=0;i<IDT_DESC_CNT;i++){make_idt_desc(&idtr[i],IDT_DESC_ATTR_DPL0,inter_entry_table[i]);}put_str(" idt_desc_init done\n");
}/*初始化可编程中断控制器8259A*//*完成有关终端的所有初始化工作*/
void idt_init(){put_str("idt_init start\n");idt_desc_init(); //初始化中断描述符表pic_init(); //初始化8259A/*加载idt*/uint64_t idt_operand = ((sizeof(idtr)-1) | ((uint64_t)(uint32_t)idtr<<16));asm volatile("lidt %0":: "m"(idt_operand));put_str("idt_init done\n");
}
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3#define TI_GDT 0
#define TI_LDT 1#define SELECTOR_K_CODE ((1<<3)+(TI_GDT<<2)+RPL0)
#define SELECTOR_K_DATA ((2<<3)+(TI_GDT<<2)+RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3<<3)+(TI_GDT<<2)+RPL0)/*-----------------------IDT描述符属性------------------*/
#define IDT_DESC_P 1 //有效位
#define IDT_DESC_DPL0 0 //描述符特权级别
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32的门
#define IDT_DESC_16_TYPE 0x0 //16位的门
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7)+(IDT_DESC_DPL0<<5)+IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7)+(IDT_DESC_DPL3<<5)+IDT_DESC_32_TYPE)
#endif // !__KERNEL_GLOBAL_H
3.用内联汇编实现端口 I/O 函数
这里是辅助实现8295A的
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-04-01 11:50:11* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-01 16:06:16* @FilePath: /OS/chapter7/7.6/lib/kernel/io.h* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
/***********************机器模式********************* b 一输出寄存器 QImode 名称,即寄存器中的最低 8 位:[a-d]l* w 一输出寄存器 HImode 名称,即寄存器中 个字节的部分,如[a-d]x* * HImode* "Half-Integer"模式,表示一个两字节的整数* QImode* “Quarter—Integer”模式,表示一个字节的整数* ***************************************************/#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"/*向端口port写入一个字节*/
static inline void outb(uint16_t port,uint8_t data){/** 对端口制定N表示0-255,d表示用dx存储端口号,* %b0表示对应al,%wl表示对于的dx*/asm volatile ("out %b0,%w1": : "a" (data), "Nd" (port));
}/*将addr处起始的word_cnt个字写入端口port*/
static inline void outsw(uint16_t port,const void* addr,uint32_t word_cnt){/********** +表示此限制即做输入,又做输出* outsw是吧ds:esi处的16位的内容写入port端口,我们在设置短描述符时,这里 outsw 将从 ds:esi 处读取一个 16 位字,并将其写入到 port 端口。* 已经将ds,es,ss断电选择子都设置为想通的值了,此时不用担心数据错乱*/asm volatile ("cld; \rep outsw":"+S"(addr),"+c"(word_cnt):"d"(port));
}/*从端口port读入的一个字节返回*/
static inline uint8_t inb(uint16_t port){uint8_t data;//asm volatile ("inb %w1, %b0":"=a"(data),"Nd"(port));asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port));return data;
}/*将从端口port读入的word_cnt个字节写入addr*/
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt){/****** insw是将从端口port处读入的16位内容写入es:edi指向的内存,* 我们在设置段描述符,已经将ds,es,ss段的选择子都设置为想通的值了,* 此时不用担心数据错乱。*/asm volatile ("cld; \rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
}
#endif // !__LIB_IO_H
4.设置8295A
static void pic_init(void){/*初始化主片*/outb (PIC_M_CTRL, 0x11); //ICW1:边沿触发,级联8259,需要ICW4outb (PIC_M_DATA, 0x20); //起始中断向量号为0x20,也就是IR【0-7】,为0x20-0x27outb (PIC_M_DATA, 0x04); //ICW3:IR2接从片outb (PIC_M_DATA, 0x01); //ICW4:8086模式,正常EOI/*初始化从片*/outb (PIC_S_CTRL, 0x11); //ICW1:边沿触发,级联8259,需要ICW4outb (PIC_S_DATA, 0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28-0x2Foutb (PIC_S_DATA, 0x02); //ICW3:设置从片连接到主片的IR2引脚outb (PIC_M_DATA, 0x01); //ICW4:8086模式,正常EOI/*打开主片上IR0,也就是目前只接受时钟产生的中断*/outb (PIC_M_DATA, 0xfe); //即第 0 位为 0,表示不屏蔽剧的时钟中断。其他位都是 1 ,表示都屏蔽。outb (PIC_S_DATA, 0xff);put_str(" pic_init done\n");
}
设置主片和从片 8259A,无论是主片,还是从片,都必须按顺序依次写入 ICW1、ICW2、 ICW3 、 ICW4。
5.开启中断
/** @Author: Adward-DYX 1654783946@qq.com* @Date: 2024-03-26 10:04:44* @LastEditors: Adward-DYX 1654783946@qq.com* @LastEditTime: 2024-04-01 15:40:17* @FilePath: /OS/chapter6/6.3.4/kernel/main.c* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE*/
#include "print.h"
#include "init.h"
void main(void){put_str("I am kernel\n");init_all();asm volatile("sti"); //为了演示中断处理,在此临时开中断,这个吧eflags寄存器中IF位置为1while(1);
}
7.6.2改进中断处理程序
用C语言定义中断处理函数,然后形成一个中断入口地址表。按照之前定义好的中断处理机制,当中断发生时,系统会用中断号找到对应的中段描述符,然后跳往汇编中我们定义好的中断处理函数(之前是打印字符串)。现在修改这个汇编中的中断处理函数,让其只是做简单处理(如保存程序执行环境),然后调用之前C中形成的中断入口地址表找到C中定义的中断处理函数去执行。汇编中的中断处理部分与C中的中断处理部分关系如下:
在interrupt.c中
A、建立一个地址数组用来存储中断处理函数的地址,一个字符串指针数组用来存储中断的名字
B、我们先写一个通用的中断处理函数,功能是打印中断向量号,参数是中断号码
C、将地址数组中每个元素初始化为B写的中断处理函数地址(后面再改);将字符串指针数组对照中断号填入中断名字(书p303),并封装成一个函数execption_init()
D、在idt_init(用来完成中断初始化的函数)中,调用exception_init;
在kernel.S中
A、引入interrupt.c中定义的中断处理程序地址数组
B、中断处理函数模板中删除打印字符串的部分
C、中断处理函数模板中加入保存上下文代码
D、中断处理函数模板中加入压入中断号代码(方便调试)与调用C中形成的中断地址数组找到中断处理函数地址的代码
E、定义恢复上下文环境并退出中断的代码,并在D中调用
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT];
//定义中断处理程序数组,在kernel.s中定义的intrXXentry
//只是中断处理程序的入口,最终调用的是ide_table中的处理程序
extern intr_handler inter_entry_table[IDT_DESC_CNT]; //声明引用定义在kernel.s中的终端处理函数入口数组/*通用的中断处理函数,一般用在异常出现的处理*/
static void general_intr_handler(uint8_t vec_nr){if(vec_nr==0x27||vec_nr==0x2f) return ;//IRQ7和IRQ15会产生伪中断,无需处理,0x2f是从片8259A上的最后一个IRQ引脚,保留项put_str("int vector:0x");put_int(vec_nr);put_char('\n');
}/*完成一般中断处理函数注册及异常名称注册*/
static void exception_init(void){int i;for(i=0;i<IDT_DESC_CNT;i++){/*idt_table数组中的函数是在进入中断后更具中断向量号调用的*//*见kernel.s的call [idt_table+1*4]*/idt_table[i] = general_intr_handler; //默认为这个函数//以后由register_handler来注册具体处理函数intr_name[i] = "unknown"; //先统一赋值为unknown}intr_name[0] = "#DE Divide Error";intr_name[1] = "#DB Debug Exception";intr_name[2] = "NMI Interrupt";intr_name[3] = "#BP Breakpoint ExceptIon";intr_name[4] = "#OF Overflow ExceptIon ";intr_name[5] = "#BR BOUND Range Exceeded Exception";intr_name[6] = "#UD Invalid Opcode Exception";intr_name[7] = "#NM Device Not Available Exception";intr_name[8] = "#DF Double Fault Exception";intr_name[9] = "Coprocessor Segment Overrun";intr_name[10] = "#TS 工nvalid TSS Exception";intr_name[11] = "#NP Segment Not Present";intr_name[12] = "#SS Stack Fault Exception";intr_name[13] = "#GP General Protection Exception";intr_name[14] = "#PF Page-Fault Exception";//intr_name[l5 ]第 15 项是 intel 保留项,未使用intr_name[16] = "#MF x87 FPU Floating-Point Error";intr_name[17] = "#AC Alignment Check Exception";intr_name[18] = "#MC Machine-Check Exception";intr_name[19] = "#XF SIMD Floating-Point Exception";
}/*完成有关终端的所有初始化工作*/
void idt_init(){put_str("idt_init start\n");idt_desc_init(); //初始化中断描述符表exception_init(); //异常名初始化并注册通常的中断处理函数pic_init(); //初始化8259A/*加载idt*/uint64_t idt_operand = ((sizeof(idtr)-1) | ((uint64_t)(uint32_t)idtr<<16));asm volatile("lidt %0":: "m"(idt_operand));put_str("idt_init done\n");
}
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了;错误码,为了保存栈中格式统一,这里不做操作
%define ZERO push 0 ;若在相关的异常中CPU没有压入错误码;为了统一栈中的格式,就手动压入一个0
extern put_str ;声明外部函数
extern idt_tablesection .data
global inter_entry_table
inter_entry_table:%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理函数都要压入中断向量号;所以一个中断类型一个中断处理程序;自己知道自己的中断向量号是多少%2;以下是保持上下问环境push dspush espush fspush gspushad ;PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI,EAX最先入栈;如果是从片上进入中断,除了往片上发送EOI,还要往主片上发送EOImov al, 0x20 ;中断结束命令EOIout 0xa0, al ;向从片发送out 0x20, al ;向主片发送push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量,调试时更加方便call [idt_table+%1*4]; 调用idt_table中的C版本中断处理函数jmp intr_exitsection .datadd intr%1entry ;存储各个中断入口程序的地址;形成intr_entry_table数组
%endmacrosection .text
global intr_exit
intr_exit:;以下是恢复上下文环境add esp, 4 ;跳过中断号popadpop gspop fspop espop dsadd esp, 4 ;跳过error_codeiretd
7.6.3调试实战:处理器进入中断时压栈出栈完整过程
帮助大家揭开“中断发生前处理器自动压栈、中断处理程序在椅中保护进程上下文,以及从中断返回时 iret 指令弹栈”这三个阶段所使用的栈,其中的数据是怎样变化的。
当发生中断时,处理器会自动压入eflags、cs、eip、error_code(如果有的话)。结合我们之前的代码,当中断发生时,会压入如下值在栈空间中:
7.7可编程计数器、定时器8253简介
7.7.1时钟–给设备打拍子
为了使所有设备之间的通信井然有序,各通信设备间必须有统一的节奏,不能各干各的,这个节奏就称为定时或时钟。时钟只是一种时间的度量,只是一种节奏。
计算机中的时钟,大致上可分为两大类:内部时钟和外部时钟。
内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频之后就是主板的外频,处理器和南北桥之间的通信就基于外频。 Intel 处理器将此外频乘以某个倍数(也称为倍频)之后便称为主频。 处理器取指令、执行指令中所消耗的时钟周期,都是基于主频的。
外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序,比如 IO 接口和处理器之间在 AID 转换时的工作时序、两个串口设备之间进行数据传输时也要事先同步时钟等 。
我们就要考虑处理器与外部设备间同步数据时的时序配合问题,如何保证运行在不同时钟节拍下的设备能够同步通信 。 解决这个问题的大体思路是: 应以处理器的内部时钟为依据来设计外部设备的时钟,既要符合处理器内部运行时序的规定,又要满足外部设备工作时序的要求。
定时器可分为不可编程定时器及可编程定时器两种,我们要接触的是第二种可编程定时器 PIC ,即
Programmable Interval Timer.
7.7.2 8253入门
在 8253 内部有 3 个独立的计数器,分别是计数器0~计数器 2,它们的端口分别是 0x40~0x42。计数器又称为通道,每个计数器都完全相同,都是 16 位大小 。各个计数器都有自己的一套寄存器资源,工作时自己用自己的,互不干涉。寄存器资源包括一个 16 位的计数初值寄存器、一个计数器执行部件和一个输出锁存器。
每个计数器都有三个引脚: CLK, GATE, OUT。
- CLK:表示时钟输入信号,即计数器自己工作的节拍,也就是计数器自己的时钟频率。每当此引
脚收到一个时钟信号,减法计数器就将计数值减 1 。 - GATE:表示门控输入信号,在某些工作方式下用于控制计数器是否可以开始计数
- OUT:示计数器输出信号。当定时工作结束,也就是计数值为0时,根据计数器的工作方式,
会在 OUT 引脚上输出相应的信号 。 此信号用来通知处理器或某个设备 : 定时完成。
计数初值寄存器、计数器执行部件和输出锁存器都是 16 位宽度的寄存器,所以高 8 位和低 8 位都可以单独访问。
7.7.3 8253控制字
控制宇寄存器,其操作端口是 0x43 ,它是 8 位大小的寄存器。
7.7.4 8253工作方式
计数器r开始计数需要两个条件:
- GATE 为高电平,即GATE为 1 ,这是由硬件来控制的。
- 计数初值己写入计数器中的减法计数器,这是由软件 out 指令控制的。
当这两个条件具备后,计数器将在下一个时钟信号 CLK 的下降沿开始计数
以上这两个条件,按照“哪个未完成”来划分,可分为软件启动和硬件启动。
- 软件启动
软件启动是指上面硬件负责的条件1已经完成,也就是 GATE 己经为 1 ,目前只差软件来完成条件 2,即尚未写入计数初值,只要软件负责的条件准备好,计数器就开始启动。当处理器用 out 指令往计数器写入计数初值,减法器将此初值加载后,计数器便开始计数。工作方式。1、 2、 3 、 4 都是用软件启动计数过程。
- 硬件启动
硬件启动是指上面软件负责的条件 2己经完成,即计数初值己写入计数器。目前只差硬件来完成条件1了,也就是门控信号 GATE 目前还是低电平,即目前 GATE=O ,只要硬件负责的条件准备好,计数器就开始启动 。GATE 引脚是由外部信号来控制的,只有当 GATE 由 0变1的上升沿出现时,计数器才开始启动计数。工作方式 1 、 5都是用硬件启动计数过程。
根据不同的工作方式,分为强制终止和自动终止 。
- 强制终止
有些工作方式中,计数器是重复计数的,当计时到期(计数值为 0 ) 后,减法计数器又会重新把计数初值寄存器中的值重新载入,继续下一轮计数,比如工作方式 2 和工作方式 3 都是采用此方式计数,此方式常见于需要周期性发信号的场合。对于采用此类循环计数工作方式的计数器,只能通过外加控制信号来将其计数过程终止,办法是破坏启动计数的条件:将 GATE置为 0 即可。
- 自动终止
有些工作方式中,计数器是单次计数,只要定时(计数) 一到期就停止,不再进行下一轮计数,所以计数过程自然就自动终止了。比如工作方式 0 、 1 、 4、 5 都是单次计数,完成后自动终止。
7.7.5 8253初始化步骤
1.往控制字寄存器端口 0x43 中写入控制字
用控制字为指定使用的计数器设置控制模式,控制模式包括该计数器工作时采用的工作方式、读写格
式及数制。
2.在所指定使用的计数器端口中写入计数初值
计数初值要写入所使用的计数器所在的端口,即若使用计数器0,就要把计数初值往 0x40 端口写入,若使用的是计数器 1 ,就要把计数初值往 0x41 端口写入,依次类推 。 计数初值寄存器是 16 位,高 8 位和低 8 位可单独使用,所以初值是 8 位或 16 位皆可。若初值是 8 位,直接往计数器端口写入即可 。 若初值为 16 位,必须分两次来写入,先写低 8 位,再写高 8 位。
7.8提高时钟中断的频率,让中断来的更猛烈一点
坦白说,我们学习 8253 的目的就是为了给 IRQO 引脚上的时钟中断信号“提速”,使其发出的中断信
号频率快一些。它默认的频率是 18.206Hz,即一秒内大约发出 18 次中断信号。我们嫌它有点慢,本节我
们将对 8253 编程,使时钟一秒内发 100 次中断信号,即中断信号频率为 100Hz.
- IRQ0 引脚上的时钟中断信号频率是由 8253 的计数器0设置的,我们要使用计数器 0。
- 时钟发出的中断信号不能只发一次,必须是周期性发出的,也就是我们要采取循环计数的工作方式, 可边的工作方式为方式 2 和方式 3,这里咱们就选择方式 2,这是标准的分频方式
- 计数器发出输出信号的频率是由计数初值决定的,所以我们要为计数器0赋予合适的计数初值。
1193180/中断信号的频率=计数器0的初始计数值
#include "timer.h"
#include "io.h"
#include "print.h"#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40 //是计数器 0 的端口号 Ox40
#define COUNTER0_NO 0 //用来在控制字中指定所使用的计数器号码,对应于控制字中的 SCI 和 SC2 位
#define COUNTER_MODE 2 //计数器的工作方式,比率发生器。
#define READ_WRITE_LATCH 3 //设置计数器的读/写/锁存方
#define PIT_CONTROL_PORT 0x43/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */
static void frequency_set(uint8_t counter_port, \uint8_t counter_no, \uint8_t rwl, \uint8_t counter_mode, \uint16_t counter_value) {
/* 往控制字寄存器端口0x43中写入控制字 */outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入counter_value的低8位 */outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */outb(counter_port, (uint8_t)(counter_value >> 8));
}/* 初始化PIT8253 */
void timer_init() {put_str("timer_init start\n");/* 设置8253的定时周期,也就是发中断的周期 */frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);put_str("timer_init done\n");
}
注意:在时钟频率高于4031HZ,就会出现GP中断,中断号为D,书上的代码由于疏忽**outb**(counter_port, (uint8_t)counter_value >> 8);
写入的counter的初值高字节全是0,实际时钟频率会由7000HZ多 ,所以之前才出现了这个GP中断打印为0xD现象。