操作系统真象还原实验记录之实验十:实现中断处理

操作系统真象还原实验记录之实验十:实现中断处理

书P319

1.相关基础知识

1.1 中断处理过程

忽视细节,整体过程如下

1.外设发出中断信号给8259A芯片
2.8259A芯片处理信号,并向CPU发送中断向量号
3.CPU根据中断向量号,在IDT表中找到对应的中断门描述符,从获取中断门描述符获取对应的中断处理程序所在代码段的选择子和偏移地址,开始执行中断处理程序
4.中断处理程序执行结束,CPU返回原程序
操作系统真象还原实验记录之实验十:实现中断处理_第1张图片

1.2 8259A芯片

操作系统真象还原实验记录之实验十:实现中断处理_第2张图片
对应上述第二步,在8259A芯片的某个引脚接收到外设的中断信号后,发生过程如下:
1.先查看8位寄存器IMR,判断中断信号是否被屏蔽。其中某位如果置1,代表该位对应的IRQ引脚接收的中断信号被屏蔽,信号丢弃。

2.未屏蔽,IRR对应位置1,表示待处理中断队列

3.寄存器PR担任优先级仲裁器,挑选优先级最高的中断信号传入INT,INTA的控制电路的INT接口。IRQ0优先级最大。

4.控制电路向CPU发送INTR信号。

5.CPU执行完当前指令后,处理该信号,通过CPU上的INTA接口向8259A上的INTA接口回复一个中断响应信号。

6.8259A接收到信号后,立即将PR选出的优先级最高中断在ISR对应的BIT置1,同时IRR对应BIT置0。ISR代表正在处理的中断

7.CPU再发出INTA信号给8259A,请求获取中断对应的中断向量号,同时,如果EOI为自动模式,ISR对应BIT置0,否则,中断处理程序结束处必须发送EOI代码

8.8259A将起始中断向量号+IRQ接口号便是该设备中断向量号通过系统数据总线发送给CPU。

9.CPU从数据总线上拿到该中断向量号后,当作索引执行中断处理程序。

操作系统真象还原实验记录之实验十:实现中断处理_第3张图片

1.3中断处理过程及保护

1.处理器根据中断向量号定位中断门描述符

2.(程序主动发起的中断)处理器进行特权级检查,权限上,门描述符DPL<当前CPL<门描述符中的目标代码段DPL,即数值上门描述符DPL>当前CPL>门描述符中的目标代码段DPL,则通过,注意,中断向量号不是选择子,只是个整数,没有RPL,特权级检查不涉及RPL

3.特权级检查通过后,将门描述符目标代码段选择子加载到cs,偏移地址加载到EIP,开始执行中断处理程序

4。中断发生后,eflags中的NT位和TF位置0,中断返回指令iret,弹出数据到寄存器cs,eip,eflags,(特权级若改变,还弹ss,esp)

IF位:只能限制外部设备的中断

NF位:新线程切换到旧线程也用iret。NF为1,代表当前任务嵌套执行,此时会从自己的TSS的“上一个任务TSS的指针”获取旧任务信息;NF为0,代表中断处返回

1.4 CPU执行中断处理程序前发生的压栈

1.4.1 寄存器入栈顺序

1.若权限上CPL小于目标代码段DPL,意味着发生了特权级由低向高的转换,所以需要切换到高特权级的栈,中断返回也需要切换回旧栈,所以,先备份ss_old,esp_old,然后在TSS中找到同目标代码段DPL级别相同的栈加载到寄存器ss,esp,再将ss_old,esp_old入新栈。

2.在新栈压入eflags寄存器

3.入栈cs_old,eip_old

4.某些异常包含错误码,用于报告异常发生在那个段,所以错误码会包含选择子等信息。所以错误码压栈
操作系统真象还原实验记录之实验十:实现中断处理_第4张图片

1.4.2 弹栈顺序及情况

1.执行iret指令前,新栈指针esp必须指在eip_old, 中断如果有错误码,处理器不会自动跳过,需要手动跳过。

