《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,计算一下XY:$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开启中断,执行中断服务

其他基本上都是寄存器的赋值,只有POPFDPUSHFD做了两个栈的操作
因为EFLAGSEAX之间不能直接复制,所以需要用到栈来做一个转运
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日
许可协议