Day06
今天进一步讲解了操作系统如何初始化各种中断信息,以及一些其它小技巧。
为了醒目起见,还是把这些技巧都放在开头吧。
一些感想和问题
源文件分割
为什么要把代码放到不同的c文件里?个人感觉最主要的两点是
- 模块化设计
- 节省单个文件修改后的编译时间(对于大工程比较有用)
当然,注意分成多个文件之后,涉及到的链接问题、依赖关系等等。
makefile的一般规则
%.gas: %.c Makefile
$(CC1) -o $*.gas $*.c
makefile 有许许多多奇怪的符号。。从网上找一些,先记一个$*
$@ # 表示目标文件
$^ # 表示所有的依赖文件
$< # 表示第一个依赖文件
$? # 表示比目标还要新的依赖文件列表
$* # 表示目标模式中 % 及其之前的部分
头文件的使用
头文件,就是只包含函数声明,以及结构体定义、宏定义等等。这可以说是最简单的接口思想:方便别人直接使用头文件,也方便修改某些参数,例如define值。
GDT与IDT
继续昨天的,首先看GDT
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
GDTR是一个48位的寄存器,低16位存放段上限(GDT的大小),高32位存放GDT的开始地址。
IDTR也是类似
_load_idtr: ; void load_idtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LIDT [ESP+6]
RET
GDTR存放了每段内存的设定,接下来是对于每段的设定
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;
}
地址为 low(2B) + mid(1B) + high(1B) = 4B
上限(大小)共20bit, 当Gbit为1时,表示1page(4KB) * 1M = 4GB
注意,limit_high 的高4位存放了段属性,要和access_right连用组成12位的段属性。
(果然是非常努力地挤到了8字节里)
然后是段属性的一些权限(低8位):
0x00: 未使用的记录表
0x92: 系统专用,可读写的段,不可执行
0x9a: 系统专用,可执行的段,可读不可写
0xf2: 应用程序用,可读写的段,不可执行
0xfa: 应用程序用,可执行的段,可读不可写
分成两种模式当然是为了保护操作系统及硬件,不多说了。
高4位为扩展访问权,构成是 GD00
G即为刚才的Gbit,D为0是16位模式,1是32位模式。
PIC
PIC(programmable interrupt controller),连接方法如下图(已经这么连了):
这样,CPU就可以处理高达15个外部中断。
然后看一下PIC的初始化(具体端口号码在下面)
void init_pic()
{
io_out8(PIC0_IMR, 0xff); //禁止所有中断
io_out8(PIC1_IMR, 0xff); // 禁止所有中断
io_out8(PIC0_ICW1, 0x11); // 边沿触发模式
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); // 边沿触发模式
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 禁止所有中断
}
IMR(interrupt message register) 中断屏蔽寄存器,当某一位为1是屏蔽对应管脚上的中断请求。
ICW(initial control word) 初始化控制数据:
ICW1和ICW4与PIC主板配线方式、中断信号和电气特性有关。
ICW3是有关主-从连接设定,主设备对应位为1指明第几个IRQ与从PIC相连,从设备指明连向主设备的第几个IRQ。
ICW2指明IRQ的功能号,因此我们可以用0x20-0x2f来对应自己的中断程序。
具体原因是,当我们发送一个INT 0x10时,会和PIC传送的2位数据连接起来,形成例如0xcd 0x10之类的指令,这样就能对应执行中断程序了。
之后,我们在IDT里对应位置设置对应的中断服务程序,就能做到我们想要的中断了。
PS:0x00-0x1f为内部中断
中断的保存现场和恢复现场
在汇编层面看一下:
_asm_inthandler27:
PUSH ES ; 保存对应寄存器到栈里
PUSH DS
PUSHAD ; 保存所有寄存器的值到栈里
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler27
POP EAX
POPAD
POP DS
POP ES
IRETD ; 中断的return指令
POP就不解释了,这样一看就明白保护现场和恢复现场,究竟做了什么事情了。
头文件不需要保护符?
在分割头文件的时候,我们经常会习惯性的加上
#ifndef XXX_H
#define XXX_H
#endif
这样的东西,好处自然不用说。不过目前作者还没有加,毕竟每个c文件都精准地只include了一次头文件。。但是还是感觉这样做有点危险)。
好多干货。。进入正题
分割源文件
harib03a
emmm已经没什么好讲的了,代码也没什么太多要解释的。。
整理makefile
harib03b
补充一下,普通规则优先级大于一般规则。
整理头文件
harib03c
也说过啦,现在文件看起来就挺清爽了。
初始化PIC
harib03d
端口号码放这里
#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
剩下的之前已经说过了。
中断处理程序的制作
harib03e
我们先制作中断服务程序
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);
putfont8_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);
putfont8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 2C (IRQ-12) : PS/2 mouse");
for (;;) {
io_hlt();
}
}
void inthandler27(int *esp)
/* PIC0からの不完全割り込み対策 */
/* Athlon64X2機などではチップセットの都合によりPICの初期化時にこの割り込みが1度だけおこる */
/* この割り込み処理関数は、その割り込みに対して何もしないでやり過ごす */
/* なぜ何もしなくていいの?
→ この割り込みはPIC初期化時の電気的なノイズによって発生したものなので、
まじめに何か処理してやる必要がない。 */
{
io_out8(PIC0_OCW2, 0x67); /* IRQ-07受付完了をPICに通知(7-1参照) */
return;
}
emmm这个7号中断,在PIC初始化的时候会用到,就先放着吧。
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
_asm_inthandler27:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler27
POP EAX
POPAD
POP DS
POP ES
IRETD
_asm_inthandler2c:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler2c
POP EAX
POPAD
POP DS
POP ES
IRETD
在调用外部函数时,需要开头加上EXTERN声明,调用时使用CALL。
汇编和c语言都写好之后,我们需要在IDT里注册这些函数。
dsctbl.c部分
void init_gdtidt()
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *)ADR_GDT;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *)ADR_IDT;
int i;
// init gdt
for(i = 0; i < LIMIT_GDT / 8; ++i)
{
set_segmdesc(gdt + i, 0, 0, 0);
}
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
load_gdtr(LIMIT_GDT, ADR_GDT);
// init idt
for(i = 0; i < LIMIT_IDT / 8; ++i)
{
set_gatedesc(idt + i, 0, 0, 0);
}
load_idtr(LIMIT_IDT, ADR_IDT);
// set idt
// 其中,2表示函数所在段号为2(根据上面可以看到即bootpack.hrb所在段),低3位有其它意思故需左移。
set_gatedesc(idt + 0x21, (int)asm_inthandler21, 2 << 3, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int)asm_inthandler27, 2 << 3, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int)asm_inthandler2c, 2 << 3, AR_INTGATE32);
}
最后几行即为中断程序的注册,AR_INTGATE32 为 0x008e,用于中断处理的有效设定。
最后是Harimain的改动
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *)ADR_BOOTINFO;
extern char hankaku[4096];
init_gdtidt();
init_pic();
io_sti();
init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 321");
putfont8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
putfont8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
char s[105];
sprintf(s, "scrnx = %d", binfo->scrnx);
putfont8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
int mx = (binfo->scrnx - 16) / 2; /* 使得鼠标坐标在中央 */
int my = (binfo->scrny - 28 - 16) / 2;
char mcursor[256];
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
io_out8(PIC0_IMR, 0xf9);
io_out8(PIC1_IMR, 0xef);
for(;;)
{
io_hlt(); /* 执行naskfunc.nas的_io_hlt */
}
}
改动为:
第8行:执行STI指令(CLI逆指令),使CPU接受来自外部设备的中断。
第26-27行:修改PIC的IMR,使之能接受来自键盘和鼠标的中断(对应为置0)
在这么多知识之后,我们终于了解如何设定中断了,明天继续。