标签:自己的 1.2 注意 hint ddr 元素 描述符 自己 时间间隔
8259A的作用是负责所有的外设中断.
cpu每次只能执行一个任务,而中断可能同时发生,所以8259A用来收集所有的中断,然后挑选出一个优先级最高的中断,传送给CPU
8259A的功能有:管理和控制可屏蔽中断,表现在屏蔽外设中断,对他们实行优先级判决,向cpu提供中断向量好等功能
每个8259A智能管理8个中断,而intel的cpu由256个中断,因此采用级联的方式,使用多个8259A管理256个中断.每个8259A有一个额外的引脚可以链接其他的8259A,被连接的那个8259A需要占用一个引脚,但是主片需要占用一个引脚去连接CPU.
8259A中的端口有:
所有的寄存器都是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.
8259A成为可编程中断控制器,说明他的工作方式很多,需要他进行设置.也就是对他进行初始化,设置为基连的方式,指定中断向量号,以及工作模式.
中断向量号是楼机上的东西,物理上他是8259A的IRQ接口号,8259A上的IRQ接口号排列顺序是固定的.但是对应的中断向量号不是固定的,是由硬件到软件的映射,通过对8259A进行设置,可以将IRQ接口映射到不同的中断向量号.
8259A内由两组寄存器,一组是用来初始化的,用来保存初始化命令字ICW1~ICW4,一共4组.另一组寄存器是操作命令寄存器,用来保存操作命令字,OCW1~OCW3一共三组.
对ICW初始化,用来设置是否使用级联,设置其实中断向量号,设置中断结束模式.某些设置之间可能相互关联,因此需要一次写入ICW1~4
对OCW的初始化,来操作8259A,就是中断屏蔽和中断结束.OCW的发送顺序不固定
ICW1用来初始化8259A的连接方式和中断信号的触发方式.连接方式是指单片工作还是多片的级联工作.触发方式是指中断请求信号是水平触发还是边缘触发.ICW1需要写入到主片的0x20和葱片的0xA0端口:
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有些低位选项基于高位
OCW1用来屏蔽在8259A上的外部设备的中断信号,实际上就是把OCW1写入IMR寄存器,OCW1写入主片0x21,或从片0xA1
M0~M7对应IRQ1~7.设置为1标识屏蔽.
OCW2用来设置中断结束方式和优先级模式.写入主片的0x20和从片的0xA0
OCW2配置复杂,各个属性位要配合在一起,组合出8259A的各种工作模式
跳过了
OCW3用来设置特殊屏蔽方式以及查询方式写入写入主片的0x20和从片的0xA0
跳过了
跳过了,最后直接贴最终的代码
中断发生的时候,都需要进行上下文的保存,这一部分的代码是相同的,因此使用汇编的模板macro
,来编写.
使用这种方法来编写,需要记录模板生成的每一个函数的地址.然后将这些地址在内存空间中顺序保存,取第一个函数的地址,作为中断处理函数的地址.
中断处理函数使用汇编来编写,是一个模板:
%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
首先定义门描述符的结构,然后定义一个数组,存储所有的中断描述符
// 定义门描述符结构.
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");
}
主要的就是这些
这一部分先添加了很多的代码,首先看一下目录结构:
└── 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
文件更改了,其他的文件都没有更改.
首先解释一下,每个新添加的文件的内容:
in
,out
操作段的一些代码,在第6章里添加了该文件idt_init()
函数用于,初始化和中断相关的事情以后,每添加一个功能,就在kernel
文件夹中,添加文件,该文件向外暴露一个XXX_init()
函数,然后由init.c
文件夹中的init_all()
函数调用,而main.c
函数中则调用init_all()
该文件暂时,定义了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
首先我们构建中断描述符表,构建前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
这样,就成为一个数组。
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
,也就是最终的中断描述符表.#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));
}
/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(); //初始化中断
}
调用/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;
}
就是新添加了几个文件的编译,还有最终链接的时候要加上链接的文件.
#! /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));
造成了:
因此要加上这个选项相关资料
下面是运行的结果.开中断以后不停地打印字符串.
现在的中断例程中调用函数都一样,是使用汇编编写的,以后中断例程中调用函数需要用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就是对应中断号的处理程序.
在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;
}
这里面相对简单,就是,先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
改变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 " );
}
}
也没什么,就是根据不同的中断打印不同的信息而已,在现在的情况下,能不断产生的中断就只有32号中断,是时钟中断
首先了解几条bochs的调试指令:
b
:打断点,在执行到该物理内存的指令的时候,停止.注意是物理内存
show int
:让bochs在发生中断的时候,打印中断相关的信息,包括:中断发生前执行了多少指令,也就是指令数时间戳,终端类型:
sb
数字:表示在执行指定数字条指令后停止
sba 数字
表示从bochs开始运行到执行执行数字条指令后停止
总的思路就是我们要捕捉在进入内核运行以后,捕捉一次外部中断,然后跟踪查看堆栈以及cpu的运行.
因为内核是加载在0xc0001500
这是虚拟地址,其物理地址是0x1500
,因此首先在0x1500
处打断点,c
让程序执行到此处的时候停止,然后show int
显示中断,然后c
,找到一次外部中断,查看其执行的总的指令数.
然后重新开始,直接调到该值数之前,再单步跟踪,这一次外部中断.
因为不涉及到特权级的转移,因此中断时候压栈,没有ss
和esp
,返回的时候也同样.
1首先b 0x1500
在物理地址0x1500
处打上断点,然后c
开始执行.当执行停止的时候表示刚要进入刚要进入内核执行,但是还没执行
然后show int
打开显示中断信息,并c
开始执行.
当中断发生的时候ctrl+c
停止,并找到exception
外部中断,因为中断发生的很快,所以几乎c回车以后,就要立马ctrl+c
:
记录第一个时钟中断发生时候执行的指令数:18241108,和第一个时钟中断要退出的时候iretd
时候执行的指令数:18242429
然后q
关闭程序,重新打开bochs
,这次直接定位到sba 18241100
,因为在18241108
之后就要发生中断了啊,所以要在这之前停下.然后c
然后观察,在18241100~18241107条指令的时候执行的都是jmp -2
,这也就是main()
中while (1)
编译的结果.
并且,下一条,如果不发生中断,那么下一条指令任然是jmp -2
接下来,查看通用寄存器r
,查看段寄存器sreg
,查看栈中信息print-stack
:
紧接着s
执行一条指令:
提示发生中断,此时cpu已经完成了硬件部分需要的压栈,以及中断处理程序中的第一条指令.
也就是idt.asm中模板的第一条指令%2
.时钟中断没有errorcode
,因此,被替换为push 0
,紧接着下一条指令就是push ds
然后查看通用寄存器r
,查看段寄存器sreg
,查看栈中信息print-stack
:
然后sba 18242420
,c
运行.到中断返回前夕.
然后单步s
执行到,下一条指令是iret
为止:
从上到下依次是:eip
,cs
,eflags
,栈中的信息,会在iretd
执行的时候,被cpu自动的将对应的数据,pop到对应的寄存器中.
然后,查看一下寄存器的值
中断返回之前的栈中的信息,被弹出到eip
,cs
,eflags
中.
标签:自己的 1.2 注意 hint ddr 元素 描述符 自己 时间间隔
原文地址:https://www.cnblogs.com/perfy576/p/9139111.html