一、今日内容总览

实验编号 实验内容 核心知识点
harib02a 接收启动信息 从 asmhead 写入的固定内存地址读取 vram / xsize / ysize
harib02b 试用结构体 struct BOOTINFO 一次读齐启动信息
harib02c 试用箭头记号 (*binfo).scrnx 简写为 binfo->scrnx
harib02d 显示字符 8×16 点阵字体 + putfont8
harib02e 增加字体 hankaku.txt 编译成目标文件并链接进系统
harib02f 显示字符串 0x00 结尾的字符串 + putfonts8_asc
harib02g 显示变量值 不依赖操作系统的 sprintf
harib02h 显示鼠标指针 16×16 位图指针(效果展示)
harib02i GDT 与 IDT 的初始化 分段、全局段号记录表、中断记录表

二、接收启动信息(harib02a)

asmhead 中定义的屏幕和显存的信息中读取 vramxsizeysize,使画面显示程序在画面模式改变时也可以正常运行。

asmhead 在切换到 32 位模式时,会把启动信息写入内存的固定位置(0x0ff0 附近),C 语言这边只需用对应类型的指针去读即可:

内存地址 内容 类型
0x0ff0 BOOTINFO 结构体起始 ——
0x0ff4 屏幕横向分辨率 scrnx short
0x0ff6 屏幕纵向分辨率 scrny short
0x0ff8 VRAM 起始地址 int

bootpack.c

/* 告诉C编译器,有一个函数在别的文件里 */

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 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_screen(char* vram, int xsize, int ysize);

#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 *vram; 
	int xsize, ysize; 
	short *binfo_scrnx, *binfo_scrny;
	int *binfo_vram;

	init_palette(); 
	binfo_scrnx = (short *) 0x0ff4; 
	binfo_scrny = (short *) 0x0ff6; 
	binfo_vram = (int *) 0x0ff8;

	xsize = *binfo_scrnx; 
	ysize = *binfo_scrny; 
	vram = (char *) *binfo_vram;

	init_screen(vram, xsize, ysize);

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


void init_palette(void)
{
	static unsigned char table_rgb[16*3] = {
		0x00, 0x00, 0x00, /* 0:黑 */ 
		0xff, 0x00, 0x00, /* 1:亮红 */ 
		0x00, 0xff, 0x00, /* 2:亮绿 */ 
		0xff, 0xff, 0x00, /* 3:亮黄 */ 
		0x00, 0x00, 0xff, /* 4:亮蓝 */ 
		0xff, 0x00, 0xff, /* 5:亮紫 */ 
		0x00, 0xff, 0xff, /* 6:浅亮蓝 */ 
		0xff, 0xff, 0xff, /* 7:白 */ 
		0xc6, 0xc6, 0xc6, /* 8:亮灰 */ 
		0x84, 0x00, 0x00, /* 9:暗红 */ 
		0x00, 0x84, 0x00, /* 10:暗绿 */ 
		0x84, 0x84, 0x00, /* 11:暗黄 */ 
		0x00, 0x00, 0x84, /* 12:暗青 */ 
		0x84, 0x00, 0x84, /* 13:暗紫 */ 
		0x00, 0x84, 0x84, /* 14:浅暗蓝 */ 
		0x84, 0x84, 0x84 /* 15:暗灰 */
	};
	set_palette(0, 15, table_rgb);
	return;
	/* C语言中的static char语句只能用于数据,相当于汇编中的DB指令 */
}

void set_palette(int start, int end, unsigned char* rgb)
{
	int i,eflags;
	eflags = io_load_eflags(); 	/* 记录中断许可标志的值*/
	io_cli();						/* 将中断许可标志置为0,禁止中断 */
	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;
}

