第七章中 中断和8259A

1 8259A

1.1 简介

8259A的作用是负责所有的外设中断.

cpu每次只能执行一个任务,而中断可能同时发生,所以8259A用来收集所有的中断,然后挑选出一个优先级最高的中断,传送给CPU

8259A的功能有:管理和控制可屏蔽中断,表现在屏蔽外设中断,对他们实行优先级判决,向cpu提供中断向量好等功能

每个8259A智能管理8个中断,而intel的cpu由256个中断,因此采用级联的方式,使用多个8259A管理256个中断.每个8259A有一个额外的引脚可以链接其他的8259A,被连接的那个8259A需要占用一个引脚,但是主片需要占用一个引脚去连接CPU.

1.2 8259A与cpu通信

8259A中的端口有:

  1. INT:8259A选出优先级最高的中断请求,发送信号给CPU
  2. INTA:中断响应信号
  3. IMR:中断屏蔽器,用来屏蔽某个外设的中断
  4. IRR:中断请求其,用与接受结果IMR过滤后的中断信号,次寄存器中保存的都是等待处理的中断,相当于8259A中维护的未处理中断信号队列
  5. PR:优先级仲裁器,多个中断同时发生时,将他与当前正在处理的中断进行比较,挑选出优先级更高的中断
  6. ISR:中断服务器,当某个中断正在被处理的时候,该中断就被保存在这个寄存器中

所有的寄存器都是8位的.

当8259A接收到一个中断后:

当某个外设发出一个中断信号是,有序主板上已经将信号通路指向8259A芯片的某个IRQ接口,所以该中断被送入8259A.8259A首先检查IMR寄存器是否已经屏蔽了该IRQ接口的中断,IMR寄存器中的位为1,表示屏蔽,直接丢弃该次中断,0表示放行.当中断放行是,IRQ接口所在的IRR寄存器中对应位设置位1,表示发生中断.IRQ接口的接口号越小,中断优先级越大.PR从IRR中挑选一个优先级最大的中断.然后8259A通过INT接口想CPU发送INTR信号,吸纳好被送入cpu的INTR接口后,cpu就知道有新的中断来了,然后通过自己的INTA接口向8259A的INTA回复一个中断响应号,8259A收到信号后,将挑选出优先级最大的中断在ISR寄存器对应的位设置位1,表示正在处理该终端,同时从IRR中置位0,之后cpu再次发送INTA信号给8259A,表示要获取中断向量好.8259A通过数据总线发送给cpu,cpu拿到以后,就用它在中断向量表或是中断描述符表中索引,找到相应的中断处理程序执行.

如果8259A的EOI通知设为手动模式,那么中断处理程序结束后必须想8259A发送EOI的代码,8259A收到后将ISR寄存器中对应的位设置位0,如果设置位自动模式,那么在接收到cpu要求中断向量号的信号后,8259A自动将ISR中的位设置位0.

1.3 8259A编程

8259A成为可编程中断控制器,说明他的工作方式很多,需要他进行设置.也就是对他进行初始化,设置为基连的方式,指定中断向量号,以及工作模式.

中断向量号是楼机上的东西,物理上他是8259A的IRQ接口号,8259A上的IRQ接口号排列顺序是固定的.但是对应的中断向量号不是固定的,是由硬件到软件的映射,通过对8259A进行设置,可以将IRQ接口映射到不同的中断向量号.

8259A内由两组寄存器,一组是用来初始化的,用来保存初始化命令字ICW1~ICW4,一共4组.另一组寄存器是操作命令寄存器,用来保存操作命令字,OCW1~OCW3一共三组.

对ICW初始化,用来设置是否使用级联,设置其实中断向量号,设置中断结束模式.某些设置之间可能相互关联,因此需要一次写入ICW1~4

对OCW的初始化,来操作8259A,就是中断屏蔽和中断结束.OCW的发送顺序不固定

