【自制OS学习笔记| Day 10】更更好的鼠标、窗口、高速计数器和图层优化

更更好的鼠标

  • 完成了昨天的画板后,出现了这样的问题:

  • 正常来说鼠标是可以隐藏在屏幕右边和下边的,但是画板似乎并不能这样……
  • 所以需要完善画面外支持。

画面外支持

  • 修改main:

if (mx > binfo->scrnx - 16) {
    mx = binfo->scrnx - 16;
}
if (my > binfo->scrny - 16) {
    my = binfo->scrny - 16;
}

改成


if (mx > binfo->scrnx - 1) {
    mx = binfo->scrnx - 1;
}
if (my > binfo->scrny - 1) {
    my = binfo->scrny - 1;
}
  • 并且不刷新屏幕外的显示区域:

void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1){
	int h, bx, by, vx, vy, bx0, by0, bx1, by1;
	unsigned char *buf, c, *vram = ctl->vram;
	struct SHEET *sht;
	/* refresh超出画面范围,进行修正*/
	if (vx0 < 0) { vx0 = 0; }
	if (vy0 < 0) { vy0 = 0; }
	if (vx1 > ctl->xsize) { vx1 = ctl->xsize; }
	if (vy1 > ctl->ysize) { vy1 = ctl->ysize; }
	for (h = 0; h <= ctl->top; h++) {
        // 中略
	}
	return;
}

窗口

  • 已经完成窗口的叠加处理了,所以现在只需要把窗口画出来。(和画鼠标一样一样的)

void make_window8(unsigned char *buf, int xsize, int ysize, char *title){
	static char closebtn[14][16] = {
		"OOOOOOOOOOOOOOO@",
		"OQQQQQQQQQQQQQ$@",
		"OQQQQQQQQQQQQQ$@",
		"OQQQ@@QQQQ@@QQ$@",
		"OQQQQ@@QQ@@QQQ$@",
		"OQQQQQ@@@@QQQQ$@",
		"OQQQQQQ@@QQQQQ$@",
		"OQQQQQ@@@@QQQQ$@",
		"OQQQQ@@QQ@@QQQ$@",
		"OQQQ@@QQQQ@@QQ$@",
		"OQQQQQQQQQQQQQ$@",
		"OQQQQQQQQQQQQQ$@",
		"O$$$$$$$$$$$$$$@",
		"@@@@@@@@@@@@@@@@"
	};
	int x, y;
	char c;
	boxfill8(buf, xsize, COL8_C6C6C6, 0,         0,         xsize - 1, 0        );
	boxfill8(buf, xsize, COL8_FFFFFF, 1,         1,         xsize - 2, 1        );
	boxfill8(buf, xsize, COL8_C6C6C6, 0,         0,         0,         ysize - 1);
	boxfill8(buf, xsize, COL8_FFFFFF, 1,         1,         1,         ysize - 2);
	boxfill8(buf, xsize, COL8_848484, xsize - 2, 1,         xsize - 2, ysize - 2);
	boxfill8(buf, xsize, COL8_000000, xsize - 1, 0,         xsize - 1, ysize - 1);
	boxfill8(buf, xsize, COL8_C6C6C6, 2,         2,         xsize - 3, ysize - 3);
	boxfill8(buf, xsize, COL8_000084, 3,         3,         xsize - 4, 20       );
	boxfill8(buf, xsize, COL8_848484, 1,         ysize - 2, xsize - 2, ysize - 2);
	boxfill8(buf, xsize, COL8_000000, 0,         ysize - 1, xsize - 1, ysize - 1);
	putfonts8_asc(buf, xsize, 24, 4, COL8_FFFFFF, title);
	for (y = 0; y < 14; y++) {
		for (x = 0; x < 16; x++) {
			c = closebtn[y][x];
			if (c == '@') {
				c = COL8_000000;
			} else if (c == '$') {
				c = COL8_848484;
			} else if (c == 'Q') {
				c = COL8_C6C6C6;
			} else {
				c = COL8_FFFFFF;
			}
			buf[(5 + y) * xsize + (xsize - 21 + x)] = c;
		}
	}
	return;
}
  • 继续在main中添加窗口

// 上略
	struct SHEET *sht_back, *sht_mouse, *sht_win;
	unsigned char *buf_back, buf_mouse[256], *buf_win;
// 中略
	init_palette();
	shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
	sht_back  = sheet_alloc(shtctl);
	sht_mouse = sheet_alloc(shtctl);
	sht_win   = sheet_alloc(shtctl);
	buf_back  = (unsigned char *) memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
	buf_win   = (unsigned char *) memman_alloc_4k(memman, 160 * 68);
	sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1); /* 没有透明色*/
	sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99);
	sheet_setbuf(sht_win, buf_win, 160, 68, -1); /* 没有透明色*/
	init_screen8(buf_back, binfo->scrnx, binfo->scrny);
	init_mouse_cursor8(buf_mouse, 99);
	make_window8(buf_win, 160, 68, "window");
	putfonts8_asc(buf_win, 160, 24, 28, COL8_000000, "Welcome to");
	putfonts8_asc(buf_win, 160, 24, 44, COL8_000000, "  Haribote-OS!");
	sheet_slide(sht_back, 0, 0);
	mx = (binfo->scrnx - 16) / 2; /* 计算画面中央位置*/
	my = (binfo->scrny - 28 - 16) / 2;
	sheet_slide(sht_mouse, mx, my);
	sheet_slide(sht_win, 80, 72);
	sheet_updown(sht_back,  0);
	sheet_updown(sht_win,   1);
	sheet_updown(sht_mouse, 2);
	sprintf(s, "(%3d, %3d)", mx, my);
	putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
	sprintf(s, "memory %dMB   free : %dKB",
			memtotal / (1024 * 1024), memman_total(memman) / 1024);
	putfonts8_asc(buf_back, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
	sheet_refresh(sht_back, 0, 0, binfo->scrnx, 48);
// 下略

Haribote-OS是原作者起的名字,没有更改

高速计数器

  • 为了练习使用窗口,写一个高速计数器(顺便引出下面的内容)

void HariMain(void){
	struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
	char s[40], keybuf[32], mousebuf[128];
	int mx, my, i;
	unsigned int memtotal, count = 0;
	struct MOUSE_DEC mdec;
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
	struct SHTCTL *shtctl;
	struct SHEET *sht_back, *sht_mouse, *sht_win;
	unsigned char *buf_back, buf_mouse[256], *buf_win;
  
// 中略
    // 前面创建窗口的方法类似
    init_palette();
	shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
	sht_back  = sheet_alloc(shtctl);
	sht_mouse = sheet_alloc(shtctl);
	sht_win   = sheet_alloc(shtctl);
	buf_back  = (unsigned char *) memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
	buf_win   = (unsigned char *) memman_alloc_4k(memman, 160 * 52);
	sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1);
	sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99);
	sheet_setbuf(sht_win, buf_win, 160, 52, -1);
	init_screen8(buf_back, binfo->scrnx, binfo->scrny);
	init_mouse_cursor8(buf_mouse, 99);
	make_window8(buf_win, 160, 52, "counter");
	sheet_slide(sht_back, 0, 0);
	mx = (binfo->scrnx - 16) / 2;
	my = (binfo->scrny - 28 - 16) / 2;
	sheet_slide(sht_mouse, mx, my);
	sheet_slide(sht_win, 80, 72);
	sheet_updown(sht_back,  0);
	sheet_updown(sht_win,   1);
	sheet_updown(sht_mouse, 2);
	sprintf(s, "(%3d, %3d)", mx, my);
	putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
	sprintf(s, "memory %dMB   free : %dKB",
			memtotal / (1024 * 1024), memman_total(memman) / 1024);
	putfonts8_asc(buf_back, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
	sheet_refresh(sht_back, 0, 0, binfo->scrnx, 48);
  
    for (;;) {
        // 在这里进行计数
		count++;
		sprintf(s, "%010d", count);
		boxfill8(buf_win, 160, COL8_C6C6C6, 40, 28, 119, 43);
		putfonts8_asc(buf_win, 160, 40, 28, COL8_000000, s);
		sheet_refresh(sht_win, 40, 28, 120, 44);

		io_cli();
		if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
			io_sti();
		} else {
// 下略
  • 刷新的问题:

    • 在刷新的时候,总是先刷新范围内的背景图层,再刷新窗口图层,肯定会发生闪烁。

处理闪烁&优化窗口管理

  • 可以认为,在一个窗口的内容变化的时候,其他窗口没有变化,依此进行刷新(因为如果别的窗口也变化的话,可以对变化的窗口依次进行这样的刷新)。
  • 因此,背景层不用刷新。
  • 但是,鼠标需要刷新(窗口的刷新会覆盖鼠标)。
  • 闪烁的实质是渲染和消除的交替。
  • 因此,可以用map的方法进行处理。
  • 另外,为了方便,SHEET本身也记录了管理它的SHTCTL的指针。

struct SHTCTL {
	unsigned char *vram, *map;		// 用一个map来记录
	int xsize, ysize, top;
	struct SHEET *sheets[MAX_SHEETS];
	struct SHEET sheets0[MAX_SHEETS];
};

struct SHTCTL *shtctl_init(struct MEMMAN *memman, unsigned char *vram, int xsize, int ysize){
	struct SHTCTL *ctl;
	int i;
	ctl = (struct SHTCTL *) memman_alloc_4k(memman, sizeof (struct SHTCTL));
	if (ctl == 0) {
		goto err;
	}
	ctl->map = (unsigned char *) memman_alloc_4k(memman, xsize * ysize);	// 初始化map
	if (ctl->map == 0) {
		memman_free_4k(memman, (int) ctl, sizeof (struct SHTCTL));
		goto err;
	}
	ctl->vram = vram;
	ctl->xsize = xsize;
	ctl->ysize = ysize;
	ctl->top = -1; /* 没有SHEET*/
	for (i = 0; i < MAX_SHEETS; i++) {
		ctl->sheets0[i].flags = 0; /* 未使用*/
		ctl->sheets0[i].ctl = ctl; /* 记录归属的SHECTL*/
	}
err:
	return ctl;
}

  • 刷新地图:

void sheet_refreshmap(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1, int h0){
	int h, bx, by, vx, vy, bx0, by0, bx1, by1;
	unsigned char *buf, sid, *map = ctl->map;
	struct SHEET *sht;
	if (vx0 < 0) { vx0 = 0; }
	if (vy0 < 0) { vy0 = 0; }
	if (vx1 > ctl->xsize) { vx1 = ctl->xsize; }
	if (vy1 > ctl->ysize) { vy1 = ctl->ysize; }
	for (h = h0; h <= ctl->top; h++) {
		sht = ctl->sheets[h];
		sid = sht - ctl->sheets0; /* 将进行了减法计算的地址作为图层号码(sheet ID)*/
		buf = sht->buf;
		bx0 = vx0 - sht->vx0;
		by0 = vy0 - sht->vy0;
		bx1 = vx1 - sht->vx0;
		by1 = vy1 - sht->vy0;
		if (bx0 < 0) { bx0 = 0; }
		if (by0 < 0) { by0 = 0; }
		if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }
		if (by1 > sht->bysize) { by1 = sht->bysize; }
		for (by = by0; by < by1; by++) {
			vy = sht->vy0 + by;
			for (bx = bx0; bx < bx1; bx++) {
				vx = sht->vx0 + bx;
				if (buf[by * sht->bxsize + bx] != sht->col_inv) {
					map[vy * ctl->xsize + vx] = sid;
				}
			}
		}
	}
	return;
}
  • 利用map刷新图层

void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1, int h0, int h1){
	int h, bx, by, vx, vy, bx0, by0, bx1, by1;
	unsigned char *buf, *vram = ctl->vram, *map = ctl->map, sid;
	struct SHEET *sht;
	/* refresh范围超出屏幕,修正*/
	if (vx0 < 0) { vx0 = 0; }
	if (vy0 < 0) { vy0 = 0; }
	if (vx1 > ctl->xsize) { vx1 = ctl->xsize; }
	if (vy1 > ctl->ysize) { vy1 = ctl->ysize; }
	for (h = h0; h <= h1; h++) {
		sht = ctl->sheets[h];
		buf = sht->buf;
		sid = sht - ctl->sheets0;
		/* 计算*/
		bx0 = vx0 - sht->vx0;
		by0 = vy0 - sht->vy0;
		bx1 = vx1 - sht->vx0;
		by1 = vy1 - sht->vy0;
		if (bx0 < 0) { bx0 = 0; }
		if (by0 < 0) { by0 = 0; }
		if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }
		if (by1 > sht->bysize) { by1 = sht->bysize; }
		for (by = by0; by < by1; by++) {
			vy = sht->vy0 + by;
			for (bx = bx0; bx < bx1; bx++) {
				vx = sht->vx0 + bx;
				if (map[vy * ctl->xsize + vx] == sid) {
					vram[vy * ctl->xsize + vx] = buf[by * sht->bxsize + bx];
				}
			}
		}
	}
	return;
}
  • 调用刷新

void sheet_refresh(struct SHEET *sht, int bx0, int by0, int bx1, int by1){
	if (sht->height >= 0) { /* 只对正在显示的图层进行刷新*/
		sheet_refreshsub(sht->ctl, sht->vx0 + bx0, sht->vy0 + by0, sht->vx0 + bx1, sht->vy0 + by1, sht->height, sht->height);
	}
	return;
}

