Featured image of post 《操作系统真象还原》第七章 —— 打开中断机制

《操作系统真象还原》第七章 —— 打开中断机制

本文介绍了内联汇编(包括基本内联汇编、扩展内联汇编)、中断处理以及如何打开中断和编写中断处理程序

本章节所有代码托管在miniOS_32

章节任务介绍

任务简介

上一节,我们构建了内核的第一个基本功能——打印字符及字符串

本节我们将介绍有关中断的知识以及如何打开中断,最终我们将使用代码打开时钟中断进行测试

本章的主要任务有:

  1. 打开中断机制,并使用时钟中断进行测试

前置知识

中断分类

按照事件来源进行划分,中断可分为外部中断和内部中断,来自CPU外部的事件叫外部中断,而来自CPU内部的事件叫内部中断

其中,外部中断按照是否可宕机划分,又可分为可屏蔽中断(意思是说,该中断事件不是太严重,还不能导致机器宕机,因此CPU可以暂时屏蔽掉不处理)和不可屏蔽中断(该类中断事件非常严重,会导致机器宕机,因此CPU应当立即处理,不可屏蔽)

另外,内部中断也可以按照中断是否正常划分为软中断和异常,其中异常又可为故障(Fault)、陷阱(Trap)、终止(Abort)三种

如下所示

  • 外部中断
    • 可屏蔽中断
    • 不可屏蔽中断
  • 内部中断
    • 软中断
    • 异常
      • 故障(Fault)
      • 陷阱(Trap)
      • 终止(Abort)

外部中断

外部中断是指来自CPU外部的中断事件,外部中断的中断源必须是某个硬件,因此外部中断也叫硬件中断,例如来自网卡、打印机、硬盘等事件的中断

如图所示,CPU为所有的硬件中断提供了两条信号线,所有来自外设的中断信号都可以通过这两条信号线链接到CPU

  1. INTR:用于接收来自硬件设备如硬盘、网卡、打印机发出的可屏蔽中断
    • 这类中断事件不会影响系统运行,因此CPU可以通过eflags寄存器的IF位将这些外设发出的中断屏蔽掉,甚至不处理
    • 中断处理程序对于这类事件的处理可分为两部分进行处理
      • 上半部处理:在关中断情况下进行处理,只完成中断应答或者硬件复位等重要性工作
      • 下半部处理:在开中断的情况下处理,开始真正处理中断发生的事件,由于在开中断情况下处理,因此如果有新的中断发生,则该旧中断的下半部就会被换下CPU,转而去执行新中断的上半部,至于旧中断的下半部何时处理则取决于调度算法
    • 可屏蔽中断的数量有限,每一种中断源都可以有一个中断向量号
  2. NMI:用于接受外部硬件发出的不可屏蔽中断。通过NMI信号线发给CPU的中断信号,都表示系统发生了致命的错误
    • 这类中断事件的发生说明一定会影响系统的运行,如电源掉电、内存读写错误、总线奇偶校验错误等,必须要处理,CPU不可对齐屏蔽,不能坐视不理
    • 所有的不可屏蔽中断只有一个中断向量号,为2号中断,这是因为不可屏蔽中断发生的时候基本任务软件已经无法解决,没必要按照事件再分配多余的中断向量号了

image-20241217162118849

内部中断

内部中断又可分为软中断和异常

  1. 软中断:由软件主动发出的中断的信号,并不属于客观上的某种错误
    • 以下我们在程序中写的可以发出的软中断指令
      • int 8位立即数,即系统调用
      • int3,调试断点指令
      • into,中断溢出指令
      • bound,检查数组索引越界指令
  2. 异常:异常是指指令执行期间CPU内部产生的错误
    • 上述软中断列举的几个中断指令,除了第一个系统调用指令,其余三个也都可成为异常,因此他们都有一定风险在指令执行期间产生错误。此外,如DIV除法指令的分母为0,将引发0号异常;处理器无法识别某个机器码时也会发起6号异常
    • 异常按照轻重程度也分为三种
      • Fault,即故障,这类错误是可修复的,如缺页异常
      • Trap,即陷阱,如int3断点调试指令,当int3发出这类异常并处理结束后可继续返回向下执行
      • Abort,即终止,这是最严重的异常类型,一旦出现错误将无法修复,程序也将无法继续执行,因此操作系统为了自保,只能将此程序从进程表中去掉

