第6天 中断处理
GDT初始化
为了兼容前几代的CPU,所以GDT段描述符看起来有些不规整,让人难以理解,GDT段描述符中存储的内容和第五天讲的一样: 分为开始地址、段大小、段属性。我们实际处理是分割成以下几个部分:
段基址也就是段开始地址被拆成了三部分,段界限也就是段大小被拆成了三部分,剩下的几位就是段属性了。
在c语言中定义以下结构体:
struct Segment_descriptor
{
// 段界限
short limit_low;
// 段基址
short base_low;
char base_mid;
// 访问权限
char access_right;
char limit_high;
char base_high;
};
设置GDT,通过左移右移获取中间、开始或结尾的几个字节并设置到gdt的各项上。
/**
* 初始化gtd
* gdt: gdt地址
* limit: 段界限,段的长度
* base: 段基址 段的起始地址
* attr: 段属性
*/
void set_segment_descriptor(struct Segment_descriptor *gdt, int base, unsigned int limit, int attr) {
// 如果段上限大于 0xffff 设置G位为1
if (limit > 0xffff) {
attr /= 0x8000;
// limit缩小4k
limit /= 0x1000;
}
gdt->limit_low = limit & 0xffff;
// 先获取高8位 之后左移获取 对中间四位进行或运算
gdt->limit_high = (limit >> 16) & 0x0f | ((attr >> 8) & 0xf0);
gdt->base_low = base & 0xffff;
gdt->base_mid = (base >> 16) & 0xff;
gdt->base_high = (base >> 24) & 0xff;
gdt->access_right = attr & 0xff;
}
编写完方法之后我们还需要初始化各GDT表项,初始化8192个段,并将基址、界限、属性全部设置初始化为0。
// GDT寄存器共有48位,前32位为全局描述符地址,也就是0x00270000,后16位是段上限。段上限只有16位,段上限指明了操作系统共初始化了多少个段
// 再这16位前13位是段索引,最大2^13=jhjjhgttytioo ll;l88878956454132 .0.0第321536544614位
// 所以可以最大初始化2^16 = 65535个,每个段8个字节 65535 / 8 = 8191。我们可以初始化8191个段,0xffff最大可以初始化8192个段,如果超了8191,后面的肯定就没办法访问了。
int i;
for (i = 0; i < 8192; i++) {
set_segment_descriptor(gdt + i, 0, 0, 0);
}
16位段属性,access_right的高四位表示扩展访问权限:
- G: 段界限为20位。如果为0时以字节方式寻址,段的寻址范围1B-1M; 如果为1则是以4k为单位寻址,段的寻址访问为4k-4G。一般32位模式设置成1,很明显,1M根本不够日常使用的。
- D/: B默认操作数大小, 为1表示按32位寻址,为0表示16位寻址,16位一般用于80286的CPU中,其他基本都是使用32位的寻址方式。
- L: 64位代码标识,保留给64位处理器使用。
- AVL: 自定义标志位,用于开发人员自定义或扩展。
低8位: - P: 如果为1表示该段存在,可以正常访问,如果为0表示该段不存在,强行访问会出现异常。
- DPL: 特权级, 分为4个特权级, 0 - 3, 0权限为最高,3为最低。
- S: 0表示系统段,1表示代码段。
- TYPE: 段的访问权限。
x = 0时不可执行; x = 1时可执行;
e = 0时向高地址增长;
e = 1时向低地址增长; w = 0时只读;
w = 1时可读可写;
a = 0时段最近未被访问;
a = 1时段最近被访问过。
c = 0时非特权依存段, 非特权依存段只能调用相同特权级别相同的代码段;
c = 1时特权依存段, 特权依存段可以调用低权限代码段;
r = 0时不可断;
r = 1时可读;
当S位等于1时, 且为数据段时TYPE四位表示X, E, W, A。当S等于1时,且为代码段时,TYPE四位表示X, C, R, A;
当S位等于0时表示系统描述符,共有以下几种取值范围:
TYPE = 0, 8, 10, 13:保留字段
TYPE = 1:可用的 16 位任务状态段(TSS)描述符
TYPE = 2:LDT 的段描述符
TYPE = 3:正忙的 16 位 TSS 描述符
TYPE = 4:16 位调用门
TYPE = 5:任务门
TYPE = 6:16 位中断门
TYPE = 7:16 位陷阱门
TYPE = 9:可用的 32 位 TSS 描述符
TYPE = 11:正忙的 32 位 TSS 描述符
TYPE = 12:32 位调用门
TYPE = 14:32 位中断门
TYPE = 15:32 位陷阱门
PIC芯片
使用中断的原因是,如果不使用中断,那么CPU就得轮询各个外部设备然后进行处理,这样做的话会占用大量无意义的资源,因为外部设备有不一定无时无刻都在发消息。而PIC芯片就是为了中断而设计的,一般主板会有两张PIC芯片,主PIC芯片与CPU相连而从PIC芯片与主PIC芯片相连。每个PIC芯片可以处理8个中断信号,当有中断信号发生时,就将主PIC芯片的输出管脚信号变成ON并通知给CPU,这时候CPU就知道有中断信号了,CPU就可以去处理中断相关的代码了。因为从芯片并没有直接与CPU相连,所以从芯片有中断时先通知给主芯片,主芯片收到之后再将管脚信号设置为ON并通知CPU。芯片连接如下图所示:
IRQ的全拼为Interupt request。
想要使用中断的前提是先得初始化GDT和IDT,之后需要初始化PIC芯片,初始化PIC芯片的方式和我们之前初始化色盘的方式差不多,只有相指定端口设置指定的值即可。
ICW1: 设置8259A芯片的工作方式,例如x86系统,是否级联,触发方式等等,我们系统又两片8259A芯片肯定是需要级联的,由于实模式的限制太搭,所以x86模式也是需要的。
ICW2: 起始终端向量,我们起始终端向量是从0x20开始,0x20前面全部都是BIOS已经定义好的,0x20以及以后都是我们自定义的中断,所以前四位是0x20,
在8259A芯片中,ICW2(Initialization Command Word 2)用于设置中断向量的起始地址123。ICW2的高5位用来确定中断向量码的范围,低3位对应IRi的特点123。
例如,如果ICW2=08H,那么IR0~IR7请求对应的中断类型码分别为:08H、09H、0AH、0BH、0CH、0DH、0EH、0FH1。
ICW3用于配置主从PIC芯片的连接关系,例如如果需要主芯片使用IRQ1来与从芯片连接那么此时ICW3位100。对于从片来说则是设置当前芯片是与主片的第几号IRQ连接的。
ICW4则是设置中断触发方式和数据连接方式,级联主从模式是不能设置缓冲模式的,所以M/S位和BUF全为0,我们的系统肯定是x86架构的所以这一位肯定是0,全嵌套模式中按IRQ的顺序来设置优先级的,如果有高级中断则优先执行高级中断,低级中断则会被挂起IRQ0优先级最高; 而特殊全嵌套模式则是所用都是同一优先级的。一般使用是全嵌套模式,因为这样就可以保证最紧急的中断优先执行。手动结束中断和自动结束中断的区别是,自动中断适用于单片8259A芯片,因为在自动模式下只能一次完了再处理下一次中断,而手动模式则可以自由设置中断的优先级,手动模式要比自动模式更为灵活。
1
其中ICW1和ICW4是固定这样配置的,因为涉及到了电气特性,不这样配置的化主板可能会发生损坏。
IMR为中断屏蔽寄存器,在初始化PIC前需要屏蔽所有中断,避免中间有中断打断我们的初始化,在初始化完成后开启中断,IMR共有8位,每位对应着一个IRQ,当该位为1时就表示屏蔽该位所对应的中断。
void init_pic() {
// IMR为中断屏蔽寄存器,该寄存器有8位,分别对应每一路IRQ信号,如果值为1,则拼屏蔽对应的信号
io_out8(PIC0_IMR, 0xff);
io_out8(PIC1_IMR, 0xff);
io_out8(PIC0_ICW1, 0x11);
// 0x20 - 0x24对应着PIC0的IRQ0-IRQ7的中断
io_out8(PIC0_ICW2, 0x20);
// 100也就是PIC0的第三位IRQ用于连接从PIC
io_out8(PIC0_ICW3, 1 << 2);
io_out8(PIC0_ICW4, 0x01);
io_out8(PIC1_ICW1, 0x11);
// 0x28 - 0x30 对应着PIC0的IRQ0-IRQ7的中断
io_out8(PIC1_ICW2, 0x28);
// 0x2当前PIC芯片与主PIC芯片的哪个IRQ连接,0x02也就是PIC0的第三个IRQ
io_out8(PIC1_ICW3, 0x2);
io_out8(PIC1_ICW4, 0x01);
// 这里又禁止了一次中断的原因是因为在初始化PIC芯片的过程中会重置中断向量参数所以我们需要再设置一次中断寄存器来保证我们设置的是正确的
// PIC0用于连接从PIC,io_out8(PIC0_IMR, 0xfb)表示PIC0除了IRQ2以外,全部禁止中断
io_out8(PIC0_IMR, 0xfb);
// PIC1全部禁止中断
io_out8(PIC1_IMR, 0xff);
}
如果又中断发生时并且CPU可以执行中断,那么CPU会命令PIC发送2个字节的数据到CPU中,假如0xcd代表INT指令,那么PIC会发送0xcd 0x??到CPU中,0x??表示我们之前设置IRQ中断号,如果是PIC0的IRQ0,那么就会发送0xcd 0x20到CPU中,CPU解析时会解析执行int 0x20,这样就可以执行我们的中断程序了
8259A各IRQ中断设备如下图所示
中断处理程序
这个中断处理程序没什么好说的,就是在屏幕中打印一串字符串,0x1f以及0x1f以前的全都是BIOS中断,我们不可以进行覆盖,0x20是时钟中断, 0x21是键盘中断。
void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
struct Boot_info *binfo = (struct Boot_info *) 0x0ff0;
boxfill8(binfo->vram, binfo->scrnx, 0x07, 0, 0, 32 * 8 - 1, 15);
printFont8_ascii(binfo->vram, binfo->scrnx, 10, 10, 0x07, "INT 21 (IRQ-1) : PS/2 keyboard");
for (;;) {
io_hlt();
}
}
中断程序代码执行完毕需要执行iretd指令进行返回,我们的c语言函数没办法执行这个指令,所以得曲线救国以下,让c语言调用我们的中断程序,之后子啊执行iretd指令。
EXTERN表示把c语言函数导入到汇编中,之后再使用call命令调用我们的函数即可。
在调用_asm_inthandler21前先将寄存器压栈,避免在调用的过程中寄存器混乱,在调用完成之后出栈恢复之前的寄存器。
EXTERN _inthandler21
_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的格式如下图所示:
Selector: 高13位代表GDT或IDT段号,倒数2位代表中断门特权级,也是0最高权限,3最低权限,最后一位表示引用的段类型如果等于0,那么选择子指向GDT;如果等于1,那么选择子指向LDT。
offset: 中断或异常处理函数的入口地址。
P: 中断门有效位,1为有效,CPU可以正常使用,0为无效,CPU可以忽略掉。
DPL: 特权级,这个位和GDT一样,分为4个特权级, 0 - 3, 0权限为最高,3为最低。
GATE TYPE: 门类型,分为中断门、陷阱门和任务门,中断门用于程序中断的0x06: 16 位中断门;0x0E: 32 位中断门;0x0F: 64 位中断门。陷阱门与中断门也类似,但是陷阱门中断完成后不会清除IF寄存器的数据的0x07: 16位陷阱门;0x0F: 32 位陷阱门;0x0F: 64 位陷阱门。任务门: 用于实现多任务的切换.0x05: 16 位任务门; 0x0D: 32 位任务门。
之后在设置以下IDT即可,我们的键盘中断是21号中断,第一个参数和第二个参数都很好理解,分别是idt表项和中断函数的地址,函数3则是设置IDT selector, 我们的bootpack是加载到GDT段2的,所以这里是selector前两位是10,键盘肯定权限最高了,所以低两位为00,第3位也是0,因为引用的是GDT段,如果引用的是IDT则是1。10000用二进制来表示等于16; 第四个参数是段属性,P位是有效位肯定是1,DPL是特权级,肯定最高特权00,下一位根据上图示也是0,Gate Type,我们调用的是32位中断门,所以取值为0x0e也就是1110,合起来等于10001110也就是0x8e。
set_gated_descriptor(idt + 0x21, (int) asm_inthandler21, 2 * 8, 0x008e);
在初始化完PIC之后需要调用sti指令和IMR指令来允许系统进行中断操作,如下图所示
void HariMain(void)
{
struct Boot_info *boot_info = (struct Boot_info *)0x0ff0;
init_gdt_idt();
init_pic();
io_sti();
init_palette();
init_screen(boot_info->vram, boot_info->scrnx, boot_info->scrny);
char mour[256];
int mx = (boot_info->scrnx - 16) / 2; /* 计算画面的中心坐标*/
int my = (boot_info->scrny - 28 - 16) / 2;
init_mouse_cursor8(mour, 0x0f);
io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */
io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */
for (;;) {
io_hlt();
}
}
运行程序随便敲一个字符则可以看到屏幕有东西输出了,这就代表我们中断设置的没什么问题了。
在bochs可以看到我们设置的第0x21号中断
知道了idt的值就可以知道我们的中断程序的起始地址了,L.Address表示我们中断的起始地址, 那就是0x0010:0x0093,0x0010表示段selector,转成二进制是00010000,只看高5位就可以知道GDT段是哪个了,0x0010是2表示GDT第二个段,查看GDT表项就可以看到第二个GDT刚好是bootpack装载的地址
x86寻址是段基址 + 段内偏移
0x280000 + 0x893 = 0x280893 就可以找到物理地址,可以看到与我们的键盘处理程序汇编代码一毛一样
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律