void sheet_slide(struct SHEET *sht, int vx0, int vy0)
{
	struct SHTCTL *ctl = sht->ctl;
	int old_vx0 = sht->vx0, old_vy0 = sht->vy0;
	sht->vx0 = vx0;
	sht->vy0 = vy0;
	if (sht->height >= 0) { /* 只对正在显示的图层进行刷新*/
		sheet_refreshmap(ctl, old_vx0, old_vy0, old_vx0 + sht->bxsize, old_vy0 + sht->bysize, 0);
		sheet_refreshmap(ctl, vx0, vy0, vx0 + sht->bxsize, vy0 + sht->bysize, sht->height);
		sheet_refreshsub(ctl, old_vx0, old_vy0, old_vx0 + sht->bxsize, old_vy0 + sht->bysize, 0, sht->height - 1);
		sheet_refreshsub(ctl, vx0, vy0, vx0 + sht->bxsize, vy0 + sht->bysize, sht->height, sht->height);
	}
	return;
}
  • 最后记得在调整图层高度的时候记得刷新map

void sheet_updown(struct SHEET *sht, int height){
//中略

	/* 重新排列图层*/
	if (old > height) {	/* 比之前的低*/
		if (height >= 0) {
			/* 提高中间的图层*/
            // 中略
			sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height + 1);			// 由于之前对图层信息进行修改使之可以记录SHTCTL,所以不再需要单独传SHTCTL
			sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height + 1, old);
		} else {
			// 中略
			sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, 0);
			sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, 0, old - 1);
		}
	} else if (old < height) {	/* 比之前的高*/
		if (old >= 0) {
        // 中略
		sheet_refreshmap(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height);
		sheet_refreshsub(ctl, sht->vx0, sht->vy0, sht->vx0 + sht->bxsize, sht->vy0 + sht->bysize, height, height);
	}
	return;
}

【自制OS学习笔记|Day 9】更好的内存管理、叠加处理和鼠标

更好的内存管理

  • 以4 KB为单位,进行向上取整。
  • 例如,6.7 KB取为8 KB,11.9 KB取为12 KB,8 KB取为8 KB。
  • 可以减少内存碎片。
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size){
    unsigned int a;
    size = (size + 0xfff) & 0xfffff000;         // 极其巧妙的取整方法
    a = memman_alloc(man, size);
    return a;
}

int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size){
    int i;
    size = (size + 0xfff) & 0xfffff000;
    i = memman_free(man, addr, size);
    return i;
}

叠加处理

  • 所谓叠加,就是一层层地绘制需要的图层。

  • 创建一个记录单个图层信息的结构体:
struct SHEET{
    unsigned char *buf;
    int bxsize, bysize, vx0, vy0, col_inv, height, flags;
};
  • buf用于记载画面上的内容存在什么位置。
  • bxsize,bysize记录了图层大小。
  • vx0,vy0记录了图层的坐标。
  • col_inv表示了颜色和透明度。
  • height表示图层高度。
  • flags用于存储一些信息。
  • 创建一个图层管理的结构体:

#define MAX_SHEETS 256

struct SHTCTL{
    unsigned char* vram;
    int xsize, ysize, top;
    struct SHEET *sheets[MAX_SHEETS];
    struct SHEET sheets0[MAX_SHEETS];
}
  • vram记录了VRAM的地址。
  • xsize,ysize记录了画面大小。
  • sheets0存储了图层。
  • sheets将图层按照高度排序后存储。
  • 显然这个结构体会占据好几KB(实际上大约超过了9 KB),所以可以用刚才的函数给他分配内存。
// 初始化
struct SHTCTL *shtctl_init(struct MEMMAN *memman, unsigned char *vram, int xsize, int ysize){
    struct SHTCTL *ctl;
    int i;
    ctl = (struct SHTCTL *) memman_alloc_4k(memman, sizeof (struct SHTCTL));
    if (ctl == 0) {
        goto err;
    }
    ctl->vram = vram;
    ctl->xsize = xsize;
    ctl->ysize = ysize;
    ctl->top = -1; /* 没有SHEET*/
    for (i = 0; i < MAX_SHEETS; i++) {
        ctl->sheets0[i].flags = 0; /* 标记为未使用*/
    }
err:
    return ctl;
}
// 分配
#define SHEET_USE 1

struct SHEET *sheet_alloc(struct SHTCTL *ctl){
    struct SHEET *sht;
    int i;
    for (i = 0; i < MAX_SHEETS; i++) {
        if (ctl->sheets0[i].flags == 0) {
            sht = &ctl->sheets0[i];
            sht->flags = SHEET_USE; /* 标记为正在使用*/
            sht->height = -1; /* 高度为-1,规定为未使用,故不显示*/
            return sht;
        }
    }
    return 0;   /* 所有的SHEET都处于正在使用状态*/
}
// 设定缓冲区
void sheet_setbuf(struct SHEET *sht, unsigned char *buf, int xsize, int ysize, int col_inv){
    sht->buf = buf;
    sht->bxsize = xsize;
    sht->bysize = ysize;
    sht->col_inv = col_inv;
    return;
}
// 设定底板高度
void sheet_updown(struct SHTCTL *ctl, struct SHEET *sht, int height){
    int h, old = sht->height; /* 存储设置前的信息*/

    /* 如果指定的高度不合适,就重新指定*/
    if (height > ctl->top + 1) {
        height = ctl->top + 1;
    }
    if (height < -1) {
        height = -1;
    }
    sht->height = height; /* 设定高度*/

    /* 下面是对各个画板进行重新排列*/
    if (old > height) { /* 比以前低*/
        if (height >= 0) {
            /* 把之间的画板向上提升*/
            for (h = old; h > height; h--) {
                ctl->sheets[h] = ctl->sheets[h - 1];
                ctl->sheets[h]->height = h;
            }
            ctl->sheets[height] = sht;
        } else {    /* 隐藏*/
            if (ctl->top > old) {
                /* 把上面的降下来*/
                for (h = old; h < ctl->top; h++) {
                    ctl->sheets[h] = ctl->sheets[h + 1];
                    ctl->sheets[h]->height = h;
                }
            }
            ctl->top--; /* 最上面的高度下降*/
        }
        sheet_refresh(ctl); /* 按照新的信息重新绘制*/
    } else if (old < height) {  /* 比以前高*/
        if (old >= 0) {
            /* 把之间的拉下去*/
            for (h = old; h < height; h++) {
                ctl->sheets[h] = ctl->sheets[h + 1];
                ctl->sheets[h]->height = h;
            }
            ctl->sheets[height] = sht;
        } else {    /* 由隐藏转换为显示*/
            /* 把上面的升高*/
            for (h = ctl->top; h >= height; h--) {
                ctl->sheets[h + 1] = ctl->sheets[h];
                ctl->sheets[h + 1]->height = h + 1;
            }
            ctl->sheets[height] = sht;
            ctl->top++; /* 显示的画板数增加了,最高的高度也增加了*/
        }
        sheet_refresh(ctl); /* 重新绘制*/
    }
    return;
}
// 刷新函数
void sheet_refresh(struct SHTCTL *ctl){
    int h, bx, by, vx, vy;
    unsigned char *buf, c, *vram = ctl->vram;
    struct SHEET *sht;
    for (h = 0; h <= ctl->top; h++) {           // 把画板的像素都复制到VRAM中,除了透明像素
        sht = ctl->sheets[h];
        buf = sht->buf;
        for (by = 0; by < sht->bysize; by++) {
            vy = sht->vy0 + by;
            for (bx = 0; bx < sht->bxsize; bx++) {
                vx = sht->vx0 + bx;
                c = buf[by * sht->bxsize + bx];
                if (c != sht->col_inv) {
                    vram[vy * ctl->xsize + vx] = c;
                }
            }
        }
    }
    return;
}
// 不改变高度只移动画板
void sheet_slide(struct SHTCTL *ctl, struct SHEET *sht, int vx0, int vy0){
    sht->vx0 = vx0;
    sht->vy0 = vy0;
    if (sht->height >= 0) { /* 如果正在显示*/
        sheet_refresh(ctl); /* 按新的图层信息刷新*/
    }
    return;
}
// 释放
void sheet_free(struct SHTCTL *ctl, struct SHEET *sht){
    if (sht->height >= 0) {
        sheet_updown(ctl, sht, -1); /* 先设置为隐藏*/
    }
    sht->flags = 0; /* 标记为未使用*/
    return;
}

完善鼠标

  • 看下现在的main吧:
// 前略
    struct SHTCTL *shtctl;
    struct SHEET *sht_back, *sht_mouse;
    unsigned char *buf_back, buf_mouse[256];