2.执行iret指令时,先判断中断是否涉及特权级改变,若当前CPL <= cs_old的RPL,说明特权级要由高向低转换,cs要加载选择子cs_old,故要先进行特权级检查,对于代码段,数值上,当前cpl≥ DPL且RPL >= DPL,则通过,弹出eip_old,cs_old

3.弹栈eflags

4.根据第2步的判断,若特权级涉及改变,弹栈ss_old和esp_old;若不涉及,当前栈指针就是ss_old

注意:中断返回时在cs_old压入cs时,还会检查DS、ES、FS、GS,若选择子指向的DPL权限高于cs_old的cpl,对应段寄存器置0.

1.5 中断类型

中断分为外部中断,内部中断
外部中断分为可屏蔽中断(键盘、网卡、硬盘等外设)
和不可屏蔽中断(即将宕机的中断)
内部中断可分为软中断(软件主动)和异常(错误)

1.5.1 可屏蔽中断

其中,可屏蔽中断Linux分为了上半部和下半部
上半部必须执行、紧急。
比如网卡缓冲区无剩余空间时,会发来中断请求,cpu将缓冲区的数据搬运到内存,这个部分是必须的,不然缓冲区数据可能被覆盖,这个就是上半部分。
然后搬运至内存后,CPU处理数据,这个就没那么紧急了,可以放到下半部来处理,也就是说可以先阻塞,cpu先执行其他优先级较高的任务。

1.5.2 不可屏蔽中断

不可屏蔽中断中断向量号为2

1.5.3 软中断

比如int 8位立即数 (系统调用)等
int 3 陷入指令,调试程序

1.5.4 异常

由轻到重
Fault/故障、Trap调试、Abort中止

某些异常可能会有单独的错误码

小结

异常和不可屏蔽中断的中断向量号是由CPU自动提供的,而来自外部设备的可屏蔽中断号是由中断代理8259A提供的,软中断是由软件提供的
中断向量号是中断门描述符的索引,对应一段中断处理程序。

1.6 8259A的编程规则

8259A可屏蔽外设中断,对它们实行优先级判决,向cpu提供中断向量号等。
intel支持256个中断,一片8259A最多可以级联9片,一个主片,8个从片,
7 * n + 1共64个,8259A最多支持64个中断,最多一层级联
这次试验的个人电脑只有两片芯片,共支持15个中断。
实模式下,8259A的IRQ0~7已经被BIOS分配了0x8 ~ 0xf中断向量号,
保护模式下,0x8~0xf号也已经被cpu分配给了各种异常。

1.6.1 编程的寄存器

一片8259A上有两组寄存器
初始化寄存器:ICW1~4
控制寄存器:OCW1~3
通过相应端口按一定顺序写入

顺序

初始化寄存器
ICW1、ICW2、ICW3、ICW4,依次填写,均8位
控制寄存器OCW1、OCW2、OCW3,初始化寄存器写完后才有效,写入顺序无关。
ICW1和OCW2、OCW3都是从偶地址端口0x20(主片)或0xA0(从片)写入
操作系统真象还原实验记录之实验十:实现中断处理_第5张图片
ICW2~ICW4和OCW1都是从奇地址端口0x21(主片)或0xA1(从片)写入
初始化之后写入奇地址端口的默认为OCW1

功能

操作系统真象还原实验记录之实验十:实现中断处理_第6张图片
ICW1用来初始化连接方式和中断信号的触发方式
第四位1表明ICW1标记
LTIM为0,表边沿触发;为1,表电平触发。
SNGL表single,为1表单片,为0表级联,主片从片均需要ICW3
IC4为1表需要ICW4 x86必须要

操作系统真象还原实验记录之实验十:实现中断处理_第7张图片

ICW2用来设置其实中断向量号
只用填写T3~T7高五位,后三位由对应IRQ接口自动自动导入

操作系统真象还原实验记录之实验十:实现中断处理_第8张图片
ICW3仅级联指定主片用于那个IRQ接口连接的自己。高五位不需要全为0

