DAY6 分割编译与中断处理
2020.10.18
今天主要整理了一下源文件,并实现中断处理。
1. 分割源文件
文档:harib03a
今天先做点热身运动再继续昨天剩下的程序。
现在的bootpack.c已经长达300行,我们决定把它分割为几部分。
将源文件分割为几部分的优缺点:
优点
- 分类得好的话,修改时容易定位到对应代码处。
- 只需要编译修改过的文件,提高make的速度。
- 单个源文件都不长,多个小文件比一个大文件更好处理。
缺点
- 源文件数量增加。
- 分类得不好的话,修改时不容易定位。
分割后每个文件都要对要调用的函数进行声明。
记得还要对Makefile进行相应的修改。
整理Makefile
文档:harib03b
源文件分割成功后,Makefile又有点长了。有很多地方都有点冗余,例如:
bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c
graphic.gas : graphic.c Makefile
$(CC1) -o graphic.gas graphic.c
dsctbl.gas : dsctbl.c Makefile
$(CC1) -o dsctbl.gas dsctbl.c
bootpack.nas : bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas
graphic.nas : graphic.gas Makefile
$(GAS2NASK) graphic.gas graphic.nas
dsctbl.nas : dsctbl.gas Makefile
$(GAS2NASK) dsctbl.gas dsctbl.nas
把他们归纳起来,利用一般规则进行优化。
# 一般規則
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
%.obj : %.nas Makefile
$(NASK) $*.nas $*.obj $*.lst
make.exe会首先寻找普通的生成规则,如果没找到就尝试用一般规则。所以有个别想用单独规则编译的文件,使用普通生成规则也不会有冲突。
整理头文件
文档:harib03c
graphic.c … 187行
dsctbl.c … 67行
bootpack.c … 81行
合计 … 335行
分割后合起来因为每个源文件都要重复声明函数头,所以比分割前的280行多了不少。
现在我们将这些重复部分全部去掉,把他们归纳起来放到名为bootpack.h的文件里。虽然扩展名改变了,但它也是C语言的文件。
//bootpack.h节选
/* asmhead.nas */
struct BOOTINFO { /* 0x0ff0-0x0fff */
char cyls;
char leds;
char vmode;
char reserve;
short scrnx, scrny;
char *vram;
};
#define ADR_BOOTINFO 0x00000ff0
/* naskfunc.nas */
void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);
/* graphic.c */
void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
在源文件前,加上
#include "bootpack.h"
就可以让编译器去读这个头文件,见到了这一行,就将该行替换成所指定文件的内容。
前文提到sprintf时,说要加上#include <stdio.h>,其实也是因为stdio.h中含有对sprintf的声明。(" ")(< >)的区别只是文件所处位置的不同,前者表示该头文件与源文件位于同一个文件里,后者表示该头文件位于编译器所提供的文件夹里。
#define把用到的地址都只写在了bootpack.h文件里,如果想要变更地址的话,只修改bootpach.h一个文件就行了。
bootpack.h … 69行
graphic.c … 156行
dsctbl.c … 51行
bootpack.c … 25行
合计 … 301行
已经缩短了34行。
2. 初始化PIC
文档:harib03d
恢复正轨,接着昨天继续做鼠标指针的移动。
为了鼠标移动必须使用中断,使用中断必须将GDT和IDT正确无误地初始化。但是,还有一件事没做,那就是初始化PIC。
PIC(Programmable Interrupt Controller 可编程中断控制器):PIC是将8个中断信号集合成一个中断信号的装置,PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。IBM的大佬们通过增加PIC来处理更多的中断信号,所以使用了两个PIC。
它们的线路连接如图:
讲清楚PIC的硬件结构才能顺利设定PIC。
//bootpack.h节选
//#define 对PIC中相应的端口号进行了声明
/* int.c */
void init_pic(void);
void inthandler21(int *esp);
void inthandler27(int *esp);
void inthandler2c(int *esp);
#define PIC0_ICW1 0x0020
#define PIC0_OCW2 0x0020
#define PIC0_IMR 0x0021
#define PIC0_ICW2 0x0021
#define PIC0_ICW3 0x0021
#define PIC0_ICW4 0x0021
#define PIC1_ICW1 0x00a0
#define PIC1_OCW2 0x00a0
#define PIC1_IMR 0x00a1
#define PIC1_ICW2 0x00a1
#define PIC1_ICW3 0x00a1
#define PIC1_ICW4 0x00a1
很多端口号都相同,但因为PIC有些细微的规则,比如写入ICW1后,紧接着一定要写入ICW2,所以即使相同,设定时也能区分开来。
//int.c的主要组成部分
//PIC的初始化程序
void init_pic(void)
/* PIC的初始化 */
{
io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */
io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2连接 */
io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */
io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-2f接收 */
io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */
io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */
return;
}
**简单介绍一下PIC的寄存器。**PIC中的寄存器都是8b的。
- IMR(Interrupt Mask Register 中断屏蔽寄存器):8位分别对应8路IRQ信号,某一位置1时,就会屏蔽对应的IRQ信号,PIC忽视该路信号。
- ICW(Initial Control Word 初始化控制数据):ICW有4个,一共4B的数据。
- ICW1和ICW4:与PIC主板配线方式、中断信号的电气特性相关,使用上述程序所示的固定值即可,乱改别的什么值的话,可能会烧坏保险丝哦(吓唬脸)。(其实会烧坏和器件冒烟的是早期电脑,如今的电脑对这种设定起反应的电路已经省略了。)
- ICW3:有关主-从连接的设定。如果全部置1,那么主PIC就能驱动8个从PIC。所以根据上面的连接图,我们设定成00000100。
- ICW2:决定了IRQ以哪一号中断通知CPU。中断发生以后,如果CPU可以受理这个中断,就会命令PIC发送两个字节的数据。这两个字节的数据被CPU解读为指令,是"0xcd 0x??"。0xcd就是调用BIOS使用的INT指令。按INT 0x20-0x2f接收中断信号IRQ0-15设定。**为什么不使用INT 0x00-0x0f?**因为INT 0x00-0x1f是在应用程序想要对操作系统干坏事的时候,CPU内部产生的系统保护通知。
3. 中断处理程序的制作
文档:harib03e
鼠标是IRQ12,键盘是IRQ1。所以我们编写了用于INT 0x2c和INT 0x21的中断处理程序。
中断处理程序(handler):中断发生时所要调用的程序。
//int.c节选
void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
for (;;) {
io_hlt();
}
}
void inthandler2c(int *esp)
/* 来自PS/2鼠标的中断 */
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 2C (IRQ-12) : PS/2 mouse");
for (;;) {
io_hlt();
}
}
中断处理完成之后,不能执行return(RET),而是必须执行IRET。所以,我要又要借助汇编语言了。
;naskfunc.nas节选
EXTERN _inthandler21, _inthandler27, _inthandler2c
_asm_inthandler21:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21
POP EAX
POPAD
POP DS
POP ES
IRETD
鼠标程序同理。
PUSHAD表示
PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP
PUSH EBP
PUSH ESI
PUSH EDI
POPAD同理。
将函数注册到IDT中
/* IDT的设定 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
2*8表示的是属于哪一个段,即段号是2,乘以8是因为低3位有着别的意思,所以低3位必须是0。
最后的AR_INTGATE32将IDT的属性设置为0x008e,表示中断处理有效。
还记得我们之前设置的段号2是谁吗?那个刚好覆盖了整个bootpack.hrb的段。
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
在bootpack.c中的HariMain开中断,并设置PIC中的IMR以便接收来自键盘和鼠标的中断
//bootpack.c的HariMain节选
io_sti(); /* 开中断
//中间的处理
io_out8(PIC0_IMR, 0xf9); /* (11111001) */
io_out8(PIC1_IMR, 0xef); /* (11101111) */
运行试试看
可以接收到来自键盘的中断了,但对鼠标还是没反应。。。
已经很棒了,明天见!