我对中断的理解:
中断,顾名思义,就是在遇到特殊的情况时,停下当前正在做的事情,转去干其他(根据特殊情况预先设计好的)事情,完成后(并不一定非要)再继续之前做的事情。
首先说说“去干其他(根据特殊情况预先设计好的)事情”。
我们可以预先设计好256件事情(处理函数),然后使用0~255这256个编号(中断向量号)表示代指。“事情”根据是否可以再次打断(中断),分为中断门(不可以被打断)和陷阱门(可以再次被打断)。
下面解释一下所谓的“遇到特殊的情况”,这个特殊的情况按照触发类型可以分为两类:
自动产生:自动产生的意思就是程序自己根据需要自己主动触发的中断,也就是使用int指令+中断号触发的。对于所有的256个中断,都可以使用int指令触发。
被动产生:被动产生,就是系统硬性规定的,在遇到某种指定的情况,就触发对应中断向量号的中断。
具体而言如下表:(摘自《自己动手写内核系列_skelix》天衣有缝,有稍微修改)
需要说明的有两点:
一.0x11~0x19这12个中断向量号, Intel预定了,但是并没有实际使用上,但将来可能会用到。因此我们可以在编写操作系统的使用直接拿来用,但是这样会有一定风险,要是Intel的下一代产品里使用了这些中断向量号,我们想要兼容它,就不得不修改代码将这几个中断向量号让出来。
二.上表中触发的原因,基本都是程序运行时发生错误产生的(0x02号中断除外),可以称之为内部中断。与之相对应的称作外部中断的东西,就是由于计算机硬件(键盘,鼠标等)触发的。这些中断是可以屏蔽掉(忽略掉)的。想要响应这些中断,也需要分配相应的中断向量号才行。具体的内容就是下面要讲到的可编程中断控制器8259A。
可编程中断控制器8259A:
8259A分自从两块,每一块都可以接收8个不同触发事件的信号,具体如下:(摘自《自己动手写操作系统》于源)
通过编程,可以指定这些中断事件对应哪些中断向量号、是否使用 从 8259A ,以及屏蔽哪些中断信号。具体的方法请参考pm32.c中的Init8259A()函数。
了解了中断源(触发源),以及中断源需要的一一对应关系的中断向量号,最后就是对各个中断向量号指定处理的动作(处理函数)了。这个我们在实验任务门的使用已经很熟悉了,就是要创建一个IDT(中断描述符表),这个IDT数组的序号就对应着中断向量号。通过宏Gate中的选择子和偏移量来指定处理的动作(处理函数)。使用lidt系统指令来加载这个IDT。
需要特别说明的是,IDT这个数组中,每一个元素(中断)占8字节,若定义256个中断的话,需要2048字节的空间,这远远超出了引导扇区512字节的限制,因此与对付上一次实验中的TSS类似,直接使用内存中不会用到的空间。先是定义一个只有一个元素的IDT,然后通过汇编指令重复复制到预先决定好的内存地址,然后再初始化具体的内容(对特定的中断向量指定特定的处理函数)。
此次实验的流程:
1.跳转到保护模式
(pm32.c中main函数)
2.重新加载新的GDT
3.显示字符串:This is Protect model.
4.为IDT模板设置一个默认函数
5.将IDT模板重复复制到指定的内存地址
6.为IDT 0x20中断(定时器中断)设置一个处理函数,不断循环加一修改显示的字符
7.为IDT 0x80中断设置一个处理函数:显示字符“I”
8.加载中断描述符IDT
9.进行中断测试(使用int指令调用任意的中断,使用sti指令启用外部中断使得定时器中断产生效果等)
以下为此次实验代码:
code:run.c
//文件:run.c //功能:编译操作系统的实验代码并创建img,生成bochs配置文件,运行bochs。 //说明:实验代码由16位部分引导程序与32位部分引导程序组成。 // 16位部分引导程序放在引导扇区的前半部分,0~79字节 // 32位部分引导程序放在引导扇区的后半部分,80~509字节 // 510、511字节放引导程序结束标记:0x55、0xaa //运行:请使用yc09编译器编译运行,点击回车再次编译运行 //作者:miao //时间:2010-1-13 #define FDISK_SIZE 1474560 //镜像大小:1.4MB //虚拟机设置 char *pmSrc = "megs: 32 /n" "romimage: file=BIOS-bochs-latest, address=0xf0000 /n" "vgaromimage: VGABIOS-elpin-2.40 /n" "floppya: 1_44=pm.img, status=inserted /n" "boot: a /n" "log: pm.out /n" "mouse: enabled=0 /n" "keyboard_mapping: enabled=1, map=x11-pc-us.map /n"; //编译指定代码文件并放入镜像指定位置 //filename:要编译的文件名 imgBuffer:保存到的镜像缓冲区 //startIndex:指定起始位置 limitSize:编译后程序限定大小 int CompileFile(char *fileName, byte *imgBuffer, int startIndex, int limitSize) { char *tempBuffer; //保存部分引导程序的临时缓冲区 //编译此部分引导程序,结果放到tempBuffer中 int length = YC_CompileCpp(&tempBuffer, fileName, 0, 0); if(length <= 0 || length >= limitSize) { printf("文件: %s 中存在一些错误或文件过大(超过%d字节):%d字节/n", fileName,limitSize,length); return 1; } printf("文件: %s 编译成功,大小为:%d字节。/n", fileName, length); //将1此部分引导程序放到镜像引导扇区缓冲区指定起始位置 memcpy(imgBuffer + startIndex, tempBuffer, length); free(tempBuffer); return 0; } int main(int argc, char **argv) { char * filePath = argv[0]; //当前文件夹路径 char fileName[MAX_PATH]; //用于缓存各个文件名 //将可执行文件的完整路径去掉文件名,保留文件夹路径 for( int i = strlen(filePath);filePath[i] != '//';i--) filePath[i] = '/0'; byte *imgBuffer = new byte[FDISK_SIZE];//镜像缓冲区 _start: //编译16位部分引导程序并放在引导扇区的前半部分,0~79字节 if(CompileFile("pm16.c", imgBuffer, 0, 80)) goto _restart; //编译32位部分引导程序并放在引导扇区的后半部分,80~509字节 if(CompileFile("pm32.c", imgBuffer, 80, 512-80-2)) goto _restart; //0000H-01FFH 为FAT引导扇区[第0扇区] 以55 AA标志结束 长度为200H(512)字节 imgBuffer[510] = 0x55; imgBuffer[511] = 0xaa;//标记软盘引导结尾 //创建操作系统镜像pm.img if(YC_writefile("pm.img", imgBuffer, FDISK_SIZE) != FDISK_SIZE) { printf("写: %s 文件过程中出现错误。/r/n", fileName); goto _restart; } printf("/n%s 创建成功。/n", fileName); //生成操作系统虚拟机配置文件pm.src YC_writefile("pm.src", pmSrc, strlen(pmSrc)); //运行虚拟机 YC_WinExec(strcat(strcpy(fileName, filePath), "bochs.exe"), "-q -f pm.src"); _restart: printf("/n点击回车重新编译运行!/n/n/n"); while(getchar() != '/n'); goto _start; return 0; }
code:pm.h
//文件:pm.h //功能:pm16.c与pm32.c的公共头文件 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-2-12 //定义GDT属性 #define DA_32 0x4000 //32位段 #define DA_DRW 0x92 //存在的可读写数据段属性值 #define DA_DRWA 0x93 //存在的已访问可读写数据段类型值 #define DA_CR 0x9A //存在的可执行可读代码段属性值 #define DA_C 0x98 //存在的只执行代码段属性值 //定义门属性 #define DA_386CGate 0x8c //386调用门类型 #define DA_386IGate 0x8e //368中断们类型 #define DA_386TSS 0x89 //可用386任务状态段类型值 typedef unsigned int t_32; //4字节 typedef unsigned short t_16; //2字节 typedef unsigned char t_8; //1字节 typedef int t_bool;//4字节 typedef unsigned int t_port;//4字节 //存储段描述符/系统段描述符 struct DESCRIPTOR //共 8 个字节 { t_16 limit_low; //Limit 2字节 t_16 base_low; //Base 2字节 t_8 base_mid; //Base 1字节 t_8 attr1; //P(1) DPL(2) DT(1) TYPE(4) 1字节 t_8 limit_high_attr2; //G(1) D(1) 0(1) AVL(1) LimitHigh(4) 1字节 t_8 base_high; //Base 1字节 }; #define Descriptor(bas,len,attr) { / (len) & 0xffff, / (bas) & 0xffff, / ((bas)>>16)&0xff, / (attr) & 0xff, / (((attr)>>8) &0xf0) + (((len)>>16) & 0x0f), / ((bas) >> 24) & 0xff } / #define Gate(slector,offset,dCount,attr) { / (offset) & 0xffff, / slector, / (dCount)&0x1f , / attr, / ((offset)>>16) &0xff, / ((offset) >> 24) & 0xff } /
code:pm16.c
//文件:pm16.c //功能:切换到保护模式,跳转到32位代码段 //说明:我试图仅在引导扇区编写保护模式的相关实验,因此将这个程序精简了很多。 // 它只负责跳转到保护模式,其他的工作都在pm32.c下完成。 // pm16.c只占引导扇区的前半部分0~79字节。 // pm32.c部分会加载到内存0x7c50处。 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-1-30 #define YCBIT 16 //告诉编译器,以16位格式编译程序 #define YCORG 0x7c00 //告诉编译器,在7c00处加载程序 #include "pm.h" //GDT界限,只负责跳转到保护模式,到时会加载新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(0x7c50, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 }; //GDT 选择子,根据GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处 #pragma pack(1) struct GDT_PTR { unsigned short size; void *addr; }GdtPtr = {sizeof(label_gdt), (char*)label_gdt}; //段界限,基地址 #pragma pack() asm void main() { mov ax, cs mov ds, ax mov es, ax //清屏 mov ah, 06h //屏幕初始化或上卷 mov aL, 00h //AH = 6, AL = 0h mov bx, 1110h //蓝色底色 mov cx, 0 //左上角: (0, 0) mov dl, 4fh //第0列 mov dh, 1fh //第0行 int 10h //显示中断 lgdt GdtPtr //加载 GDTR //cli //关中断 //打开地址线A20 in al, 92h or al, 00000010b out 92h, al //准备切换到保护模式,置cr0的PE位为1 mov eax, cr0 or eax, 1 mov cr0, eax //真正进入保护模式 jmp dword SelectorCode32:0x0 }
code:pm32.c
//文件:pm32.c //功能:保护模式下32位代码段,功能为加载新的GDT,设置默认中断处理函数,生成IDT描述符, // 设置定时器中断处理函数等,进行中断调用测试 //说明:32位部分引导程序放在镜像引导扇区的后半部分,80~509字节中,程序大小不能超过这个限制 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果 ,点击回车再次编译运行 //作者:miao //时间:2010-2-12 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "pm.h" #define retf db 0xcb //因为yc09不识别retf指令,所以使用宏定义一个retf指令 #define ProtecAddr 0x7c50 //进入保护模式后的程序基址 asm void Init8259A(); //初始化可编程中断控制器8259A asm void SpuriousHandler(); //默认的中断处理函数 asm void ClockHandler(); //定时器中断处理函数,加一修改屏幕的一个字符 asm void UserIntHandler(); //软中断 0x80号中断处理函数 ,显示字符“I” asm void DispStr(); //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 //GDT 选择子,根据pm32.c中的GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处代码段,可执行可读 #define SelectorVideo 8*2 //指向显存首地址 #define SelectorData32 8*3 //指向32位段处,这样,在程序中的变量就可以读写了 //GDT界限,注意,这个与pm16.c中的GDT不同,从pm16.c跳转过来后会立即载入这个新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(ProtecAddr,0xfffff,DA_CR|DA_32), //32位代码段(pm32.c),可执行可读, //注意:必须要加DA_32,否则调用iretd指令会出错: //[CPU ] iret: return CS selector null Descriptor(0xb8000, 0xffff, DA_DRW), //显存地址段,可读可写 Descriptor(ProtecAddr,0xfffff,DA_DRW|DA_32),//令32位代码段(pm32.c)的变量可以读写 }; #pragma pack(1) struct GDT_PTR { t_16 size; void *addr; }; #pragma pack() GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)&label_gdt + ProtecAddr}; //段界限,基地址 #define IdtAddr 0x8000 //存放IDT描述符结构体的基址 #define IdtNum 0x81 //创建0x81=129个中断 (中断号:0x0~0x80) //加载IDT时需要用到 GDT_PTR IdtPtr = {(IdtNum)*8, IdtAddr}; //段界限,基地址 //IDT中断描述符模板(为了节省程序字节空间,在此只是创建一个IDT模板,然后根据中断数重复拷贝到起始地址IdtAddr后) DESCRIPTOR label_idt[] = { // 选择子 偏移量 参数个数 属性 Gate(SelectorCode32, 0, 0, DA_386IGate), }; char Msg1[] = "This is Protect model."; //32 位代码段. 由实模式跳入 asm void main() { lgdt cs:GdtPtr //加载新的GDTR mov eax, SelectorVideo mov gs, ax //视频段选择子(目的) mov eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写 mov ds, ax //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg1 //源数据偏移 mov edi, ((80 * 0 + 0) * 2) //目的数据偏移。屏幕第0行, 第0列。 call DispStr //为IDT模板设置一个默认函数 mov eax, &SpuriousHandler //默认函数地址 mov word label_idt, ax //放到偏移量前两个字节 shr eax, 16 mov word label_idt+6, ax //放到偏移量后两个字节 //在指定内存地址生成IDT描述符结构体数组 mov eax, IdtAddr //目的,加上es的0x0 _CreateIDT: mov esi, &label_idt //源,加上ds的0x7c50 mov edi, eax mov ecx, 0x8 //有8字节,重复8次 cld rep movsb //movs byte es:edi, ds:esi 此时:es=0x0,ds=0x7c50 add eax, 0x8 cmp eax,IdtAddr+IdtNum*0x8 //创建129个中断 (中断号:0x0~0x80) jne _CreateIDT //为IDT 0x20中断(定时器中断)设置一个处理函数,不断循环加一修改显示的字符 mov eax, &ClockHandler //默认函数地址 mov word es:[IdtAddr+0x20*0x8], ax //放到偏移量前两个字节 shr eax, 16 mov word es:[IdtAddr+0x20*0x8+6], ax //放到偏移量后两个字节 //为IDT 0x80中断设置一个处理函数:显示字符“I” mov eax, &UserIntHandler //默认函数地址 mov word es:[IdtAddr+0x80*0x8], ax //放到偏移量前两个字节 shr eax, 16 mov word es:[IdtAddr+0x80*0x8+6], ax //放到偏移量后两个字节 //加载中断描述符IDT lidt IdtPtr call Init8259A //初始化可编程中断控制器8259A //int 0x0 //测试其他默认中断 int 0x80 sti //打开中断 _dead: jmp _dead } //延时程序,给8259控制器一个反应时间 asm void IoDelay() { nop nop nop nop ret } //初始化可编程中断控制器8259A asm void Init8259A() { mov al, 0x11 out 0x20, al//主8259,ICW1 call IoDelay out 0xa0, al//从8259,ICW1 call IoDelay mov al, 0x20//IRQ0对应中断向量0x20 out 0x21,al//主8259,ICW2 call IoDelay mov al, 0x28//IRQ8对应中断向量0x28 out 0xa1, al//从8259,ICW3 call IoDelay mov al, 0x04//IR2对应从8259 out 0x21, al//主8259,ICW3 call IoDelay mov al, 0x02//对应I主8259的IR2 out 0xa1, al//主8259,ICW3 call IoDelay mov al, 0x01 out 0x21,al//主8259,ICW4 call IoDelay out 0xa1, al//从8259,ICw4 call IoDelay mov al, 0xfe//1111 1110 只开启定时器中断 out 0x21, al//主8259,OCW1 call IoDelay mov al, 0xff//1111 1111 屏蔽从8259所有中断 out 0xa1, al//从8259,OCW1 call IoDelay ret } //默认的中断处理函数 asm void SpuriousHandler() { mov ah, 14h //蓝底红字(ah = 14h) mov al,'!' mov gs:[((80 * 1 + 0) * 2)], ax _dead: jmp _dead iretd } //定时器中断处理函数,加一修改屏幕的一个字符 asm void ClockHandler() { inc word gs:[((80 * 2+ 0) * 2)]//覆盖的内容可以为空,连背景颜色一起修改 inc byte gs:[((80 * 3+ 0) * 2)] //覆盖的内容必须不能为空,否则看不出效果 mov al, 0x20 out 0x20, al//发送EOI iretd } //软中断 0x80号中断处理函数 ,显示字符“I” asm void UserIntHandler() { mov ah, 14h //蓝底红字(ah = 14h) mov al,'I' mov gs:[((80 * 3 + 0) * 2)], ax iretd } //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 asm void DispStr() { mov ah, 14h //蓝底红字(ah = 14h) //循环逐个将字符串输出 _DispStr: mov al, ds:[esi]//因为可读,才能用cs指向当前段的Msg字符串 inc esi cmp al, '/0' //判断是否字符串结束 jz _stop mov gs:[edi], ax add edi, 2 jmp _DispStr _stop: //显示完毕 ret }