中断描述符表

中断向量号与门描述符

image-20241217165817224

上表即是计算机支持的所有中断和异常

中断机制本质上是来了一个中断信号后,调用相应的中断处理程序进行处理,所以CPU为了统一中断管理,就为每个中断信号分配一个整数,用此整数作为中断的D,这个整数就是所谓的中断向量,然后用此D作为中断描述符表中的索引,然后找到对应的表项,进而即可从中找到对应的中断处理程序

其中

  • 异常和不可屏蔽的中断向量号由CPU自动提供
  • 可屏蔽中断的中断向量号由中断代理(接下来要介绍的8259A)提供
  • 软中断的中断向量号有软件提供

由于一个中断向量号对应一个中断处理程序,所以需要有一张表对这个对应过程进行管理

在实模式下,用于存储中断处理程序入口的表叫中断向量表(Interrupt Vector Table,IVT)

我们接下来要介绍的是在保护模式下使用的中断描述符表(Interrupt Descriptor Table,IDT)

中断描述符表是保护模式下用于存储中断处理程序入口的表,当CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序

IDT中除了有中断描述符,还有任务门描述、陷阱门描述符、调用门描述符,他们统称为门描述符,IDT中只有门描述符

所有的描述符都是8个字节,段描述符描述的是一段内存(如内存的基址、大小和属性),而门描述符描述的是一段代码(中断处理程序)

以下是中断门描述符的格式,我们将来要通过构造它来填充IDT

image-20241217171203927

以下是其他三种门描述符的格式

image-20241217171342849

image-20241217171357817

image-20241217171410516

其中,在现代linux系统中,任务门和调用门用的很少,而我们在本节中也主要使用中断门描述符

中断描述符表寄存器

GDT需要有一个寄存器(IDTR)用于存储GDT的地址,IDT也需要有一个寄存器来存储IDT的地址,即IDTR,以下是IDTR的结构格式

image-20241217171849572

其中0 ~ 15位存储IDT的表界限(表大小-1),16 ~ 47位存储IDT表基址

加载IDT的指令方式

  • lidt 48位内存数据

中断处理过程

完整的中断过程分为CPU外和CPU内两部分。

  • CPU外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到CPU。
  • CPU 内:CPU执行该中断向量号对应的中断处理程序。

发生在CPU外的中断过程由接下来介绍的中段代理8259A完成

以下是发生在CPU内部的中断处理过程

image-20241217172237867

如图所示

  • 由于IDT中全都是门描述符,所以图中某门描述符”表示中断门、陷阱门或任务门
  • 中断发生后,eflags中的NT位和TF位会被置0
    • 如果中断对应的门描述符是中断门,标志寄存器 eflags中的IF位被自动置0,避免中断嵌套,即中断处理过程中又来了个新的中断,这是为防止在处理某个中断的过程中又来了一个相同的中断。这表示默认情况下,处理器会在无人打扰的方式下执行中断门描述符中的中断处理例程
    • 若中断发生时对应的描述符是任务门或陷阱门的话,CPU是不会将IF位清0的。因为陷阱门主要用于调试,它允许 CPU响应更高级别的中断,所以允许中断嵌套。而对任务门来说,这是执行一个新任务
  • **从中断返回的指令是iret,它从栈中弹出数据到寄存器cseipeflags**等,根据特权级是否改变,判断是否要恢复旧栈。也就是说是否将栈中位于SS_old和ESP_old 位置的值弹出到寄存ss和 esp。当中断处理程序执行完成返回后,通过iret指令从栈中恢复eflags 的内容

可编程中断控制器8259A

8259A芯片介绍