// 中略
    init_palette();
    shtctl = shtctl_init(memman, binfo->vram, binfo->scrnx, binfo->scrny);
    sht_back  = sheet_alloc(shtctl);
    sht_mouse = sheet_alloc(shtctl);
    buf_back  = (unsigned char *) memman_alloc_4k(memman, binfo->scrnx * binfo->scrny);
    sheet_setbuf(sht_back, buf_back, binfo->scrnx, binfo->scrny, -1); /* 没有透明色*/
    sheet_setbuf(sht_mouse, buf_mouse, 16, 16, 99); /* 透明色号99*/
    init_screen8(buf_back, binfo->scrnx, binfo->scrny);
    init_mouse_cursor8(buf_mouse, 99); /* 背景色号99*/
    sheet_slide(shtctl, sht_back, 0, 0);
    mx = (binfo->scrnx - 16) / 2; /* 按照显示在画面中间来计算坐标*/
    my = (binfo->scrny - 28 - 16) / 2;
    sheet_slide(shtctl, sht_mouse, mx, my);
    sheet_updown(shtctl, sht_back,  0);
    sheet_updown(shtctl, sht_mouse, 1);
    sprintf(s, "(%3d, %3d)", mx, my);
    putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
    sprintf(s, "memory %dMB   free : %dKB",
            memtotal / (1024 * 1024), memman_total(memman) / 1024);
    putfonts8_asc(buf_back, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
    sheet_refresh(shtctl);
// 下面是操作鼠标的部分
    for (;;) {
        io_cli();
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
            io_stihlt();
        } else {
            if (fifo8_status(&keyfifo) != 0) {
                i = fifo8_get(&keyfifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(buf_back, binfo->scrnx, COL8_008484,  0, 16, 15, 31);
                putfonts8_asc(buf_back, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
                sheet_refresh(shtctl);
            } else if (fifo8_status(&mousefifo) != 0) {
                i = fifo8_get(&mousefifo);
                io_sti();
                if (mouse_decode(&mdec, i) != 0) {
                    /* 对鼠标三个字节信息的显示*/
                    sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
                    if ((mdec.btn & 0x01) != 0) {
                        s[1] = 'L';
                    }
                    if ((mdec.btn & 0x02) != 0) {
                        s[3] = 'R';
                    }
                    if ((mdec.btn & 0x04) != 0) {
                        s[2] = 'C';
                    }
                    boxfill8(buf_back, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
                    putfonts8_asc(buf_back, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
                    /* 移动光标*/
                    mx += mdec.x;
                    my += mdec.y;
                    if (mx < 0) {
                        mx = 0;
                    }
                    if (my < 0) {
                        my = 0;
                    }
                    if (mx > binfo->scrnx - 16) {
                        mx = binfo->scrnx - 16;
                    }
                    if (my > binfo->scrny - 16) {
                        my = binfo->scrny - 16;
                    }
                    sprintf(s, "(%3d, %3d)", mx, my);
                    boxfill8(buf_back, binfo->scrnx, COL8_008484, 0, 0, 79, 15);
                    putfonts8_asc(buf_back, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
                    sheet_slide(shtctl, sht_mouse, mx, my); /* 刷新画面*/
                }
            }
        }
    }

优化叠加处理

  • 之前的刷新,是要在画面变化的时候把整个屏幕都刷新。
  • 而实际上鼠标并不会占用很多的面积,所以上面的操作非常浪费资源。
  • 所以只需要重绘移动前后的部分即可。
void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1){
    int h, bx, by, vx, vy;
    unsigned char *buf, c, *vram = ctl->vram;
    struct SHEET *sht;
    for (h = 0; h <= ctl->top; h++) {
        sht = ctl->sheets[h];
        buf = sht->buf;
        for (by = 0; by < sht->bysize; by++) {
            vy = sht->vy0 + by;
            for (bx = 0; bx < sht->bxsize; bx++) {
                vx = sht->vx0 + bx;
                if (vx0 <= vx && vx < vx1 && vy0 <= vy && vy < vy1) {
                    c = buf[by * sht->bxsize + bx];
                    if (c != sht->col_inv) {
                        vram[vy * ctl->xsize + vx] = c;
                    }
                }
            }
        }
    }
    return;
}
  • 同时优化显示鼠标坐标的部分,那么归结到最后是要优化sheet的refresh:
void sheet_refresh(struct SHTCTL *ctl, struct SHEET *sht, int bx0, int by0, int bx1, int by1){
    if (sht->height >= 0) { /* 要刷新的部分正在显示,那就按照新的图层信息刷新这个部分*/
        sheet_refreshsub(ctl, sht->vx0 + bx0, sht->vy0 + by0, sht->vx0 + bx1, sht->vy0 + by1);
    }
    return;
}
  • 以上指定坐标的时候是指定VRAM中的位置而不是图像上的位置。

继续提高刷新速度

  • 问题在于,进行刷新的时候,虽然不再需要刷新整个屏幕,但是需要判断整个屏幕哪里需要刷新。
void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, int vx1, int vy1){
    int h, bx, by, vx, vy, bx0, by0, bx1, by1;
    unsigned char *buf, c, *vram = ctl->vram;
    struct SHEET *sht;
    for (h = 0; h <= ctl->top; h++) {
        sht = ctl->sheets[h];
        buf = sht->buf;
        /* 利用vx0~vy1,计算bx0~by1*/
        bx0 = vx0 - sht->vx0;
        by0 = vy0 - sht->vy0;
        bx1 = vx1 - sht->vx0;
        by1 = vy1 - sht->vy0;
        if (bx0 < 0) { bx0 = 0; }                           // 参阅(1)
        if (by0 < 0) { by0 = 0; }
        if (bx1 > sht->bxsize) { bx1 = sht->bxsize; }       // 参阅(2)
        if (by1 > sht->bysize) { by1 = sht->bysize; }
        for (by = by0; by < by1; by++) {
            vy = sht->vy0 + by;
            for (bx = bx0; bx < bx1; bx++) {
                vx = sht->vx0 + bx;
                c = buf[by * sht->bxsize + bx];
                if (c != sht->col_inv) {
                    vram[vy * ctl->xsize + vx] = c;
                }
            }
        }
    }
    return;
}
  • 参阅(1):
    • 当刷新范围在图层外面的时候,可以发现刷新坐标甚至变为负数,那么就可以忽略负数部分,从0开始刷新。
  • 参阅(2):
    • 这个部分处理的是在(1)的基础上进行拓展,也就是鼠标以另外的位置覆盖的时候的处理情况。

【自制OS学习笔记|Day 8】内存!内存!

  • 高速缓存(cache)
  • CPU与内存的通信和CPU与内部寄存器的通信速度差太多,所以在CPU中加入了极其宝贵的存储器——高速缓存。高速缓存容量很小。
  • 386CPU没有高速缓存,486以后才有。
  • 访问内存时,把访问到的内容存进高速缓存,下次用到同样的地址的数据时直接从高速缓存中读取。
  • 写入内存时先更新高速缓存。
  • 例如,在进行循环for(i = 0; i < 100; ++i)时,先在高速缓存中把i的值累加,最后把99写入内存。

内存容量检查

  • 如果内存出了问题,但是写入读取的时候其实是在操作高速缓存,就会检测不出问题,所以需要先关闭高速缓存。
#define EFLAGS_AC_BIT      0x00040000
#define CR0_CACHE_DISABLE  0x60000000

unsigned int memtest(unsigned int start, unsigned int end){
    char flg486 = 0;
    unsigned int eflg, cr0, i;

    /* 确认CPU是386还是486以上*/
    eflg = io_load_eflags();
    eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */
    io_store_eflags(eflg);
    eflg = io_load_eflags();
    if ((eflg & EFLAGS_AC_BIT) != 0) { /* 如果是386,那么即使设定AC=1,AC还是会变成0*/
        flg486 = 1;
    }
    eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
    io_store_eflags(eflg);

    if (flg486 != 0) {
        cr0 = load_cr0();
        cr0 |= CR0_CACHE_DISABLE; /* 禁止缓存*/
        store_cr0(cr0);
    }

    i = memtest_sub(start, end);

    if (flg486 != 0) {
        cr0 = load_cr0();
        cr0 &= ~CR0_CACHE_DISABLE; /* 允许缓存*/
        store_cr0(cr0);
    }

    return i;
}
  • 在486以上,EFLAGS寄存器的第18位是AC标志位,而如果是386,则没有这个标志位,所以会一直为0,那么就可以写入这个标志位再读取,来判断CPU的种类。
  • 为了禁止高速缓存,需要写入CR0寄存器,用到的函数需要汇编语言:
_load_cr0:      ; int load_cr0(void);
        MOV     EAX,CR0
        RET

_store_cr0:     ; void store_cr0(int cr0);
        MOV     EAX,[ESP+4]
        MOV     CR0,EAX
        RET
  • 进行内存检查:
unsigned int memtest_sub(unsigned int start, unsigned int end){
    unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
    for (i = start; i <= end; i += 4) {
        p = (unsigned int *) i;
        old = *p;           /* 记录下修改前的值*/
        *p = pat0;          /* 试写*/
        *p ^= 0xffffffff;   /* 反转*/
        if (*p != pat1) {   /* 检查反转结果*/
not_memory:
            *p = old;
            break;
        }
        *p ^= 0xffffffff;   /* 再次反转*/
        if (*p != pat0) {   /* 检查值是否恢复*/
            goto not_memory;
        }
        *p = old;           /* 恢复修改前的值*/
    }
    return i;
}
  • 对start和end之间的内存进行写入测试,确定多少内存可用。
  • 加速检查:
unsigned int memtest_sub(unsigned int start, unsigned int end){
    unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
    for (i = start; i <= end; i += 0x1000) {
        p = (unsigned int *) (i + 0xffc);
        old = *p;           /* 记录下修改前的值*/
        *p = pat0;          /* 试写*/
        *p ^= 0xffffffff;   /* 反转*/
        if (*p != pat1) {   /* 检查反转结果*/
not_memory:
            *p = old;
            break;
        }
        *p ^= 0xffffffff;   /* 再次反转*/
        if (*p != pat0) {   /* 检查值是否恢复*/
            goto not_memory;
        }
        *p = old;           /* 恢复修改前的值*/
    }
    return i;
}
  • 每次检查4 KB的最后4 Byte,因为开机时内存已经检查过了(BIOS),所以只用简单确定多少内存可用即可。

不成功?!!

  • 然而上面的C语言部分不能成功,因为:面向编译器编程的语言太强大了。
  • 编译器做了优化:
    • 内存写入pat0后反转再和pat1比较肯定相等啊,if那一部分不可能成立,删掉。
    • 下面又一次反转再和pat0比较,那不还是相等嘛,删掉。
    • 反转后的比较都删去了,那么写入什么的也没有用啊,删掉。
    • 那么也就是*p中没有写入任何东西啊,删掉。
    • 那么pat0,pat1之类的值也没有用啊,删掉。
  • 结果就是什么都没发生🙄
  • 所以还是汇编大法好:
_memtest_sub:   ; unsigned int memtest_sub(unsigned int start, unsigned int end)
        PUSH    EDI                     ; 还是要使用EBX, ESI, EDI
        PUSH    ESI
        PUSH    EBX
        MOV     ESI,0xaa55aa55          ; pat0 = 0xaa55aa55;
        MOV     EDI,0x55aa55aa          ; pat1 = 0x55aa55aa;
        MOV     EAX,[ESP+12+4]          ; i = start;
mts_loop:
        MOV     EBX,EAX
        ADD     EBX,0xffc               ; p = i + 0xffc;
        MOV     EDX,[EBX]               ; old = *p;
        MOV     [EBX],ESI               ; *p = pat0;
        XOR     DWORD [EBX],0xffffffff  ; *p ^= 0xffffffff;
        CMP     EDI,[EBX]               ; if (*p != pat1) goto fin;
        JNE     mts_fin
        XOR     DWORD [EBX],0xffffffff  ; *p ^= 0xffffffff;
        CMP     ESI,[EBX]               ; if (*p != pat0) goto fin;
        JNE     mts_fin
        MOV     [EBX],EDX               ; *p = old;
        ADD     EAX,0x1000              ; i += 0x1000;
        CMP     EAX,[ESP+12+8]          ; if (i <= end) goto mts_loop;
        JBE     mts_loop
        POP     EBX
        POP     ESI
        POP     EDI
        RET
mts_fin:
        MOV     [EBX],EDX               ; *p = old;
        POP     EBX
        POP     ESI
        POP     EDI
        RET

进行内存管理

  • 简单举个例子:
  • 一共有128MB内存,现在App A需要100KB,App B需要1.2MB……在操作系统中这类事情时刻在发生。
  • 这样,操作系统就需要知道什么地方多少内存能用(空闲),什么地方多少内存不能用(占用)。
  • 作为操作系统,我们需要考虑的是,在应用程序申请内存的时候,怎么找到合适的空间。在应用程序退出的时候,怎么释放对应的空间。
  • 栗子A:
  • 现在有128 MB内存,以4 KB为单位进行管理。
  • 也就是有32768个空位,可以拿出32768 Byte的区域,用来表明哪些内存空间被占用,哪些没有。
char a[32768];
for (i = 0; i < 1024; ++i){
    a[i] = 1;           /* 一直到4 MB为止,标记为正在使用*/
}
for (i = 1024; i < 32768; ++i){
    a[i] = 0;           /* 剩余的空间都标记为空*/
}
  • 需要100 KB空间的时候:
    j = 0;
tryAgain:
    for (i = 0; i < 25; ++i){
        if (a[j + 1] != 0){
            ++j;
            if (j < 32768 - 25)
                goto tryAgain;
            //"没有可用内存了"
        }
    }
    //"内存分配失败,把j到j+24重新标记成0"
  • 如果找到了足够可用的空间,可以标记为正在使用,然后从j的值计算出对应的地址。
for (i = 0; i < 25; ++i){
    a[j + i] = 1;
}
//"分配规划好的空间:从j*0x1000开始的100 KB"
  • 如果要释放这段空间,因为我们以4 KB(0x1000)为单位进行管理,就可以这样:
j = 0x00123000 / 0x1000         /* 假设刚才取得的空间是从0x00123000开始的*/
for (i = 0; i < 25; ++i){
    a[j + i] = 0;
}
  • 另外,如果用bit来管理空间而不是使用Byte(char),可以更加节省空间。实际上,Windows就是用类似这种方法管理软盘空间的(不过单位会大一些)。
  • 栗子B:列表管理法

  • 『从xxx号开始的yyy大小的空间都是空闲的』
struct FREEINFO{
    unsigned int addr, size;
}

struct MEMMAN{
    int frees;
    struct FREEINFO free[1000]; 
}

    struct MEMMAN memman;
    memman.frees = 1; /* 现在的可用list只有一项*/
    memman.free[0].addr = 0x00400000;   /* 从0x00400000开始有124 MB可用*/
    memman.free[0].size = 0x07c00000;
  • 寻找可用空间:
for (i = 0; i < memman.frees; ++i){
    if (memman.free[i].size >= 100 * 1024){
        //"找到了"
        //"把对应空间划分出来,重新记录内存分部"
    }
}
//"内存分配失败"
  • 释放内存:
    • 把内存记录的数量进行调整
    • 判断能否合并
    • 能合并的话继续调整
  • 当内存过于零碎的时候,存储记录的空间就不够了,这个时候就要暂时割舍掉小块的未使用的空间,待有空余的时候通过内存检查再把这块空间找回来。

看下实战代码:

#define MEMMAN_FREES       4090    /* 大约32KB */

struct FREEINFO {   /* 可用信息*/
    unsigned int addr, size;
};

struct MEMMAN {     /* 管理信息*/
    int frees, maxfrees, lostsize, losts;
    struct FREEINFO free[MEMMAN_FREES];
};

void memman_init(struct MEMMAN *man){
    man->frees = 0;         /* 可用信息个数*/
    man->maxfrees = 0;      /* frees最大值*/
    man->lostsize = 0;      /* 释放失败的内存大小总和*/
    man->losts = 0;         /* 释放失败次数*/
    return;
}

unsigned int memman_total(struct MEMMAN *man){
/* 报告空余内存大小合计*/
    unsigned int i, t = 0;
    for (i = 0; i < man->frees; i++) {
        t += man->free[i].size;
    }
    return t;
}

unsigned int memman_alloc(struct MEMMAN *man, unsigned int size){
/* 分配*/
    unsigned int i, a;
    for (i = 0; i < man->frees; i++) {
        if (man->free[i].size >= size) {
            /* 找到了足够的内存*/
            a = man->free[i].addr;
            man->free[i].addr += size;
            man->free[i].size -= size;
            if (man->free[i].size == 0) {
                /* free[i]变为0就删掉一条可用信息*/
                man->frees--;
                for (; i < man->frees; i++) {
                    man->free[i] = man->free[i + 1]; /* 代入结构体*/
                }
            }
            return a;
        }
    }
    return 0; /* 没有可用空间*/
}

int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size){
/* 释放*/
    int i, j;
    /* 为了便于归纳内存,free数组中的信息按照addr的顺序排列*/
    /* 因此,先决定应该放在哪里*/
    for (i = 0; i < man->frees; i++) {
        if (man->free[i].addr > addr) {
            break;
        }
    }
    /* free[i - 1].addr < addr < free[i].addr */
    if (i > 0) {
        /* 前面有可用内存*/
        if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
            /* 可以与前面的内存归纳在一起*/
            man->free[i - 1].size += size;
            if (i < man->frees) {
                /* 后面也有*/
                if (addr + size == man->free[i].addr) {
                    /* 和后面的可用内存归纳在一起*/
                    man->free[i - 1].size += man->free[i].size;
                    /* man->free[i]删除*/
                    /* free[i]变成0后归纳到前面*/
                    man->frees--;
                    for (; i < man->frees; i++) {
                        man->free[i] = man->free[i + 1]; /* 代入结构体*/
                    }
                }
            }
            return 0; /* OK*/
        }
    }
    /* 不能和前面的归纳在一起*/
    if (i < man->frees) {
        /* 但是可以和后面的归纳*/
        if (addr + size == man->free[i].addr) {
            man->free[i].addr = addr;
            man->free[i].size += size;
            return 0; /* OK*/
        }
    }
    /* 前后都不能*/
    if (man->frees < MEMMAN_FREES) {
        /* free[i]之后的向后移动,腾出空间*/
        for (j = man->frees; j > i; j--) {
            man->free[j] = man->free[j - 1];
        }
        man->frees++;
        if (man->maxfrees < man->frees) {
            man->maxfrees = man->frees; /* 更新可用空间数量的最大值*/
        }
        man->free[i].addr = addr;
        man->free[i].size = size;
        return 0; /* OK*/
    }
    /* 不能往后移动*/
    man->losts++;
    man->lostsize += size;
    return -1; /* 失败*/
}
  • 最后添加了输出,看下效果吧:

【自制OS学习笔记|Week 01特别篇】进入32位

​ 32位处理器,计算机中的位数指的是CPU一次能处理的最大位数。32位计算机的CPU一次最多能处理32位数据,例如它的EAX寄存器就是32位的,当然32位计算机通常也可以处理16位和8位数据。在Intel由16位的286升级到386的时候,为了和16位系统兼容,它先推出的是386SX,这种CPU内部预算为32位,外部数据传输为16位。直到386DX以后,所有的CPU在内部和外部都是32位的了。

——引用自百度百科。

  • CPU是怎么进入32位的:
  • 第一阶段:4位和8位低档微处理器(1971~1973)

    • 典型产品是Intel 4004和8008。
    • 采用PMOS工艺,集成度低(4000个晶体管/片),系统结构和指令系统都比较简单,主要采用机器语言或简单的汇编语言,指令数目较少(20多条指令),基本指令周期为20~50μs,用于简单的控制场合。
  • 第二阶段:8位中高档微处理器(1974~1977)
    • 典型产品是Intel8080/8085、Motorola公司、Zilog公司的Z80等。
    • 采用NMOS工艺,集成度提高约4倍,运算速度提高约10~15倍(基本指令执行时间1~2μs)。

    • 指令系统比较完善,具有典型的计算机体系结构和中断、DMA等控制功能。

    • 软件方面除了汇编语言外,还有BASIC、FORTRAN等高级语言和相应的解释程序和编译程序,在后期还出现了操作系统。

    • 1974年,Intel推出8080处理器,并作为Altair个人电脑的运算核心,Altair在《星舰奇航》电视影集中是企业号太空船的目的地。电脑迷当时可用395美元买到一组Altair的套件。它在数个月内卖出数万套,成为史上第一款下订单后制造的机种。Intel 8080晶体管数目约为6千颗。
      
  • 第三阶段:16位微处理器(1978~1984)

* 典型产品是Intel公司的8086/8088,Motorola公司的M68000,Zilog公司的Z8000等微处理器。
* 其特点是采用HMOS工艺,集成度(20000~70000晶体管/片)和运算速度(基本指令执行时间是0.5μs)都比第2代提高了一个数量级。
* 指令系统更加丰富、完善,采用多级中断、多种寻址方式、段式存储机构、硬件乘除部件,并配置了软件系统。
* ```
    80286(也被称为286)是英特尔首款能执行所有旧款处理器专属软件的处理器,这种软件相容性之后成为英特尔全系列微处理器的注册商标,在6年的销售期中,估计全球各地共安装了1500万部286个人电脑。Intel 80286处理器晶体管数目为13万4千颗。1984年,IBM公司推出了以80286处理器为核心组成的16位增强型个人计算机IBM PC/AT。由于IBM公司在发展个人计算机时采用 了技术开放的策略,使个人计算机风靡世界。
    ```
  • 第四阶段:32位微处理器(1985~1992)

    • 典型产品是Intel公司的80386/80486,Motorola公司的M69030/68040等。
    • 其特点是采用HMOS或CMOS工艺,集成度高达100万个晶体管/片,具有32位地址线和32位数据总线。每秒钟可完成600万条指令(Million Instructions Per Second,MIPS)。

    • 80386DX的内部和外部数据总线是32位,地址总线也是32位,可以寻址到4GB内存,并可以管理64TB的虚拟存储空间。它的运算模式除了具有实模式和保护模式以外,还增加了一种“虚拟86”的工作方式,可以通过同时模拟多个8086微处理器来提供多任务能力。80386SX是Intel为了扩大市场份额而推出的一种较便宜的普及型CPU,它的内部数据总线为32位,外部数据总线为16位,它可以接受为80286开发的16位输入/输出接口芯片,降低整机成本。80386SX推出后,受到市场的广泛的欢迎,因为80386SX的性能大大优于80286,而价格只是80386的三分之一。Intel 80386 微处理器内含275,000 个晶体管——比当初的4004多了100倍以上,这款32位元处理器首次支持多工任务设计,能同时执行多个程序。Intel 80386晶体管数目约为27万5千颗。
      
    • 1989年,我们大家耳熟能详的80486芯片由英特尔推出。这款经过四年开发和3亿美元资金投入的芯片的伟大之处在于它首次实破了100万个晶体管的界限,集成了120万个晶体管,使用1微米的制造工艺。80486的时钟频率从25MHz逐步提高到33MHz、40MHz、50MHz。
      80486是将80386和数学协微处理器80387以及一个8KB的高速缓存集成在一个芯片内。80486中集成的80487的数字运算速度是以前80387的两倍,内部缓存缩短了微处理器与慢速DRAM的等待时间。并且,在80x86系列中首次采用了RISC(精简指令集)技术,可以在一个时钟周期内执行一条指令。它还采用了突发总线方式,大大提高了与内存的数据交换速度。由于这些改进,80486的性能比带有80387数学协微处理器的80386 DX性能提高了4倍。
      

我们的OS是怎么进入32位的

1.

; PIC屏蔽一切中断
;   根据AT兼容机的规格,如果要初始化PIC,就必须在CLI之后进行
;   否则有时会挂起,随后进行PIC的初始化

        MOV     AL,0xff
        OUT     0x21,AL
        NOP                     ; 如果连续执行OUT指令,有些机器会无法正常运行
        OUT     0xa1,AL

        CLI                     ; 禁止CPU级别的中断
  • NOP指令执行后,不产生任何结果,可编程控制器中的用户程序全部清除后,用户程序存储器中的指令全部变成NOP指令。在调试程序时,若要观察某些指令的影响,而又不想改变指令的步序号,可以把这些指令变成NOP。也就是让CPU休息一个时钟长。
  • OUT I/O端口地址,AX/AL;表示将累加器的数据输出给外部设备,如果向外设端口输出一个字节则用8位累加器AL,若输出一个字则用16位累加器AX。

2.

; 设定A20GATE

        CALL    waitkbdout
        MOV     AL,0xd1
        OUT     0x64,AL
        CALL    waitkbdout
        MOV     AL,0xdf         ; enable A20
        OUT     0x60,AL
        CALL    waitkbdout
  • waitkbdout相当于C语言中的wait_KBC_sendready。
  • 这里发送的指令,是指令键盘控制电路的附属端口输出0xdf,这个端口连接着主板上很多地方,通过这个端口是、发送不同的指令,就能实现不同的控制功能。
  • 0xdf开启了A20GATE(变为ON)。它能使内存的1MB以上的部分变为可使用状态。最初出现电脑的时候CPU比较弱(看上面),所以内存也不会超过1MB(也就是16位CPU的最大寻址空间),之后有了32位CPU(这个情况是不是类似鼠标呢,笑)。

3.

; 切换到保护模式

[INSTRSET "i486p"]              ; 声明使用486指令

        LGDT    [GDTR0]         ; 设定临时GDT
        MOV     EAX,CR0         ; 将GDT的首地址和界限赋给GDTR寄存器
        AND     EAX,0x7fffffff  ; 设bit31为0(禁止分页)
        OR      EAX,0x00000001  ; 设bit0为1(进入保护模式)
        MOV     CR0,EAX
        JMP     pipelineflush
pipelineflush:
        MOV     AX,1*8          ;  可读写的段 32bit,初始化段寄存器,各段寄存器的段号为1
        MOV     DS,AX
        MOV     ES,AX
        MOV     FS,AX
        MOV     GS,AX
        MOV     SS,AX
  • 因为之后还要在保护模式下设定GDT,所以先设定一个临时的GDT。
  • CR0 – control register 0,是一个非常重要的寄存器,只有操作系统才能操作。
  • 保护模式下,段寄存器的解释不是16位,而是使用GDT。实模式和保护模式(「受保护的虚拟内存地址模式」)的区别在于:前者使用段寄存器的值直接指定地址值的一部分,后者通过GDT使用段寄存器的值指定并非实际存在的地址。
  • 通过代入CR0而切换到保护模式时,要马上执行JMP指令。因为变成保护模式后,机器语言的解释要发生变化。CPU为了加快指令的执行速度而使用了管道(pipeline)这一机制。亦即在执行前一条指令的时候就开始解释下一条(甚至后两条)指令。而因为模式变了,就要重新解释,所以需要JMP。
  • 进入保护模式后,段寄存器的意思也发生变化,不再是乘以16。除了CS以外的所有段寄存器都从0x0000变为0x0008,也就是’gdt+1’。

4.

; bootpack(也就是C语言的部分)

        MOV     ESI,bootpack    ; 传送源
        MOV     EDI,BOTPAK      ; 目的地
        MOV     ECX,512*1024/4
        CALL    memcpy

; 磁盘数据最终传送到它本来的位置

; 首先从启动扇区开始

        MOV     ESI,0x7c00      ; 传送源
        MOV     EDI,DSKCAC      ; 目的地
        MOV     ECX,512/4
        CALL    memcpy

; 剩余

        MOV     ESI,DSKCAC0+512 ; 传送源
        MOV     EDI,DSKCAC+512  ; 目的地
        MOV     ECX,0
        MOV     CL,BYTE [CYLS]
        IMUL    ECX,512*18*2/4  ; 从柱面数变成字节数/4
        SUB     ECX,512/4       ; 减去IPL
        CALL    memcpy
  • DSKCAC是0x00100000,所以第二部分实际上是从0x7c00复制512 Byte到0x00100000,也就是将启动扇区复制到1 MB以后的内存中去。(牵扯到柱面数的计算)
  • 第三部分把开始于0x00008200的磁盘内容复制到0x00100200。
  • IMUL:乘法运算。
  • SUB:减法运算。

5.

; 汇编语言这部分的工作已经结束了,下面交给C语言的部分

; bootpackの起動

        MOV     EBX,BOTPAK
        MOV     ECX,[EBX+16]
        ADD     ECX,3           ; ECX += 3;
        SHR     ECX,2           ; ECX /= 4;
        JZ      skip            ; 没有要传送的东西时
        MOV     ESI,[EBX+20]    ; 传送源
        ADD     ESI,EBX
        MOV     EDI,[EBX+12]    ; 目的地
        CALL    memcpy
skip:
        MOV     ESP,[EBX+12]    ; 栈初始值
        JMP     DWORD 2*8:0x0000001b
  • SHR:向右移位。
  • 这段代码解析了C语言的二进制文件的头部,在EBX中对应了BOTPAK:
    • [EBX + 16]:二进制后的16个地址,也就是0x11a8
    • [EBX + 20]:二进制后的20个地址,也就是0x10c8
    • [EBX + 12]:二进制后的12个地址,也就是0x00310000
  • JZ – jump if zero,为0则跳转。

5.

内存分布:

0x00000000 ~ 0x000fffff:在启动中使用,之后包含了BIOS、VRAM等内容(1 MB)

0x00100000 ~ 0x00267fff:用于保存软盘的内容(1440 KB)

0x00268000 ~ 0x0026f7ff:空(30 KB)

0x0026f800 ~ 0x0026ffff:IDT(2 KB)

0x00270000 ~ 0x0027ffff:GDT (64 KB)

0x00280000 ~ 0x002fffff:C语言对应的部分 (512 KB,留得比较多但是留得多并没有什么坏处)

0x00300000 ~ 0x003fffff:栈,和一些别的东西(1 MB)

6.

waitkbdout:
        IN       AL,0x64
        AND      AL,0x02        ; 空读,为了清空数据接收缓冲区中的垃圾数据
        JNZ     waitkbdout      ; AND的结果不是0,就跳转到waitkbdout
        RET
  • JNZ – jump if not zero。
  • 从0x60号设备开始IN读入,如果有键盘代码或者积累的鼠标数据,就顺便读取出来。

7.

memcpy:
        MOV     EAX,[ESI]
        ADD     ESI,4
        MOV     [EDI],EAX
        ADD     EDI,4
        SUB     ECX,1
        JNZ     memcpy          ; 减法运算结果不为0,跳转到memcpy
        RET

8.

        ALIGNB  16
GDT0:
        RESB    8               ; NULL sector
        DW      0xffff,0x0000,0x9200,0x00cf ; 可以读写的段 32bit
        DW      0xffff,0x0000,0x9a28,0x0047 ; 可以执行的段 32bit(bootpack用)

        DW      0
GDTR0:
        DW      8*3-1
        DD      GDT0

        ALIGNB  16
bootpack:
  • ALIGNB,一直进行DB 0,直到时机合适(地址能被16整除)的时候停止。
  • GDT0是一种特定的GDT,0是空区域(NULL sector),不能定义段。实际上,GDT0是LGDT指令,也就是通知GDT0来读取GDT。这里写入了16位段的上限和32位段的起始地址。

至此,我们进入了32位模式。

【自制OS学习笔记|Day 7】鼠标!鼠标!

解析鼠标指令

记录

    unsigned char mouse_dbuf[3], mouse_phase;
    enable_mouse();
    mouse_phase = 0; /* 等待鼠标返回0xfa*/

    for (;;) {
        io_cli();
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
            io_stihlt();
        } else {
            if (fifo8_status(&keyfifo) != 0) {
                i = fifo8_get(&keyfifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484,  0, 16, 15, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
            } else if (fifo8_status(&mousefifo) != 0) {
                i = fifo8_get(&mousefifo);
                io_sti();
                if (mouse_phase == 0) {
                    /* 等待鼠标0xfa*/
                    if (i == 0xfa) {
                        mouse_phase = 1;
                    }
                } else if (mouse_phase == 1) {
                    /* 接收鼠标的第一Byte*/
                    mouse_dbuf[0] = i;
                    mouse_phase = 2;
                } else if (mouse_phase == 2) {
                    /* 接收鼠标的第二Byte*/
                    mouse_dbuf[1] = i;
                    mouse_phase = 3;
                } else if (mouse_phase == 3) {
                    /* 接收鼠标的第三Byte*/
                    mouse_dbuf[2] = i;
                    mouse_phase = 1;
                    /* 得到了鼠标的3个Byte,将它们输出在屏幕上*/
                    sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);
                    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
                    putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
                }
            }

鼠标向左移动(第一个Byte是键盘,下同)

鼠标向右移动

鼠标向上移动

鼠标向下移动

鼠标左键单击

  • 其中能发现:
    • 如果只是移动鼠标,第一个Byte永远是08、18、28、38。
    • 如果鼠标左键单击,第一个Byte的后四位会变成9,如果点击右键、滚轮,总之这个值会在8~F之间变化。
    • 第二个Byte与鼠标左右移动有关系。
    • 第三个Byte和鼠标上下移动有关系。

优化代码

  • 用结构体整理鼠标的数据:
struct MOUSE_DEC {
    unsigned char buf[3], phase;
    int x, y, btn;
};

int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat){
    if (mdec->phase == 0) {
        /* 等待鼠标发送0xfa,然后舍弃之*/
        if (dat == 0xfa) {
            mdec->phase = 1;
        }
        return 0;
    }
    if (mdec->phase == 1) {
        /* 等待第一个Byte*/
        if ((dat & 0xc8) == 0x08) {
            /* 这条if是检测第一个Byte是否合法*/
            mdec->buf[0] = dat;
            mdec->phase = 2;
        }
        return 0;
    }
    if (mdec->phase == 2) {
        /* 接收第二个Byte*/
        mdec->buf[1] = dat;
        mdec->phase = 3;
        return 0;
    }
    if (mdec->phase == 3) {
        /* 接收第三个Byte*/
        mdec->buf[2] = dat;
        mdec->phase = 1;
        mdec->btn = mdec->buf[0] & 0x07;
        mdec->x = mdec->buf[1];
        mdec->y = mdec->buf[2];
        if ((mdec->buf[0] & 0x10) != 0) {
            mdec->x |= 0xffffff00;
        }
        if ((mdec->buf[0] & 0x20) != 0) {
            mdec->y |= 0xffffff00;
        }
        mdec->y = - mdec->y; /* 鼠标的y方向和画面方向相反(坐标系的锅)*/
        return 1;
    }
    return -1; /* 反正是失败了*/
}
  • 检测第一个Byte是否合法:
    • 鼠标偶尔会发生接触不良等问题,可能导致信号丢失,通过检测第一个Byte可以过滤掉有问题的信号。
  • if (mdec->phase == 3) 中:
    • mdec->btn = mdec->buf[0] & 0x07:
      • 鼠标按键的状态,放在buf[0]的低三位,通过buf[0] & 0x07就可以取出这三位。
    • mdec->x、mdec->y首先使用buf[1]和buf[2],但是x,y坐标的移动和第一个Byte也有关(之前提到的第一Byte的高四位0、1、2、3等),需要进行相应的处理(也就是这个if里面的两个if)。

移动鼠标指针

  • 既然鼠标的解(guan)读(shu)已经完成了,那就开始移动画面上的鼠标吧!
/* 上面省略*/
            else if (fifo8_status(&mousefifo) != 0) {
                i = fifo8_get(&mousefifo);
                io_sti();
                if (mouse_decode(&mdec, i) != 0) {
                    /* 显示鼠标数据,lcr的大小写代表左键、中键、右键的状态*/
                    sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y);
                    if ((mdec.btn & 0x01) != 0) {
                        s[1] = 'L';
                    }
                    if ((mdec.btn & 0x02) != 0) {
                        s[3] = 'R';
                    }
                    if ((mdec.btn & 0x04) != 0) {
                        s[2] = 'C';
                    }
                    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31);
                    putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
                    /* 鼠标指针的移动*/
                    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 隐藏鼠标*/
                    mx += mdec.x;
                    my += mdec.y;
                    if (mx < 0) {
                        mx = 0;
                    }
                    if (my < 0) {
                        my = 0;
                    }
                    if (mx > binfo->scrnx - 16) {
                        mx = binfo->scrnx - 16;
                    }
                    if (my > binfo->scrny - 16) {
                        my = binfo->scrny - 16;
                    }
                    sprintf(s, "(%3d, %3d)", mx, my);
                    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 隐藏坐标*/
                    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 重新显示坐标*/
                    putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 画出新的鼠标*/
                }
            }
