中断系统配置

1、创建GDT表及其表项

在进入保护模式时,当时临时创建了一个简易的GDT表,这里重新创建GDT表和表项。

在进入保护模式后,所有的内存访问操作,都是通过GDT表(也包含LDT表),GDT表每项称为段描述符,具体结构如下:

GDT表大致分为三部分:base addr(基地址)、limit(界限)、一些属性值

GDT表最大为64KB,每个表项为8字节。在GDT表设置好后,需要通过lgdt()将表的信息加载到GDTR寄存器中。

GDTR寄存器包含两部分:

        基址(Base Address):GDT 在内存中的起始地址。

        限制(Limit):GDT 的大小减去 1(以字节为单位)。

GDTR 是一个 CPU 寄存器,用于存储 GDT 的基址和大小,指定了CPU 在 GDT 中查找描述符的位置和 GDT 的总大小。

相关代码实现(cpu.c 和 cpu.h)

/**
 * 设置段描述符
 */
void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr) {
    segment_desc_t * desc = gdt_table + selector  / sizeof(segment_desc_t);  //将指针 desc 指向 gdt_table 中对应索引位置的段描述符

    //设置段描述符的各个字段
    desc->limit15_0 = limit & 0xffff;
	desc->base15_0 = base & 0xffff;
	desc->base23_16 = (base >> 16) & 0xff;
	desc->attr = attr | (((limit >> 16) & 0xf) << 8);
	desc->base31_24 = (base >> 24) & 0xff;
}

/**
 * 初始化GDT
 *将这段代码将所有 GDT 表项清空,确保没有旧的数据残留,为后续的段描述符设置做好准备。
 */
void init_gdt(void) {
	// 全部清空
    for (int i = 0; i < GDT_TABLE_SIZE; i++) {
        segment_desc_set(i * sizeof(segment_desc_t), 0, 0, 0);
    }
}

/**
 * CPU初始化
 */
void cpu_init (void) {
    init_gdt();
}

#pragma pack(1)   //告诉编译器按照 1 字节对齐来存储结构体中的成员变量。

/**
 * GDT描述符
 */
typedef struct _segment_desc_t {
	uint16_t limit15_0;  //它表示段的大小范围,最大可表示 64KB 的段
	uint16_t base15_0;
	uint8_t base23_16;
	uint16_t attr;   //段的属性(attr),包含段类型、描述符特权级别(DPL)、段存在位等信息
	uint8_t base31_24;
}segment_desc_t;

#pragma pack()   //恢复结构体默认的对齐方式

void cpu_init (void);
void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr);

2、保护模式下的内存管理简介

1)IA-32 内存管理机制

  • 分段(Segmentation):将内存划分为若干段,每个段都有起始地址和长度,并用来提供不同的内存视图,如代码段、数据段和堆栈段等。
  • 分页(Paging):将内存划分为固定大小的页(通常是 4KB),并使用页表将虚拟地址映射到物理地址,以支持虚拟内存管理和地址空间隔离。

        即段式存储与页式存储

2)段式存储特点

  • 将线性地址空间转变成多个段(segments)

  • 每个段带有相关的保护机制

  • 有多种类型的段:数据、代码、栈、门、tss

  • 使用的地址为逻辑地址,即段选择子+偏移

3)页式存储的特点

  • 将线性地址转换为逻辑地址
  •  在较小的内存上实现更大的虚拟内容
  •  按需加载等功能

4)平坦模型

IA32的段式存储比较复杂,因此使用平坦模型:所有的内存段(数据段、代码段、栈段等)都设置成从0地址开始,长度覆盖到最大地址空间(4GB)

以下是实现平坦模型的基本步骤:

  1. 初始化 GDT(全局描述符表):设置代码段和数据段描述符的基址为 0,段长度为 4GB,权限设定为允许读取和执行。

  2. 设置段寄存器:将所有的段寄存器(CSDSSSESFSGS)指向 GDT 中的相应段描述符。

  3. 启用分页:通过控制寄存器 CR0 和页表来启用分页模式,提供虚拟内存管理功能