void init_screen(char* vram, int xsize, int ysize)
{
	boxfill8(vram, xsize, COL8_008484, 0, 0, xsize - 1, ysize - 29); 
	boxfill8(vram, xsize, COL8_C6C6C6, 0, ysize - 28, xsize - 1, ysize - 28); 
	boxfill8(vram, xsize, COL8_FFFFFF, 0, ysize - 27, xsize - 1, ysize - 27); 
	boxfill8(vram, xsize, COL8_C6C6C6, 0, ysize - 26, xsize - 1, ysize - 1); 
	boxfill8(vram, xsize, COL8_FFFFFF, 3, ysize - 24, 59, ysize - 24); 
	boxfill8(vram, xsize, COL8_FFFFFF, 2, ysize - 24, 2, ysize - 4); 
	boxfill8(vram, xsize, COL8_848484, 3, ysize - 4, 59, ysize - 4); 
	boxfill8(vram, xsize, COL8_848484, 59, ysize - 23, 59, ysize - 5); 
	boxfill8(vram, xsize, COL8_000000, 2, ysize - 3, 59, ysize - 3); 
	boxfill8(vram, xsize, COL8_000000, 60, ysize - 24, 60, ysize - 3); 
	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize - 4, ysize - 24); 
	boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize - 4); 
	boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize - 3, xsize - 4, ysize - 3); 
	boxfill8(vram, xsize, COL8_FFFFFF, xsize - 3, ysize - 24, xsize - 3, ysize - 3);

	return;
}

这样写虽然能跑,但每读一个值就要手动造一个指针,比较繁琐,于是就有了下一节的结构体方案。


三、试用结构体(harib02b)

这里非常巧妙地使用了结构体指针,通过 binfo = (struct BOOTINFO *) 0x0ff0; 顺便把结构体中各成员变量的地址都对应到了 asmhead.nas 中保存的值的地址:

struct BOOTINFO { 
	char cyls, leds, vmode, reserve; 
	short scrnx, scrny; 
	char *vram; 
};

void HariMain(void)
{
	char *vram; 
	int xsize, ysize; 
	struct BOOTINFO *binfo;

	init_palette(); 
	binfo =  (struct BOOTINFO *) 0x0ff0;

	xsize = (*binfo).scrnx; 
	ysize = (*binfo).scrny; 
	vram = (*binfo).vram;

	init_screen(vram, xsize, ysize);

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

小提示:原笔记中 ysize = *(*binfo).scrny; 多写了一个解引用符 *,属于笔误,应改为 ysize = (*binfo).scrny;


四、试用箭头记号(harib02c)

在 C 语言里常常会用到 (*binfo).scrnx 这种表现手法,因此出现了一种不使用括号的省略表达方式,即 binfo->scrnx,我们称之为箭头标记方式

使用箭头,可以将 xsize = (*binfo).scrnx; 写成 xsize = binfo->scrnx;,简单又方便。不过我们还想更简洁些,即连变量 xsize 都不用,而是直接以 binfo->scrnx 来代替 xsize

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; 
	init_palette(); 
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);

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

五、显示字符(harib02d)

那么怎么显示字符呢?

字符可以用 8×16 的长方形像素点阵来表示。想象一个下图左边的数据,然后按下图右边所示的方法置换成 0 和 1,这个方法好像不错。然后根据这些数据在画面上打上点就肯定能显示出字符了。8“位”是一个字节,而 1 个字符是 16 个字节。

字符点阵的表示方法:8×16 的 0/1 点阵

/* 其余函数声明同前,略 */