/* 下面省略*/

不完美的地方:背景色

因为我们没有考虑好背景色的问题,所以在鼠标移动到下面的时候会出现坑爹的情况:

「任务栏」的背景色被抹掉辣!

  • 相应的问题将在之后的笔记——内存管理篇中提到。
  • 原因是对鼠标不应该盖住的部分的处理——在不应该盖住嗯部分重新填充背景色(就是那个原谅色)。

【自制OS学习笔记|Day 5&6】中断、缓冲区和键盘鼠标

操作GDTR和IDTR

  • 通过汇编语言的函数来操作:
_load_gdtr:     ; void load_gdtr(int limit, int addr);
    MOV         AX,[ESP+4]      ; 取出段上限
    MOV         [ESP+6],AX      ; 把段上限赋值给[ESP+6]
    LGDT        [ESP+6]
    RET
  • GDTR是一个很特殊的48位寄存器,不能使用MOV,唯一的赋值方式是指定一个内存地址,通过LGDT从这里读取6 Byte存入GDTR。
  • GDTR的低16位是段上限,也就是GDT的有效字节数-1.高32位表示GDT的开始地址。
  • 在刚开始执行这个函数的时候,DWORD[ESP+4]存放了段上限,DWORD[ESP+8]存放的是地址,所以通过取出段上限然后赋值给段上限后两个Byte的位置来转化成6 Byte的信息。举个例子,如果原来的数据是[FF FF 00 00 00 00 27 00],这样操作后就是[FF FF FF FF 00 00 27 00],然后从第三个FF开始为GDTR赋值。
  • IDTR的结构和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;
    return;
}
  • 复习:段的信息包括:
    • 段的大小
    • 段的起始地址
    • 段的管理属性(禁止写入、禁止执行、系统专用等)
  • 段的地址:
    • 用32位表示(32位下),这个地址又成为段的基址,这个base分为low(2 Byte),mid(1 Byte)和high(1 Byte)表示,目的是兼容80286。
  • 段上限:
    • 表示一个段有多少字节。但是段上限最大为4GB,也就是需要用4 Byte来表示,而这样太大了,所以通过页(page)来表示。
    • 段的属性一共20位,其中有一个标志位(称为G bit),设定为1则段上限单位是页(=4 KB),否则为Byte。
    • 段上限卸载limit_low和limit_high中,limit_high的高4位用来记录一部分段属性。
  • 段属性:
    • 分为12位,高4位放在limit_high中,所以为了方便对其进行处理:
    • xxxx0000xxxxxxxx(x=0或1)
    • 高4位称为「拓展访问权」,从386开始使用。这四位是由GD00构成,G是G bit,D是段的模式(1为32位,0为16位)。
    • 低8位从80286就开始了,举例:
      • 00000000(0x00):未使用的记录表。
      • 10010010(0x92):系统专用(Ring 0),可读写,不可执行。
      • 10011010(0x9a):系统专用(Ring 0),可执行,可读不可写。
      • 11110010(0xf2):应用程序用(Ring 3),可读写,不可执行。
      • 11111010(0xfa):应用程序用(Ring 3),可执行,可读不可写。
    • Ring 1和Ring 2权限一般由设备驱动器使用。

