当前位置: 首页 > news >正文

自制操作系统day6(GDTR、段描述符、PIC、实模式和保护模式、16位到32位切换、中断处理程序、idt的设定、EFLAG寄存器)(ai辅助整理)

day6

分割源文件(harib03a


优点

  1. 按照处理内容进行分类,如果分得好的话,将来进行修改时,容易找到地方。
  2. 如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度。
  3. 单个源文件都不长。多个小文件比一个大文件好处理。
  4. 看起来很酷(笑)。
    缺点
  5. 源文件数量增加。
  6. 分类分得不好的话,修改时不容易找到地方。
    在这里插入图片描述

makefile修改

在这里插入图片描述

整理头文件(harib03c


将所有的宏,常量,数据结构和函数声明都放在一个.h头文件当中,编译时将.h的内容插到.c文件当中

解析头文件中的宏,常量,数据结构和函数声明并用于后续的编译

头文件的作用

  1. 数据结构定义:
    • struct BOOTINFO:描述了启动信息,包括屏幕分辨率、显存地址等。这些信息由启动代码(如引导扇区)提供,用于初始化操作系统的图形界面。
    • struct SEGMENT_DESCRIPTORstruct GATE_DESCRIPTOR:用于描述全局段表(GDT)和中断描述符表(IDT)的结构。
  2. 常量定义:
    • 定义了与中断控制器相关的端口地址(如 PIC0_ICW1)。
    • 定义了颜色常量(如 COL8_000000 表示黑色,COL8_FFFFFF 表示白色)和内存地址常量(如 ADR_BOOTINFO 表示启动信息的地址)。
  3. 函数声明:
    • 包括汇编函数(如 io_hltio_cli)和 C 函数(如 init_paletteinit_screen8)。
    • 汇编函数通常用于直接与硬件交互,而 C 函数用于更高级别的操作,如初始化图形界面或设置鼠标指针。
  4. 模块化管理:
    • 将不同功能的代码分离到不同的文件中(如 graphic.c 处理图形相关功能,dsctbl.c 处理段表和中断表),通过头文件统一管理这些模块的接口。

编译时的运作

  1. 预处理阶段:
    • 编译器在编译 bootpack.c 时,会通过 #include "bootpack.h" 将 bootpack.h 的内容插入到 bootpack.c 中。
    • 头文件中的宏、常量、数据结构和函数声明会被解析并用于后续的编译。
  2. 编译阶段:
    • 编译器根据头文件中声明的函数原型检查 bootpack.c 中的函数调用是否正确。
    • 如果头文件中声明的函数在其他源文件中实现(如 init_palette 在 graphic.c 中实现),编译器会生成对这些函数的外部引用。
  3. 链接阶段:
    • 编译器将 bootpack.c 和其他源文件(如 graphic.c、dsctbl.c)编译成目标文件后,链接器会将这些目标文件合并为一个可执行文件。
    • 链接器会根据头文件中的声明找到函数和变量的定义地址,并将它们正确地链接起来。
  4. 运行时:
    • 系统启动时,bootpack.c 中的 HariMain 函数作为入口点被调用。
    • HariMain 调用的所有函数(如 init_gdtidtinit_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 函数时,limitaddr 是作为参数传递的:
    • 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

  • AX0xFFFF)写入 ESP+6ESP+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)。

最终栈布局总结

地址     字节值      解释
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 后两字节保持原值

关键点

  1. 小端存储:所有 32 位值(如 0x00270000)按字节逆序存储。
  2. 数据覆盖:通过写入 ESP+6 ~ ESP+7,将 limit 的低16位与 addr 的32位合并为连续的6字节。
  3. 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-116 位limit_low段上限的低 16 位,表示段的大小(字节数 - 1)。
2-316 位base_low段基址的低 16 位,表示段的起始地址。
48 位base_mid段基址的中间 8 位。
58 位access_right段的访问权限属性,包括是否可读写、是否可执行、段类型等。
64 位limit_high段上限的高 4 位。
64 位flags段的扩展属性,包括 G 位(粒度)和 D 位(操作数大小)。
78 位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 位):段的具体类型。
      • 代码段:是否可执行、可读等。
      • 数据段:是否可写、扩展方向等。

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 自动设置)。

存储示例