为了让CPU获得每个外部设备的中断信号,最好的方式是在CPU中为每一个外设准备一个引脚接收中断,但这是不可能的,计算机中挂了很多外部设备,而且外设数量是没有上限的,无论CPU 中准备多少引脚都不够用。

上一节中我们说到,可屏蔽中断是通过INTR信号线进入CPU的,一般可独立运行的外部设备,如打印机、声卡等,其发出的中断都是可屏蔽中断,都共享这一根INTR信号线通知CPU。

但是,任务是串行在 CPU 上执行的,CPU每次只能执行一个任务,如果同时有多个外设发出中断,而 CPU只能先处理一个,它无法指定先响应哪个。同时,为了不使这些中断丢失,也需要为它们单独维护一个中断队列

这就是中断代理的作用,由它负责对所有可屏蔽中断仲裁,决定哪个中断优先被 CPU受理,同时向CPU提供中断向量号等功能

image-20241217174431495

如图所示,有关8259A芯片需要做以下几点说明

  • Intel 处理器共支持 256 个中断,但8259A 只可以管理8个中断,所以为了多支持一些中断设备,需要将多个8259A 组合,也就是级联,有了级联这种组合后,每一个 8259A就被称为1片。
  • 若采用级联方式,即多片8259A芯片串连在一起,最多可级联9个,也就是最多支持 64个中断(片8259A通过级联可支持7n+1个中断源)。
  • 级联时只能有一片 8259A为主片 master,其余的均为从片 slave
  • 来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向CPU发送INT中断信号

8259A芯片编程

在8259A 内部有两组寄存器

  • 一组是初始化命令寄存器组,用来保存初始化命令字(Imitialization Command Words,ICW),ICW共4个,ICW1~ICW4。

  • 另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW共3个,OCW1~OCW3。

所以,我们对8259A的编程,也分为初始化和操作两部分

  1. 初始化部分操作,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是往 8259A 的端口发送一系列 ICW。
    • 由于从一开始就要决定 8259A的工作状态,所以要一次性写入很多设置,某些设置之间是具有关联、依赖性的,也许后面的某个设置会依赖前面某个ICW 写入的设置所以这部分要求严格的顺序,必须依次写入ICW1、ICW2、ICW3、ICW4。
  2. 操作部分是用OCW来操作控制8259A,前面所说的中断屏蔽和中断结束,就是通过往8259A端口发送 OCW 实现的。
    • OCW的发送顺序不固定,3个之中先发送哪个都可以。

编写中断处理程序

启用中断

代码目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.
├── bin
├── boot					//计算机启动代码文件
│   ├── include
│   │   └── boot.inc
│   ├── loader.S
│   └── mbr.S
├── kernel
│   ├── global.h			//各种描述符、段选择子等字段的宏定义文件
│   ├── init.c
│   ├── init.h
│   ├── interrupt.c			//构造IDT、IDTR、打开时钟中断文件
│   ├── interrupt.h
│   ├── kernel.S			//定义中断处理程序文件
│   └── main.c
├── lib
│   ├── kernel
│   │   ├── io.h			//将对8259A的汇编控制代码封装为易用的c代码
│   │   ├── print.h
│   │   └── print.S
│   └── stdint.h
├── Makefile				//编译脚本,Makefile形式的编译脚本
├── start.sh				//编译脚本,有更清晰的编译命令和编译顺序,方便读者理解
└── test					//内联汇编测试文件
    ├── base_asm.c
    ├── inlineASM.c
    ├── mem.c
    ├── reg.c
    └── reg_constraint.c

代码核心逻辑

  1. 定义中断处理程序
  2. 通过中断处理程序构造中断描述符
  3. 把中断描述符填充进IDT构造IDT
  4. 在中断代理中暂时只打开时钟中断,也就是说只有时钟中断才能通过中断代理告知CPU,其余的中断信号都被拦截
定义中断处理程序

/kernel/kernel.S

中断处理程序通过宏进行循环构造

汇编中宏语法格式为

1
2
3
4
;宏定义的语法格式为:
;       %macro 宏名 参数个数             ;参数顺序由占位符表示,如%1表示第一个参数,%2表示第二个参数
;           宏定义
;       %endmacro