PIC和键盘、鼠标

  • PIC – programmable interrupt controller,可编程中断控制器。
  • CPU只能处理一个中断,所以通过PIC辅助处理。PIC可以监视8个中断信号,只要有一个中断信号传入,就会通知CPU。
  • IBM使用2个PIC来控制15个中断信号:
  • PIC的寄存器
    • IMR – interrupt mask register,中断屏蔽寄存器,8位对应8路IRQ信号,某一位为1则该位信号被屏蔽。屏蔽的原因一般有二:正在对中断进行设定时再接受别的中断会引起混乱、某个IRQ没有连接设备的时候会因为静电干扰影响操作系统。
  • ICW – initial control word,初始化控制数据,这里的word并不是16位的意思。ICW有4个,一共4 Byte。
    • ICW1与ICW4与:与主板的配线方式、中断信号的电气特性有关,设定为固定值。一般来说对二者的设定会被忽略。
    • ICW3:设定主-从连接,设定第几个IRQ与从PIC相连,已经被设定为00000100。

    • ICW2:可以进行设定。

      • 在中断发生后,如果CPU可以处理,CPU会令PIC发送两个Byte,而CPU和PIC用数据信号线连接,这样CPU会将这两个Byte看作从内存读入的程序。PIC发送「0xcd 0x??」,就执行成了INT 0x??

      • 以INT 0x20~0x2f接收IRQ0~15,因为INT 0x00~0x1f是CPU用来系统保护的。

利用中断处理来操作键盘鼠标

  • 鼠标是IRQ12,键盘是IRQ1。
  • 先用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();
    }
}
  • 在中断处理完成后,不能使用return(RET),而是要使用中断专用的IRETD来返回,所以需要借助汇编语言。
        EXTERN  _inthandler21, _inthandler27, _inthandler2c ; 调用C语言的函数

_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
  • 这里使用了栈(stack)型的缓冲区。
  • PUSH和POP:
    • PUSH将数据压栈,将寄存器中的值保存到该地址对应的内存中。
    • POP反之。

PUSH EAX实际上相当于:

​ ADD ESP,-4

​ MOV [SS:ESP],EAX

POP EAX相当于:

​ MOV EAX,[SS:ESP]

​ ADD ESP,4

  • 所以,在进行这样的操作:
PUSH EAX
PUSH ECX
PUSH EDX
; 一些操作
POP EDX
POP ECX
POP EAX

之后,即便「一些操作」的位置更改了EAX、ECX、EDX的值,它们最后还是会恢复到原来的值。

  • PUSHAD和POPAD:
    • PUSHAD相当于:
    • PUSH EAX
      PUSH ECX
      PUSH EDX
      PUSH EBX
      PUSH ESP
      PUSH EBP
      PUSH ESI
      PUSH EDI
      
    • 反之,POPAD等价于将上述寄存器相反顺序POP。

  • 注册到IDT:

    • set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 << 3, AR_INTGATE32);
      
    • 2 << 3是因为段号是2,低3位有别的意义,必须为0。
    • 号码为2的段保存了C语言编译后的内容。

获取按键编码

#define PORT_KEYDAT        0x0060

void inthandler21(int *esp){
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;  /* 获取启动信息存储在一个结构体中*/
    unsigned char data, s[4];
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC,IRQ-01已经处理完毕*/
    data = io_in8(PORT_KEYDAT);

    sprintf(s, "%02X", data);
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);

    return;
}
  • 在通知PIC的过程中,0x61代表IRQ1,同理,0x63代表IRQ3。如果不执行这一步,PIC就不会再继续监视IRQ1,那么之后键盘再输入什么信息就感知不到了。

按下T键的时候

松开T键的时候

加快中断处理

  • 思路:
    • 中断处理是打断CPU的工作,强行要求CPU进行处理,而且中断期间不再接受别的中断(例如在处理键盘事件的时候不能进行网络传输、鼠标移动等等),这就要求中断处理必须快快快。

1 初级方式——使用简单的缓冲区

#define PORT_KEYDAT        0x0060

struct KEYBYFF {
    unsigned char data, flag;
}