ICW1用来初始化8259A的连接方式和中断信号的触发方式.连接方式是指单片工作还是多片的级联工作.触发方式是指中断请求信号是水平触发还是边缘触发.ICW1需要写入到主片的0x20和葱片的0xA0端口:

  1. IC4为1表示在后面是否要写入ICW4
  2. SNGL为1表示单片0还是级联1
  3. ADI用来设置8085的调用时间间隔
  4. LTIM设置中断的检测方式,0表示边缘触发,1表示水平触发
  5. 4位为1,固定的
  6. 其余位为0

ICW2用于设置其实中断向量号,,就是前面的硬件IRQ接口到逻辑中断向量号的映射.,ICW2写到主片的0x21端口和葱片的0xA1端口.只需要设置IRQ0中断向量号,其他的是顺序向下排列的.只需要填写高5位的T3~T7.高5位其实表示该8259A芯片的序号,低3位则表示8个向量号

ICW3在级联模式下需要.且主片和从片有自己不同的结构,主片ICW3中设置1的哪一位,对应IRQ接口用与链接从片,为0则表示外部设备.从片ICW3不需要指定那个IRQ与主片相连,因为他有一个专门的线.从片上设置的是主片上与自己相连的那个IRQ号.当中断相应是,主片发送与从片做基连的IRQ号,所有从片都收到该号,然后与自己的低3位对比,如果一直,那么表示是发给字节的(低3位表示主片只有8个引脚).

ICW4有些低位选项基于高位

  1. SFNM,特殊全嵌套模式,0表示开启
  2. BUF,是否工作在缓冲模式下,当多个8259A级联的时候,当工作在缓冲模式下,M/S用来规定是主片还是从片,1表示主片.当非缓冲模式时,该位无效
  3. AEOI表示自动结束中断,0表示非自动模式,1表示自动模式
  4. uPM.兼容捞处理器,0表示8080或是8085,1表示x86

OCW1用来屏蔽在8259A上的外部设备的中断信号,实际上就是把OCW1写入IMR寄存器,OCW1写入主片0x21,或从片0xA1

M0~M7对应IRQ1~7.设置为1标识屏蔽.

OCW2用来设置中断结束方式和优先级模式.写入主片的0x20和从片的0xA0

OCW2配置复杂,各个属性位要配合在一起,组合出8259A的各种工作模式

跳过了

OCW3用来设置特殊屏蔽方式以及查询方式写入写入主片的0x20和从片的0xA0

跳过了

1.4 代码

跳过了,最后直接贴最终的代码

2 中断处理程序

中断发生的时候,都需要进行上下文的保存,这一部分的代码是相同的,因此使用汇编的模板macro,来编写.

使用这种方法来编写,需要记录模板生成的每一个函数的地址.然后将这些地址在内存空间中顺序保存,取第一个函数的地址,作为中断处理函数的地址.

2.1 中断处理函数

中断处理函数使用汇编来编写,是一个模板:

%macro 模板名称 参数个数
    ...
%endmacro 

在使用模板参数的时候,使用%n,编译器,会将其自动替换.

然后模板需要填充的信息为:

模板名称 参数1,参数2,...
模板名称 参数1,参数2,...
模板名称 参数1,参数2,...
...

然后完整的代码为,这里定义了20个函数:

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop 
%define ZERO push 0

; 引用外部函数
extern put_str
extern idt_table


section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2   
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2
        
            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

            call [idt_table+%1*4]

            add esp,4   ;跳过参数

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            
            popad   
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code
            
            iret 
        ;这一段,主要是为了获取每个函数的地址
        section .data
            dd intr%1entry
    %endmacro 
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

在编译器编译后,text段和data段就分开保存了.因此dd intr%1entry这一段代码,编译结束有以后是在内存地址上紧靠的.并且加上了section .data来保证,这些数据连续.(后面的时候会发生不连续的事情,解决办法只需要将 .data改一个名字就行了)

为了验证,这些数据在内存上连续,首先,给每个地址加上一个标签:

也就是在dd intr%1entry之前加上intr%1addr:

        section .data
            intr%1addr:
            dd intr%1entry