5)相关代码

/**
 * 初始化GDT
 */
void init_gdt(void) {
	// 全部清空
    for (int i = 0; i < GDT_TABLE_SIZE; i++) {
        segment_desc_set(i * sizeof(segment_desc_t), 0, 0, 0);
    }

    //数据段
    segment_desc_set(KERNEL_SELECTOR_DS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_DATA
                     | SEG_TYPE_RW | SEG_D);

    // 只能用非一致代码段,以便通过调用门更改当前任务的CPL执行关键的资源访问操作
    segment_desc_set(KERNEL_SELECTOR_CS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_CODE
                     | SEG_TYPE_RW | SEG_D);


    // 加载gdt
    lgdt((uint32_t)gdt_table, sizeof(gdt_table));
}

设置的权限:

#define SEG_G				(1 << 15)		// 设置段界限的单位,1-4KB,0-字节
#define SEG_D				(1 << 14)		// 控制是否是32位、16位的代码或数据段
#define SEG_P_PRESENT	    (1 << 7)		// 段是否存在

#define SEG_DPL0			(0 << 5)		// 特权级0,最高特权级
#define SEG_DPL3			(3 << 5)		// 特权级3,最低权限

#define SEG_S_SYSTEM		(0 << 4)		// 是否是系统段,如调用门或者中断
#define SEG_S_NORMAL		(1 << 4)		// 普通的代码段或数据段

#define SEG_TYPE_CODE		(1 << 3)		// 指定其为代码段
#define SEG_TYPE_DATA		(0 << 3)		// 数据段

#define SEG_TYPE_RW			(1 << 1)		// 是否可写可读,不设置为只读

6)逻辑地址、线性地址、物理地址关系

逻辑地址是由程序生成的地址,又称为虚拟地址。由程序员或编译器在代码中显式或隐式地定义

逻辑地址包含两个部分:

        1)段选择子(Segment Selector):指定内存的哪个段(如代码段、数据段、栈段)被访问。段选择子是一个指向段描述符表(如 GDT 或 LDT)的索引。

        2)段内偏移量(Offset):指定段内的偏移量或地址。

线性地址是逻辑地址经过分段(Segmentation)机制转换后得到的地址。它是一个统一的内存地址空间中的地址

在使用 x86 的分段机制时,线性地址的计算方式如下:

  • 从段描述符中获取段的基址(Base Address)。
  • 将段的基址与逻辑地址中的段内偏移量相加,得到线性地址。

物理地址是最终用于访问内存芯片的地址,也就是内存单元的实际地址。它是线性地址经过分页(Paging)机制转换后得到的地址。

7)逻辑地址到线性地址的转换

在程序进行内存访问时,会进行逻辑地址的转换,转换到线性地址(暂不考虑分页机制),转换过程如下:

3、重新加载GDT

        之前创建的GDT表有些细节并未处理,而且原表项太小,以及该内存有其他用途,如栈

        GDT表项格式

以下仅界限表项的属性部分:

  • S:1-代码段或数据段,0 - 系统段
  • DPL:特权级,取值0-3
  • P:段是否存在,1存在,0不存在
  • D/B:指示代码或栈的大小是32位还是16位,1 - 32位,0-16位
  • G:段界限的单位。1 - 界限的单位是4KB,0 - 字节。
  • L和AVL:我们不没有用到,使用0即可。
  • type:见下表,用于描述段的具体类型

在原来的基础上设置了门描述符

/**
 * 设置门描述符
 */
void gate_desc_set(gate_desc_t * desc, uint16_t selector, uint32_t offset, uint16_t attr) {
	desc->offset15_0 = offset & 0xffff;
	desc->selector = selector;
	desc->attr = attr;
	desc->offset31_16 = (offset >> 16) & 0xffff;
}
/*
 * 调用门描述符
 */
typedef struct _gate_desc_t {
	uint16_t offset15_0;
	uint16_t selector;
	uint16_t attr;
	uint16_t offset31_16;
}gate_desc_t;

