OSASK学习第5天:结构体、文字显示与 GDT/IDT 初始化
文章目录
一、今日内容总览
| 实验编号 | 实验内容 | 核心知识点 |
|---|---|---|
| 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 中定义的屏幕和显存的信息中读取 vram、xsize、ysize,使画面显示程序在画面模式改变时也可以正常运行。
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 个字节。

/* 其余函数声明同前,略 */
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;
}
运行结果:

六、增加字体(harib02e)
这节通过 hankaku.txt(保存了字体信息的文本)编译后制成 obj 目标文件,再链接进 haribote.sys 系统文件,来实现 main 函数对字体信息的访问。
构建流程:hankaku.txt → makefont.exe → hankaku.bin → bin2obj.exe → hankaku.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();
}
}
运行结果:

七、显示字符串(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)画白色文字,一像素之差就做出了带阴影效果的标题,很有意思。
运行结果:

八、显示变量值(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();
}
}
运行结果:

九、显示鼠标指针(harib02h)
用 16×16 的位图数据画出鼠标指针:

说明:本篇笔记此处代码未完整记录(原笔记中代码块为空),仅保留效果图。实现思路是用一个 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 用于中断处理,为后面实现鼠标、键盘中断做准备。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)