操作系统实验七:保护模式之中断测试实验
我对中断的理解:中断,顾名思义,就是在遇到特殊的情况时,停下当前正在做的事情,转去干其他(根据特殊情况预先设计好的)事情,完成后(并不一定非要)再继续之前做的事情。
首先说说“去干其他(根据特殊情况预先设计好的)事情”。
我们可以预先设计好256件事情(处理函数),然后使用0~255这256个编号(中断向量号)表示代指。“事情”根据是否可以再次打断(中断),分为中断门(不可以被打断)和陷阱门(可以再次被打断)。
下面解释一下所谓的“遇到特殊的情况”,这个特殊的情况按照触发类型可以分为两类:
自动产生:自动产生的意思就是程序自己根据需要自己主动触发的中断,也就是使用int指令+中断号触发的。对于所有的256个中断,都可以使用int指令触发。
被动产生:被动产生,就是系统硬性规定的,在遇到某种指定的情况,就触发对应中断向量号的中断。
具体而言如下表:(摘自《自己动手写内核系列_skelix》天衣有缝,有稍微修改)
中断向量号 触发原因
0x00 除零错
0x01 调试异常
0x02 非可屏蔽中断 (NMI)
0x03 断点 (INT 3 指令)
0x04 溢出 (INTO 指令)
0x05 越界 (BOUND 指令)
0x06 无效的指令
0x07 无协处理器
0x08 双重错误
0x09 协处理器越界
0x0A 无效的 TSS
0x0B 段不存在
0x0C 栈溢出
0x0D 通用保护异常(内存引用或其他检查保护),Windows 9x 蓝屏就是它的杰作
0x0E 页错误
0x0F Intel 保留
0x10 协处理器错误
0x11-0x19 Intel 保留
需要说明的有两点:0x00 除零错
0x01 调试异常
0x02 非可屏蔽中断 (NMI)
0x03 断点 (INT 3 指令)
0x04 溢出 (INTO 指令)
0x05 越界 (BOUND 指令)
0x06 无效的指令
0x07 无协处理器
0x08 双重错误
0x09 协处理器越界
0x0A 无效的 TSS
0x0B 段不存在
0x0C 栈溢出
0x0D 通用保护异常(内存引用或其他检查保护),Windows 9x 蓝屏就是它的杰作
0x0E 页错误
0x0F Intel 保留
0x10 协处理器错误
0x11-0x19 Intel 保留
一.0x11~0x19这12个中断向量号, Intel预定了,但是并没有实际使用上,但将来可能会用到。因此我们可以在编写操作系统的使用直接拿来用,但是这样会有一定风险,要是Intel的下一代产品里使用了这些中断向量号,我们想要兼容它,就不得不修改代码将这几个中断向量号让出来。
二.上表中触发的原因,基本都是程序运行时发生错误产生的(0x02号中断除外),可以称之为内部中断。与之相对应的称作外部中断的东西,就是由于计算机硬件(键盘,鼠标等)触发的。这些中断是可以屏蔽掉(忽略掉)的。想要响应这些中断,也需要分配相应的中断向量号才行。具体的内容就是下面要讲到的可编程中断控制器8259A。
可编程中断控制器8259A:
8259A分自从两块,每一块都可以接收8个不同触发事件的信号,具体如下:(摘自《自己动手写操作系统》于源)
主 8259A
序号 触发源头
IRQ0 时钟
IRQ1 键盘
IRQ2 来自 从 8259A
IRQ3 串口2
IRQ4 串口1
IRQ5 LPT2
IRQ6 软盘
IRQ7 LPT1
从 8259A
IRQ8 实时时钟
IRQ9 重定向IRQ2
IRQ10 保留
IRQ11 保留
IRQ12 PS\2 鼠标
IRQ13 FPU 异常
IRQ14 AT 温盘
IRQ15 保留
通过编程,可以指定这些中断事件对应哪些中断向量号、是否使用 从 8259A ,以及屏蔽哪些中断信号。具体的方法请参考pm32.c中的Init8259A()函数。序号 触发源头
IRQ0 时钟
IRQ1 键盘
IRQ2 来自 从 8259A
IRQ3 串口2
IRQ4 串口1
IRQ5 LPT2
IRQ6 软盘
IRQ7 LPT1
从 8259A
IRQ8 实时时钟
IRQ9 重定向IRQ2
IRQ10 保留
IRQ11 保留
IRQ12 PS\2 鼠标
IRQ13 FPU 异常
IRQ14 AT 温盘
IRQ15 保留
了解了中断源(触发源),以及中断源需要的一一对应关系的中断向量号,最后就是对各个中断向量号指定处理的动作(处理函数)了。这个我们在实验任务门的使用已经很熟悉了,就是要创建一个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"; //因为yc09编译器不支持长返回指令 retf,所以使用这个函数作为临时解决方法。 //在汇编代码中需要用到 retf指令 时,使用 ret nop nop 这个三个连续指令代替, //在编译代码后,调用此函数将ret指令的机器指令改为retf的机器指令。 //imgBuffer:编译后的二进制文件 size:文件的字节大小 //汇编与机器指令对照:ret == 0xc3 retf == 0xcb nop == 0x90 void ret_To_retf(byte *imgBuffer,int size) { while(--size) if(imgBuffer[size] == 0xc3) //这里保留着一个bug,若imgBuffer的最后两字节为0x3,会出现数组访问越界错误。 if(imgBuffer[size+1] == 0x90 && imgBuffer[size+2] == 0x90) { imgBuffer[size] = 0xcb; printf("将一个ret改为了retf.\n"); } } //编译指定代码文件并放入镜像指定位置 //filename:要编译的文件名 imgBuffer:保存到的镜像缓冲区 //startIndex:指定起始位置 limitSize:编译后程序限定大小 int CompileFile(char *fileName, byte *imgBuffer, int startIndex, int limitSize,bool isneed) { 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); if(isneed) { ret_To_retf(imgBuffer + startIndex,length); } 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,false)) goto _restart; //编译32位部分引导程序并放在引导扇区的后半部分,80~509字节 if(CompileFile("pm32.c", imgBuffer, 80, 512-80-2,true)) 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-2-12 #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; }; #pragma pack() GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)label_gdt}; //段界限,基地址 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,否则 Bochs中调用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//0001001b:表示需要ICW4 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 }
[ 本帖最后由 miaowangjian 于 2010-2-13 08:16 编辑 ]