假设:

  • 段基址(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 + 0x21asm_inthandler21
  • IRQ2 (0x22): 级联 PIC1 的中断(必须保持开启)
  • IRQ3 (0x23): COM2 串口(未使用)
  • IRQ4 (0x24): COM1 串口(未使用)
  • IRQ5 (0x25): LPT2 并口(未使用)
  • IRQ6 (0x26): 软盘控制器(未使用)
  • IRQ7 (0x27): LPT1 并口 → 对应 idt + 0x27asm_inthandler27

PIC1 (从芯片,端口 0xA0-0xA1)slave PIC 的中断源:

  • IRQ8 (0x28): CMOS 实时时钟(未使用)
  • IRQ9 (0x29): 自由中断(未使用)
  • IRQ10 (0x2A): 自由中断(未使用)
  • IRQ11 (0x2B): 自由中断(未使用)
  • IRQ12 (0x2C): PS/2 鼠标中断 → 对应 idt + 0x2casm_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
  1. ICW2的作用:
  • 决定IRQ以哪个中断号通知CPU
  • 主PIC默认设置为0x20(IRQ0-7对应INT 20-27)
  • 从PIC默认设置为0x28(IRQ8-15对应INT 28-2f)
  1. 为什么不能使用0x00-0x1f:
  • 这些中断号被CPU保留用于内部异常处理(如除零错误、页错误等)
  • 如果IRQ使用这些号码,CPU无法区分是硬件中断还是系统异常
  • 这是x86架构的设计规范
  1. 中断触发机制:
  • 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)

  1. 基本特点
    • 16位模式,兼容最早的8086处理器
    • 直接物理内存访问,最大寻址空间1MB(20位地址线)
    • 使用段寄存器:偏移量的方式计算物理地址(物理地址=段寄存器×16+偏移量)
  2. 典型应用
    • 计算机启动时CPU自动进入实模式
    • BIOS运行环境
    • DOS操作系统的工作模式
  3. 局限性
    • 无内存保护机制
    • 无特权级别划分
    • 无法支持多任务和现代操作系统需求

保护模式(Protected Mode)

  1. 基本特点
    • 32/64位模式(在您的代码中使用了32位保护模式)
    • 支持4GB内存空间(32位)或更大(64位)
    • 引入分段和分页内存管理机制
    • 支持4个特权级(0-3级),0级最高(内核),3级最低(用户程序)
  2. 关键机制
    • 全局描述符表(GDT)和局部描述符表(LDT)
    • 中断描述符表(IDT)
    • 内存分页机制(可选)
    • 硬件级任务切换支持
  3. 优势
    • 内存保护:防止程序越界访问
    • 特权隔离:内核和用户程序分离
    • 支持虚拟内存
    • 支持多任务操作系统

注意:从实模式切换到保护模式是操作系统启动过程中的关键步骤,通过load_gdtrload_idtr等函数完成了这一转换。

主要就是这个权限的分离,通过上面所说的关键机制实现

为什么不能用bios中断:

  1. 运行模式不同
    • BIOS中断运行在16位实模式下
    • 您的操作系统运行在32位保护模式下
    • 保护模式下无法直接调用实模式的BIOS中断
  2. 内存管理差异
    • 保护模式下使用分段和分页内存管理
    • BIOS中断处理程序假设CPU处于实模式
    • 直接调用会导致内存访问错误
  3. 中断上下文不同
    • BIOS中断处理程序假设特定的寄存器状态和栈布局
    • 保护模式下的中断上下文与之不兼容
  4. 功能需求差异
    • BIOS提供的是通用硬件抽象
    • 操作系统需要特定的、可定制的硬件控制
    • 例如:键盘缓冲区管理、鼠标坐标处理等
  5. 性能考虑
    • 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  ; 跳转到保护模式代码段

注意:

  1. 顺序不能错:必须先设置GDT,再加载GDTR,最后修改CR0
  2. 特权级设置:代码段描述符的AR_CODE32_ER(0x409a)包含特权级信息
  3. 地址空间:保护模式下使用32位地址线,可访问4GB内存
  4. 中断处理:必须重新设置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。
它表示这是用于中断处理的有效设定。
*/

