填充中断描述符表IDT,使用中断
通过初始化中断控制芯片,编码中断函数,实现BIOS中断
操作系统的中断是一种异步事件,用于通知 CPU 某个事件已经发生,例如硬件设备完成数据传输、发生错误或用户发起的系统调用。当操作系统收到中断请求时,它会挂起当前执行的任务,并调用相应的中断处理程序(interrupt handler)来处理该事件。
中断可以被看作是一种硬件通知机制,允许系统在不同的时间点响应外部事件。操作系统会为每个中断请求分配一个唯一的中断号,并通过 IDT(Interrupt Descriptor Table)来管理和映射中断处理程序的位置和参数。中断处理程序是由内核编写的低级代码,用于处理特定类型的中断请求,并执行适当的操作,例如更新系统状态、将数据存储到缓存中、读取输入设备等。
在 x86 架构的计算机中,有两种类型的中断:软中断(software interrupt)和硬中断(hardware interrupt)。软中断需要程序员显式地触发,例如通过系统调用调用特定函数;而硬中断是由外部硬件设备发出的,例如键盘、鼠标或磁盘驱动器。硬中断通常采用 IRQ(Interrupt Request)信号进行通信,并由中断控制器(如 8259A)进行控制和管理。
参考:
深入理解Linux中断机制
中断描述符表
可编程中断控制器8259A
《操作系统真相还原》
1.中断向量表IDT
中断描述符
中断描述符表是保护模式下用于存储中断处理程序的数据结构。CPU在接收到中断时,会根据中断向量在中断描述符表中检索对应的描述符。IDT 中的每个条目都称为中断描述符,并由一个 8 字节的描述符来表示。
- Offset:描述中断处理程序的入口地址。它是一个 32 位或 64 位值,指向中断处理程序代码的起始地址。
- Selector:处理程序所在代码段的选择符,指向代码段描述符在 GDT 或 LDT 表中的位置。
- Type:描述符的类型。它定义了中断门(Interrupt Gate)、陷阱门(Trap Gate)或任务门(Task Gate)等不同类型中断描述符的特性。 DPL:描述符的特权级别。它定义了哪些代码可以使用中断描述符,并为其分配相应的访问权限。D为0则表示16位模式下门,1则为32位的门
- 0b110:表示中断门(Interrupt Gate)。中断门会将处理器从当前代码转向对应的中断处理程序,并在处理程序执行完毕后返回原来的位置,也就是底层代码不会被中断处理程序修改。
- 0b111:表示陷阱门(Trap Gate)。陷阱门和中断门类似,不同之处在于它会保留中断发生时的 CPU 状态,并在处理程序返回时重新恢复这些状态。因此,陷阱门常常被用来实现调试器等功能。
- 0b100:表示任务门(Task Gate)。任务门是一种特殊的中断门,用于实现任务切换。当处理器遇到一个任务门时,会切换到其中指定的任务并开始执行。
- S:为0表示为系统段,这里必须为0。
- P:描述符存在位。如果为 0,则该描述符无效,处理器会抛出“未定义操作码”异常并导致中断处理失败。
- DPL:门特权级。
中断描述符表中的所有描述符共同组成了中断向量表,每个中断向量代表了一种中断类型。当中断发生时,硬件将中断向量号传递给中断描述符表,操作系统就可以使用相应的中断描述符来调用正确的中断处理程序。段描述符中描述的是一片内存区域,而门描述符中描述的是一段代码。
// 定义中断门描述符结构体
typedef struct interrupt_gate_t {
unsigned short offset_low; // 偏移地址低 16 位 0 ~ 15 位
unsigned short selector; // 描述符所在段的选择子
unsigned char reserved; // 保留不用
unsigned char type:4; // 任务门/中断门/陷阱门
unsigned char segment : 1; // segment = 0 表示系统段
unsigned char DPL:2; // 特权级
unsigned char present : 1; // 是否有效
unsigned short offset_high; // 偏移地址高 16 位 16 ~ 31 位
} __attribute__((packed)) interrupt_gate_t;
中断向量
Linux 操作系统支持 256 种中断号,其中一部分被保留给操作系统内核使用,另外一部分则可以被用户应用程序或模块使用。
- 0-19,是特定的异常与错误
- 20-31,映射中断控制芯片的IRQ0-IRP15
- 32-255,用户自定义使用。比如系统调用,都是用的0x80中断
中断向量的作用和选择子类似,它们都用来在描述符表中索引 个描述符。中断向量专用于中断描述符表,其中没有 RPL 字段。
实模式内存分布中,0-0x3ff 的是中断向量表 IVT ,它是实模式下用于存储中断处理程序入口的表。大小为1KB,每个中断向量4B,可以存储256个。
因此对比中断向量表,中断描述符表有两个区别:
- 中断描述符表地址不限制,在哪里都可以
- 中断描述符表中的每个描述符用 8字节描述。
CPU内部有个中断描述符寄存器IDTR,,只有寄存器 IDTR指向了 IDT ,当 CPU 接收到中断向量号时才能找到中断向量处理程序,这样中断系统才能正常运作。该寄存器的结构图如下图:
- 第0-15位是表界限,即IDT减1,可容纳8192个中段描述符;
- 第16~47位时IDT的基地址。与GDTR类似。
2.中断执行过程
寄存器在中断处理过程中的作用说明:
- EFLAGS 寄存器:EFLAGS 寄存器用来存储 CPU 的标记位,包括进位标志、零标志、符号标志等等。在中断处理过程中,EFLAGS寄存器的部分标记位可能会被修改,以反映中断处理过程中的状态。
- CS 寄存器:CS(Code Segment)寄存器用于存储当前程序代码段的段选择子。在中断发生时,CPU 会根据中断向量号从 IDT表读取相应的中断描述符,其中包括一个代码段选择子。CPU 根据这个选择子将 CS寄存器的值修改为新的值,以便正确跳转到中断处理程序的代码段。
- EIP 寄存器:EIP(Instruction Pointer)寄存器用于存储 CPU 下一条指令执行的地址。在中断发生时,CPU会将当前的 EIP 值保存到堆栈中,并将 EIP 设为中断向量所对应的中断处理程序入口地址,以便开始执行中断处理程序。
- SS 和 ESP 寄存器:在中断处理过程中,CPU 会将当前程序的堆栈指针(ESP)和堆栈段选择子(SS)的值保存到堆栈中,并将 SS 和ESP 的值修改为新的栈段和栈指针。这个新的栈用于存储中断处理程序执行过程中所需要的数据和状态信息。
- DS 和 ES 寄存器:在中断处理过程中,处理程序可能需要访问其他数据段或额外的数据空间。因此,在进入中断处理程序后,CPU 会自动将DS 和 ES 寄存器的值更新为相应的选择子,以便访问中断服务例程需要的数据段。
- EAX、EBX、ECX、EDX、EDI 和 ESI寄存器:这些通用寄存器可用于存储各种类型的数据。在中断处理程序运行时,这些寄存器可能用于保存一些状态信息或临时变量,也可能会被修改以存储处理结果。
- EBP 寄存器:EBP 寄存器常用作堆栈帧指针,在函数调用时保存局部变量和参数。在一个中断处理程序中,EBP可利用它的内容来检查现场保存的数据并调用归还数据的操作。
在 x86 架构的计算机系统中,当中断发生时,CPU 会自动执行一些压栈操作,以保护当前的现场(程序状态,即代码的执行位置和寄存器的值等),并存储关于中断的一些信息。下面是详细说明:
-
1.SS 和 ESP 寄存器:CPU 首先将 SS(堆栈段选择子)和 ESP(堆栈指针)寄存器的值按照以下方式压入堆栈中:
- (1)首先将 SS 压入堆栈中。
- (2)然后将 ESP 压入堆栈中。
这样做的目的是为了保存当前进程正在使用的堆栈指针和堆栈段选择子,以便在中断处理程序运行结束后可以正确恢复堆栈指针和堆栈段选择子。
-
2.EFLAGS 寄存器:接下来,CPU 将 EFLAGS 寄存器的值压入堆栈中。EFLAGS 寄存器中存储了各种标志位,如进位标志、零标志、符号标志等等,这些标志位可能在中断处理程序中被修改,因此,在压栈的时候也需要将
EFLAGS寄存器的值保存到堆栈中,以便在处理完中断后能够恢复现场。 -
3.CS 和 EIP 寄存器:CPU 接着将 CS(代码段选择子)和 EIP(指令指针)寄存器的值压入堆栈中:
- (1)首先将 CS 压入堆栈中。
- (2)然后将 EIP 压入堆栈中。
这样做的目的是为了保存程序当前的执行状态,以便在中断处理程序运行结束后能够正确返回到中断触发时原来的执行位置。
-
4.中断向量号:最后,CPU 将中断向量号压入堆栈中,以提供中断处理程序获取中断号的信息。
+-----------+
ESP -> | SS | (由于堆栈指针往下移动,所以先把SS存放到了堆栈中)
+-----------+
| ESP |
+-----------+
| EFLAGS |
+-----------+
| CS |
+-----------+
| EIP |
+-----------+
| Vector |
+-----------+
3.硬件中断 8259a
对于Linux 内核来说,中断信号通常分为两类:硬件中断和软件中断(异常)。每个中断是由0-255之间的一个数字来标识。
- 中断 int0–int31(0x00–0x1f),每个中断的功能由Intel公司固定设定或保留用,属于软件中断,但Intel公司称之为异常。因为这些中断是在CPU执行指令时探测到异常情况而引起的。通常还可分为故障(Fault)和陷阱(traps)两类。
- 中断 int32-int255(0x20–0xff)可以由用户自己设定。在Linux系统中,则将int32–int47(0x20–0x2f)对应于8259A中断控制芯片发出的硬件中断请求信号IRQ0-IRQ15,并把程序编程发出的系统调用(system_call)中断设置为int128(0x80)。
硬件中断,可由8259a芯片告知CPU。
结构
8259A 芯片包含两个级联的芯片,主芯片(Master)和从属芯片(Slave)。主芯片负责接收 CPU 发来的中断请求信号,并根据优先级将请求转发到从属芯片或直接处理。从属芯片则负责管理更多的硬件设备请求,并通知主芯片进行处理。主芯片和从属芯片通过一个独立的线路进行连接,以便实现级联。
初始化顺序
- 1.向 8259A 芯片发送初始化命令字
为了初始化 8259A 芯片,首先需要向主芯片和从属芯片分别发送初始命令字(Initialization Command Word,简称 ICW),以启动初始化过程。ICW 可以通过三个 I/O 端口(0x20、0x21、0xA0、0xA1)进行传输。在向芯片发送 ICW 时,需按照一定的顺序进行,以确保初始化能够成功。
8259A 芯片的初始化顺序如下:
1.向主芯片发送 ICW1;
2.向主芯片发送 ICW2;
3.向从属芯片发送 ICW1(如果启用了级联模式);
4.向从属芯片发送 ICW2(如果启用了级联模式);
5.向主芯片或从属芯片发送 OCW,以设置中断屏蔽掩码,控制芯片的工作状态等;
6.接收来自硬件设备的中断请求。
; 配置8259a芯片,响应中断用
.init_8259a:
; 初始化 8259A 主芯片
; 发送初始化命令字 ICW1 到端口 0x20
mov al, 0x11 ; 边沿触发,级联需要发送ICW 4
out 0x20, al
; 发送 ICW2 到端口 0x21(设置主芯片的中断向量表偏移量)
mov al, 0x20 ; 起始中断向量 0x20~0x27
out 0x21, al
; 发送 ICW3 到端口 0x21(设置主芯片的 IRQ 线路连接方式)
mov al, 0x04 ; IR2级联从片
out 0x21, al
; 发送 ICW4 到端口 0x21(设置主芯片的工作模式)
mov al, 0x03 ; 表示x86模式 自动EOI
out 0x21, al
; 初始化 8259A 从芯片
; 发送初始化命令字 ICW1 到端口 0xA0
mov al, 0x11 ; 需要发送ICW 4
out 0xA0, al
; 发送 ICW2 到端口 0xA1(设置从芯片的中断向量表偏移量)
mov al, 0x28 ; 起始中断向量 0x28
out 0xA1, al
; 发送 ICW3 到端口 0xA1(设置从芯片与主芯片连接方式)
mov al, 0x02 ; 2号
out 0xA1, al
; 发送 ICW4 到端口 0xA1(设置从芯片的工作模式)
mov al, 0x03
out 0xA1, al
- 2.设置 IRQ 向量表
IRQ 向量表是用于记录中断处理程序入口地址的数据表格,其中记录了每个 IRQ 线路对应的中断向量号。通过将 IRQ 向量表与中断处理程序相关联,可以在收到中断请求时快速地定位到对应的处理程序,并跳转到相应的代码段中执行相应的操作。
- 3.设置中断屏蔽掩码
中断屏蔽掩码是一个 8 位二进制数,标识每个 IRQ 线路是否被禁止中断。通过设置中断屏蔽掩码,可以控制系统中哪些中断请求可以被接受,哪些中断请求应该被忽略。
- 4.向 8259A 芯片发送结束命令字
在完成以上初始化操作后,需要向主芯片和从属芯片分别发送结束命令字(End of Initialization Command Word,简称 EOI),以告知 8259A 芯片初始化过程已完成。在结束命令字被发送到芯片后,芯片会开始正常工作,等待接收中断请求信号并向 CPU 发送相应的中断向量。
#define PIC_8259a_M_CTRL 0x20 // 主片的控制端口
#define PIC_8259a_M_DATA 0x21
#define PIC_8259a_S_CTRL 0xa0
#define PIC_8259a_S_DATA 0xa1
#define PIC_8259a_EOI 0x20 // 通知中断控制器中断结束
void send_eoi(int idt_index){
if (idt_index >= 0x20 && idt_index < 0x28){
out_byte(PIC_8259a_M_CTRL,PIC_8259a_EOI);
}else if (idt_index >= 0x28 && idt_index < 0x30){
out_byte(PIC_8259a_M_CTRL,PIC_8259a_EOI);
out_byte(PIC_8259a_S_CTRL,PIC_8259a_EOI);
}
}
4.测试
测试除0中断:
%macro INTERRUPT_HANDLER 1
global interrupt_handler_%1
interrupt_handler_%1:
pushad
push dword %1 ; 将中断号压入栈中
push dword message_%1 ; 将附加信息字符串的地址压入栈中
call printk ; 调用 printk 函数输出信息
add esp, 8 ; 将栈指针移回原来的位置
push %1
call exception_handler
add esp, 4
popad
iret
message_%1:
db 'Interrupt %d occurred', 0x0a, 0
%endmacro
int a = 10 / 0; // 测试除0中断