以下是中断处理程序的宏

这段中断处理程序的代码逻辑很简单,当其被调用时

  • 首先打印字符串intr_str
  • 然后给8259A芯片发生信号结束中断
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
%macro VECTOR 2                         ;定义VECTOR宏,该宏用于定义中断处理程序
                                        ;参数1表示中断向量号,参数2表示错误码

section .text                           ;中断处理程序的代码段
intr%1entry:                            ;这是一个标号,用于表示中断程序的入口地址,即中断程序的函数名
    %2                                  ;如果一个中断有错误码,则什么都不做,否则压入0作填充

;这段代码是为了模拟中断发生,如果我们中断调用后这段字符串打印出来了,说明我们的中断逻辑是正确的
    push intr_str                       ;压入put_str的调用参数
    call put_str
    add esp,4                           ;回收put_str函数压入的字符串参数的栈帧空间
                                        ;由于intr_str是一个指针类型,而该系统为32bit,因此该参数占用4个字节空间

;上述中断发生模拟结束后要结束中断 
    mov al,0x20                         ;中断结束命令EOI
    out 0xa0,al                         ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
    out 0x20,al                         ;向从片发送OCW2,其中EOI位为1,告知结束中断

    add esp,4                           ;抛弃有错误码的中断(如果有)
    iret                                ;中断返回
    
section .data
    dd intr%1entry                      ;存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位

%endmacro

这段宏共有两个参数,这两个参数都是占位符

  • 参数一表示中断号
    • 这个中断号是拿来填充中断处理程序的函数名
    • 比如这段宏调用VECTOR 0x00,ZERO,第一个参数是0x00,于是它代表的中断处理程序的函数名就是intr0x00entry
  • 参数二是一个填充字段。
    • 中断处理程序在被调用前CPU会压入ss、eip、eflags寄存器等值进行保存,但是有点中断处理程序还会额外压入错误码信息,而有的不压入,于是为了保持栈结构的统一,定义了这个参数,这个参数是一个操作
    • 其作用是,如果某个中断处理程序压入了错误码,则该参数代表的操作为空操作,什么也不做
    • 否则就压入4字节的0以作填充

image-20241217200322298

以下是通过调用上述宏构造的33个中断处理程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
; 调用之前写好的VECTOR宏来批量生成中断处理函数,
; 传入参数是中断号码与上面中断宏的%2步骤,这个步骤是什么都不做,还是压入0,详情看p303
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个中断处理程序,是因为中断向量0 ~ 19为处理器内部固定的异常类型,20 ~ 31是Imntel保留的,可用的最低中断向量号是32

构造IDT

/kernel/interrupt.c

首先按照中断描述符的格式定义中断描述符的结构体

1
2
3
4
5
6
7
8
9
// 定义中断描述符结构体
struct gate_desc
{
    uint16_t func_offset_low_word;  // 中断处理程序在目标代码段的偏移量(低字)
    uint16_t selector;              // 选择子字段
    uint8_t dcount;                 // 此项为双字计数字段,是门描述符中的第4字节。这个字段无用
    uint8_t attribute;              // 属性字段(包含P位、DPL位、S位、TYPE位)
    uint16_t func_offset_high_word; // 中断处理程序在目标代码段的偏移量(高字)
};

image-20241217171203927

我们需要通过中断描述符构造IDT,当然也需要定义IDT,其实就是个gate_desc(中断描述符j结构体定义)类型的数组

1
2
// 定义中断门描述符结构体数组,形成中断描述符表idt,该数组中的元素是中断描述符
static struct gate_desc idt[IDT_DESC_CNT];

接下来首先按照中断描述符的格式填充中断描述符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*
函数功能:构建中断描述符
函数实现:按照中断描述符结构体定义填充字段
参数:
    中断门描述符地址
    属性
    中断处理函数
*/
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以构造IDT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*
函数功能:构建中断描述符表idt
函数实现:循环调用make_idt_desc构建中断描述符,形成中断描述符表idt
参数:中断描述符表中的某个中断描述符地址,属性字段,中断处理函数地址
*/
static void idt_desc_init(void)
{
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
    }
    put_str("   idt_desc_init done\n");
}

