《30天自制操作系统》笔记-day4

本文最后更新于 2026年7月13日 晚上

Day4

Day3中成功点亮了屏幕,但画面上没有任何内容,只有一个黑屏

C语言内存写入

前置条件

ESP:32位栈指针寄存器,永远指向栈顶的内存地址,也就是寄存器中存储的是栈顶的内存地址

参数,局部变量,返回地址都存在ESP栈内存里面,不是ESP寄存器中

[ESP + 偏移]就是去栈内存中去读取参数

图示:

程序

上图对应的是下面的一段程序

1
2
3
4
5
_write_mem8: ; void write_mem8(int addr, int data);
MOV ECX,[ESP + 4] ; [ESP + 4]中存放的是地址,存到ECX中
MOV AL,[ESP + 8] ; [ESP + 8]中存放的是数据, 存到AL
MOV [ECX],AL
RET

这段程序实际就是把data存储到了内存地址中

因为如果与C语言联用只允许ECX/EAX/EDX这3个寄存器允许自由使用,所以这里使用了ECX

调用

在编译之前还需要在naskfunc.nas上添加一行

1
[INSTRSET "i486p"]  ; 指定程序给80486使用

在bootpack.c文件中调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void io_hlt(void); //声明函数
void write_mem8(int addr, int data);

void HariMain(void){

int i;

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

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

程序解释

在asmhead.nas中我们定义了显存的起始位置,以及屏幕的尺寸

1
2
3
4
; 代码节选
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000

这里for循环起始地址是0xa0000,计算一下X和Y:$320 \times 200 = 64000(FA00)$
所以可以计算一下现在的显存地址$\texttt{0xa0000} + \texttt{0xfa000} - 1 = \texttt{0xaf9ff}$

书中这里用的是i<=0xaffff,从硬件层面VGA图形模式是64KB,也就是说0xaffff直接填满了64KB的显存窗口
但是我这里用的0xaf9fff,只填满了62KB,也就是320x200,后面的操作其实都看不到,可以减少一些无用的操作

write_mem8(i,15);直接把每一个像素点都设置为白色

运行的结果就是显示全白

显示不同样式

既然这里可以显示不同的颜色,我们可以用一些数学变换来画一些花纹,文章写了一个画条纹的程序

代码节选

1
2
3
for(i = 0xa0000; i<= 0xaf9ff; ++i){
write_mem8(i,i & 0xff);
}

i & 0xff这里要解释一下

  • &:与运算,运算规则:两个位都为 1 时结果为 1,否则为 0,示例:1&1=1,1&0=0,0&1=0,0&0=0
  • i&0xff:这个算式其实就和子网划分的原理一样

详解:
虽然地址有20位,但是进行与运算的只有后八位,所以前面的就给省略了,因为不管高位的十二位怎么变化,后八位都始终是0~255循环变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0000 0000
1111 1111
---------
000000000 //色号0

0000 0001
1111 1111
---------
000000001 //色号1

.
.
.
.

1111 1110
1111 1111
---------
1111 1110 //色号254

1111 1111
1111 1111
---------
1111 1111 //色号255

//新的循环

0000 0000
1111 1111
---------
000000000 //色号0

当前程序的颜色输出就是如上这样依次输出
因为屏幕是按行排布,所以颜色一行一行的一次排布

运行结果:

更多

其实如上程序,也可以做一个256个颜色不停闪烁的版本,也只需要修改几段代码
完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void io_hlt(void); //声明函数
void write_mem8(int addr, int data);

void HariMain(void){

int i,j;
while(1){
for(j = 0; j <= 255 ;++j){
for(i = 0xa0000; i<= 0xaf9ff; ++i){
write_mem8(i,j);
}
}
}
for(;;){
io_hlt();
}
}

运行就会看到不停地闪烁的屏幕,如果细看的话会看到有一些会看到横向的条纹/撕裂(扫描线逐行填充的痕迹),其实这也印证了屏幕是按行排列的

指针

write_mem8(i,i & 0xff);这一句代码如果有一点计算机基础的话就可以知道,可以用指针来做代替

但是指针怎么指明内存长度呢
如果我单纯的写一个*i = i & 0xff这个是BYTE,WORD,DWORD中的哪一个?

有一点基础的C语言基础的话就可以知道

  • char:1个字节,也就是BYTE
  • short:2个字节,也就是WORD
  • int:4个字节,也就是DWORD
1
2
3
char *p  //相当于BYTE类地址
short *p //相当于WORD类地址
int *p //相当于DWORD类地址

tips
不带星号的变量:没有地址长度一说,只有存储数据的容器大小,char/short/int类似于AL/AX/EAX
带星号的变量:在 32 位保护模式下,指针变量自身永远占用 4 字节(32 位地址总线),但是他指向的内存地址长度是随数据类型变化的,相当于MOV [ECX],AL,MOV [ECX],AX,MOV [ECX],EAX

使用指针代替write_mem8(i,i & 0xff);,效果其实一样
示例代码:

1
2
3
char *p;
p = i;
*p = i & 0xff;

运行:

书中指针的应用(1) 和 指针的应用(2) 这两章其实实现的功能都是一样的只是写法不一样

  1. 基地址+偏移量
  2. c语言数组形式,其实c语言的数组也是一种指针

色号的设定

现在制作的操作系统颜色只有八位也就是256种颜色,一般我们用来表示颜色的形式就是以0xffffff为例的六位十六进制形式,很明显256种颜色对于六位十六进制很少,但是有一种方法可以用任意一个的编码对应一个颜色,但是不能有二义性,一个编码一个颜色 (一对一)

书的作者取了16个颜色,这十六个颜色对一个简单的操作系统来说没什么问题

编码如下:

编码 颜色
0x000000 黑
0xff0000 亮红
0x00ff00 亮绿
0xffff00 亮黄
0x0000ff 亮蓝
0xff00ff 亮紫
0x00ffff 浅亮蓝
0xffffff 白
0xc6c6c6 亮灰
0x840000 暗红
0x008400 暗绿
0x848400 暗黄
0x000084 暗蓝
0x840084 暗紫
0x008484 浅暗蓝
0x848484 暗灰

颜色相关的函数

1
2
3
4
5
6
7
8
9
10
// 针对色彩的上层代码
void init_palette(void); // 初始化颜色
void set_palette(int start, int end,unsigned char* rgb); // 把颜色输出到调色板

// 上层的代码还需要一些下层的寄存器操作依赖
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);