void putfont8(char *vram, int xsize, int x, int y, char c, char *font);
...
...
...
void HariMain(void)
{
	static char font_A[16] = { 
		0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24, 
		0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00 
		};
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; 
	init_palette(); 
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	
	putfont8(binfo->vram, binfo->scrnx, 0, 0, COL8_000000, font_A);

	for(;;){
		io_hlt();
	}
}
...
...
...
void putfont8(char *vram, int xsize, int x, int y, char c, char *font) 
{ 
 int i; 
 char *p, d /* data */; 
 for (i = 0; i < 16; i++) { 
 p = vram + (y + i) * xsize + x; 
 d = font[i]; 
 if ((d & 0x80) != 0) { p[0] = c; } 
 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; 
}

运行结果:

harib02d 运行结果:点阵画出的字符 A


六、增加字体(harib02e)

这节通过 hankaku.txt(保存了字体信息的文本)编译后制成 obj 目标文件,再链接进 haribote.sys 系统文件,来实现 main 函数对字体信息的访问。

构建流程hankaku.txtmakefont.exehankaku.binbin2obj.exehankaku.obj → 链接进 haribote.sys

修改后的 Makefile

TOOLPATH = ../z_tools/
INCPATH  = ../z_tools/haribote/

MAKE     = $(TOOLPATH)make.exe -r
NASK     = $(TOOLPATH)nask.exe
CC1      = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
OBJ2BIM  = $(TOOLPATH)obj2bim.exe
MAKEFONT = $(TOOLPATH)makefont.exe
BIN2OBJ  = $(TOOLPATH)bin2obj.exe
BIM2HRB  = $(TOOLPATH)bim2hrb.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul
EDIMG    = $(TOOLPATH)edimg.exe
IMGTOL   = $(TOOLPATH)imgtol.com
COPY     = copy
DEL      = del

# 默认动作

default :
	$(MAKE) img

# 镜像文件生成

ipl10.bin : ipl10.nas Makefile
	$(NASK) ipl10.nas ipl10.bin ipl10.lst

asmhead.bin : asmhead.nas Makefile
	$(NASK) asmhead.nas asmhead.bin asmhead.lst

bootpack.gas : bootpack.c Makefile
	$(CC1) -o bootpack.gas bootpack.c

bootpack.nas : bootpack.gas Makefile
	$(GAS2NASK) bootpack.gas bootpack.nas

bootpack.obj : bootpack.nas Makefile
	$(NASK) bootpack.nas bootpack.obj bootpack.lst

naskfunc.obj : naskfunc.nas Makefile
	$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst

hankaku.bin : hankaku.txt Makefile
	$(MAKEFONT) hankaku.txt hankaku.bin

hankaku.obj : hankaku.bin Makefile
	$(BIN2OBJ) hankaku.bin hankaku.obj _hankaku

bootpack.bim : bootpack.obj naskfunc.obj hankaku.obj Makefile
	$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
		bootpack.obj naskfunc.obj hankaku.obj
# 3MB+64KB=3136KB

bootpack.hrb : bootpack.bim Makefile
	$(BIM2HRB) bootpack.bim bootpack.hrb 0

haribote.sys : asmhead.bin bootpack.hrb Makefile
	copy /B asmhead.bin+bootpack.hrb haribote.sys

haribote.img : ipl10.bin haribote.sys Makefile
	$(EDIMG)   imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl10.bin len:512 from:0 to:0 \
		copy from:haribote.sys to:@: \
		imgout:haribote.img

# 其他指令

img :
	$(MAKE) haribote.img

run :
	$(MAKE) img
	$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
	$(MAKE) -C ../z_tools/qemu

install :
	$(MAKE) img
	$(IMGTOL) w a: haribote.img

clean :
	-$(DEL) *.bin
	-$(DEL) *.lst
	-$(DEL) *.gas
	-$(DEL) *.obj
	-$(DEL) bootpack.nas
	-$(DEL) bootpack.map
	-$(DEL) bootpack.bim
	-$(DEL) bootpack.hrb
	-$(DEL) haribote.sys

src_only :
	$(MAKE) clean
	-$(DEL) haribote.img

bootpack.c 中通过 extern char hankaku[4096]; 声明字体数据,用 hankaku + 'A' * 16 定位某个字符的点阵(每个字符占 16 字节):

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; 
	extern char hankaku[4096];

	init_palette(); 
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	
	putfont8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, hankaku + 'A' * 16); 
	putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16); 
	putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16); 
	putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16); 
	putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16); 
	putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);

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

运行结果:

harib02e 运行结果:屏幕显示 ABC 123


七、显示字符串(harib02f)

添加下面这个函数,实现字符串的显示。因为在 C 语言中字符串的最后一定是 0x00,可以根据这个特性识别是否结束:

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

main 函数如下:

void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; 
	extern char hankaku[4096];

	init_palette(); 
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	
	putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123"); 
 	putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS."); 
 	putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
	
	for(;;){
		io_hlt();
	}
}

最后两行用(31, 31)画黑色文字、再在(30, 30)画白色文字,一像素之差就做出了带阴影效果的标题,很有意思。

运行结果:

harib02f 运行结果:显示字符串和带阴影的标题


八、显示变量值(harib02g)

这个 sprintf 函数,是本次使用的名为 GO 的 C 编译器附带的函数。它在制作者的精心设计之下能够不使用操作系统的任何功能。或许有人会认为,什么呀,那样的话,怎么不做个 printf 函数呢?这是因为输出字符串的方法,各种操作系统都不一样,不管如何精心设计,都不可避免地要使用操作系统的功能。而 sprintf 不同,它只对内存进行操作,所以可以应用于所有操作系统。