其中intr_entry_table保存的就是kernel.S定义的33个中断处理程序的入口地址(函数地址)

打开时钟中断

最后一步的工作就是初始化8259A芯片,同时打开时钟中断,只让时钟中断的信号经过中断代理告知CPU

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

/*初始化可编程中断控制器8259A*/
static void pic_init()
{
    /* 初始化主片 */
    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

    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    outb(PIC_M_DATA, 0xfe);
    outb(PIC_S_DATA, 0xff);

    put_str("pic_init done\n");
}

其中有关对8259A的操作和控制函数定义在

/lib/kernel/io.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"

// 一次送一字节的数据到指定端口,static指定只在本.h内有效,inline是让处理器将函数编译成内嵌的方式,就是在该函数调用处原封不动地展开
// 此函数有两个参数,一个端口号,一个要送往端口的数据
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));
}

// 利用outsw(端口输出串,一次一字)指令,将ds:esi指向的addr处起始的word_cnt(存在ecx中)个字写入端口port,ecx与esi会自动变化
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));
} // S表示寄存器esi/si

/* 将从端口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");
} // D表示寄存器edi/di                       //通知编译器,内存已经被改变了

#endif
填充IDTR,加载IDT

调用上述函数,完成所有的初始化工作后,按照IDTR的格式构造IDTR,以加载IDT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

/*完成有关中断的所有初始化工作*/
void idt_init()
{
    put_str("idt_init start\n");
    idt_desc_init(); // 构建中段描述符表
    pic_init();      // 初始化中断控制器,只接受来自时钟中断的信号

    /* 加载idt */
    uint64_t idt_operand = (((uint64_t)(uint32_t)idt << 16) | (sizeof(idt) - 1)); // 定义要加载到IDTR寄存器中的值
    asm volatile("lidt %0" : : "m"(idt_operand));
    put_str("idt_init done\n");
}

image-20241217171849572

中断测试

/kernel/main.c

1
2
3
4
5
6
7
8
9
#include "print.h"
#include "init.h"
void main(void)
{
    put_str("I am kernel\n");
    init_all();			// 初始化中断,同时只放行时钟中断
    asm volatile("sti"); // 为演示中断处理,在此临时开中断,sti是开中断指令
    while (1);
}

完整源码可参见miniOS_32

编译运行

编译脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
 
#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S

#编译main函数
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/main.c
#编译interrupt
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/init.c