struct KEYBUF keybuf;

void inthandler21(int *esp){
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 通知PIC,IRQ1已经处理完毕*/
    data = io_in8(PORT_KEYDAT);
    if (keybuf.flag == 0) {
        keybuf.data = data;
        keybuf.flag = 1;
    }
    return;
}
  • 在缓冲区为空(keybuf.flag = 0)的时候,存入数据,否则舍弃。
  • 修改上一天(是今天上半部分)的实现:
for (;;) {
        io_cli();
        if (keybuf.flag == 0) {
            io_stihlt();
        } else {
            i = keybuf.data;
            keybuf.flag = 0;
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }
  • 先使用io_hlt()屏蔽中断,然后检查keybuf.flag(否则肯定会乱套的啦)。
    • flag是0,说明还没有按键被按下。那么就直接HLT,但是不能执行io_hlt()而是要io_stihlt()。
      • 因为CLI屏蔽了中断,所以直接HLT会导致即使按下键盘,HLT也不会结束,所以要先STI然后HLT。
      • 如果先STI后HLT,那么一旦STI后HLT前产生了中断,keybuf中的数据就不会被察觉到。根据CPU的规范,如果机器语言中STI后紧跟HLT,那么二者之间的中断会被推迟到HLT后进行处理。所以使用io_stihlt()。(通过汇编语言写)
    • 否则,先保存下来按键码,然后flag置0并且STI,允许继续接受中断。
  • 问题:按下和松开右Ctrl的时候,显示的都是”E0″?然而在之前(也就是T键的演示的时候),分别是”1D”和”9D”?
    • 原因:按下右Ctrl的时候,会产生2 Byte的键码”E0 1D”,松开后会产生”E0 9D”,在这种情况下,会产生两个中断,第一次发送”E0″,第二次发送”1D(9D)”。

2 更好的解决方案:使用FIFO缓冲区(就是队列辣)

  • 刚才的问题在于:缓冲区只有一个Byte,可以把它拓展成32 Byte。
#define PORT_KEYDAT        0x0060

struct KEYBUF {
    unsigned char data[32];
    int next;
}

struct KEYBUF keybuf;

void inthandler21(int *esp){
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* 这里就不用注释了吧*/
    data = io_in8(PORT_KEYDAT);
    if (keybuf.next < 32) {
        keybuf.data[keybuf.next] = data;
        keybuf.next++;
    }
    return;
}
  • 这样取得数据:
for (;;) {
        io_cli();
        if (keybuf.next == 0) {
            io_stihlt();
        } else {
            i = keybuf.data[0];
            keybuf.next--;
            for (j = 0; j < keybuf.next; j++) {         // 显然这里的效率非常低
                keybuf.data[j] = keybuf.data[j + 1];
            }
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }

3 优化FIFO缓冲区,提高速度

  • 可以通过两个下标,一个是读取的next,一个是写入的next。
  • 当写入的next超过缓冲区的大小的时候,读取的next应该也会接近缓冲区末尾,那么前面的数据就没用了,可以强制写入的next为0,实现循环。读取的next类同。
#define PORT_KEYDAT        0x0060

struct KEYBUF {
    unsigned char data[32];
    int next_r, next_w, len;        // len表示缓冲区能写入多少数据(Byte)
}

struct KEYBUF keybuf;

void inthandler21(int *esp){
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);
    data = io_in8(PORT_KEYDAT);
    if (keybuf.len < 32) {
        keybuf.data[keybuf.next_w] = data;
        keybuf.len++;
        keybuf.next_w++;
        if (keybuf.next_w == 32) {
            keybuf.next_w = 0;
        }
    }
    return;
}
for (;;) {
        io_cli();
        if (keybuf.len == 0) {
            io_stihlt();
        } else {
            i = keybuf.data[keybuf.next_r];
            keybuf.len--;
            keybuf.next_r++;
            if (keybuf.next_r == 32) {
                keybuf.next_r = 0;
            }
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }

4 再次对FIFO缓冲区动刀,为迎接鼠标做准备

  • 鼠标的移动,是发送3 Byte。
  • 再次修改缓冲区的实现,并且改为可变大小:
struct FIFO8 {
    unsigned char *buf;
    int p, q, size, free, flags;
}
  • 用size保存缓冲区大小。p表示下一个写入地址,q表示下一个读取地址,free表示是否有数据。
  • 初始化:
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf){
    fifo->size = size;
    fifo->buf = buf;
    fifo->free = size;      /* 缓冲区大小*/
    fifo->flags = 0;
    fifo->p = 0;            /* 下一个写入位置*/
    fifo->q = 0;            /* 下一个读取位置*/
    return;
}
  • 为了事后确认是否溢出,可以通过flags来操作:
#define FLAGS_OVERRUN 0x0001

int fifo8_put(struct FIFO8 *fifo, unsigned char data){
    if (fifo->free == 0) {
        /* 空余没有了,溢出*/
        fifo->flags |= FLAGS_OVERRUN;
        return -1;
    }
    fifo->buf[fifo->p] = data;
    fifo->p++;
    if (fifo->p == fifo->size) {
        fifo->p = 0;
    }
    fifo->free--;
    return 0;
}
  • 读取
int fifo8_get(struct FIFO8 *fifo){
    int data;
    if (fifo->free == fifo->size) {
        /* 缓冲区为空,返回-1*/
        return -1;
    }
    data = fifo->buf[fifo->q];
    fifo->q++;
    if (fifo->q == fifo->size) {
        fifo->q = 0;
    }
    fifo->free++;
    return data;
}
  • 获得状态
int fifo8_status(struct FIFO8 *fifo){
    return fifo->size - fifo->free;         // 积攒数据的量
}

鼠标!

  • 如果稍微动一下鼠标就会产生中断的话,那只能在使用操作系统的时候拔掉鼠标了(然而PS/2不支持热插拔哦)。
  • IBM设计了鼠标控制电路,这样,可以先激活鼠标控制电路,等控制电路准备好后激活鼠标。
  • 鼠标控制电路包含在键盘控制电路中。
#define PORT_KEYDAT                0x0060
#define PORT_KEYSTA                0x0064
#define PORT_KEYCMD                0x0064
#define KEYSTA_SEND_NOTREADY   0x02
#define KEYCMD_WRITE_MODE      0x60
#define KBC_MODE               0x47

void wait_KBC_sendready(void){
    /* 等待键盘控制电路准备完毕*/
    for (;;) {
        if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
            break;
        }
    }
    return;
}

void init_keyboard(void)
{
    /* 初始化键盘控制电路*/
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, KBC_MODE);
    return;
}
  • 如果键盘控制电路可以接受CPU的指令了,那么CPU从设备号码0x0064处所读取的数据的倒数第二位应该是0。
  • init_keyboard一边确认是否可以向键盘控制电路传送信息,一边发送模式控制指令。
  • 0x47是鼠标模式。
  • 激活鼠标:
#define KEYCMD_SENDTO_MOUSE        0xd4
#define MOUSECMD_ENABLE            0xf4

void enable_mouse(void){
    /* 激活鼠标*/
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
    return; /* 顺利的话,键盘控制会返回ACK(0xfa)*/
}
  • 这个函数和init_keyboard的区别在于,如果往键盘控制电路发送指令0xd4,下一个数据就会自动发送给鼠标。
  • ACK的发送表示,即使鼠标完全不动,也会产生一个鼠标中断。

从鼠标接受数据

struct FIFO8 mousefifo;

void inthandler2c(int *esp){
/* 之前提到过鼠标的INT 0x0c*/
    unsigned char data;
    io_out8(PIC1_OCW2, 0x64);   /* 通知PIC1 IRQ-12的受理已经完成*/
    io_out8(PIC0_OCW2, 0x62);   /* 通知PIC0 IRQ-02的受理已经完成*/
    data = io_in8(PORT_KEYDAT);
    fifo8_put(&mousefifo, data);
    return;
}
  • 另外,从鼠标获得数据的方法和从键盘获取的方法类似,只是由于鼠标的数据比较多,就把鼠标的缓冲区设定为128 Byte。
    fifo8_init(&keyfifo, 32, keybuf);
    fifo8_init(&mousefifo, 128, mousebuf);

    enable_mouse();

    for (;;) {
        io_cli();
        if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) {
            io_stihlt();
        } else {
            if (fifo8_status(&keyfifo) != 0) {
                i = fifo8_get(&keyfifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484,  0, 16, 15, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
            } else if (fifo8_status(&mousefifo) != 0) {
                i = fifo8_get(&mousefifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }
    }
  • 这样就可以准备好移动鼠标辣!

【自制OS学习笔记|Day 4】字体显示和中断处理

使用点阵字体

  • 可以通过把字符转化成点阵来处理。
  • 字母A为例,可以表示成:

0 0 0 0 0 0 0 0

0 0 0 1 1 0 0 0

0 0 0 1 1 0 0 0

0 0 0 1 1 0 0 0

0 0 0 1 1 0 0 0

0 0 1 0 0 1 0 0

0 0 1 0 0 1 0 0

0 0 1 0 0 1 0 0

0 0 1 0 0 1 0 0

0 1 1 1 1 1 1 0

0 1 0 0 0 0 1 0

0 1 0 0 0 0 1 0

0 1 0 0 0 0 1 0

1 1 1 0 0 1 1 1

0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0

化为16进制,也就是

static char font_A[16] = {
    0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
    0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
}
  • 使用函数将字体显示在屏幕上:
void putfont8(char *vram, int xsize, int x, int y, char c, char *font){
    int i;
    char *p, d;
    for (i = 0; i < 16; ++i){
        p = vram + (y + i) * xsize + x;     // 要输出的位置
        d = font[i];
        if ((d & 0x80) != 0) {p[0] = c;}    // 依次检查d的每一位,如果是1则输出(位运算)
        if ((d & 0x40) != 0) {p[1] = c;}
        if ((d & 0x20) != 0) {p[2] = c;}
        if ((d & 0x10) != 0) {p[3] = c;}
        if ((d & 0x08) != 0) {p[4] = c;}
        if ((d & 0x04) != 0) {p[5] = c;}
        if ((d & 0x02) != 0) {p[6] = c;}
        if ((d & 0x01) != 0) {p[7] = c;}
    }
    return;
}

  • 在本次使用的「hankaku」字体集中,声明数组extern char hankaku[4096];来存储数据,用hankaku + 'A' * 16的方式调用数据。

显示字符串

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s){
    extern char hankaku[4096];
    for (; *s != 0x00; s++) {
        putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
        x += 8;
    }
    return;
}

鼠标

  • 使用同样的方法进行绘制。
  • 需要显示背景色的地方,不对VRAM进行更改即可。
void init_mouse_cursor8(char *mouse, char bc){
    static char cursor[16][16] = {
        "**************..",
        "*OOOOOOOOOOO*...",
        "*OOOOOOOOOO*....",
        "*OOOOOOOOO*.....",
        "*OOOOOOOO*......",
        "*OOOOOOO*.......",
        "*OOOOOOO*.......",
        "*OOOOOOOO*......",
        "*OOOO**OOO*.....",
        "*OOO*..*OOO*....",
        "*OO*....*OOO*...",
        "*O*......*OOO*..",
        "**........*OOO*.",
        "*..........*OOO*",
        "............*OO*",
        ".............***"
    };
    int x, y;

    for (y = 0; y < 16; y++) {
        for (x = 0; x < 16; x++) {
            if (cursor[y][x] == '*') {
                mouse[y * 16 + x] = COL8_000000;
            }
            if (cursor[y][x] == 'O') {
                mouse[y * 16 + x] = COL8_FFFFFF;
            }
            if (cursor[y][x] == '.') {
                mouse[y * 16 + x] = bc;         // 这里的BC是指背景色
            }
        }
    }
    return;
}

void putblock8_8(char *vram, int vxsize, int pxsize,
    int pysize, int px0, int py0, char *buf, int bxsize){
    int x, y;
    for (y = 0; y < pysize; y++) {
        for (x = 0; x < pxsize; x++) {
            vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
        }
    }
    return;
}
  • 只需要这样初始化鼠标即可。
init_mouse_cursor8(mcursor, COL8_008484);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
// binfo是一个结构体指针,存储了来自显卡BIOS的信息,也就是前几天的320*200分辨率等信息。

使用GDT和IDT

分段(segmentation)

  • 考虑汇编语言的ORG指令,问题在于,如果指定ORG 0x1234,但是内存却因为某些原因不能写入0x1234,就会出问题。当进行多任务时,如果两个程序都ORG 0x1234,显然会出现很大的问题(或者类似的位置使得程序的数据互相覆盖),但是似乎并没有出现过这样的问题。
  • 将内存分为很多的块,每个块的起始地址都认为是0x0000,这样每个程序都ORG 0也不会出现问题,这样分割出来的块成为段(segment)。
  • 在32位下,使用[DS:EBX]来指定内存地址的时候,不再是通过DS*16 + EBX的方式,而是用DS所表示的段的起始地址,通过EBX来指定。同时,即使省略段寄存器,仍然是默认为指定了DS,这一点同16位。
  • 使用段的时候需要注意以下信息:段的大小,段的起始地址,段的管理属性(禁止写入,禁止执行,系统专用等)。
  • CPU用8 Byte来存储这些信息,但是即便在32位模式下,用于指定段的寄存器也只有16位。而且因为CPU设计的原因,段寄存器最低3位不能使用,所以范围是0~8191。这样可以通过类似之前调色板的方法来处理。

GDT

  • 可以定义8192个段,每个段的相关信息都是8 Byte,所以需要8192*8=64KB来存储这些信息,显然需要存储在内存中。
  • GDT – global (segment) description table,全局段号记录表。将GDT放在内存中,然后将d对应内存的起始地址和有效设定个数放在CPU的特殊寄存器-GDTR (global (segment) descriptor table register)中。

IDT

  • interrupt descriptor table,中断记录表。当CPU遇到外部状态变化或者内部偶然发生的一些错误时就会临时切换过去处理突发事件,这称作中断功能。
  • IDT记录了0~255的中断号码和对应调用函数的关系。
  • 先设定GDT。
struct SEGMENT_DESCRIPTOR {
    short limit_low, base_low;
    char base_mid, access_right;
    char limit_high, base_high;
};

struct GATE_DESCRIPTOR {
    short offset_low, selector;
    char dw_count, access_right;
    short offset_high;
};

void init_gdtidt(void){
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; // 这个地址只是随便决定的
    struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800; // 这个地址只是随便决定的,但是通过汇编语言的部分这个地方实际上是头文件
    int i;

    /* GDT 初始化 */
    for (i = 0; i < 8192; i++) {
        set_segmdesc(gdt + i, 0, 0, 0);         // 上限、基址、访问权限都设定为0
    }
    set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
    /* 段号为1的段,上限为0xffffffff也就是4GB,地址是0,表示CPU所能管理的全部内存本身。
       段的属性是0x4092,卖个关子*/
    set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
    /* 段号为2的段,大小是512 KB,地址是0x280000,是为了ORG 0的机器语言准备的*/
    load_gdtr(0xffff, 0x00270000);
    /* C语言不能为GDTR赋值,借助汇编语言*/

    /* IDT 初始化 */
    for (i = 0; i < 256; i++) {
        set_gatedesc(idt + i, 0, 0, 0);
    }
    load_idtr(0x7ff, 0x0026f800);

    return;
}

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;
}

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 >> 8) & 0xff;
    gd->access_right = ar & 0xff;
    gd->offset_high  = (offset >> 16) & 0xffff;
    return;
}