操作系统真象还原实验记录之实验十:实现中断处理_第9张图片
BUF为1,表8259A在缓冲模式下工作
M/S:当在缓冲模式下,为1表主片,为0表从片;非缓冲模式M/S位无效。
AEOI:表自动结束中断,8259A在收到中断结束信号后才能继续处理下一个中断,AEOI为1,表自动结束中断;为0,表手动结束中断
uPM为0表8080或8085处理器,为1表x86处理器

操作系统真象还原实验记录之实验十:实现中断处理_第10张图片

第3~4位表OCW2标识

OCW2的R、SL、EOI组合在一起,可以定义多种中断结束方式和优先级循环方式
操作系统真象还原实验记录之实验十:实现中断处理_第11张图片
OCW3见书

操作系统真象还原实验记录之实验十:实现中断处理_第12张图片
OCW1写入IMR中断屏蔽器,置1表屏蔽

本次实验对8259A芯片编程代码截取如下

#define PIC_M_CTRL 0x20	       // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21	       // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0	       // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1	       // 从片的数据端口是0xa1


/* 初始化可编程中断控制器8259A */
static void pic_init(void) {

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);    // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);    // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);    // ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);    // ICW4: 8086模式, 正常EOI
   
  /* IRQ2用于级联从片,必须打开,否则无法响应从片上的中断
  主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭 */
   outb (PIC_M_DATA, 0xfe);

/* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
   outb (PIC_S_DATA, 0xff);

   put_str("   pic_init done\n");
}

主片ICW2起始中断向量号置为0x20,IRQ0接时钟,故时钟中断是32号
从片ICW2起始中断向量号为0x28
中断处理程序结束iret前,设置了OCW2,将EOI置1,其余全置0,相当于向8259A发送EOI命令,ISR的对应位置0.
注意OCW2必须在ICW4的AEOI为0表非自动传递中断结束命令前提下才有效。

   ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
   mov al,0x20                   ; 中断结束命令EOI
   out 0xa0,al                   ; 向从片发送
   out 0x20,al                   ; 向主片发送

操作系统真象还原实验记录之实验十:实现中断处理_第13张图片

2.实验代码

2.1 kernel.s

[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 intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少

   %2				 ; 中断若有错误码会压在eip后面 
	push intr_str;
	call put_str;
	add esp, 4;

   ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
   mov al,0x20                   ; 中断结束命令EOI
   out 0xa0,al                   ; 向从片发送
   out 0x20,al                   ; 向主片发送
   
	add esp, 4					;跨过错误码
	iret;
	
section .data
   dd    intr%1entry	 ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro

VECTOR 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,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE 
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO 
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO 
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO 
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE 
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO 
VECTOR 0x20,ZERO	;时钟中断对应的入口

代码功能总结:
定义了33个中断处理程序
数组intr_entry_table记录每个程序入口地址

%macro VECTOR 2
…(含一个代码节,一个数据节)
%endmacro
这个部分的代码有代码节,数据节,共33份
其中每份代码节还有 int[0-32]entry 的标号。
编译器会将这33份代码节整合成一个连续的代码段
33份数据节整合成一个连续的数据段
然后再给标号分配物理地址。
故33个数据节构成的数据段依次连续写入了33个代码节的标号(即物理地址,大小4字节)
形成了一个记录33个中断程序入口地址的数组。

intr_entry_table:写在VECTOR 上面,这说明编译器是先整合数据节,再整合代码节的。

虽然定义了33个中断处理程序,但是从0x14到0x1f这12个中断向量号本实验是不可能产生的

代码

   ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
   mov al,0x20                   ; 中断结束命令EOI
   out 0xa0,al                   ; 向从片发送
   out 0x20,al                   ; 向主片发送

本次实验在初始化所有8259A芯片上的ICW4寄存器时,将其中的AEOL位均设置为0,表示非自动结束中断,这意味着需要中断处理程序增加额外的代码来手动结束中断。
因而,中断处理程序尾部有了上述代码,设置OCW2寄存器,一旦OCW2寄存器上的第五位EOI位被置为1,则会令对应的ISR位置为0,表示中断服务结束。

2.2 interrupt.c

#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"

#define PIC_M_CTRL 0x20	       // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21	       // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0	       // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1	       // 从片的数据端口是0xa1

#define IDT_DESC_CNT 0x81      // 目前总共支持的中断数

#define EFLAGS_IF   0x00000200       // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))


/*中断门描述符结构体*/
struct gate_desc {
   uint16_t    func_offset_low_word;
   uint16_t    selector;
   uint8_t     dcount;   //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
   uint8_t     attribute;
   uint16_t    func_offset_high_word;
};

// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT];   // idt是中断描述符表,本质上就是个中断门描述符数组

/********    定义中断处理程序数组    ********

/********************************************/
extern intr_handler intr_entry_table[IDT_DESC_CNT];	    // 声明引用定义在kernel.S中的中断处理函数入口数组

/* 初始化可编程中断控制器8259A */
static void pic_init(void) {

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);    // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);    // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);    // ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);    // ICW4: 8086模式, 正常EOI
   
  /* IRQ2用于级联从片,必须打开,否则无法响应从片上的中断
  主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭 */
   outb (PIC_M_DATA, 0xf8);

/* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
   outb (PIC_S_DATA, 0xbf);

   put_str("   pic_init done\n");
}

/* 创建中断门描述符 */
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, lastindex = IDT_DESC_CNT - 1;
   for (i = 0; i < IDT_DESC_CNT; i++) {
      make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 
   }
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
 * 中断处理程序为单独的syscall_handler */

   put_str("   idt_desc_init done\n");
}


/*完成有关中断的所有初始化工作*/
void idt_init() {
   put_str("idt_init start\n");
   idt_desc_init();	   // 初始化中断描述符表

   pic_init();		   // 初始化8259A

   /* 加载idt */
   uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
   asm volatile("lidt %0" : : "m" (idt_operand));
   put_str("idt_init done\n");
}

代码功能总结:
idt_desc_init():填写33个中断处理程序对应的中断门描述符
pic_init() ; 初始化8259A芯片
加载idt:即赋值IDTR

操作系统真象还原实验记录之实验十:实现中断处理_第14张图片

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;
}

这个函数是用于往IDT表填充33个中断门描述符,解释一下填充的细节。
最后一个参数function是kernel.S写好的33个中断处理程序入口地址数组的首地址。
接下来梳理一下加载内核实验,
分析一下33个中断处理程序大概应该在内核的什么地方

//1.编译main.c
gcc -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
//2.编译init.c
gcc -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
//3.编译interrupt.c
gcc -m32  -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
//4.编译print.S
nasm -f elf -o build/print.o lib/kernel/print.s
//5.编译kernel.S
nasm -f elf -o build/kernel.o kernel/kernel.s
//6.链接以上文件,生成kernel.bin
ld -m elf_i386  -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o

操作系统真象还原实验记录之实验十:实现中断处理_第15张图片
第六步,链接上述五个编译好的文件,形成一个ELF格式文件kernel.bin
kernel.bin文件就是内核文件。
-Ttext指明了起始虚拟地址;
-e指明了在上述5个文件的众多函数中,函数名为main的函数作为起始函数;
因此上述这6步代码,第六步链接将这五个文件的所有函数整合成多个代码段、数据段段,然后链接在了一起,并以main为主函数,相应的填写elf头,程序头表,形成了一个elf格式文件。
loader.s中再将kernel.bin的每一个段(其中包括了kernel.s中的33个中断处理程序)最终加载到了起始0x1000处依次往上,其中main函数的代码首地址位于0x1500。loader.s完成了加载内核后,jmp近转移将eip赋值为虚拟偏移地址0xc0001500。

从loader.s再到main.c,cs本质上始终未曾变过,这是因为我们在往GDT写段描述符的时候,就写了4个段描述符,分别为第0号哑段、数据段、代码段、显存段段描述符,你不管构建什么代码段选择子,最终都是指向同一个代码段描述符。其中数据段、代码段段基址全为0,显存段段基址为0xc000b800。
为什么这样设计呢?
因为进入保护模式后通用寄存器32位,段寄存器不变的情况下寻址空间依然是4GB,平坦模式,一个代码段描述符足够了,数据段、显存段也是如此。