#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/print.o $(pwd)/bin/init.o $(pwd)/bin/interrupt.o $(pwd)/bin/kernel.o

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf bin/*

运行结果如下

image-20241217210426346

在bochs中调试IDT结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<bochs:2> info idt
Interrupt Descriptor Table (base=0xc0002c40, limit=263):
IDT[0x00]=32-Bit Interrupt Gate target=0x0008:0xc0001810, DPL=0
IDT[0x01]=32-Bit Interrupt Gate target=0x0008:0xc0001829, DPL=0
IDT[0x02]=32-Bit Interrupt Gate target=0x0008:0xc0001842, DPL=0
IDT[0x03]=32-Bit Interrupt Gate target=0x0008:0xc000185b, DPL=0
IDT[0x04]=32-Bit Interrupt Gate target=0x0008:0xc0001874, DPL=0
IDT[0x05]=32-Bit Interrupt Gate target=0x0008:0xc000188d, DPL=0
IDT[0x06]=32-Bit Interrupt Gate target=0x0008:0xc00018a6, DPL=0
IDT[0x07]=32-Bit Interrupt Gate target=0x0008:0xc00018bf, DPL=0
IDT[0x08]=32-Bit Interrupt Gate target=0x0008:0xc00018d8, DPL=0
IDT[0x09]=32-Bit Interrupt Gate target=0x0008:0xc00018f0, DPL=0
IDT[0x0a]=32-Bit Interrupt Gate target=0x0008:0xc0001909, DPL=0
IDT[0x0b]=32-Bit Interrupt Gate target=0x0008:0xc0001921, DPL=0
IDT[0x0c]=32-Bit Interrupt Gate target=0x0008:0xc0001939, DPL=0
IDT[0x0d]=32-Bit Interrupt Gate target=0x0008:0xc0001952, DPL=0
IDT[0x0e]=32-Bit Interrupt Gate target=0x0008:0xc000196a, DPL=0
IDT[0x0f]=32-Bit Interrupt Gate target=0x0008:0xc0001982, DPL=0
IDT[0x10]=32-Bit Interrupt Gate target=0x0008:0xc000199b, DPL=0
IDT[0x11]=32-Bit Interrupt Gate target=0x0008:0xc00019b4, DPL=0
IDT[0x12]=32-Bit Interrupt Gate target=0x0008:0xc00019cc, DPL=0
IDT[0x13]=32-Bit Interrupt Gate target=0x0008:0xc00019e5, DPL=0
IDT[0x14]=32-Bit Interrupt Gate target=0x0008:0xc00019fe, DPL=0
IDT[0x15]=32-Bit Interrupt Gate target=0x0008:0xc0001a17, DPL=0
IDT[0x16]=32-Bit Interrupt Gate target=0x0008:0xc0001a30, DPL=0
IDT[0x17]=32-Bit Interrupt Gate target=0x0008:0xc0001a49, DPL=0
IDT[0x18]=32-Bit Interrupt Gate target=0x0008:0xc0001a62, DPL=0
IDT[0x19]=32-Bit Interrupt Gate target=0x0008:0xc0001a7a, DPL=0
IDT[0x1a]=32-Bit Interrupt Gate target=0x0008:0xc0001a93, DPL=0
IDT[0x1b]=32-Bit Interrupt Gate target=0x0008:0xc0001aab, DPL=0
IDT[0x1c]=32-Bit Interrupt Gate target=0x0008:0xc0001ac3, DPL=0
IDT[0x1d]=32-Bit Interrupt Gate target=0x0008:0xc0001adc, DPL=0
IDT[0x1e]=32-Bit Interrupt Gate target=0x0008:0xc0001af4, DPL=0
IDT[0x1f]=32-Bit Interrupt Gate target=0x0008:0xc0001b0c, DPL=0
IDT[0x20]=32-Bit Interrupt Gate target=0x0008:0xc0001b25, DPL=0

改进中断

代码目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── bin
├── boot					//计算机启动代码文件
│   ├── include
│   │   └── boot.inc
│   ├── loader.S
│   └── mbr.S
├── kernel
│   ├── global.h			//各种描述符、段选择子等字段的宏定义文件
│   ├── init.c
│   ├── init.h
│   ├── interrupt.c			//构造IDT、IDTR、打开时钟中断文件
│   ├── interrupt.h
│   ├── kernel.S			//定义中断处理程序文件
│   └── main.c
├── lib
│   ├── kernel
│   │   ├── io.h			//将对8259A的汇编控制代码封装为易用的c代码
│   │   ├── print.h
│   │   └── print.S
│   └── stdint.h
├── Makefile				//编译脚本,Makefile形式的编译脚本
├── start.sh				//编译脚本,有更清晰的编译命令和编译顺序,方便读者理解

改进代码

上述我们代码中,我们通过宏定义了33个中断处理程序,但是由一个问题,那就是我们的中断处理程序的具体实现是通过汇编写的,这很不利、于我们将来使用c语言自定义中断处理程序的具体实现,针对这个问题,我们主要做以下几点改进

修改/kernel/kernel.S对中断处理程序的定义逻辑

中断在进入中断处理程序之后,具体的处理实现通过调用c函数来完成,而不是全部耦合进汇编代码里

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
extern idt_table

section .data                           ;定义数据段
 
global intr_entry_table                 ;定义中断处理程序数据,数组的元素值是中断处理程序,共33个
intr_entry_table:

;宏定义的语法格式为:
;       %macro 宏名 参数个数
;           宏定义
;       %endmacro
%macro VECTOR 2                         ;定义VECTOR宏,该宏用于定义中断处理程序
section .text                           ;中断处理程序的代码段
intr%1entry:                            ;这是一个标号,用于表示中断程序的入口地址,即中断程序的函数名
    %2                                  ;压入中断错误码(如果有)
 
    push ds                             ; 以下是保存上下文环境
    push es
    push fs
    push gs
    pushad
 
                                        ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
    mov al,0x20                         ; 中断结束命令EOI
    out 0xa0,al                         ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
    out 0x20,al                         ;向从片发送OCW2,其中EOI位为1,告知结束中断
 
    push %1			                    ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
    call [idt_table + %1*4]             ; 调用idt_table中的C版本中断处理函数
    jmp intr_exit
    
section .data
    dd intr%1entry                      ;存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacro
 
section .text
global intr_exit
intr_exit:
                                        ; 以下是恢复上下文环境
    add esp, 4			                ; 跳过中断号
    popad
    pop gs
    pop fs
    pop es
    pop ds
    add esp, 4			                 ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0                    
    iretd				                 ; 从中断返回,32位下iret等同指令iretd

在上述修正后的代码里,当所有初始化工作完成,开始执行中断处理的时候,我们通过调用c函数完成,这样就把具体的中断处理逻辑解放了出来,而不是全部耦合进一段代码里

可以看到,具体的调用其实是按照中断号(宏定义的第一个参数%1)调用IDT的对应表项,这个表项里对应的是具体中断实现函数

1
call [idt_table + %1*4]             ; 调用idt_table中的C版本中断处理函数

具体的调用实现在/kernel/interrupt.c文件中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#if 1
/*用c语言定义中断处理函数的具体实现,不再用汇编语言在kernel.S中定义中断处理函数的实现*/
// 参数:中断向量号
static void general_intr_handler(uint8_t vec_nr)
{
    // 伪中断向量,无需处理
    if (vec_nr == 0x27 || vec_nr == 0x2f)
    {
        return;
    }
    put_str("int vector:0x");
    put_int(vec_nr);
    put_char('\n');
}
#endif