贴出最终的kernel.bin`中的信息:

readelf -a kernel.bin

2.2 安装中断处理程序

首先定义门描述符的结构,然后定义一个数组,存储所有的中断描述符

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数
// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

然后一个函数用来填充,需要传入attr的原因在于,中断描述符有DPL,不同的中断描述符可能可以被用户进程调用,或者只允许在内核态使用.因此需要传入这个参数.而func_addr就是实际的中断处理函数,也就是在上面使用汇编模板编写的函数idt_entry_table.

static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, idt_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

主要的就是这些

3 代码

这一部分先添加了很多的代码,首先看一下目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b
│   ├── boot
│   │   ├── include
│   │   │   └── boot.inc
│   │   ├── loader.asm
│   │   └── mbr.asm
│   ├── build
│   ├── kernel
│   │   ├── global.h
│   │   ├── idt.asm
│   │   ├── init.c
│   │   ├── init.h
│   │   ├── interrupt.c
│   │   ├── interrupt.h
│   │   └── main.c
│   ├── lib
│   │   ├── kernel
│   │   │   ├── io.h
│   │   │   ├── print.asm
│   │   │   └── print.h
│   │   └── libint.h
│   └── start.sh
└── hd60m.img

旧的文件,只有main.c文件更改了,其他的文件都没有更改.

首先解释一下,每个新添加的文件的内容:

  1. /lib/io.h :该文件是使用内联汇编编写的,主要封装了in,out操作段的一些代码,在第6章里添加了该文件
  2. /kernel/global.h:该文件主要用于保存以后kernel所有用到的一些宏和常量
  3. /kernel/idt.asm:该文件中主要用于构建了33个中断处理程序,还有一个由这些中断处理程序的地址组成的数组.
  4. /kernel/interupt.h,/kernel/interrupt.c:这两个文件,主要是用于构建中断描述符表,设置8259A,加载数据到idtr寄存器中.对外提供一个总的idt_init()函数用于,初始化和中断相关的事情
  5. /kernel/init.h,/kernel/init.c:中主要是一个集合,用于调用所有和初始化相关的函数.

以后,每添加一个功能,就在kernel文件夹中,添加文件,该文件向外暴露一个XXX_init()函数,然后由init.c文件夹中的init_all()函数调用,而main.c函数中则调用init_all()

3.1 global.h(新加)

该文件暂时,定义了4个段选择子,然后还有和中断描述符有关的宏,

#ifndef _ERNEL_GLOBAL_H
#define _ERNEL_GLOBAL_H
#include "libint.h"

// -------------------- 段选择子 --------------------
#define  RPL0  0
#define  RPL1  1
#define  RPL2  2
#define  RPL3  3

#define TI_GDT 0
#define TI_LDT 1

// 这是在保护模式下,c语言时候,使用的选择子
#define SELECTOR_CODE      ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_DATA      ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_STACK   SELECTOR_DATA 
#define SELECTOR_GS    ((3 << 3) + (TI_GDT << 2) + RPL0)
// -------------------- 段选择子 --------------------

// -------------------- IDT --------------------
// 只定义了门描述符中,属性部分,就是p位,s位,type位
// 因为其他的位,是使用c语言动态补全的
#define  IDT_DESC_P  1 
#define  IDT_DESC_DPL0   0
#define  IDT_DESC_DPL3   3
#define  IDT_DESC_32_TYPE     0xE   
#define  IDT_DESC_16_TYPE     0x6   
#define  IDT_DESC_ATTR_DPL0  ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define  IDT_DESC_ATTR_DPL3  ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)
// -------------------- IDT --------------------

#endif

3.2 idt.asm(新加)

首先我们构建中断描述符表,构建前32个中断处理程序,都是打印一个字符串.因此在该文件中需要使用put_str函数,但是又不能引入头文件(因为中断处理程序使用汇编编写),所以使用extern引用外部符号.

然后,因为每个中断号,cpu可能会压入一个错误代码,也可能是不压入,所以需要处理这个错误代码,需要主动的跳过.biao

再然后如果开启的是手动模式,就需要在中断处理程序中显示的发送EOI信号.

再者,因为要手写全部的32个中断处理信号过于麻烦,所以使用模板的方式:

%macro 模板名字 参数个数

%endmacro
模板名字 参数1,参数2
模板名字 参数1,参数2
模板名字 参数1,参数2
模板名字 参数1,参数2
...
模板名字 参数1,参数2

当在模板中使用参数的时候,就使用%n数字,取对应的参数,直接在模板中替换

因此,对于那些cpu不压入参数的中断,统一的在中断处理程序的一开始压入一个0,然后在最后的时候,再统一的esp-4

所以最终的代码为:

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop 
%define ZERO push 0

; 引用外部函数
extern put_str

section .data

; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global intr_entry_table
intr_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2   
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            push ds
            push es
            push fs
            push gs
            pushad

            %2
            push intr_str
            call put_str
            add esp,4

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            popad
            pop gs
            pop fs
            pop es
            pop ds
            
            add esp,4
            iret 
        section .data
            dd intr%1entry
    %endmacro 
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

这里用的代码比较巧妙,首先section .datasection .code在编译后,一定是不在同一个segment内的。而且,section .data的数据会紧凑的靠在一起。而section .data中的数据有两部分:intr_entry_tabledd intr%1entry

因此,最终编译后intr_entry_table后面就会跟着好几个dd intr%1entry这样,就成为一个数组。

3.3 interupt.h/interupt.c(新加)

interupt.h文件主要是暴露了interupt.c中的,那个idt_init()函数,所以很简单:

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "libint.h"
void idt_init(void);
#endif

interupt.c文件的主要内容:

  1. 定义门描述符的结构GateDesc,然后用它定义一个数组static struct GateDesc idt[IDT_DESC_COUNTS],其地址作为中断描述符表.
  2. 根据idt.asm构建好的intr_entry_table,去填充完整的idt,也就是最终的中断描述符表.
  3. 初始化8259A
#include "interrupt.h"
#include "libint.h"
#include "global.h"
#include "io.h"
#include "print.h"

// 和8259A 设置相关的端口
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1


#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数

// 引用 idt.asm 中构建的那个数组.这个数组中保存了所有33个中断处理程序的地址
extern void *intr_entry_table[IDT_DESC_COUNTS];

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

// 该函数用来填充一个中断描述符表中的表项.
static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

// 初始化 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");
}

// 一个总的函数,调用以上的两个初始化函数.并加载idtr
void idt_init()
{
    put_str("idt_init start \n");
    idt_desc_init();
    pic_init();

    // 低 16位是界限,界限是idt的长度-1,高16位是所在地址.都没问题
    uint64_t idtr = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    // 然后加载 iidtr
    asm volatile("lidt %0"
                 :
                 : "m"(idtr));
}

3.4 /kernel/init.h,/kernel/init.c(新加)

/kernel/init.h 头文件暴露接口而已:

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

/kernel/init.c文件页是很简单的,就是调用各个文件暴露出来的那个xxx_init()而已:

#include "init.h"
#include "print.h"
#include "interrupt.h"

/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();   //初始化中断
}

3.5 main.c

调用/kernel/init.h文件中的init_all()

并且打开中断:

#include "print.h"
#include "init.h"
int main(int argc, char const *argv[])
{
    set_cursor(880);    
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('\r');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    put_str("\n put_char\n");

    // 初始化
    init_all(); 
    
    put_str("interrupt on\n");
    asm volatile("sti");          // 开中断
    
//  asm volatile("cli");          //关中断
   
    while (1)
    {
    }

    return 0;
}

3.6 start.sh

就是新添加了几个文件的编译,还有最终链接的时候要加上链接的文件.

#! /bin/bash

# 编译mbr.asm
echo "----------nasm starts----------"

if !(nasm -o mbr.bin ./boot/mbr.asm -I ./boot/include/);then
    echo "nasm error"
    exit
fi

# 刻录mbr.bin
echo "----------dd starts  ----------"
if !(dd if=./mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc);then
    echo "dd error"
    exit
fi

# 编译 loader.asm
echo "----------nasm starts----------"

if !(nasm -o loader.bin boot/loader.asm -I ./boot/include/);then
    echo "nasm error"
    exit
fi

# 刻录loader.bin
echo "----------dd starts  ----------"
if !(dd if=./loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc);then
    echo "dd error"
    exit
fi

# 编译 print.asm
echo "----------nasm print----------"
if !(nasm -f elf -o print.o ./lib/kernel/print.asm -I ./boot/include/ -I ./lib);then
    echo "nasm error"
    exit
fi

# 编译 interrupt.c
echo "----------nasm interrupt----------"
if !(gcc -o interrupt.o -m32 -fno-stack-protector -c ./kernel/interrupt.c  -I ./lib -I ./lib/kernel);then
    echo "nasm error"
    exit
fi

# 编译 init.c
echo "----------nasm init----------"
if !(gcc -o init.o -m32 -c ./kernel/init.c  -I ./lib -I ./lib/kernel);then
    echo "nasm error"
    exit
fi

# 编译 idt.asm
echo "----------nasm idt----------"
if !(nasm -f elf -o idt.o ./kernel/idt.asm -I ./boot/include/ -I ./lib);then
    echo "nasm error"
    exit
fi


# 编译内核
echo "----------gcc -c kernel.bin  ----------"
if !(gcc -o kernel.o -m32 -c ./kernel/main.c  -I ./lib -I ./lib/kernel);then
    echo "dd error"
    exit
fi
# 链接 
echo "----------ld  kernel starts  ----------"
if !(ld -Ttext 0xc0001500 -m elf_i386 -e main -o kernel.bin ./kernel.o ./idt.o ./print.o ./init.o ./interrupt.o);then
    echo "dd error"
    exit
fi

# 刻录 kernel.bin
echo "----------dd starts  ----------"
if !(dd if=./kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc);then
    echo "dd error"
    exit
fi

# 删除临时文件
sleep 1s  
rm -rf mbr.bin
rm -rf loader.bin
rm -rf kernel.bin
rm -rf kernel.o
rm -rf print.o
rm -rf idt.o
rm -rf init.o
rm -rf interrupt.o

# 运行bochs
bochs

另外,要注意:

在编译interrupt.c的时候,加上了选项-fno-stack-protector,是因为,该文件中的asm volatile("lidt %0": : "m"(idtr));造成了:

因此要加上这个选项相关资料

3.7 运行

下面是运行的结果.开中断以后不停地打印字符串.

4 改进

现在的中断例程中调用函数都一样,是使用汇编编写的,以后中断例程中调用函数需要用c语言编写.

因此,我们在interrupt.c中新添加一个函数idt_hander(uint8_t vec)用于接受一个中断号,然后内部根据中断号,执行不同的中断程序.

然后,新建一个数组idt_table,这个数组长为IDT_DESC_COUNTS,元素位void*,然后里面值都是统一的:是idt_hander(uint8_t vec)的地址.(以后可能会根据需要,不同元素赋不同的函数地址)

再然后,idt.asm中不再call put_str而是,call [idt_table+%1*4],%1就是中断号,乘4就是对应中断号的处理程序.

4.1 interrupt.h/interrupt.c

在interrupt.h/interrupt.c中加入开关中断的函数,以后开关中断就不用使用内联汇编.同时加上,能够获取当前中断开关状态的函数.

首先需要定一个enum枚举两种开关状态.然后一个获取当前状态的函数,主要是获取eflags寄存器的值,然后判断第10位是否是1.然后返回是否开关中断了.然后开关中断的函数,先获取当前状态,再进行开关.返回之前的状态.至于为什么要这样做,可能后面会用到吧.暂时不清楚.

当然最主要的,还是加上让idt.asm使用的中断例程中调用函数地址的数组.构建该数组.

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "libint.h"

void idt_init( void );

/* 定义中断的两种状态:
 * INTR_OFF值为0,表示关中断,
 * INTR_ON值为1,表示开中断 */
enum IntrStatus
{              // 中断状态
    INTR_OFF,  // 中断关闭
    INTR_ON    // 中断打开
};

enum IntrStatus intr_get_status( void );
enum IntrStatus intr_set_status( enum IntrStatus );
enum IntrStatus intr_enable( void );
enum IntrStatus intr_disable( void );
void            register_handler( uint8_t vector_no, void* function );

#endif

interrupt.h新添加的代码主要是为了,暴露开关中断的函数.

而,interrupt.c中,新代码1,新代码5,是添加开关中断的部分.

其他的新代码则是为了构建一个中断例程中调用函数地址的数组.

#include "interrupt.h"
#include "libint.h"
#include "global.h"
#include "io.h"
#include "print.h"

// 和8259A 设置相关的端口
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1

#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数


// 用于获取 eflags 寄存器中内容的宏,本身很简单:
// EFLAG_VAR 是个用来存放 eflags 变量,使用寄存器传参.
// 然后先 pushfl ,压栈eflags,然后 popl 到 EFLAG_VAR 所在的寄存器
// 那么EFLAG_VAR 中就是 eflags 的值了
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" \
                                           : "=g"(EFLAG_VAR))
//  这个宏只是避免使用魔数而已,使用 EFLAGS_IF 表示第10位为1,也就是if位为1
#define EFLAGS_IF 0x00000200


// 引用 idt.asm 中构建的那个数组.这个数组中保存了所有33个中断处理程序的地址
extern void *idt_entry_table[IDT_DESC_COUNTS];

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

// 用来存储每一个中断例程中调用函数的地址,暂时全都是 idt_handle
void *idt_table[IDT_DESC_COUNTS];
// 用来存储每一个中断例程中调用函数的名字,在idt_handle中打印一下而已
char *idt_name[IDT_DESC_COUNTS];

// 该函数用来填充一个中断描述符表中的表项.
static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, idt_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

// 初始化 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");
}

//
static void idt_handle(uint8_t vec)
{
    // 这里是处理伪中断的,不清楚是什么....
    if (vec == 0x27 || vec == 0x2f)
    {
        return;
    }
    // 目前只是简单的打印一下和该中断号相关的信息
    put_str("int vector: 0x");
    put_char(':');
    put_str(idt_name[vec]);
    put_char('\n');
}

// 初始化 idt_table
static void exception_init()
{
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // 都设置位一个值.
        idt_table[i] = idt_handle;
        idt_name[i] = "unknow";
    }
    idt_name[0] = " 0:#DE Divide Error";
    idt_name[1] = " 1:#DB Debug Exception";
    idt_name[2] = " 2:NMI Interrupt";
    idt_name[3] = " 3:BP Breakpoint Exception";
    idt_name[4] = " 4:#OF Overflow Exception";
    idt_name[5] = " 5:#BR BOUND Range Exceeded Exception";
    idt_name[6] = " 6:#UD Invalid Opcode Exception";
    idt_name[7] = " 7:#NM Device Not Available Exception";
    idt_name[8] = " 8:#DF Double Fault Exception";
    idt_name[9] = " 9:Coprocessor Segment Overrun";
    idt_name[10] = "10:#TS Invalid TSS Exception";
    idt_name[11] = "11:#NP Segment Not Present";
    idt_name[12] = "12:#SS Stack Fault Exception";
    idt_name[13] = "13:#GP General Protection Exception";
    idt_name[14] = "14:#PF Page-Fault Exception";
    // idt_name[15] 第15项是intel保留项,未使用
    idt_name[16] = "16:#MF x87 FPU Floating-Point Error";
    idt_name[17] = "17:#AC Alignment Check Exception";
    idt_name[18] = "18:#MC Machine-Check Exception";
    idt_name[19] = "19:#XF SIMD Floating-Point Exception";
    idt_name[32] = "32:timer";
}


// 一个总的函数,调用以上的两个初始化函数.并加载idtr
void idt_init()
{
    put_str("idt_init start \n");
    idt_desc_init();
    exception_init();

    pic_init();

    // 低 16位是界限,界限是idt的长度-1,高16位是所在地址.都没问题
    uint64_t idtr = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    // 然后加载 iidtr
    asm volatile("lidt %0"
                 :
                 : "m"(idtr));
}

enum intr_status intr_enable()
{
    enum intr_status old_status;
    if (INTR_ON == intr_get_status())
    {
        old_status = INTR_ON;
        return old_status;
    }
    else
    {
        old_status = INTR_OFF;
        asm volatile("sti"); // 开中断,sti指令将IF位置1
        return old_status;
    }
}

/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable()
{
    enum intr_status old_status;
    if (INTR_ON == intr_get_status())
    {
        old_status = INTR_ON;
        asm volatile("cli"::: "memory"); 
        return old_status;
    }
    else
    {
        old_status = INTR_OFF;
        return old_status;
    }
}

/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status)
{
    return status & INTR_ON ? intr_enable() : intr_disable();
}

/* 获取当前中断状态 */
enum intr_status intr_get_status()
{
    uint32_t eflags = 0;
    GET_EFLAGS(eflags);
    return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

// 为指定的中断,设置一个新的中断处理函数
void register_handler( uint8_t vector_no, void* function )
{
    idt_table[ vector_no ] = function;
}

4.2 idt.asm

这里面相对简单,就是,先extern idt_table,然后,在中断例程中调用函数的代码中,压栈中断号push %1,call [idt_table+%1*4]

也就是改变的是,不在去put_str,而是去调用idt_table中的中断例程中调用函数.配合模板macro可以构建出33个不同的函数例程.

这里要区分,中断例程中调用函数和中断例程:中断例程在中断描述符表中的,而中断例程中调用的函数,则是,恩,他的字面意思.

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop 
%define ZERO push 0

; 引用外部函数
extern put_str
extern idt_table


section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2   
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2
        
            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

            call [idt_table+%1*4]

            add esp,4   ;跳过参数

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            
            popad   
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code
            
            iret 
        section .data
            dd intr%1entry
    %endmacro 
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

4.3 main.c

改变main.c中开中断的方式:

#include "console.h"
#include "debug.h"
#include "init.h"
#include "interrupt.h"
#include "memory.h"
#include "print.h"
#include "process.h"
#include "thread.h"

// 这里一定要先声明,后面定义
// 不然会出错,我也不知道为啥,应该是因为改变了地址?
// 就是在ld中
void k_thread_a( void* );
void k_thread_b( void* );
void u_prog_a( void );
void u_prog_b( void );
int  test_var_a = 0, test_var_b = 0;

int main( int argc, char const* argv[] )
{
    set_cursor( 880 );
    put_char( 'k' );
    put_char( 'e' );
    put_char( 'r' );
    put_char( 'n' );
    put_char( 'e' );
    put_char( 'l' );
    put_char( '\n' );
    put_char( '\r' );
    put_char( '1' );
    put_char( '2' );
    put_char( '\b' );
    put_char( '3' );
    put_str( "\n put_char\n" );

    init_all();

    put_str( "interrupt on\n" );

    void* addr = get_kernel_pages( 3 );
    put_str( "\n get_kernel_page start vaddr is " );
    put_int( ( uint32_t )addr );
    put_str( "\n" );

    // 改变执行流
    thread_start( "k_thread_b", 8, k_thread_b, "argB " );
    thread_start( "k_thread_a", 31, k_thread_a, "argA1 " );

    process_execute( u_prog_a, "user_prog_a" );

    process_execute( u_prog_b, "user_prog_b" );

    put_str( "\n start" );

    BREAK();

    intr_enable();  // 打开中断,使时钟中断起作用

    while ( 1 )
    {
    }
    return 0;
}

void k_thread_a( void* arg )
{
    char* para = arg;

    while ( 1 )
    {
        console_put_str( para );
    }
}

void k_thread_b( void* arg )
{
    char* para = arg;

    while ( 1 )
    {
        console_put_str( para );
    }
}

/* 测试用户进程 */
void u_prog_a( void )
{
    while ( 1 )
    {
        console_put_str( "a " );
    }
}

/* 测试用户进程 */
void u_prog_b( void )
{
    while ( 1 )
    {
        console_put_str( "b " );
    }
}

4.4 运行

也没什么,就是根据不同的中断打印不同的信息而已,在现在的情况下,能不断产生的中断就只有32号中断,是时钟中断

4 处理器处理中断的完整过程

4.1 用到的调试指令

首先了解几条bochs的调试指令:

b :打断点,在执行到该物理内存的指令的时候,停止.注意是物理内存

show int:让bochs在发生中断的时候,打印中断相关的信息,包括:中断发生前执行了多少指令,也就是指令数时间戳,终端类型:

sb 数字:表示在执行指定数字条指令后停止

sba 数字表示从bochs开始运行到执行执行数字条指令后停止

4.2 思路

总的思路就是我们要捕捉在进入内核运行以后,捕捉一次外部中断,然后跟踪查看堆栈以及cpu的运行.

因为内核是加载在0xc0001500这是虚拟地址,其物理地址是0x1500,因此首先在0x1500处打断点,c让程序执行到此处的时候停止,然后show int显示中断,然后c,找到一次外部中断,查看其执行的总的指令数.

然后重新开始,直接调到该值数之前,再单步跟踪,这一次外部中断.

4.3 开始

因为不涉及到特权级的转移,因此中断时候压栈,没有ssesp,返回的时候也同样.

4.3.1 找到第一个时钟中断

1首先b 0x1500在物理地址0x1500处打上断点,然后c开始执行.当执行停止的时候表示刚要进入刚要进入内核执行,但是还没执行

然后show int打开显示中断信息,并c开始执行.

当中断发生的时候ctrl+c停止,并找到exception外部中断,因为中断发生的很快,所以几乎c回车以后,就要立马ctrl+c:

记录第一个时钟中断发生时候执行的指令数:18241108,和第一个时钟中断要退出的时候iretd时候执行的指令数:18242429

4.3.2 执行到第一个时钟中断之前

然后q关闭程序,重新打开bochs,这次直接定位到sba 18241100,因为在18241108之后就要发生中断了啊,所以要在这之前停下.然后c

然后观察,在18241100~18241107条指令的时候执行的都是jmp -2,这也就是main()while (1)编译的结果.

并且,下一条,如果不发生中断,那么下一条指令任然是jmp -2

4.3.3 查看中断之前的寄存器和栈

接下来,查看通用寄存器r,查看段寄存器sreg,查看栈中信息print-stack:

4.3.4 发生中断

紧接着s执行一条指令:

提示发生中断,此时cpu已经完成了硬件部分需要的压栈,以及中断处理程序中的第一条指令.

也就是idt.asm中模板的第一条指令%2.时钟中断没有errorcode,因此,被替换为push 0,紧接着下一条指令就是push ds

4.3.5 中断发生后的寄存器和栈

然后查看通用寄存器r,查看段寄存器sreg,查看栈中信息print-stack:

4.3.6 找到中断返回的指令

然后sba 18242420 ,c运行.到中断返回前夕.

然后单步s执行到,下一条指令是iret为止:

4.6.4 中断返回前的栈和寄存器

从上到下依次是:eip,cs,eflags,栈中的信息,会在iretd执行的时候,被cpu自动的将对应的数据,pop到对应的寄存器中.

然后,查看一下寄存器的值

4.6.5 中断返回后的栈和寄存器

中断返回之前的栈中的信息,被弹出到eip,cs,eflags中.

猜你喜欢

转载自www.cnblogs.com/perfy576/p/9139111.html
今日推荐