第6天 分割编译与中断处理
1.分割源文件
由于bootpack.c代码行数太多,不便于观看也不便于修改,于是根数函数的功能将它分割为几个文件,下图为示意图。
还需要修改一下Makefile,流程如下:
由于只是简单把代码转移一下位置,就不展示完成后的界面了。
2.整理Makefile
现在Makefile又太长了,对他进行修改。
%.gas: %.c
$(CC1) -o $*.gas $*.c
%.nas: %.gas
$(GAS2NASK) $*.gas $*.nas
%是Makefile中的通配符,可以将大量同类型的文件,只用一条规则就完成构建。而且普通生成规则比这种规则优先级更高,所以即使产生冲突也没有问题。$*就是%的内容,你可能会问为什么不都使用%?因为下面是不能使用%的。
因为主程序还是没改变,这次也不放界面出来。
3.整理头文件
学过C语言的人应该都知道头文件,把编写的函数放在头文件中,方便其他文件进行调用,也可以使得在模块化编程中减少重复的代码。这次新建一个头文件,命名为bootpack.h,把要用到的函数都放到这里面来。
/* asmhead.nas */
struct BOOTINFO {
char cyls; /* 启动区读硬盘督导何处为止 */
char leds; /* 启动时键盘LED的状态 */
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);
void init_screen(char *vram, int x, int y);
(以下略)
在C文件中添加bootpack.h的引用,又可以减少不少重复代码。
由于主程序流程依然没有改变,本节也无可展示内容。
4.初始化PIC
IDT和GDT的内容都在第五章中解释了,如果遗忘了可以再去看看。
但就算我们完成了IDT和GDT的初始化还是不能用中断,因为还没有初始化PIC。
PIC的意思是可编程中断控制器(Programmable Interrupt Controller),每个PIC可以处理8个中断信号,但8个中断信号不够用,便采取了PIC级联的方式增加处理中断信号的数目,可处理的中断信号数从8个变成了15个,其连接图如下所示:
8259A芯片就是PIC芯片(我的收藏的博客中有两篇介绍8259A的博客,本文的内容大多也来源于此),因为从片占据了主片的IR2,所以一共只能处理15个中断信号。
接下来看看代码吧。
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由IRQ2连接 */
io_out8(PIC1_ICW3, 0x2); /* PIC1有IRQ2连接 */
io_out8(PIC1_ICW4, 0x01); /* 无缓冲区模式 */
io_out8(PIC0_IMR, 0xfb); /* 11111011 PIC1以外全部禁止 */
io_out8(PIC1_IMR, 0xff); /* 11111111 禁止所有中断 */
}
在bootpack.h中定义了这些端口:
#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
很奇怪,为什么有不少端口号是一样的啊?这样分得清要对哪个进行操作吗?这些问题之后会做解释。其中PIC0开头的是8259A主片的端口,PIC1开头的是8259A从片的端口。
8259A芯片中有4个ICW寄存器(Initial Control Word,初始化控制数据),对ICW寄存器的操作要连续进行,即按照ICW1、ICW2、ICW3、ICW4的顺序进行,所以,即使ICW2、ICW3、ICW4的地址一样,8259A芯片也能分得出是对哪个寄存器进行操作。下面对ICW寄存器做主要的介绍。
ICW1
当发送的字节第5比特位(D4)=1,端口地址为0x0020或0x00a0时,表示对ICW1进行操作。
位 | 名称 | 含义 |
D7 | A7 | 这几位对8086/88处理器无用。 |
D6 | A6 | |
D5 | A5 | |
D4 | 1 | 恒为1。 |
D3 | LTIM | 0 - 边沿触发方式; 1 - 电平触发中断方式。 |
D2 | ADI | 对8086/88系统无用。 |
D1 | SNGL | 0 - 多片8259A芯片; 1 - 单片8259A芯片。 |
D0 | IC4 | 0 - 不需要ICW4; 1 - 需要ICW4。 |
在linux0.11内核中,ICW1被设置为0x11,表示中断请求是边沿触发,多片8259A芯片级联且需要发送ICW4。
ICW2
用于设置芯片送出的终端号的高5位,在设置了ICW1之后,当地址为0x0021或0x00a1时,表示对ICW2进行设置。
位 | 名称 | 含义 |
D7 | A15/T7 | 在使用8086/88处理器的系统或兼容系统中,T7~T3是中断号的高5位,与8259A芯片自动设置的低3位(8259A按IR0~IR7三维编码值自动填入)组成一个8位中断号。8259A在收到第2个中断响应脉冲时,把此中断号送到数据线上,以供CPU读取。 |
D6 | A14/T6 | |
D5 | A13/T5 | |
D4 | A12/T4 | |
D3 | A11/T3 | |
D2 | A10 | 一般都设置为0 |
D1 | A9 | |
D0 | A8 |
Linux-0.11系统把主片的ICW2设置为0x20,表示主片中断请求0~7对应的中断号是0x20~0x27,把从片的ICW2设置成0x28,表示从片中断请求8~15级对应的中断号是0x28~0x2f。另外0x00~0x1f的中断号要么是系统异常中断,要么就是厂家预留给以后使用的,所以不能用。
ICW3
位 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
主片 | S7 | S6 | S5 | S4 | D3 | D2 | D1 | D0 |
从片 | 0 | 0 | 0 | 0 | 0 | ID2 | ID1 | ID0 |
对于主片,Si=1表示IRi与从片级联,则该中断请求引脚的信号来自从片。
对于从片,ID2~ID0三个比特位对应个从片的标识号,即连接到主片的中断级。当某个从片接收到级联线(CAS2~CAS0)输入的值与自己的ID2~ID0相等时,从片应该向数据总线发送自己当前被选中的中断请求的中断号。
Linux-0.11把主片的ICW3设置为0x04,即S2=1,其余各位为0,表示主片的IR2连接一个从片,从片的ICW3被设置为0x02,即其标识号为2,表示此从片连接到主片的IR2引脚。因此,中断优先级的排列次序为:0级最高,1级次之,接下来是8~15级,最后是主片的3~7级。
ICW4
当ICW1的D0置为1时,表示需要ICW4。
位 | 名称 | 含义 |
D7 | 0 | 恒为0 |
D6 | 0 | |
D5 | 0 | |
D4 | SFNM | 0 - 普通全嵌套方式 1 - 特殊全嵌套方式 |
D3 | BUF | 0 - 非缓冲方式 1 - 缓冲方式 |
D2 | M/S | 0 - 缓冲方式下从片 1 - 缓冲方式下主片 |
D1 | AEOI | 0 - 非自动结束中断方式 1 - 自动结束中断方式 |
D0 | MPM | 0 - MCS80/85系统 1 - 8086/88处理器系统 |
Linux-0.11中主片和从片的ICW4都被设置为0x01.表示设置为普通全嵌套方式、非缓冲方式、非自动结束中断方式,并用于8086及其兼容机。
ICW的内容就介绍结束了,但代码还有PIC0_IMR的定义,这是什么?在8259A芯片中,除了ICW寄存器,还有OCW (操作命令字,Operation Command Words)寄存器,被用来设置和管理8259A的工作方式。8259A中一共有3个OCW寄存器,代码中的PIC0_IMR就是OCW1,下面是关于OCW1的介绍。
OCW1
地址为0x21或0xA1,用于对8259A中IMR(中断屏蔽寄存器)进行读写操作。
位 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
名称 | M7 | M6 | M5 | M4 | M3 | M2 | M1 | M0 |
若Mi=1,则屏蔽对应的中断请求级IRi;若Mi=0,则允许IRi。另外,屏蔽高优先级中断并不会影响低优先级中断的请求。
由于本次没有使用OCW2和OCW3,在这里就不做介绍了。
代码讲解完毕,开始运行程序吧。
虽然界面还是没有改变,但这是为了接下来使用鼠标和键盘做铺垫。
5.中断处理程序的制作
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");
while (1)
{
io_hlt();
}
}
这是键盘的中断服务函数,目前就只是让它显示一段文字就可以了,更复杂的操作等到之后再添加。
_asm_inthandler21:
PUSH ES
PUSH DS
PUSHAD
MOV EAX, ESP
PUSH EAX
MOV AX, SS ; C语言认为SS、ES、DS是一个段
MOV DS, AX ; 在执行C语言之前把它们设置成一样的
MOV ES, AX ; 不然函数inthandler21就不能顺利执行
CALL _inthandler21
POP EAX
POPAD
POP DS
POP ES
IRETD
这是中断保存现场的操作,另一部分操作由硬件自动完成就不用我们操心了(这是机组的知识,相信大家都知道的吧)。
PUSHAD指令:把通用寄存器压入栈,当操作数大小是32位时,入栈的顺序是:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI.
POPAD指令:把栈中的值弹出到通用寄存器中,当操作室大小是32位时,出栈顺序是:EDI、ESI、EBP、ESP、EBX、EDX、ECX、EAX。
栈属于数据结构的知识,书上也讲得很详细,我就不赘述了(其实就是我不想讲)。
中断服务函数写好了,但系统怎么调用它呢?系统怎么知道函数的位置呢?还记得我们写的IDT的设定吗?我们只要将程序入口地址传入其中,系统就会在中断时根据这个地址执行中断服务程序,如下是代码部分:
/* IDT的设定 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
2 * 8表示使用第2段,乘8是因为低3位不能用(应该是因为段寄存器低3位不能用的缘故)。AR_INTGATE32为0x8e,我之前博客介绍过IDT结构,这个意思是此段存在于内容,特权级为0(最高),是中断描述符。
来运行看看效果吧。
点击键盘就会显示字符串,但鼠标则是没什么反应,主要是因为鼠标的设置还没有完成,那是下一天的事情了。
今天的内容还是比较多的,主要是寄存器的介绍,不过对应着数据慢慢看还是不难,要等到学到内存管理,用到一些算法上的知识的时候,才会让人有些理不清逻辑。今天的内容就到此为止,期待你收看下一天的博客(虽然还没有写)( ̄▽ ̄)~*。