《30天自制操作系统》06_day_学习笔记

时间:2021-09-12 08:09:33

harib03a:
  内容没有变化 ;P109 从这里开始,代码开始工程化了.
  将原本300多行的bootpack.c分割成了三部分:
        graphic.c      : 用来处理界面图像
        dsctbl.c        : 用来处理中断和段表(GDT,IDT)等
        bootpack.c    : 和后面的bootpack.h文件一起用来封装函数
  修改了Makefile中的文件生成步骤(当然后面需要bootpack.h 头文件):
    修改前:bootpack.c-->bootpack.bim
    修改后:graphic.c  -->graphic.obj
        dsctbl.c    -->dsctbl.obj
        bootpack.c-->bootpack.obj
        graphic.obj+dsctbl.obj+bootpack.obj+其他接口信息-->bootpack.bim
harib03b:
  整理Makefile;简化了Makefile中的一些内容;
  感觉原理和DBMS中的LIKE字符串匹配神似,有木有。
  看看这个书本上的这个例子:归纳成一般规则:

bootpack.gas : bootpack.c Makefile
  $(CC1) -o bootpack.gas bootpack.c
graphic.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

harib03c:
  P111 整理头文件;这里实际上还是在做代码优化工作;为后续准备
  我们发现bootpack.c被分割之后,经常需要调用bootpack.c中的函数
  因此,来了一个头文件bootpack.h;把bootpack.c中实现的函数封装起来
  这样每次需要调用函数时只要#include一个bootpack.h 就 OK了!!

/* bootpack.h */
struct BOOTINFO { /* 0x0ff0-0x0fff */
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);
/* Cgraphic.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_screen8(char *vram, int x, int y);
void putfont8(char *vram, int xsize, int x, int y, char c, char *font);
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s);
void init_mouse_cursor8(char *mouse, char bc);

  接下来我们来看下LGDT和LIDT是怎么个情况:

  LGDT和LIDT都是分别是给48为的段寄存器GDTR和48位的中断寄存器IDTR赋值,他们的原理基本相同;下面以LGDT为例详细说明一下。

  1、段信息结构体struct SEGMENT_DESCRIPTOR (如下图所示)

    • 包括段的大小;
    • 段的起始地址;
    • 段的管理属性;

    《30天自制操作系统》06_day_学习笔记

  按照CPU的结构要求,将段信息归结成8个字节写入内存中;之后再由指令LGDT装入段寄存器GDTR中;

  注意:寄存器GDTR长度为6个字节。写入段寄存器中的信息只是短信息(8个字节)中的高位的6个字节,后面两个字节存储的是段的管理属性-access_right ;

//请对照上面图片看;
//表示段信息的结构体(一共48位)
//段地址 32位:base_low(2字节)+base_mid(1字节)+base_high(1字节)
//剩余4个字节||||我们知道段上限为4GB;一个32位的数值
//如果剩余这4个字节全部用来表示段上限那么就没有空间表示段信息了;
//HOW TO DO??
//段上限 20位:段属性中的标志位Gbit为1时,limit的单位不解释成字节,而翻译成页(1页=4KB)
// limit_low 和limit_high
//注 意:limit_high中的上4位写的是段属性
struct SEGMENT_DESCRIPTOR { //段信息结构体定义8字节
short limit_low, base_low; //2字节
char base_mid, access_right; //1字节
char limit_high, base_high; //1字节
};
struct GATE_DESCRIPTOR { //中断信息结构体定义8字节
short offset_low, selector;
char dw_count, access_right;
short offset_high;
};
/*将段信息的8个字节写入内存;便于段寄存器读取;
;struct SEGMENT_DESCRIPTOR 在头文件中;
;段大小;;段起始地址;;段的管理属性(写入,执行,系统专用等) */
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 >> ) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> ) & 0x0f) | ((ar >> ) & 0xf0);
sd->base_high = (base >> ) & 0xff;
return;
} void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> ) & 0xff;
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> ) & 0xffff;
return;
}

  2、LGDT和LIDT的具体实现

    《30天自制操作系统》06_day_学习笔记

