Day06

今天进一步讲解了操作系统如何初始化各种中断信息,以及一些其它小技巧。

为了醒目起见,还是把这些技巧都放在开头吧。

一些感想和问题

源文件分割

为什么要把代码放到不同的c文件里?个人感觉最主要的两点是

  1. 模块化设计
  2. 节省单个文件修改后的编译时间(对于大工程比较有用)

当然,注意分成多个文件之后,涉及到的链接问题、依赖关系等等。

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),连接方法如下图(已经这么连了):

PIC
这样,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)

在这么多知识之后,我们终于了解如何设定中断了,明天继续。