4、触发运行中的异常(除0异常)

5、添加中断门描述符

中断们描述符的配置:

  1. 需要准备一个IDT表,并通过LIDT指令加载到IDTR寄存器中。
  2. 在IDT表中,通过选择子索引到对应的GDT表象,进而定位到中断处理程序的基地址。
  3. 加上偏移量后,可以得到中断处理程序在内存中的具体地址

6、初始化IDT表

IDT表的作用与配置

  1. 进入保护模式后,CPU需要一个新的向量表,即IDT表,用于异常和中断处理。
  2. DT表通过IDTR寄存器指向,需要提供符合中断门描述符的表格。
  3. IDT表中的每个条目需要填入选择子和偏移量,指向中断处理程序的代码段。

IDT表的加载与测试

  1. 使用LIDT指令加载IDT表到IDTR寄存器,类似LGDT指令用于GDT表。
  2. 在QEMU中调试IDT表的加载过程,确认表首地址和大小。
  3. 通过QEMU的monitor window验证IDT表加载成功。

7、捕获除0异常

中断保护现场

为了简单期见,直接将所有的寄存器进行压栈保护,压入方法时,CPU会自动保存一部分,其余在中断程序中,通过push指令压入一部分

汇编中断处理函数的实现

  1. 中断处理函数必须用汇编语言编写,因为C语言编译的函数返回指令不符合要求。
  2. 中断处理函数的入口点需要全局声明,并在汇编文件中实现。
  3. 中断处理函数的一般结束指令是iret,这是x86架构上返回中断的指令。

8、解析异常栈信息

发生异常时,硬件会自动压入一部分数据到栈中,下图所示,根据是否有特权级变化,有两种不同类型的压栈方法。

使用pusha指令压入各种寄存器的值,通过push es等指令压入其它段寄存器的值

我们知道对于异常处理函数,压入栈的寄存器相当于传递给它的参数,难点在于压入栈中的寄存器数量太多。因此使用结构体exception_frame_t映射栈中这些寄存器的值,然后在栈中压入所有寄存器值存储的起始位置

9、利用宏异常处理代码

通过引宏的概念来减少代码的重负

  1. 定义异常处理宏,包含异常类型和号码参数。
  2. 宏根据参数值调用特定C处理函数。
  3. 压入错误码和异常号码,提供异常详细信息

10、处理其它类型的所有异常

11、初始化中断控制器

8259定时器中断:

  • 8259用于管理中断,最大只支持对8个中断进行管理
  • 外设的中断请求信号连接到IRQ?,然后再由8259进行处理,最后通过INT信号传递给处理器,产生中断请求信号。处理器在收到INT的请求信号后,跳转到对应的中断处理程序中执行。
  • 硬件部分上,计算机内部已经配置完毕,使用两片8259级联,最多管理15种类型的中断

8259的工作模式较为复杂,本课程只做了非常简单的配置,不考虑中断嵌套、优先级等问题

12、中断的打开与关闭

中断控制受两部分控制

  1. 首先是8259中断控制器控制着外设的中断信号是否允许到达CPU,这项功能由其内部寄存器IMR控制。该控制器为8位,其中bit0控制该芯片的IRQ0,bit1控制IRQ1,依此类推
  2. cpu内部的EFLAGS标志寄存器中的IF位控制中CPU是否响应着任意来自8259的中断信号。如果为1,则允许,如果为0则禁止。

13、启动定时器并打开中断

8253是一颗带有3个内部计数器的定时器的芯片,用于为计算机提供相关的定时和计数功能,主要关心定时器/计数器

为实现对8253进行配置,可通过如下端口进行设置。
端口地址    名称

0x40         定时器0数据端口
0x41         定时器1数据端口
0x42         定时器2数据端口
0x43         模式和命令端口

本项目只需要一个可以周期性产生中断的定时器,无需考虑各种硬件方面的东西。因此,只需要将定时器设置成自动周期性触发中断即可

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值