;LGDT给48位寄存器GDTR赋值;
;方法:从指定地址的高6个字节;
;底16位为段上限;高32位为GDT表开始的地址
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+] ; limit
MOV [ESP+],AX
LGDT [ESP+]
RET
;LIDT赋值,原理和上面基本相同
_load_idtr: ; void load_idtr(int limit, int addr);
MOV AX,[ESP+] ; limit
MOV [ESP+],AX
LIDT [ESP+]
RET

  3、段管理属性ar(limit_high的高四位+access_right)

    •   在段信息的结构体中,我们可以看到access_right定义类型为char,一个字节(8位);
    •   段管理属性是有两部分组成的:段上限(大小)的高四位(也就是limit_high的高四位)+access_right(8位) = 12位
    •   ar的高四位是“拓展访问权限”
    •   ar的底8位是决定系统模式和应用模式的(有用户进程和系统进程的感觉,有木有)

 harib03d:

  请对照书P117的图看  初始化PIC(programmable interrupt controller-可编程中断控制器),顾名思义:控制中断的东西

  CPU单独只能处理一个中断,因此需要辅助芯片来帮助CPU处理不同的中断信号;

  主PIC(PIC0)和CPU相连;从PIC( PIC1 )和主PIC的IRQ2相连;扩展了16个中断口

  PIC芯片的主要寄存器:

    IMR:中断屏蔽寄存器;8位,对应8路IRQ信号;信号1屏蔽该路中段;

    ICW:初始化控制数据;一共有4个寄存器

    •  ICW1:定值,由主板配线方式决定;
    •  ICW4:定值,由主板配线方式决定;
    •  ICW3:定值,主-从CPI的连接设定;(以上三个都由芯片本身的属性决定)
    •  ICW2:16位,对应从IRQ0-IRQ15;决定PIC芯片发送哪一号中断通知CPU。
/* int.c */
#include "bootpack.h"
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, << ); /* 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, ); /* PIC1由IRQ2连接 */
io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */ io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外的全部禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */
return;
}

 harib03e:

  设置键盘和鼠标中断;笔者将键盘中断设在了PIC的IRQ12,将鼠标设在了IRQ1;

   调用的终端号分别为0x2c(0010 1100) 和 0x21(0010 0001)   可以对照P117的PIC芯片图看。

void inthandler21(int *esp)
/* PS/2的键盘中断 */
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; //加载BIOS
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, , , * - , );
//获取键盘中断之后,显示这个字符串INT 21 (IRQ-1) : PS/2 keyboard
putfonts8_asc(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
for (;;) { //之后 让CPU等着 !!
io_hlt();
}
}

  下面介绍了键盘中断函数的具体实现的汇编代码;

  笔者在这里介绍了堆栈的一些知识,这段汇编代码就比较简单了:

_asm_inthandler21:
PUSH ES ;关键寄存器压栈,函数必做
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS ;将DS和ES调整到与SS相等
MOV DS,AX
MOV ES,AX
CALL _inthandler21 ;调用函数_asm_inthandler21
POP EAX ;返回后,将寄存器回复原来
POPAD
POP DS
POP ES
IRETD ;最后执行IRETD

  我们已经编写了键盘的响应函void inthandler21(int *esp);也理解了该函数的汇编具体在内存中怎么做的_asm_inthandler21:

  接下来就要把键盘中断号写到中断表中,这样当执行INT时,就可以调用相应的中断程序了(在这里是函数void inthandler21(int *esp))

    /* IDT设定; */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, * , AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, * , AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, * , AR_INTGATE32);

  差不多了,make run 以后,只要敲了键盘,就会相应0x21中断,接着CPU会在中断表中找到这个中断程序(在这里是函数void inthandler21(int *esp))的入口了。于是就会执行这个中断程序,而这个程序的内容就是输出字符串

    INT 21 (IRQ-1) : PS/2 keyboard