sprintf 函数的使用方法是:sprintf(地址,格式,值,值,值,……)

关于格式的详细说明:

格式 含义
%d 单纯的十进制数
%5d 5 位十进制数。如果是 123,则在前面加上两个空格,变成 " 123",强制达到 5 位
%05d 5 位十进制数。如果是 123,则在前面加上 0,变成 “00123”,强制达到 5 位
%x 单纯的十六进制数,字母部分使用小写 abcdef
%X 单纯的十六进制数,字母部分使用大写 ABCDEF
%5x 5 位十六进制数。如果是 456(十进制),则在前面加上两个空格,变成 " 1c8",强制达到 5 位(还有 %5X 的形式)
%05x 5 位十六进制数。如果是 456(十进制),则在前面加上两个 0,变成 “001c8”,强制达到 5 位(还有 %05X 的形式)
void HariMain(void)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; 
	extern char hankaku[4096];
	char* s;

	init_palette(); 
	init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
	
	sprintf(s, "scrnx = %d", binfo->scrnx);
	putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, s); 
 	
	
	for(;;){
		io_hlt();
	}
}

运行结果:

harib02g 运行结果:屏幕显示 scrnx = 320


九、显示鼠标指针(harib02h)

用 16×16 的位图数据画出鼠标指针:

harib02h 运行结果:屏幕角落的鼠标指针

说明:本篇笔记此处代码未完整记录(原笔记中代码块为空),仅保留效果图。实现思路是用一个 16×16 的字符数组描述指针形状,再写一个 putblock8_8 函数把位图数据整体拷贝到 VRAM 的指定位置。需要的朋友可以参考原书对应小节补全代码。


十、GDT 与 IDT 的初始化(harib02i)

这节为了实现鼠标功能,引入了中断还有分段的概念。

分段:按照自己喜欢的方式,将合计 4GB 的内存分成很多块(block),每一块的起始地址都看作 0 来处理。这很方便,有了这个功能,任何程序都可以先写上一句 ORG 0。像这样分割出来的块,就称为(segment)。

为了表示一个段,需要有以下信息:

  • 段的大小是多少
  • 段的起始地址在哪里
  • 段的管理属性(禁止写入,禁止执行,系统专用等)

GDT 是 “global (segment) descriptor table” 的缩写,意思是全局段号记录表。将这些数据整齐地排列在内存的某个地方,然后将内存的起始地址和有效设定个数放在 CPU 内被称作 GDTR 的特殊寄存器中,设定就完成了。

IDT 是 “interrupt descriptor table” 的缩写,直译过来就是中断记录表。当 CPU 遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件,这就是中断功能。

GDT 的初始化:

/* GDT的初期化 */
for (i = 0; i < 8192; i++) {
	set_segmdesc(gdt + i, 0, 0, 0);
}

段寄存器共 16 位,但是有效的表示段索引的就 13 位——8192 个,通过 for 循环把 GDT 的空间都初始化为 0。

load_gdtr(0xffff, 0x00270000);

GDTR 加载后的值:

  • Limit(界限):0xffff(最大段大小 = 0xffff + 1 = 65536 字节 = 64KB)
  • Base Address(基地址):0x00270000

十一、今日小结

  • 启动信息:asmhead 把 vram / xsize / ysize 等信息放在 0x0ff0 附近的固定内存地址,C 代码用指针(或结构体)直接读取,画面程序就不再依赖硬编码的分辨率了。
  • 结构体与箭头struct BOOTINFO 的内存布局与 asmhead 写入的数据一一对应;(*p).member 等价于 p->member
  • 字体显示:字符用 8×16 点阵表示(1 字符 16 字节);putfont8 按位判断逐点画字,hankaku 字体表通过 makefont + bin2obj 编译链接进内核。
  • 字符串显示:C 字符串以 0x00 结尾,putfonts8_asc 遍历直到结束符,每画一个字符 x += 8
  • sprintf:只操作内存、不依赖操作系统,是裸机环境下格式化输出的好帮手。
  • GDT / IDT:分段把内存切成以 0 为起始地址的段;GDT 记录段的描述信息(大小、起始地址、属性),通过 GDTR 寄存器装载;IDT 用于中断处理,为后面实现鼠标、键盘中断做准备。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