30天自制OS学习笔记 (六)分割编译与中断处理

本文详细介绍了嵌入式系统开发过程中的关键步骤,包括源文件分割、Makefile优化、头文件整理,以及GDT/IDT设置、中断处理程序的编写等高级主题。深入探讨了中断控制器(PIC)初始化、中断处理程序制作,和如何在IDT中注册中断处理函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.分割源文件 & 2.整理Makefile & 3.整理头文件
源文件分割图如下
在这里插入图片描述
这样整理一下,清爽多了。对应源文件的分割,我们还要修改Makefile,流程如下
在这里插入图片描述
随着bootpack.c分割成多个文件,Makefile的内容变多了许多。为了简化Makefile的内容,可将处理相同类型文件的语句规则替换为一般规则。可将

bootpack.gas : bootpack.c Makefile
	$(CC1) -o bootpack.gas bootpack.c
	
raphic.gas : graphic.c Makefile
	$(CC1) -o graphic.gas graphic.c
	
dsctbl.gas : dsctbl.c Makefile
	$(CC1) -o dsctbl.gas dsctbl.c

替换为一般规则:

%.gas : %.c Makefile
	$(CC1) -o $*.gas $*.c

其他类型的文件也可以这样写,减少了Makefile的行数。
make.exe会首先寻找普通的生成规则,如果没找到就尝试用一般规则,普通规则优于一般规则。

由于各个源文件都要重复声明所用到的函数,所以源文件总行数并没有减少。于是将各源文件重复部分去掉,归纳起来放入名为bootpack.h的文件里,虽然扩展名变了,但它也是C语言的文件。像这样,仅由函数声明和#define等组成的文件,我们称之为头文件,指放在程序头部的文件。
在编译某个源文件时,我们要让编译器去读这个头文件,做法是在这个源文件的前面加上 #include “bootpack.h” 。
在这里插入图片描述

4.解决上次遗留问题——GDT/IDT的设置
首先说明一下naskfunc.nas的_load_gdtr

_load_gdtr:			;void load_gdtr(int limit,int addr);
		MOV		AX,[ESP+4]   ;limit
		MOV		[ESP+6],AX
		LIDT    [ESP+6]
		RET

这个函数用来将制定的段上限(limit)和地址值赋给名为GDTR的48位寄存器。这是一个很特别的48位寄存器,不能用常用的MOV指令来赋值。给它赋值的唯一方法就是指定一个内存地址,从指定的地址读取6个字节(48位),然后赋值给GDTR寄存器。完成这一任务的指令就是LGDT。
该寄存器的低16位(即内存最初2个字节)是段上限,剩下的高32位(即剩余的4字节),代表GDT的开始地址。

代码的执行含义:
在这里插入图片描述
如果从[ESP+6]开始读6字节的话,正好是我们想要的结果。

下面说一下dsctbl.c中的set_segmdesc函数。

struct SEGMENT_DESCRIPTOR {
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
	if (limit > 0xfffff) {
		ar |= 0x8000; /* G_bit = 1 */
		limit /= 0x1000;
	}
	sd->limit_low    = limit & 0xffff;
	sd->base_low     = base & 0xffff;
	sd->base_mid     = (base >> 16) & 0xff;
	sd->access_right = ar & 0xff;
	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
	sd->base_high    = (base >> 24) & 0xff;
	return;
}

这个函数是按照CPU的规格要求,将段的信息归结成8个字节吸入内存的。8个字节的内容如下:
在这里插入图片描述
段的地址当然是用32位表示,在结构体中base分为了3段,合起来刚好32位。程序使用了移位运算和AND的运算符往各个字节里填入相应的值。

为了不超出8字节,段上限只能用20位表示,这样最大只能指定到1MB为止(32位 寻址范围2的32次方 等于4GB)。为了表示4G内存,我们使用了分页,1P=4KB(1MB*4KB=4GB),通过Gbit位来表示,为1表示段上限的单位是页,为0表示为Byte。这个标志位在段的属性中。

20位的段上限分别写入limit_low和limit_high中,共24位,其中把12位段属性的高4位放入limit_high的高4位里。ar高4位是“扩展访问权”,到386以后才存在,由“GD00”组成,G为刚才的G_bit,D指段的模式,1指32位,0指16位。
低8位从286时代就有了,简单介绍一下:
在这里插入图片描述