选择子计算规则:

  1. 索引号2 表示使用GDT中的第3个描述符(从0开始计数)

    • gdt[0]:空描述符(保留)
    • gdt[1]:数据段描述符
    • gdt[2]:代码段描述符
  2. 乘法原理8 是因为每个描述符占8字节

    • 描述符0的地址 = GDT基地址 + 0*8
    • 描述符1的地址 = GDT基地址 + 1*8
    • 描述符2的地址 = GDT基地址 + 2*8
  3. 选择子结构

    
    | 15..3 | 2 | 1..0 |索引号   TI  RPL
    • 这里 2*8 = 0x0010(二进制 0000 0000 0001 0000
    • TI=0(使用GDT),RPL=00(请求特权级0,内核模式)

对应到实际内存:

当CPU通过这个选择子访问代码段时,会:

  1. 0x0010 >> 3 = 2 得到GDT索引号
  2. 通过 gdtr 寄存器找到GDT基地址
  3. 基地址 + 2*8 找到代码段描述符
  4. 用描述符中的基地址和限制值进行内存访问验证

IRETD 的作用

  1. 全称IRETD 是 “Interrupt Return Doubleword” 的缩写。
  2. 功能:它从堆栈中弹出返回地址和处理器状态(EFLAGS 寄存器),并将控制权交还给中断发生前的代码。
  3. 适用环境IRETD 专门用于保护模式(Protected Mode)下的 32 位环境。如果是在实模式(Real Mode)下,通常使用 IRET

工作原理

当中断发生时,CPU 会自动将以下内容压入堆栈:

  1. 返回地址(包括 CS 段寄存器和 EIP 指令指针)。
  2. EFLAGS 寄存器的值。
  3. 如果是任务切换或特权级改变,还会保存额外的段寄存器(如 SS 和 ESP)。

IRETD 的作用是:

  1. 从堆栈中依次弹出 EIP、CS 和 EFLAGS 的值。
  2. 恢复中断发生前的 CPU 状态。
  3. 将程序控制权返回到中断发生前的代码。

使用场景

  • 在操作系统内核中,用于处理硬件或软件中断。
  • 在自定义的中断服务例程(ISR)中,用于结束中断处理。

注意事项

  • 如果堆栈中的数据不正确(例如被破坏或未正确保存),IRETD 会导致程序崩溃或行为异常。
  • 在 64 位模式下,IRETD 被替换为 IRETQ,用于处理 64 位地址。

示例代码

以下是一个简单的中断服务例程的伪代码,展示了 IRETD 的使用:

section .text
global isr_handlerisr_handler:; 保存通用寄存器pusha; 处理中断逻辑; ...; 恢复通用寄存器popa; 从中断返回IRETD

总结

IRETD 是一个关键的汇编指令,用于从中断返回并恢复 CPU 的状态。它在操作系统开发和底层硬件编程中非常重要。如果您正在编写中断处理程序,请确保堆栈的内容正确无误,以避免潜在问题。

pushad

PUSH 指令

  • 作用:将一个寄存器或立即数的值压入堆栈。

  • 堆栈变化:堆栈指针(ESP)会减少 4(在 32 位模式下),然后将值存储到新的堆栈顶。

  • 代码中的用途:这两行代码将段寄存器 ESDS 的值保存到堆栈中,以便稍后恢复。这是为了保护中断处理程序修改这些寄存器时不会破坏原有的值。

    PUSH ES

    PUSH DS


PUSHAD 指令

  • 作用:将所有通用寄存器的值(EAXECXEDXEBXESPEBPESIEDI)按顺序压入堆栈。
  • 堆栈变化ESP 会减少 32(每个寄存器 4 字节,共 8 个寄存器)。
  • 代码中的用途:这行代码保存了所有通用寄存器的值,确保中断处理程序可以安全地使用这些寄存器,而不会影响中断返回后的程序状态。

完整的保存和恢复过程

  1. 保存状态
    • PUSH ESPUSH DS 保存段寄存器。
    • PUSHAD 保存所有通用寄存器。
  2. 恢复状态
    • POPAD 恢复所有通用寄存器。
    • POP DSPOP ES 恢复段寄存器。

EFLAG寄存器

  1. 状态标志位
    • 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):溢出标志。表示有符号数运算是否溢出
  2. 控制标志位
    • DF (Direction Flag, 位10):方向标志。控制字符串操作的方向(0=递增,1=递减)
  3. 系统标志位
    • 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);

以便接受来自键盘和鼠标的中断

相关文章:

  • STM32 I2C硬件读写
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Hidden Search Widget (交互式搜索框)
  • 拉普拉斯高斯(LoG)滤波器掩模的注意事项
  • vue3 + vite 使用tailwindcss
  • 关于收集 Android Telephony 网络信息的设计思考2
  • Git 提交大文件 this exceeds GitHub‘s file size limit of 100.00 MB
  • 【WebRTC】源码更改麦克风权限
  • windows服务器部署jenkins工具(二)
  • npm、pnpm、yarn 各自优劣深度剖析
  • DeepSeek赋能智能家居:构建高智能、低延迟的物联网生态
  • HarmonyOS NEXT端云一体化工程目录结构
  • STM32项目分享:智能家居(机智云)升级版
  • 学习STC51单片机10(芯片为STC89C52RC)
  • 网络学习-利用reactor实现http请求(六)
  • esp32cmini SK6812 2个方式
  • JavaScript APIs学习day2--DOM!!
  • Open CASCADE学习|刚体沿曲线运动实现方法
  • 前端学习(5)—— JavaScript(WebAPI)
  • 文件上传功能uploadify.js报updateSettings is not a function
  • EasyRTC嵌入式音视频通信SDK一对一音视频通信,打造远程办公/医疗/教育等场景解决方案
  • 株洲手机网站建设/微信公众平台开发
  • cms网站/专业竞价托管哪家好
  • 网站开发应走什么科目/网站搜索引擎优化技术
  • 360免费建站空间/网站页面优化包括
  • 公司网站彩页怎么做/雅虎搜索引擎
  • 临沂网站建设服务/企业网络规划设计方案