【自制OS学习笔记|Day 3】C语言和汇编语言++,VRAM使用和屏幕绘图

用C语言实现内存写入

汇编语言和C语言函数——传参

  • 实现一个向内存中指定位置存放一个8 bit的函数:
_write_mem8:    ; void write_mem8(int addr. int data);
        MOV     ECX,[ESP+4]
        MOV     AL,[ESP+8]
        MOV     [ECX],AL
        RET
  • 这个函数在C语言中的使用方式类似于write_mem8(0x1234, 0x56),在汇编语言的动作相当于MOV BYTE[0x1234],0x56
  • ESP – Extended stack pointer,用于指向栈的栈顶(下一个压入栈的活动记录的顶部),而栈由高地址向低地址成长,函数调用是用入栈的方式传递参数,故在函数处理参数时,ESP+4就是最后一个入栈的参数的地址,ESP+8就是再前一个参数的地址。(划重点)
  • 所以,相当于把第一个参数int addr存放在ECX中,第二个参数int data存放在AL中,然后把AL中的数据存放在[ECX]。
  • 已经进入了32位模式,所以要积极使用32位的寄存器,此时使用16位的寄存器会使机器语言的字节数增加,速度也会变慢。
  • 在与C语言联合使用的时候,部分寄存器可以自由使用,比如EAX、ECX、EDX,有的寄存器只能使用其值而不能改变其值(类似于const),因为它们在C语言编译生成的机器语言中用于记忆非常重要的值。

尝试使用

void io_hlt(void);
void write_mem8(int addr, int data);

void HariMain(void){
    int i;

    for (i = 0xa0000; i <= 0xaffff; ++i){
        write_mem8(i, 15);
    }

    for (;;){
        io_htl();
    }
}
  • 做了什么?
    • 查阅INT 0x10(回忆Day 02),此时的VRAM在0xa0000~0xaffff
    • 在VRAM中全部写入15,也就是白色(注意色彩设置)。

显示条纹图案

  • 代码如下:
    for (i = 0xa000; i <= 0xaffff; ++i){
        write_mem8(i, i & 0x0f);
    }
  • 放一张图吧:

使用指针,告别write_mem8

  • 实际上write_mem8(i, i & 0x0f);就是*i = i & 0x0f。但是这样并不能编译成功,会出现error: invalid type argument of 'unary *'
  • 实际上上面的错误在于进行了这样的操作:MOVE [0x1234],0x56,由前面的笔记可以知道,错误在于没有为[0x1234]指定类型(BYTE,WORD还是DWORD)。

利用强制类型转换完成目标

举出下面两个方法:

p = (char *) 0xa0000;

for (i = 0; i <= 0xffff; ++i){
    *(p + i) = i & 0x0f;
}
p = (char *) 0xa0000;

for (i = 0; i <= 0xffff; ++i){
    p[i] = i & 0x0f;
}

好了可以告别write_mem8了

设置色号

  • 色号可以按照程序员的要求来设定。
  • 本次使用如下色号:
色号 颜色
#000000
#ff0000 亮红
#00ff00 亮绿
#ffff00 亮黄
#0000ff 亮蓝
#ff00ff 亮紫
#00ffff 浅亮蓝
#ffffff
#c6c6c6 亮灰
#840000 暗红
#008400 暗绿
#848400 暗黄
#000084 暗蓝
#840084 暗紫
#008484 暗浅蓝
#848484 暗灰
  • 用C语言写的初始化调色板的函数:
void init_palette(void){
    static unsigned char table_rgb[16 * 3] = {
        0x00, 0x00, 0x00,
        0xff, 0x00, 0x00,
        0x00, 0xff, 0x00,
        0xff, 0xff, 0x00,
        0x00, 0x00, 0xff,
        0xff, 0x00, 0xff,
        0x00, 0xff, 0xff,
        0xff, 0xff, 0xff,
        0xc6, 0xc6, 0xc6,
        0x84, 0x00, 0x00,
        0x00, 0x84, 0x00,
        0x84, 0x84, 0x00,
        0x00, 0x00, 0x84,
        0x84, 0x00, 0x84,
        0x00, 0x84, 0x84,
        0x84, 0x84, 0x84
    };
    set_palette(0, 15, table_rgb);
    return;
}
  • 设置调色板的函数:
void set_palette(int start, int end, unsigned char *rgb){
    int i, eflags;
    eflags = io_load_eflags();
    io_cli();
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++) {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    io_store_eflags(eflags);
    return;
}
  • 两个函数作用等价。

set_palette做了什么

  • 调色板的访问
  • 首先在一连串的访问中屏蔽中断(例如CLI)
  • 将想要设定的调色板号码写入0x03c8,紧接着,按RGB的顺序写入0x03c9。如果还想继续设定下一个调色板,则省略调色板号码,再按照RGB的顺序写入0x03c9。
  • 如果想要读取当前调色板状态,首先将调色板号码写入0x03c7,再从0x03c9读取3次。读取的顺序就是RGB,如果要读取下一个调色板,同样省略调色板号码,按RGB读取。
  • 如果最初执行了CLI,最后要执行STI。

CLI和STI

  • CLI(clear interrupt flag)将中断标志(interrupt flag)置为0,STI(set interrupt flag)将中断标志置为1。CPU遇到中断请求时,若中断标志为1则立即处理中断请求,否则忽略中断请求。

EFLAGS

  • 是由16位寄存器FLAGS拓展而来的32位寄存器。由于没有特定的类似JC、JNC指令,就需要读入EFLAGS,然后判断第九位是0还是1(第九位是中断标志)。

EFLAGS

你没看错,第0个就是进位标志!

  • set_palette需要在设定调色板之前首先执行CLI,结束之后STI。所以做了两个函数io_load_eflags()io_store_eflags()

看代码:

[FORMAT "WCOFF"]                ; 制作目标文件的模式 
[INSTRSET "i486p"]              ; 使用到486的指令
[BITS 32]                       ; 32位模式的机器语言
[FILE "naskfunc.nas"]           ; 源程序文件名

        GLOBAL  _io_hlt, _io_cli, _io_sti, _io_stihlt
        GLOBAL  _io_in8,  _io_in16,  _io_in32
        GLOBAL  _io_out8, _io_out16, _io_out32
        GLOBAL  _io_load_eflags, _io_store_eflags

[SECTION .text]

_io_hlt:    ; void io_hlt(void);
        HLT
        RET

_io_cli:    ; void io_cli(void);
        CLI
        RET

_io_sti:    ; void io_sti(void);
        STI
        RET

_io_stihlt: ; void io_stihlt(void);
        STI
        HLT
        RET

_io_in8:    ; int io_in8(int port);
        MOV     EDX,[ESP+4]     ; port
        MOV     EAX,0
        IN      AL,DX
        RET

_io_in16:   ; int io_in16(int port);
        MOV     EDX,[ESP+4]     ; port
        MOV     EAX,0
        IN      AX,DX
        RET

_io_in32:   ; int io_in32(int port);
        MOV     EDX,[ESP+4]     ; port
        IN      EAX,DX
        RET

_io_out8:   ; void io_out8(int port, int data);
        MOV     EDX,[ESP+4]     ; port
        MOV     AL,[ESP+8]      ; data
        OUT     DX,AL
        RET

_io_out16:  ; void io_out16(int port, int data);
        MOV     EDX,[ESP+4]     ; port
        MOV     EAX,[ESP+8]     ; data
        OUT     DX,AX
        RET

_io_out32:  ; void io_out32(int port, int data);
        MOV     EDX,[ESP+4]     ; port
        MOV     EAX,[ESP+8]     ; data
        OUT     DX,EAX
        RET

_io_load_eflags:    ; int io_load_eflags(void);
        PUSHFD      ; PUSH EFLAGS
        POP     EAX
        RET

_io_store_eflags:   ; void io_store_eflags(int eflags);
        MOV     EAX,[ESP+4]
        PUSH    EAX
        POPFD       ; POP EFLAGS
        RET

来看下新的指令:

  • PUSHFD – push flags double-word,将标志位的值用DWORD压栈。
    • 因为不能执行MOV EAX,EFLAGS,所以用PUSHFD POP EAX,先压栈然后赋值给EAX。
  • POPFD – pop flags double-word,将标志位的值用DWORD出栈。
    • 因为不能执行MOV EFLAGS,EAX,所以用PUSH EAX POPFD,先压栈然后赋值给EAX。

第一个有返回值的函数

  • io_load_eflags是这个笔记中第一个有返回值的函数。
  • 根据C语言的规则,执行RET时,EAX中的值就被认为是函数的返回值。(划重点)

绘制矩形

  • 之前设置的分辨率是320*200,左上点坐标为(0,0),右下点坐标是(319,199),那么像素点(x,y)就在VRAM的这个位置:0xa0000 + x + 320y

绘制矩形的代码:

#define COL8_000000        0
#define COL8_FF0000        1
#define COL8_00FF00        2
#define COL8_FFFF00        3
#define COL8_0000FF        4
#define COL8_FF00FF        5
#define COL8_00FFFF        6
#define COL8_FFFFFF        7
#define COL8_C6C6C6        8
#define COL8_840000        9
#define COL8_008400        10
#define COL8_848400        11
#define COL8_000084        12
#define COL8_840084        13
#define COL8_008484        14
#define COL8_848484        15