上层函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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(); // 存储完整eflags寄存器
io_cli(); // 直接关闭所有中断,不允许设置颜色时被打扰
io_out8(0x03c8,start); // 0x03c8索引端口,第几号颜色

for(i = start; i <= end; ++i){

// 0x03c9数据端口,给RGB发送数据,在显卡上形成颜色
io_out8(0x03c9, rgb[0] / 4); // R 硬件 DAC 每个通道只有 6bit (0~63),程序 RGB 是 0~255,除以 4 做值域压缩
io_out8(0x03c9, rgb[1] / 4); // G
io_out8(0x03c9, rgb[2] / 4); // B

// RGB三个元素填完之后0x03c8的索引自动+1

rgb += 3; // 跳到下一个颜色,重新填充
}

io_store_eflags(eflags); // 颜色设置完毕,恢复原来的中断状态
return ;

}

void init_palette(void):初始化颜色,定义每个颜色的编号,并写入显卡

void set_palette(int start, int end,unsigned char* rgb):向显卡写入颜色

  1. 存储完整的中断寄存器,并清除所有所有中断
  2. 通过0x03c8索引端口设置调色板编号
  3. 通过0x03c9数据端口向显卡发送颜色数据(RGB)
  4. 每次发完一个颜色数据,索引就会自动+1
  5. rgb指针向后移3个地址,准备填充下一个颜色
  6. 所有颜色发送完了之后,恢复中断状态