/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void)
{ // 完成一般中断处理函数注册及异常名称注册
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
        /* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
        见kernel/kernel.S的call [idt_table + %1*4] */
        idt_table[i] = general_intr_handler; // 默认为general_intr_handler
                                             // 以后会由register_handler来注册具体处理函数。
        intr_name[i] = "unknown";            // 先统一赋值为unknown
    }
    intr_name[0] = "#DE Divide Error";
    intr_name[1] = "#DB Debug Exception";
    intr_name[2] = "NMI Interrupt";
    intr_name[3] = "#BP Breakpoint Exception";
    intr_name[4] = "#OF Overflow Exception";
    intr_name[5] = "#BR BOUND Range Exceeded Exception";
    intr_name[6] = "#UD Invalid Opcode Exception";
    intr_name[7] = "#NM Device Not Available Exception";
    intr_name[8] = "#DF Double Fault Exception";
    intr_name[9] = "Coprocessor Segment Overrun";
    intr_name[10] = "#TS Invalid TSS Exception";
    intr_name[11] = "#NP Segment Not Present";
    intr_name[12] = "#SS Stack Fault Exception";
    intr_name[13] = "#GP General Protection Exception";
    intr_name[14] = "#PF Page-Fault Exception";
    // intr_name[15] 第15项是intel保留项,未使用
    intr_name[16] = "#MF x87 FPU Floating-Point Error";
    intr_name[17] = "#AC Alignment Check Exception";
    intr_name[18] = "#MC Machine-Check Exception";
    intr_name[19] = "#XF SIMD Floating-Point Exception";
}

上述代码的逻辑是

我们把具体的中断处理实现通过general_intr_handler完成,然后把general_intr_handler放在IDT中,这样便可通过中断号调用对应的中断处理程序了

image-20241217205156769

编译运行

image-20241217210645708

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8
网站已运行 132天8小时11分钟
发表了20篇文章 · 总计 138,209字