void HariMain(void){
    char *p;

    init_palette();

    p = (char *) 0xa0000;

    boxfill8(p, 320, COL8_FF0000,  20,  20, 120, 120);
    boxfill8(p, 320, COL8_00FF00,  70,  50, 170, 150);
    boxfill8(p, 320, COL8_0000FF, 120,  80, 220, 180);

    for (;;) {
        io_hlt();
    }
}

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1){
    int x, y;
    for (y = y0; y <= y1; y++) {
        for (x = x0; x <= x1; x++)
            vram[y * xsize + x] = c;
    }
    return;
}

  • 同样的,经过修改,就可以画出一个假的GUI界面了(逃

【自制OS学习笔记|Day 2】IPL、进入x86以及引入C语言

新使用的指令(x86)

  • JC – jump if carry,如果进位标志是1就跳转。
    • 进位标志(carry flag)是一个只能存储1 bit信息的寄存器,是用来表示是否进位的,因为才CPU中最为简单易用,所以也在别的地方经常用到。
  • JNC – jump if not carry,顾名思义,和JC相反。
  • JAE – jump if above or equal,当大于等于时跳转。
  • JBE – jump if below or euqal,当小于等于时跳转。

IPL

  • Initial Program Loader,启动程序加载器。启动区只有512 Byte,实际的操作系统很大,根本装不进去,所以实际上是把加载操作系统本身的程序放在启动区里。

添加的部分:

        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; 柱面0
        MOV     DH,0            ; 磁头0
        MOV     CL,2            ; 扇区2

        MOV     AH,0x02         ; AH=0x02 : 读盘
        MOV     AL,1            ; 1个扇区
        MOV     BX,0
        MOV     DL,0x00         ; A驱动器
        INT     0x13            ; 调用磁盘BIOS
        JC      error
  • 据查,INT 0x13负责:

    磁盘读、写,扇区校验,以及寻道

    AH=0x02 读盘

    AH=0x03 写盘

    AH=0x04 校验

    AH=0x0c 寻道

    AL=处理对象的扇区数(只能同时处理连续的扇区)

    CH=柱面号&0xff

    CL=扇区号(0~5位)|(柱面号&0x300)>> 2

    DH=磁头号

    ES:BX=缓冲地址(校验及寻道时不使用)

    返回值:

    FLAGS.CF=0,没有错误,AH==0

    FLAGS.CF=1,有错误,错误号存入AH

  • CH、CL、DH、DL分别是柱面号、扇区号、磁头号、驱动器号。这里我们使用了0,0,2,0。
  • 缓冲区地址:这是个内存地址,表明我们要把从软盘读出的数据装载到内存的哪个位置,但是BX只能表示0~0xffff,最大也只有64 K,所以增加了EBX寄存器(32 bit)。但是EBX是很久之后的事情,所以使用了段寄存器。

  • 指定内存地址的时候可以ES:BX,[ES:BX]表示ES*16 + BX的内存地址。

  • 这次我们指定ES=0x0820,BX=0,所以把数据装载到了0x8200到0x83ff的地方。

  • 默认的段寄存器是DS,可以省略,所以昨天的[1234]实际上是[DS:1234],因此DS要先指定为0,否则会引起混乱。

软盘试错

软盘很不靠谱,我们决定当读取出错的时候可以重新读取5次,5次之后仍然失败再放弃读取。

看下新增的部分:

; 读取磁盘

        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; 柱面0
        MOV     DH,0            ; 磁头0
        MOV     CL,2            ; 扇区2

        MOV     SI,0            ; 我们用SI记录失败次数
retry:
        MOV     AH,0x02         ; AH=0x02 : 读入磁盘
        MOV     AL,1            ; 1个扇区
        MOV     BX,0
        MOV     DL,0x00         ; A驱动器
        INT     0x13            ; 调用磁盘BIOS
        JNC     fin             ; 没有出错的话跳转到fin
        ADD     SI,1            ; SI加一
        CMP     SI,5            ; SI和5比较
        JAE     error           ; SI >= 5 跳转到error
        MOV     AH,0x00
        MOV     DL,0x00         ; A驱动去
        INT     0x13            ; 重置驱动器
        JMP     retry

继续读取

; 读取磁盘

        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; 柱面0
        MOV     DH,0            ; 磁头0
        MOV     CL,2            ; 扇区2
readloop:
        MOV     SI,0            ; 记录失败次数
retry:
        MOV     AH,0x02         ; AH=0x02 : 读入磁盘
        MOV     AL,1            ; 1个扇区
        MOV     BX,0
        MOV     DL,0x00         ; A驱动器
        INT     0x13            ; 调用磁盘BIOS
        JNC     fin             ; 没有出错的话跳转到fin
        ADD     SI,1            ; SI加一
        CMP     SI,5            ; SI和5比较
        JAE     error           ; SI >= 5 跳转到error
        MOV     AH,0x00
        MOV     DL,0x00         ; A驱动去
        INT     0x13            ; 重置驱动器
        JMP     retry
next:
        MOV     AX,ES           ; 将内存地址后移0x200
        ADD     AX,0x0020
        MOV     ES,AX           ; 因为没有ADD ES,0x020,这里绕个弯
        ADD     CL,1            ; CL加一
        CMP     CL,18           ; CL和18比较
        JBE     readloop        ; CL <= 18 跳转到readloop
        MOV     CL,1
        ADD     DH,1
        CMP     DH,2
        JB      readloop        ; DH < 2,跳转到readloop
        MOV     DH,0
        ADD     CH,1
        CMP     CH,CYLS
        JB      readloop        ; CH < CYLS,跳转到readloop
  • 现在程序可以把从软盘读入的数据填满内存0x08200~0x34fff了

继续开发

  • 一般向一个空软盘保存文件时,可以发现:
    • 文件名会写在0x002600以后的地方
    • 文件的内容会写在0x004200以后的地方
  • 所以,为了执行磁盘映像上0x004200地址的程序,那么由于磁盘上的内容装载在内存0x8000的位置,我们就应该找0x8000+0x4200=0xc200的位置,用ORG和JMP来操作即可。

准备进入32位模式

  • 注意:在32位下不能调用16位的BIOS功能,先把要用的BIOS写在前面,然后进入32位。(也可以返回但是非常麻烦……)
; BOOT_INFO相关
CYLS    EQU     0x0ff0          ; 设定启动区
LEDS    EQU     0x0ff1
VMODE   EQU     0x0ff2          ; 颜色位数
SCRNX   EQU     0x0ff4          ; X分辨率
SCRNY   EQU     0x0ff6          ; Y分辨率
VRAM    EQU     0x0ff8          ; 图像缓冲区的开始地址

        ORG     0xc200          ; 程序载入内存

        MOV     AL,0x13         ; VGA、320x200x8bit彩色
        MOV     AH,0x00
        INT     0x10
        MOV     BYTE [VMODE],8  ; 记录画面模式
        MOV     WORD [SCRNX],320
        MOV     WORD [SCRNY],200
        MOV     DWORD [VRAM],0x000a0000

; 用BIOS取得键盘上各种LED指示灯的状态

        MOV     AH,0x02
        INT     0x16            ; keyboard BIOS
        MOV     [LEDS],AL

fin:
        HLT
        JMP     fin

另外,在这里,原作者把系统改名为「纸娃娃操作系统」

拥抱C语言

  • 程序入口:
void HariMain(void){
fin:
    /*虽然想在这里使用HTL,但是这是C语言!*/
    goto fin;
}
  • 实现C语言的HLT
[FORMAT "WCOFF"]            ; 制作目标文件的模式
[BITS 32]                   ; 制作32位模式使用机器语言

; 制作目标文件信息

[FILE "naskfun.nas"]        ; 源文件名信息

    GLOBAL  _io_hlt         ; 程序中包含的函数名


; 实际的函数:

[SECTION .text]         ; 目标文件中写了这些之后再写程序

_io_hlt:    ;对应 void io_hlt(void);
        HLT
        RET
  • 用汇编语言写了一个函数,叫做io_hlt。
  • 用汇编写的函数,之后要与obj文件链接,所以也需要编译成obj文件,因此设定输出格式为WCOFF,32位机器语言。
  • 要链接的函数要用GLOBAL声明,并且前面加上_(下划线)用以链接。
  • RET相当于C的return;

现在的bootpack.c:

void io_hlt(void);

void HariMain(void){
fin:
    io_hlt();
    goto fin;
}

【自制OS学习笔记|Day 1】寄存器和指令

部分寄存器简介

CPU里有一种名为寄存器的存储电路,在机器语言中就相当于变量的功能。

下面记录了部分16位的寄存器,它们都可以存储16位的二进制数。

  • AX – accumulator,累加寄存器
  • CX – counter,计数寄存器
  • DX – data,数据寄存器
  • BX – base,基址寄存器
  • SP – stack pointer,栈指针寄存器
  • BP – base pointer,基址指针寄存器
  • SI – source index,源变址寄存器
  • ES – extra segment,附加段寄存器
  • CS – code segment,代码段寄存器
  • SS – stack segment,栈段寄存器
  • DS – data segment,数据段寄存器
  • FS – segment part 2,没有名称
  • GS – segment part 3,没有名称

另外,CPU中还有8个8位寄存器

  • AL – 累加寄存器低位
  • CL – 计数寄存器低位
  • DL – 数据寄存器低位
  • BL – 基址寄存器低位
  • AH – 累加寄存器高位
  • CH – 计数寄存器高位
  • DH – 数据寄存器高位
  • BH – 基址寄存器高位

它们的名字看起来很像,实际上是有原因的:

  • AX寄存器共有16位,其中0到7位称为AL,8到15位称为AH(L为low,H为High,X为Extend,也就是说从8位拓展到16位)
  • BP、SP、SI、DI不能区分H和L,如果必须的话,可以先MOV AX, SI将SI的值赋值给AX,然后用AL、AH取值。

32位寄存器

  • 在16位寄存器的前面加上E(还是Extend……)就是32位寄存器的名称,亦即

    EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

  • 32位中的低16位就是对应的16位寄存器。

  • 如果需要使用高16位,可以先移位,把高16位移位到低16位。

部分指令简介

先看一些今天的成果:

; 分号是用来写注释的
; 向教程原作者致敬,这份代码也会命名为helloos

        ORG     0x7c00          ; 指明程序的装载地址

; 以下这段是标准FAT12格式软盘专用的代码

        JMP     entry
        DB      0x90
        DB      "HELLOIPL"      ; 启动区的名称,可以是任意的字符串,但是必须是8 Byte
        DW      512             ; 每个扇区(sector)的大小,512 Byte
        DB      1               ; 簇(cluster)的大小,1 Sector
        DW      1               ; FAT的起始位置(一般从第一个sector开始)
        DB      2               ; FAT的个数,为2
        DW      224             ; 根目录的大小,一般设置成224项即可
        DW      2880            ; 该磁盘的大小,必须为2880扇区(也就是一张软盘的大小)
        DB      0xf0            ; 磁盘的种类,为0xf0
        DW      9               ; FAT的长度,为9 sector
        DW      18              ; 1个磁道(track)有18个扇区
        DW      2               ; 磁头数,为2
        DD      0               ; 不使用分区,为0
        DD      2880            ; 重写一次磁盘大小
        DB      0,0,0x29        ; 这里不讲它的意义,固定如此
        DD      0xffffffff      ; (可能是)卷标号码
        DB      "HELLO-OS   "   ; 磁盘的名称(必须为11 Byte)
        DB      "FAT12   "      ; 磁盘的格式名称(必须为8 Byte)
        RESB    18              ; 先空出18 Byte

; 程序主体

entry:
        MOV     AX,0            ; 初始化寄存器
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX
        MOV     ES,AX

        MOV     SI,msg
putloop:
        MOV     AL,[SI]
        ADD     SI,1            ; 令SI加一
        CMP     AL,0
        JE      fin
        MOV     AH,0x0e         ; 显示一个文字
        MOV     BX,15           ; 指定字符颜色(暂时没有什么卵用)
        INT     0x10            ; 调用显卡BIOS
        JMP     putloop
fin:
        HLT                     ; 令CPU停止,并等待指令
        JMP     fin             ; 无限循环

msg:
        DB      0x0a, 0x0a      ; 两个换行
        DB      "hello, world"
        DB      0x0a            ; 换行
        DB      0

        RESB    0x7dfe-$        ; 从当前位置填写0x00直到0x001fe

        DB      0x55, 0xaa

; 以下是启动区以外部分的输出

        DB      0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
        RESB    4600
        DB      0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
        RESB    1469432

其中用到了一些指令:

  • DB – define byte,往文件里直接写入1 Byte的指令,有了DB指令,就可以做出任何数据。
  • DW – define word,在这里word是16 Byte的意思,也就是写入16 Byte。
  • DD – define double word,在这里double word是32 Byte。
  • RESB – reserve byte,保留字节,例如 RESB 10就是从现在的地址空出10个Byte,写入0x00。
    • 那么这个RESB 0x1fe-$是什么意思呢?
    • 在这里$是一个变量,它提示我们这一行的字节数(当然还会有别的意思),在这里我们用0x1fe减去$,也就是填写0x00直到0x1fe。
  • MOV – move,赋值,例如MOVE AX,0就是给AX赋值为0,MOV SS,AX就可以理解为C语言的SS = AX;
    • 另外,用中括号的地址是内存中的地址,例如MOV BYTE [678],123的意思是用内存的678位置存放123,BYTE表示存放的大小是BYTE,同理,MOVE WORD [678],123执行的时候,123会被理解为一个16进制的数值,低位的01111011保存在678,高位的00000000保存在679。
    • 在汇编语言里指定内存地址时,要写成:数据大小 [地址]的形式。
    • MOV指令有一个规则,源数据和目的数据必须位数相同,否则会找不到相应的机器语言,编译时会报错。所以可以省略数据大小,写成MOV AL,[SI]的形式。
  • ORG – origin,在开始执行的时候,机器语言会被装载到内存中ORG指定的位置,如果没有它,有几个指令就不能正确地翻译和执行。另外,有了这条命令,$的含义也就随之变化,它不再是指输出文件的第几个Byte,而是代表了将要读入的内存地址。
    • 为什么是0x7c00呢?
    • 根据最初的规定,0x0007c00-0x0007dff是启动区内容的装载地址。
  • JMP – jump,跳转,和对应的标签entry:一起使用,这和C语言的goto很像。
  • ADD,加法,例如ADD SI,1就是SI加1。
  • CMP – compare,比较后面的二者。
  • JE – jump if equal,如果相等就跳转。
  • INT – interrupt,通过BIOS调用一系列操作,例如INT 0x10就是调用16号操作,它的功能是操作显卡。
  • HLT – halt,令CPU停止动作,类似于待机,只要外部发生变化,例如按下键盘,CPU就会醒过来,这里使用HLT是为了避免无限循环浪费电(逃