下层函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
_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

_write_mem8: ; void write_mem8(int addr, int data);
MOV ECX,[ESP + 4] ; [ESP + 4]中存放的是地址,存到ECX中
MOV AL,[ESP + 8] ; [ESP + 8]中存放的是数据, 存到AL
MOV [ECX],AL
RET

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

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

_io_in32: ; void 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 AX,[ESP + 8] ; data
OUT DX,AX
RET

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

_io_load_eflags: ; void io_load_eflags(void);
PUSHFD ; push eflags
POP EAX
RET

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

中断标志IF(EFLAGS第九位):

  1. CLI:把IF位设为0,CPU屏蔽所有外部硬件中断
  2. STI:把IF位设为1,CPU开启中断,执行中断服务

其他基本上都是寄存器的赋值,只有POPFD和PUSHFD做了两个栈的操作
因为EFLAGS和EAX之间不能直接复制,所以需要用到栈来做一个转运
io_load_eflags获取当前的状态,就是要先把eflags压进栈,再弹出来,弹出来的值给EAX
io_store_eflags恢复原本状态,就是要先把EAX的值压进栈,再弹出来,弹出来的值给eflags

因为现在我们的调色盘里面只有16个颜色,所以我们只能用0xf去做与运算,运行结果如下

图像绘制

计算公式
$$ 0xa0000 + x + y \times 320$$
$y \times 320$:跳过y行,从第y+1行开始画图
$x$:从第0列开始右移x列

矩形的绘制函数

1
2
3
4
5
6
7
8
9
10
11
void boxfill8(unsigned char *vram, int xsize, unsigned char color, 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] = color;
}
}
return ;

}

$(x_0,y_0),(x_1,y_1)$是主对角线上的两个点,根据两个点可以绘制一个矩形出来

vram[y * xsize + x] = color;就是上述公式的代码体现,从指定的地址开始填充颜色通过循环形成一个矩形

书中调用boxfill8做了三原色矩形的一个示例

绘制任务栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void HariMain(void){

char *vram;
int xsize = 320, ysize = 200;
init_palette();
vram = (char *)0xa0000;

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 - 24, 59, ysize - 4);

// 右下角方格(凹)
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);

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

这里绘制了一个简易的桌面和一个任务栏,任务栏上有两个方格,运行结果如下

总结

Day4 从汇编正式跨入了 C 语言的领域

  1. C 与汇编的互调用:通过栈传递参数([ESP + 偏移]),C 函数调用汇编函数时参数按顺序压栈,汇编侧按偏移读取即可。
  2. 显存直接写入:显存起始地址 0xa0000,320×200 模式下每个像素占 1 字节(256 色),直接向该地址写入色号即可点亮对应像素,这是图形显示的最底层原理。
  3. 指针的本质:char *p 指向的内存操作宽度为 1 字节——指针的类型决定了读写内存时的数据宽度,这对应汇编中 MOV [ECX],AL / AX / EAX 的区别。
  4. 调色板机制:256 色模式下,色号并非直接存储 RGB,而是作为索引查表。通过 0x03c8(索引端口)和 0x03c9(数据端口)向显卡 DAC 写入 RGB 映射,实现任意 256 种颜色的自定义。
  5. 中断控制:操作硬件(如设置调色板)时需要 CLI 关中断保证原子性,完成后 STI 恢复,这是系统编程的基本素养。
  6. 图形绘制公式:vram[y * xsize + x] —— 将二维坐标映射到一维帧缓冲,是后续所有图形绘制的基础。

从”只能点亮像素”到”能画矩形、画任务栏”,Day4 迈出了从裸机到 GUI 的第一步。


《30天自制操作系统》笔记-day4
https://www.ming-ice-tea.top/2026/07/13/《30天自制操作系统》笔记-day4/
作者
Ming
发布于
2026年7月13日
许可协议