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中的端口有:
- INT:8259A选出优先级最高的中断请求,发送信号给CPU
- INTA:中断响应信号
- IMR:中断屏蔽器,用来屏蔽某个外设的中断
- IRR:中断请求其,用与接受结果IMR过滤后的中断信号,次寄存器中保存的都是等待处理的中断,相当于8259A中维护的未处理中断信号队列
- PR:优先级仲裁器,多个中断同时发生时,将他与当前正在处理的中断进行比较,挑选出优先级更高的中断
- 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端口:
- IC4为1表示在后面是否要写入ICW4
- SNGL为1表示单片0还是级联1
- ADI用来设置8085的调用时间间隔
- LTIM设置中断的检测方式,0表示边缘触发,1表示水平触发
- 4位为1,固定的
- 其余位为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有些低位选项基于高位
- SFNM,特殊全嵌套模式,0表示开启
- BUF,是否工作在缓冲模式下,当多个8259A级联的时候,当工作在缓冲模式下,M/S用来规定是主片还是从片,1表示主片.当非缓冲模式时,该位无效
- AEOI表示自动结束中断,0表示非自动模式,1表示自动模式
- 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
文件更改了,其他的文件都没有更改.
首先解释一下,每个新添加的文件的内容:
- /lib/io.h :该文件是使用内联汇编编写的,主要封装了
in
,out
操作段的一些代码,在第6章里添加了该文件 - /kernel/global.h:该文件主要用于保存以后kernel所有用到的一些宏和常量
- /kernel/idt.asm:该文件中主要用于构建了33个中断处理程序,还有一个由这些中断处理程序的地址组成的数组.
- /kernel/interupt.h,/kernel/interrupt.c:这两个文件,主要是用于构建中断描述符表,设置8259A,加载数据到idtr寄存器中.对外提供一个总的
idt_init()
函数用于,初始化和中断相关的事情 - /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 .data
和section .code
在编译后,一定是不在同一个segment内的。而且,section .data
的数据会紧凑的靠在一起。而section .data
中的数据有两部分:intr_entry_table
和dd 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文件的主要内容:
- 定义门描述符的结构
GateDesc
,然后用它定义一个数组static struct GateDesc idt[IDT_DESC_COUNTS]
,其地址作为中断描述符表. - 根据
idt.asm
构建好的intr_entry_table
,去填充完整的idt
,也就是最终的中断描述符表. - 初始化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 开始
因为不涉及到特权级的转移,因此中断时候压栈,没有ss
和esp
,返回的时候也同样.
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
中.
原文地址:https://www.cnblogs.com/perfy576/p/9139111.html