当在mbr.s时,为实模式,cs全为0,ip的偏移地址就是实际物理地址;
到了loader.s进入保护模式后,cs第一次改变(通过jmp远转移刷新流水线的方式),加载了代码段选择子。
当loader.s加载完内核后,近跳转0xC0001500,进入main.c,
main.c执行死循环,最终时间片用完,响应中断,跳转到了时钟中断处理程序(第33个/32号中断处理程序)。也就是跳转到了0x1500上方某处。跳转借助了
make_idt_desc这个函数往第33个中断门描述符填写的选择子以及32位偏移地址。32位偏移地址就是function数组的第33个成员。
选择子是SELECTOR_K_CODE,内容和之前的选择子是一模一样的。

这个函数中,给每个中断门描述符设置的代码段选择子均为SELECTOR_K_CODE
意思是内核代码段选择子
定义在global.h
操作系统真象还原实验记录之实验十:实现中断处理_第16张图片
RPL依然是0,指向的仍然是GDT中的第一个代码段段描述符。

2.3 init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"


/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();	     // 初始化中断
 
}

2.4 main.c

#include "print.h"
#include "init.h"

int main(void) {
   put_str("I am kernel\n");
   init_all();
	asm volatile("sti");
	while(1);
}

asm volatile(“sti”);
将eflags寄存器的IF位置为1
这里要和8259A芯片上的IMR中断屏蔽寄存器作个区分。
8259A属于中断代理芯片,本质是将中断请求信号传给CPU,CPU相应后,再把中断向量号通过数据总线传给CPU。
如果8259A芯片上的IMR没有屏蔽某个外设比如时钟的中断请求,那么8259A会把时钟中断请求传达给CPU,这个时候CPU要检查eflags的IF位,IF位1,表示不屏蔽可屏蔽外部中断;
IF若为0,表示屏蔽所有可屏蔽外部中断,即使收到8259A传递给CPU的中断请求,CPU依然不予响应。

2.4 头文件

init.h

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

interrupt.h

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif

intr_handler是用来修饰33个中断处理程序入口地址数组intr_entry_table[IDT_DESC_CNT]的,该数组位于某个数据段,成员均是地址。

void*是空指针类型,表地址。

io.h

/**************	 机器模式   ***************
	 b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。
	 w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[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) {
/*********************************************************
 a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号, 
 %b0表示对应al,%w1表示对应dx */ 
   asm volatile ( "outb %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端口, 我们在设置段描述符时, 
   已经将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));
   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

平时写函数都是把函数体写在.c的文件里,函数声明写在.h里,其他文件要调用就include .h的文件即可。但是这次却把这些函数写在了.h里。static作用域只是此文件,也就是只有此文件才可以内部调用,而一个.c文件一旦include这个io.h文件,相当于io.h里面的函数在.c文件的内部。

对端口的读写或一段连续的内存对端口读写都可以调用汇编指令inb(AT&A)/ins(intel), outsw等。
但是为了内核使用c语言在main函数完成8259A的设置,所以可以使用c函数封装内联汇编形式,这些函数都是static inline,意味着当调用此函数时不再是函数调用,而是就地展开,不再需要传递参数入栈,执行快,但程序体积更庞大。
之所以这样是因为对端口操作属于I/O操作,非常慢,一旦用户程序调用他们,可能会等很长时间,所以在函数调用方面就要尽可能快来追求更短的总时间

global.h
#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     0x6   // 16位的门,不用,定义它只为和32位门区分
#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

3.实验步骤

1.编译main.c

gcc -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c

2.编译init.c

gcc -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c

3.编译interrupt.c

gcc -m32  -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c

4.编译print.s

nasm -f elf -o build/print.o lib/kernel/print.s

5.编译kernel.s

nasm -f elf -o build/kernel.o kernel/kernel.s

6.链接以上文件,生成kernel.bin

ld -m elf_i386  -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o

7.kernel.bin刻入磁盘

dd if=/home/Seven/bochs2.68/bin/build/kernel.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=200 seek=9 conv=notrunc

8.运行

./bochs -f bochsrc.disk

结果图

操作系统真象还原实验记录之实验十:实现中断处理_第17张图片

你可能感兴趣的:(操作系统,操作系统,linux)