自制操作系统day6(GDTR、段描述符、PIC、实模式和保护模式、16位到32位切换、中断处理程序、idt的设定、EFLAG寄存器)(ai辅助整理)
day6
分割源文件(harib03a)
优点
- 按照处理内容进行分类,如果分得好的话,将来进行修改时,容易找到地方。
- 如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度。
- 单个源文件都不长。多个小文件比一个大文件好处理。
- 看起来很酷(笑)。
缺点 - 源文件数量增加。
- 分类分得不好的话,修改时不容易找到地方。
makefile修改
整理头文件(harib03c)
将所有的宏,常量,数据结构和函数声明都放在一个.h头文件当中,编译时将.h的内容插到.c文件当中
解析头文件中的宏,常量,数据结构和函数声明并用于后续的编译
头文件的作用
- 数据结构定义:
struct BOOTINFO
:描述了启动信息,包括屏幕分辨率、显存地址等。这些信息由启动代码(如引导扇区)提供,用于初始化操作系统的图形界面。struct SEGMENT_DESCRIPTOR
和struct GATE_DESCRIPTOR
:用于描述全局段表(GDT)和中断描述符表(IDT)的结构。
- 常量定义:
- 定义了与中断控制器相关的端口地址(如
PIC0_ICW1
)。 - 定义了颜色常量(如
COL8_000000
表示黑色,COL8_FFFFFF
表示白色)和内存地址常量(如ADR_BOOTINFO
表示启动信息的地址)。
- 定义了与中断控制器相关的端口地址(如
- 函数声明:
- 包括汇编函数(如
io_hlt
、io_cli
)和 C 函数(如init_palette
、init_screen8
)。 - 汇编函数通常用于直接与硬件交互,而 C 函数用于更高级别的操作,如初始化图形界面或设置鼠标指针。
- 包括汇编函数(如
- 模块化管理:
- 将不同功能的代码分离到不同的文件中(如 graphic.c 处理图形相关功能,
dsctbl.c
处理段表和中断表),通过头文件统一管理这些模块的接口。
- 将不同功能的代码分离到不同的文件中(如 graphic.c 处理图形相关功能,
编译时的运作
- 预处理阶段:
- 编译器在编译 bootpack.c 时,会通过
#include "bootpack.h"
将 bootpack.h 的内容插入到 bootpack.c 中。 - 头文件中的宏、常量、数据结构和函数声明会被解析并用于后续的编译。
- 编译器在编译 bootpack.c 时,会通过
- 编译阶段:
- 编译器根据头文件中声明的函数原型检查 bootpack.c 中的函数调用是否正确。
- 如果头文件中声明的函数在其他源文件中实现(如
init_palette
在 graphic.c 中实现),编译器会生成对这些函数的外部引用。
- 链接阶段:
- 编译器将 bootpack.c 和其他源文件(如 graphic.c、
dsctbl.c
)编译成目标文件后,链接器会将这些目标文件合并为一个可执行文件。 - 链接器会根据头文件中的声明找到函数和变量的定义地址,并将它们正确地链接起来。
- 编译器将 bootpack.c 和其他源文件(如 graphic.c、
- 运行时:
- 系统启动时,
bootpack.c
中的HariMain
函数作为入口点被调用。 HariMain
调用的所有函数(如init_gdtidt
、init_palette
)会根据头文件中的声明和链接器的地址分配被正确执行。
- 系统启动时,
总结
bootpack.h 是整个项目的**核心接口文件,**它将不同模块的功能整合在一起,提供了统一的接口定义。在编译时,它确保了代码的模块化和可维护性,同时在链接阶段保证了函数和变量的正确引用。
关于GDTR
_load_gdtr: ; void load_gdtr(int limit, int addr);MOV AX,[ESP+4] ; limitMOV [ESP+6],AXLGDT [ESP+6]RET
GDTR低16位为段上限,等于GDT的有效字节数-1,因为从0开始计数
高32位表示段的起始地址
为什么是 [ESP + 4]
而不是 [ESP + 2]
?
1. 栈的对齐和参数传递规则
- 在 x86 架构中,栈通常以 4 字节(32 位)对齐,即使传递的是 16 位数据,编译器也会按照 32 位对齐的规则将其压入栈中。
- 在调用
load_gdtr
函数时,limit
和addr
是作为参数传递的:limit
是一个 16 位的值,但它会被扩展为 32 位(低 16 位存储实际值,高 16 位填充为 0)。addr
是一个 32 位的值。
因此,limit
被存储在 [ESP + 4]
(从栈顶偏移 4 字节的位置),而不是 [ESP + 2]
。
GDTR详细解释
_load_gdtr: ; void load_gdtr(int limit, int addr);MOV AX,[ESP+4] ; limitMOV [ESP+6],AXLGDT [ESP+6]RET
以下是按 8位(1字节)为单位 的栈布局变化,分阶段展示 _load_gdtr
函数的执行过程:
阶段 0:初始状态(调用函数前)
假设调用函数时参数按顺序压栈(小端存储):
地址 字节值 解释
ESP+0x00 → [Return Address] ; 返回地址(4字节,假设为 0x11223344)
ESP+0x04 → 0xFF ; limit 低字节(0x0000ffff → FF FF 00 00)
ESP+0x05 → 0xFF
ESP+0x06 → 0x00
ESP+0x07 → 0x00
ESP+0x08 → 0x00 ; addr 低字节(0x00270000 → 00 00 27 00)
ESP+0x09 → 0x00
ESP+0x0A → 0x27
ESP+0x0B → 0x00
- 参数布局:
limit
(32位):0x0000ffff
→ 存储为FF FF 00 00
(ESP+4 ~ ESP+7)。addr
(32位):0x00270000
→ 存储为00 00 27 00
(ESP+8 ~ ESP+11)。
阶段 1:执行 MOV AX, [ESP+4]
-
从
ESP+4
读取 2 字节(limit
的低16位):复制下载ESP+4 → 0xFF (低字节) ESP+5 → 0xFF (高字节)
-
AX
寄存器值变为0xFFFF
。 -
栈布局不变(仅读取数据)。
阶段 2:执行 MOV [ESP+6], AX
-
将
AX
(0xFFFF
)写入ESP+6
和ESP+7
的位置:复制下载ESP+6 → 0xFF (低字节) ESP+7 → 0xFF (高字节)
-
覆盖原栈数据:
- 原
limit
的高16位(ESP+6 ~ ESP+7:00 00
)被覆盖为FF FF
。 - 原
addr
的低2字节(ESP+8 ~ ESP+9:00 00
)不受影响(写入的是ESP+6 ~ ESP+7
)。
- 原
-
新栈布局:
地址 字节值 解释
ESP+0x00 → [Return Address]
ESP+0x04 → 0xFF ; limit 低字节(保持原值)
ESP+0x05 → 0xFF
ESP+0x06 → 0xFF ; 新写入的低16位(覆盖原 limit 高16位)
ESP+0x07 → 0xFF
ESP+0x08 → 0x00 ; addr 低字节(保持原值)
ESP+0x09 → 0x00
ESP+0x0A → 0x27
ESP+0x0B → 0x00
阶段 3:执行 LGDT [ESP+6]
- 从
ESP+6
开始读取 6 字节,构造 GDTR 结构:
地址 字节值 对应字段
ESP+6 → 0xFF ; GDTR 低字节 → Limit 低8位
ESP+7 → 0xFF ; GDTR 高字节 → Limit 高8位(Limit = 0xFFFF)
ESP+8 → 0x00 ; Base Address 字节0(低字节)
ESP+9 → 0x00 ; Base Address 字节1
ESP+A → 0x27 ; Base Address 字节2
ESP+B → 0x00 ; Base Address 字节3(高字节)
- GDTR 最终值:
- Limit:
0xFFFF
(有效字节数为0xFFFF + 1 = 64KB
)。 - Base Address:
0x00270000
(小端存储为00 00 27 00
)。
- Limit:
最终栈布局总结
地址 字节值 解释
ESP+0x00 → [Return Address]
ESP+0x04 → FF FF FF FF ; 修改后的 limit(原 limit 低16位 + 新写入的 FF FF)
ESP+0x08 → FF FF 00 00 ; addr 前两字节被覆盖为 FF FF(但实际未影响 addr 的完整值)
ESP+0x0C → 27 00 ... ; addr 后两字节保持原值
关键点
- 小端存储:所有 32 位值(如
0x00270000
)按字节逆序存储。 - 数据覆盖:通过写入
ESP+6 ~ ESP+7
,将limit
的低16位与addr
的32位合并为连续的6字节。 - GDTR 格式:前2字节为
Limit
,后4字节为Base Address
,完全符合LGDT
指令要求。
通过逐步覆盖栈中数据,最终成功构造了 GDTR 需要的 6 字节结构!
关于段描述符
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{if (limit > 0xfffff) {ar |= 0x8000; /* G_bit = 1 */limit /= 0x1000;}sd->limit_low = limit & 0xffff;sd->base_low = base & 0xffff;sd->base_mid = (base >> 16) & 0xff;sd->access_right = ar & 0xff;sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);sd->base_high = (base >> 24) & 0xff;return;
}
结构总结
段描述符是一个 8 字节(64 位) 的结构,用于描述内存段的属性、大小和基址。它是 CPU 在保护模式下管理内存段的核心数据结构。以下是段描述符的详细结构:
段描述符的组成
段描述符的 8 字节分为以下几个部分:
字节偏移 | 位数范围 | 字段名称 | 描述 |
---|---|---|---|
0-1 | 16 位 | limit_low | 段上限的低 16 位,表示段的大小(字节数 - 1)。 |
2-3 | 16 位 | base_low | 段基址的低 16 位,表示段的起始地址。 |
4 | 8 位 | base_mid | 段基址的中间 8 位。 |
5 | 8 位 | access_right | 段的访问权限属性,包括是否可读写、是否可执行、段类型等。 |
6 | 4 位 | limit_high | 段上限的高 4 位。 |
6 | 4 位 | flags | 段的扩展属性,包括 G 位(粒度)和 D 位(操作数大小)。 |
7 | 8 位 | base_high | 段基址的高 8 位。 |
字段详细说明
1. 段基址(Base Address)
- 段基址是一个 32 位的地址,表示段的起始地址。
- 它被分为 3 个部分存储:
base_low
:低 16 位。base_mid
:中间 8 位。base_high
:高 8 位。
- 通过这 3 个字段组合,可以完整表示一个 32 位地址。
2. 段上限(Limit)
- 段上限是一个 20 位的值,表示段的大小(字节数 - 1)。
- 它被分为 2 个部分存储:
limit_low
:低 16 位。limit_high
:高 4 位。
- 如果 G 位(粒度位)为 0,则段上限以字节为单位,最大值为 1 MB(
0xFFFFF
)。 - 如果 G 位为 1,则段上限以 4 KB 为单位,最大值为 4 GB(
0xFFFFF * 4KB
)。
3. 访问权限(Access Rights,access_right
)
-
访问权限是一个 8 位的字段,定义了段的类型和访问权限。
-
具体位的含义如下:
7 6 5 4 3 2 1 0 P DPL 1 S TYPE
- P(1 位):段是否存在(Present)。
- 1:段存在。
- 0:段不存在。
- DPL(2 位):描述符特权级(Descriptor Privilege Level)。
- 值为 0-3,0 表示最高权限,3 表示最低权限。
- S(1 位):描述符类型。
- 1:代码段或数据段。
- 0:系统段(如 TSS、LDT)。
- TYPE(4 位):段的具体类型。
- 代码段:是否可执行、可读等。
- 数据段:是否可写、扩展方向等。
- P(1 位):段是否存在(Present)。
4. 扩展属性(Flags,flags
)
-
扩展属性是
limit_high
的高 4 位,定义了段的额外属性:G D 0 A
- G(1 位):粒度(Granularity)。
- 0:段上限以字节为单位。
- 1:段上限以 4 KB 为单位。
- D(1 位):操作数大小(Default Operation Size)。
- 0:16 位模式。
- 1:32 位模式。
- 0(1 位):保留位,必须为 0。
- A(1 位):访问位(Accessed)。
- 0:段未被访问。
- 1:段已被访问(由 CPU 自动设置)。
- G(1 位):粒度(Granularity)。
存储示例
假设:
- 段基址(Base Address)为
0x00270000
。 - 段上限(Limit)为
0xFFFFF
(4 GB,G 位为 1)。 - 访问权限(Access Rights)为
0x9A
(可执行、可读、系统段)。 - 扩展属性(Flags)为
0xC
(G 位为 1,D 位为 1)。
段描述符的 8 字节存储如下:
Byte 0-1: FF FF (limit_low)
Byte 2-3: 00 00 (base_low)
Byte 4: 00 (base_mid)
Byte 5: 9A (access_right)
Byte 6: CF (limit_high + flags: limit_high=F, flags=C)
Byte 7: 27 (base_high)
完整的段描述符为:
[FF FF 00 00 00 9A CF 27]
总结
段描述符是一个 8 字节的结构,包含段的基址、段上限和访问权限等信息。它的设计兼顾了 80286 和 80386 的兼容性,同时通过 G 位和 D 位扩展了段的大小和操作模式。通过 set_segmdesc
函数,可以按照 CPU 的要求将这些信息正确地写入内存。
初始化PIC(harib03d)
_io_out8: ; void io_out8(int port, int data);MOV EDX,[ESP+4] ; 从栈中获取端口号参数MOV AL,[ESP+8] ; 从栈中获取8位数据参数OUT DX,AL ; 向指定端口写入8位数据RET ; 无返回值
//bootpack.h
void init_pic(void);
#define PIC0_ICW1 0x0020
#define PIC0_OCW2 0x0020
#define PIC0_IMR 0x0021
#define PIC0_ICW2 0x0021
#define PIC0_ICW3 0x0021
#define PIC0_ICW4 0x0021
#define PIC1_ICW1 0x00a0
#define PIC1_OCW2 0x00a0
#define PIC1_IMR 0x00a1
#define PIC1_ICW2 0x00a1
#define PIC1_ICW3 0x00a1
#define PIC1_ICW4 0x00a1
//int.c
void init_pic(void)
/* PIC初始化 */
{io_out8(PIC0_IMR, 0xff ); /* 屏蔽主PIC(PIC0)所有中断 */io_out8(PIC1_IMR, 0xff ); /* 屏蔽从PIC(PIC1)所有中断 */io_out8(PIC0_ICW1, 0x11 ); /* 初始化命令字1: 边沿触发,级联模式,需要ICW4 */io_out8(PIC0_ICW2, 0x20 ); /* 初始化命令字2: 主PIC中断向量基址为0x20 */io_out8(PIC0_ICW3, 1 << 2); /* 初始化命令字3: 从PIC连接到主PIC的IRQ2 */io_out8(PIC0_ICW4, 0x01 ); /* 初始化命令字4: 8086模式 */io_out8(PIC1_ICW1, 0x11 ); /* 从PIC初始化命令字1: 同主PIC */io_out8(PIC1_ICW2, 0x28 ); /* 从PIC中断向量基址为0x28 */io_out8(PIC1_ICW3, 2 ); /* 从PIC标识号为2(对应主PIC的IRQ2) */io_out8(PIC1_ICW4, 0x01 ); /* 从PIC初始化命令字4: 同主PIC */io_out8(PIC0_IMR, 0xfb ); /* 允许从PIC中断(IRQ2),其他保持屏蔽 */io_out8(PIC1_IMR, 0xff ); /* 保持从PIC所有中断屏蔽 */return;
}//程序中的PIC0和PIC1,分别指主PIC和从PIC。
//具体的端口号码写在bootpack.h里,
//写入ICW1之后,紧跟着一定要写入ICW2等,所以即使端口号
//相同,也能够很好地区别开来。
-
PIC是“programmable interrupt controller”的缩写,意思是“可编程中断控制器”。
-
-
PIC是将8个中断信号(interrupt request,缩写为IRQ。)集合成一个中断信号的装置。PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。
各 IRQ 对应的中断源及其在代码中的体现:
PIC0 (主芯片,端口 0x20-0x21)master PIC 的中断源:
- IRQ0 (0x20): 定时器中断(代码中未显式使用)
- IRQ1 (0x21): 键盘中断 → 对应
idt + 0x21
和asm_inthandler21
- IRQ2 (0x22): 级联 PIC1 的中断(必须保持开启)
- IRQ3 (0x23): COM2 串口(未使用)
- IRQ4 (0x24): COM1 串口(未使用)
- IRQ5 (0x25): LPT2 并口(未使用)
- IRQ6 (0x26): 软盘控制器(未使用)
- IRQ7 (0x27): LPT1 并口 → 对应
idt + 0x27
和asm_inthandler27
PIC1 (从芯片,端口 0xA0-0xA1)slave PIC 的中断源:
- IRQ8 (0x28): CMOS 实时时钟(未使用)
- IRQ9 (0x29): 自由中断(未使用)
- IRQ10 (0x2A): 自由中断(未使用)
- IRQ11 (0x2B): 自由中断(未使用)
- IRQ12 (0x2C): PS/2 鼠标中断 → 对应
idt + 0x2c
和asm_inthandler2c
- IRQ13 (0x2D): FPU 异常(未使用)
- IRQ14 (0x2E): 主 IDE 控制器(未使用)
- IRQ15 (0x2F): 次 IDE 控制器(未使用)
- ibm设置了两个pic,中断信号有15个
- 与CPU直接相连的PIC称为主PIC(master PIC),与主PIC相连的PIC称为从
PIC(slave PIC)。主PIC负责处理第0到第7号中断信号,从PIC负责处理第8到第15
号中断信号。
PIC的寄存器
它们都是8位寄存器。
- IMR是“interrupt mask register”的缩写,意思是“中断屏蔽寄存器”。1,就屏蔽,该位的IRQ
- ICW是“initial control word”的缩写,意为“初始化控制数据”。
- ICW有4个,分别编号为1~4,共有4个字节的数据。
- ICW1和ICW4与PIC主板配线方式、中断信号的电气特性等有关
- ICW3是有关主—从连接的设定,对主PIC而言,第几号IRQ与从PIC相连,是用8位来设定的。但我们所用的电脑并不是这样的,所以就设定成00000100。对从PIC来说,该从PIC与主PIC的第几号相连,用3位来设定。
- 这上面的icw都硬件决定,软件方面不能更改
- 不同的操作系统可以进行独特设定的就只有ICW2
- 这个ICW2,决定了IRQ以哪一号中断通知CPU
- ICW2的作用:
- 决定IRQ以哪个中断号通知CPU
- 主PIC默认设置为0x20(IRQ0-7对应INT 20-27)
- 从PIC默认设置为0x28(IRQ8-15对应INT 28-2f)
- 为什么不能使用0x00-0x1f:
- 这些中断号被CPU保留用于内部异常处理(如除零错误、页错误等)
- 如果IRQ使用这些号码,CPU无法区分是硬件中断还是系统异常
- 这是x86架构的设计规范
- 中断触发机制:
- PIC通过发送0xcd(INT指令)加中断号来触发CPU中断
- 例如:IRQ0会触发PIC发送0xcd 0x20,CPU执行INT 0x20
注意:0xcd 0x20在CPU看来,从内存读进来的程序是完全一样的 ,所以CPU就把送过来的“0xcd 0x??”作为机器语言执行。这恰恰就是把数据当作程序来执行的情况。这里的0xcd就是调用BIOS时使用的那个INT指令。我们在程序里写的“INT 0x10”,最后就被编译成了“0xcd0x10”。所以,CPU上了PIC的当,按照PIC所希望的中断号执行了INT指令。
实模式和保护模式
首先先解释一下实模式和保护模式,然后解释为什么要编写中断处理程序而不是用bios中断
实模式(Real Mode)
- 基本特点:
- 16位模式,兼容最早的8086处理器
- 直接物理内存访问,最大寻址空间1MB(20位地址线)
- 使用段寄存器:偏移量的方式计算物理地址(物理地址=段寄存器×16+偏移量)
- 典型应用:
- 计算机启动时CPU自动进入实模式
- BIOS运行环境
- DOS操作系统的工作模式
- 局限性:
- 无内存保护机制
- 无特权级别划分
- 无法支持多任务和现代操作系统需求
保护模式(Protected Mode)
- 基本特点:
- 32/64位模式(在您的代码中使用了32位保护模式)
- 支持4GB内存空间(32位)或更大(64位)
- 引入分段和分页内存管理机制
- 支持4个特权级(0-3级),0级最高(内核),3级最低(用户程序)
- 关键机制:
- 全局描述符表(GDT)和局部描述符表(LDT)
- 中断描述符表(IDT)
- 内存分页机制(可选)
- 硬件级任务切换支持
- 优势:
- 内存保护:防止程序越界访问
- 特权隔离:内核和用户程序分离
- 支持虚拟内存
- 支持多任务操作系统
注意:从实模式切换到保护模式是操作系统启动过程中的关键步骤,通过load_gdtr
和load_idtr
等函数完成了这一转换。
主要就是这个权限的分离,通过上面所说的关键机制实现
为什么不能用bios中断:
- 运行模式不同:
- BIOS中断运行在16位实模式下
- 您的操作系统运行在32位保护模式下
- 保护模式下无法直接调用实模式的BIOS中断
- 内存管理差异:
- 保护模式下使用分段和分页内存管理
- BIOS中断处理程序假设CPU处于实模式
- 直接调用会导致内存访问错误
- 中断上下文不同:
- BIOS中断处理程序假设特定的寄存器状态和栈布局
- 保护模式下的中断上下文与之不兼容
- 功能需求差异:
- BIOS提供的是通用硬件抽象
- 操作系统需要特定的、可定制的硬件控制
- 例如:键盘缓冲区管理、鼠标坐标处理等
- 性能考虑:
- BIOS中断处理较慢
- 操作系统需要高效的中断处理
- 自定义处理程序可以优化性能
16位到32位切换的重要步骤
在dsctbl.c
中初始化GDT:
1. 准备GDT(全局描述符表)
dsctbl.cvoid init_gdtidt(void)
{// 设置GDT描述符set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW); // 数据段set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, AR_CODE32_ER); // 代码段// ...
}
2. 加载GDTR寄存器
通过naskfunc.nas
中的汇编指令加载GDT:
_load_gdtr:MOV AX,[ESP+4] ; 加载GDT界限值MOV [ESP+6],AXLGDT [ESP+6] ; 加载GDTR寄存器RET
3. 设置CR0寄存器进入保护模式
通常在启动代码(如asmhead.nas
)中包含以下关键指令:
MOV EAX,CR0AND EAX,0x7fffffff ; 禁用分页OR EAX,0x00000001 ; 设置保护模式位MOV CR0,EAX ; 正式进入保护模式
4. 初始化IDT和PIC
在int.c
中初始化可编程中断控制器:
int.cvoid init_pic(void)
{io_out8(PIC0_ICW2, 0x20); // 主PIC中断向量基址io_out8(PIC1_ICW2, 0x28); // 从PIC中断向量基址// ...
}
5. 远跳转刷新流水线
进入保护模式后立即行远跳转:
JMP DWORD 2*8:0x0000001b ; 跳转到保护模式代码段
注意:
- 顺序不能错:必须先设置GDT,再加载GDTR,最后修改CR0
- 特权级设置:代码段描述符的AR_CODE32_ER(0x409a)包含特权级信息
- 地址空间:保护模式下使用32位地址线,可访问4GB内存
- 中断处理:必须重新设置IDT,不能继续使用实模式的中断向量表
中断处理程序制作(harib03e)
鼠标是IRQ12,键盘是IRQ1,所以我们编写了用于INT 0x2c和INT 0x21的中断处理程序(handler),即中断发生时所要调用的程序。
void inthandler21(int *esp)
/* PS/2键盘中断处理程序 */
{// 获取BOOTINFO结构体指针,该结构体包含系统启动信息struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;// 在屏幕顶部绘制黑色矩形区域(用于显示中断信息)boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);// 在黑色区域显示白色文字,说明这是键盘中断(IRQ1)putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");// 无限循环,暂时不做其他处理for (;;) {io_hlt(); // 执行HLT指令使CPU进入休眠状态}
}void inthandler2c(int *esp)
/* PS/2鼠标中断处理程序 */
{// 获取BOOTINFO结构体指针struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;// 在屏幕顶部绘制黑色矩形区域boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);// 显示鼠标中断信息(IRQ12)putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 2C (IRQ-12) : PS/2 mouse");// 无限循环for (;;) {io_hlt(); // CPU休眠}
}void inthandler27(int *esp)
/* PIC0的不完全中断处理程序 */
/* 在Athlon64X2等多核处理器中,由于PIC初始化时的时序问题,这个中断可能会被触发一次 */
/* 这个中断处理函数不需要实际处理任何设备中断 */
/* 为什么不需要处理?因为这个中断是由PIC芯片本身的电气特性引起的伪中断,所以不需要进行任何实质性的处理 */
{// 向PIC发送EOI(End Of Interrupt)命令,通知中断处理完成// 0x67参数表示: // - 0x60: EOI命令的基础值// - 0x07: 指定IRQ7(这是PIC的级联中断线)io_out8(PIC0_OCW2, 0x67); return;
}
_asm_inthandler21:PUSH ESPUSH DSPUSHADMOV EAX,ESPPUSH EAXMOV AX,SSMOV DS,AXMOV ES,AXCALL _inthandler21POP EAXPOPADPOP DSPOP ESIRETD_asm_inthandler27:PUSH ESPUSH DSPUSHADMOV EAX,ESPPUSH EAXMOV AX,SSMOV DS,AXMOV ES,AXCALL _inthandler27POP EAXPOPADPOP DSPOP ESIRETD_asm_inthandler2c:PUSH ESPUSH DSPUSHADMOV EAX,ESPPUSH EAXMOV AX,SSMOV DS,AXMOV ES,AX
;关于在DS和ES中放入SS值的部分,因为C语言自以为是地认为“DS也好,ES也好,
;SS也好,它们都是指同一个段”,所以如果不按照它的想法设定的话,函数
;inthandler21就不能顺利执行。CALL _inthandler2cPOP EAXPOPADPOP DSPOP ESIRETD
IRETD和代码结构解释:
在您提供的代码片段中,IRETD
是一个 x86 汇编指令,通常用于从中断服务例程返回到调用程序。以下是对其的详细解释:
然后要往中断向量表里写入中断程序地址(idt的设定)
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
/*
第一个参数:写明程序所对应的中断地址
第二个参数:写明程序的地址
第三个参数:代码段选择子(selector),这里2表示GDT中的第2个描述符,*8是因为每个描述符占8字节GDT中的每个描述符占8个字节,所以索引号需要乘以8来得到正确的偏移量“2 * 8” 也可以写成 “2<<3”, 当然,写成16也可以。
第四个参数:属性值(0x008e),表示32位中断门,AR_INTGATE32将IDT的属性,设定为0x008e。
它表示这是用于中断处理的有效设定。
*/
选择子计算规则:
-
索引号:
2
表示使用GDT中的第3个描述符(从0开始计数)- gdt[0]:空描述符(保留)
- gdt[1]:数据段描述符
- gdt[2]:代码段描述符
-
乘法原理:
8
是因为每个描述符占8字节- 描述符0的地址 = GDT基地址 + 0*8
- 描述符1的地址 = GDT基地址 + 1*8
- 描述符2的地址 = GDT基地址 + 2*8
-
选择子结构:
| 15..3 | 2 | 1..0 |索引号 TI RPL
- 这里
2*8
=0x0010
(二进制0000 0000 0001 0000
) - TI=0(使用GDT),RPL=00(请求特权级0,内核模式)
- 这里
对应到实际内存:
当CPU通过这个选择子访问代码段时,会:
- 用
0x0010 >> 3
= 2 得到GDT索引号 - 通过
gdtr
寄存器找到GDT基地址 - 基地址 + 2*8 找到代码段描述符
- 用描述符中的基地址和限制值进行内存访问验证
IRETD
的作用
- 全称:
IRETD
是 “Interrupt Return Doubleword” 的缩写。 - 功能:它从堆栈中弹出返回地址和处理器状态(EFLAGS 寄存器),并将控制权交还给中断发生前的代码。
- 适用环境:
IRETD
专门用于保护模式(Protected Mode)下的 32 位环境。如果是在实模式(Real Mode)下,通常使用IRET
。
工作原理
当中断发生时,CPU 会自动将以下内容压入堆栈:
- 返回地址(包括 CS 段寄存器和 EIP 指令指针)。
- EFLAGS 寄存器的值。
- 如果是任务切换或特权级改变,还会保存额外的段寄存器(如 SS 和 ESP)。
IRETD
的作用是:
- 从堆栈中依次弹出 EIP、CS 和 EFLAGS 的值。
- 恢复中断发生前的 CPU 状态。
- 将程序控制权返回到中断发生前的代码。
使用场景
- 在操作系统内核中,用于处理硬件或软件中断。
- 在自定义的中断服务例程(ISR)中,用于结束中断处理。
注意事项
- 如果堆栈中的数据不正确(例如被破坏或未正确保存),
IRETD
会导致程序崩溃或行为异常。 - 在 64 位模式下,
IRETD
被替换为IRETQ
,用于处理 64 位地址。
示例代码
以下是一个简单的中断服务例程的伪代码,展示了 IRETD
的使用:
section .text
global isr_handlerisr_handler:; 保存通用寄存器pusha; 处理中断逻辑; ...; 恢复通用寄存器popa; 从中断返回IRETD
总结
IRETD
是一个关键的汇编指令,用于从中断返回并恢复 CPU 的状态。它在操作系统开发和底层硬件编程中非常重要。如果您正在编写中断处理程序,请确保堆栈的内容正确无误,以避免潜在问题。
pushad
PUSH
指令
-
作用:将一个寄存器或立即数的值压入堆栈。
-
堆栈变化:堆栈指针(
ESP
)会减少 4(在 32 位模式下),然后将值存储到新的堆栈顶。 -
代码中的用途:这两行代码将段寄存器
ES
和DS
的值保存到堆栈中,以便稍后恢复。这是为了保护中断处理程序修改这些寄存器时不会破坏原有的值。PUSH ES
PUSH DS
PUSHAD
指令
- 作用:将所有通用寄存器的值(
EAX
、ECX
、EDX
、EBX
、ESP
、EBP
、ESI
、EDI
)按顺序压入堆栈。 - 堆栈变化:
ESP
会减少 32(每个寄存器 4 字节,共 8 个寄存器)。 - 代码中的用途:这行代码保存了所有通用寄存器的值,确保中断处理程序可以安全地使用这些寄存器,而不会影响中断返回后的程序状态。
完整的保存和恢复过程
- 保存状态:
PUSH ES
和PUSH DS
保存段寄存器。PUSHAD
保存所有通用寄存器。
- 恢复状态:
POPAD
恢复所有通用寄存器。POP DS
和POP ES
恢复段寄存器。
EFLAG寄存器
- 状态标志位:
- CF (Carry Flag, 位0):进位标志。用于表示无符号数运算的进位或借位
- PF (Parity Flag, 位2):奇偶标志。表示结果中1的个数是否为偶数
- AF (Auxiliary Carry Flag, 位4):辅助进位标志。用于BCD运算
- ZF (Zero Flag, 位6):零标志。表示运算结果是否为零
- SF (Sign Flag, 位7):符号标志。表示运算结果的符号(0为正,1为负)
- OF (Overflow Flag, 位11):溢出标志。表示有符号数运算是否溢出
- 控制标志位:
- DF (Direction Flag, 位10):方向标志。控制字符串操作的方向(0=递增,1=递减)
- 系统标志位:
- TF (Trap Flag, 位8):陷阱标志。用于单步调试
- IF (Interrupt Flag, 位9):中断允许标志。控制是否响应可屏蔽中断
- IOPL (I/O Privilege Level, 位12-13):I/O特权级。决定当前任务的I/O权限
- NT (Nested Task, 位14):嵌套任务标志。表示当前任务是否嵌套在另一个任务中
- RF (Resume Flag, 位16):恢复标志。用于调试异常处理
- VM (Virtual-8086 Mode, 位17):虚拟8086模式标志
- AC (Alignment Check, 位18):对齐检查标志
- VIF (Virtual Interrupt Flag, 位19):虚拟中断标志
- VIP (Virtual Interrupt Pending, 位20):虚拟中断挂起标志
- ID (ID Flag, 位21):ID标志。表示CPU是否支持CPUID指令
修改pic的IMR
io_out8(PIC0_IMR, 0xf9);
io_out8(PIC1_IMR, 0xef);
以便接受来自键盘和鼠标的中断