建立中断机制
1.简介
上一节,我们绘制了鼠标图案,遗憾的是,鼠标箭头是死的,动不了,要想让鼠标移动,我们需要为内核建立中断机制
当我们移动鼠标时,鼠标会给CPU发送信号,CPU接收到信号后,终止当前的运算,执行内核给定的代码以处理鼠标发送的信号
中断信号的发送机制
每一个8259A控制器有8根中断信号线 总共可以接入15个外设硬件
一般情况下 鼠标接入的是从8259A所对应的IRQ4这根信号线
鼠标发送信号时 先通过管线IRQ4将信号传递到从8259A
然后通过管线IRQ2传递到主8259A
最后信号再传递给CPU 键盘产生的中断通过主8259A的IRQ1管线向CPU发送信号
既然有硬件,那就需要对其初始化后才能使用,对硬件的控制我们前面已经说过,需要通过端口发送命令来完成,要配置这两个控制器,我们需要对指定端口发送1字节的数据,这个一字节(8 bit)的数据,我们称之为ICW(initialization control word).主8259A对应的端口地址是20h,21h, 从8259A对应的端口是A0h和A1h. 对端口发送数据时,顺序是定死的,不能违背: 1. 往端口20h(主片)或A0h(从片)发送ICW1 2. 往端口21h(主片)或A1h(从片)发送ICW2 3. 往端口21h(主片)或A1h(从片)发送ICW3 4. 往端口20h(主片)或A0h(从片)发送ICW4
(这里略过一些概念的东西·· 毕竟比较晦涩难懂 别睡着了)
2.代码控制
下面我们通过代码配置两个中断控制器:
(1)先向主8259A发生ICW1:
mov al, 011h out 020h, al
011h 对应的二进制是00010001 对应ICW1的说明
由于ICW1[0]=1表示需要发送ICW4, ICW1[1] = 0 说明有级联8259A(我们买来的电脑都是级联的)
ICW1[2] =0 表示用8字节来设置中断向量号
ICW1[3]=0表示中断形式是边沿触发 ICW[4]必须设置为1
ICW[5,6,7]必须是0
(那是相当的晦涩了·· 看个大概就好!后面慢慢懂吧 都是硬件的部分 我不是很关心)
(下面很长一部分都是这些 刚兴趣的可以去看看:https://blog.csdn.net/tyler_download/article/details/52716839)
如果你和我一样不敢兴趣 那就先和我一起往下走吧!
综合以上 我们得到了如下的代码:
init8259A: mov al, 011h out 02h, al call io_delay out 0A0h, al call io_delay mov al, 020h out 021h, al call io_delay mov al, 028h out 0A1h, al call io_delay mov al, 004h out 021h, al call io_delay mov al, 002h out 0A1h, al call io_delay mov al, 003h out 021h, al call io_delay out 0A1h, al call io_delay mov al, 11111101b ;允许键盘中断 out 21h, al call io_delay mov al, 11111111b out 0A1h, al call io_delay ret
上边的这段代码 就是汇编部分 调用硬件中断的代码
那二话不说 汇编有了 我们去上C语言:
前面我们处理了硬件如何发送信号的问题
接下来 我们来看看 CPU接收到信号后 如何执行内核指定的代码
要执行相应代码 CPU必须知道代码所在的内存位置 这个信息是通过中断描述符表来实现的
中断描述符的数据结构:
struct GATE_DESCRIPTOR { short offset_low; short selector; char dw_count; char attribute; short offset_high; };
中断描述符跟前面说到的全局描述符类似 也是用于描述内存性质的
只不过它专门用于描述可执行代码所在的内存
offset_low 和 offset_high 合在一起作为中断函数在代码执行段中的偏移
selector 用来指向全局描述符表中的某个描述符 中断函数的代码就处于该描述符所指向的段中
dw_count设置为0 attribute设置为08Eh
如何在内核中加载中断描述符表:
;Gate selecotor, offset, DCount, Attr %macro Gate 4 dw (%2 & 0FFFFh) dw %1 dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) dw ((%2>>16) & 0FFFFh) %endmacro
上面汇编代码中 %2对应的是4字节的地址偏移
把地址偏移的低2字节放到中断门的前两字节
接下来的一字节是宏定义的第一个参数 是中断代码所在的代码段的全局描述符
第三行设置中断描述符的属性 当前写死为08Eh
最后一行设置中断代码偏移的高二字节
在内核代码里 当全局描述符表加载到CPU后 就是我们加载中断描述符表的时机了
首先初始化一个中断描述符:
LABEL_IDT: %rep 255 Gate SelectorCode32, SpuriousHandler,0, DA_386IGate %endrep IdtLen equ $ - LABEL_IDT IdtPtr dw IdtLen - 1 dd 0
上面代码中 通过指令%rep 255 重复定义255个中断描述符
这么说来 CPU其实可以支持255种中断 其中两个8259A芯片的15个中断信号就包含在255个中断中 SpuriousHandler是中断代码的入口 我们把255个中断的处理代码都设置成SpuriousHandler 也就是无论哪个中断发生,都调用这个函数来处理:
xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_IDT mov dword [IdtPtr + 2], eax lidt [IdtPtr]
上面代码跟以前我们加载全局描述符表是一样的 由于加载全局描述符时我们使用指令cli关闭了中断功能
因此我们需要回复中断功能 CPU才能相应来自8259A芯片的信号:
[SECTION .s32] [BITS 32] LABEL_SEG_CODE32: ;initialize stack for c code mov ax, SelectorStack mov ss, ax mov esp, TopOfStack mov ax, SelectorVram mov ds, ax mov ax, SelectorVideo mov gs, ax sti %include "write_vga_desktop.asm" jmp $
上面的代码通过运行指令sti 恢复中断功能
最后再看看SpuriousHandler的实现
_SpuriousHandler: SpuriousHandler equ _SpuriousHandler - $$ call intHandlerFromC iretd
当点击键盘 引发中断时 _SpuriousHandler的代码被调用 它又调用了C模块实现的函数intHandlerFromC
我们看看C语言怎么实现intHandlerFromC的
void intHandlerFromC(char* esp) { char*vram = bootInfo.vgaRam; int xsize = bootInfo.screenX, ysize = bootInfo.screenY; boxfill8(vram, xsize, COL8_000000, 0,0,32*8 -1, 15); showString(vram, xsize, 0, 0, COL8_FFFFFF, "PS/2 keyboard"); for (;;) { io_hlt(); } show_char(); }
(虽然晦涩难懂 但是还要看一看的!不可直接略过的!)
3.编译运行
下面又到了编译的时候了···
编译C文件
i386-elf-gcc -m32 -fno-asynchronous-unwind-tables -s -c -o write_vga_desktop.o write_vga_desktop.c
反汇编o文件
./objconv -fnasm write_vga_desktop.o write_vga_desktop.asm
删除无用部分
修改kernel
%include "write_vga_desktop.asm"
修改boot(直接放大一点)直接读了20个扇区 肯定够用了
mov AL, 20 ; AL 表示要练习读取几个扇区
编译boot
nasm -o boot.bat boot.asm
编译kernel
nasm -o kernel.bat kernel.asm
运行java 生成system.img