32位模式下,CPU有系统模式和应用模式之分,CPU到底是处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权位0x9a的段,还是位于访问权为0xfa的段。

5.初始化PIC
PIC是“programmable interrupt controller”的缩写,意思是可编程中断控制器。与中断关系密切。在设计上,CPU只能处理一个中断,这不够用,所以在设计电脑时就增加了几个辅助芯片,如今已被集成在一个芯片组里。
在这里插入图片描述
在这里插入图片描述
与CPU直接相连的PIC称主PIC(master PIC),与主PIC相连的PIC称从PIC(slave PIC)。主PIC负责处理0-7号中断信号,从PIC负责处理8-15号中断信号。从PIC通过2号IRQ与主PIC相连。
在这里插入图片描述
PIC的初始化程序:

void init_pic(void)  /* PIC初始化  PIC0指主PIC,PIC1指从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](https://img-blog.csdnimg.cn/20181214214727254.png)接收 */
	io_out8(PIC1_ICW4, 0x01); /* 无缓冲区模式 */
	
	io_out8(PIC0_IMR, 0xfb); /* 11111011 PIC1以外全部禁止 */
	io_out8(PIC0_IMR, 0xff); /* 11111111 禁止所有中断 */
	
	return;
}

从CPU的角度来看,PIC是外部设备,CPU使用OUT指令进行操作。PIC内部有很多寄存器(都是8位),用端口号码对彼此进行区别,以决定是写入哪一个寄存器。
IMR是“interrupt mask register”的缩写,意思是“中断屏蔽寄存器”。8位分别对应8路IRQ信号。如果某一位的值是1,则该位所对应的IRQ信号被屏蔽,PIC就忽视该路信号。
在这里插入图片描述
ICW有4个,分别编号1~4,共有4个字节的数据。ICW1和ICW4与PIC主板配线方式、中断信号的电气特性等有关,电脑设定值都是上述程序所示的固定值。在这里插入图片描述在这里插入图片描述在这里插入图片描述
这次是以INT 0x20~0x2f接收中断信号IRQ 0 ~15而设定的。直接用INT 0x20 ~0x2f不行吗?这样不就一致了吗。之所以不能用,是因为应用程序想要对操作系统干坏事时,CPU会自动产生INT 0x00 ~0x1f,如果IRQ与这些号码重复了,CPU就分不清它到底是IRQ还是CPU系统保护通知。

6.中断处理程序的制作
键盘是IRQ1,鼠标是IRQ12,首先编写用于INT 0x2c和INT 0x21的中断处理程序(需要在IDT里注册关联),即中断发生时所要调用的程序。
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();
	}
}

函数只是显示一条信息,然后保持待机状态。鼠标的程序与此类似。函数接收了esp指针但还没有用。
中断处理完成之后,不能执行return(=RET)而必须执行IRETD指令。现在用汇编语言来写这个指令。
naskfunc.nas节选:

_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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面将函数注册到IDT中,在dsctal.c中的init_gdtidt里加入以下语句:

set_gatedesc(idt + 0x21, (int)asm_inthandler21, 2*8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int)asm_inthandler2c, 2*8, AR_INTGATE32);

asm_inthandler21注册在idt的第0x21号,如果发生中断了,CPU会自动调用asm_inthandler21。这里的2 * 8表示的是asm_inthandler21属于哪一个段,即段号是2,乘以8是因为低3位有着别的意思,这里低3位必须是0。所以2 *8也可以写成2<<3 或16。
在这里插入图片描述
这部分内容还是有些疑问的,往后读几天,等我回来再看看。

在执行完gdt\idt\pic初始化程序后,要执行STI指令,让IF(中断许可标志)变为1,是CPU接受来自外部的中断,CPU中断信号只有一根,所以IF也只有一个。

在HariMain的最后,修改PIC的IMR,以便接受来自键盘和鼠标的中断。

只有按下键盘某个键,或动一动鼠标,中断信号就会传到CPU,然后CPU执行中断处理程序,输出信息。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值