《30天自制操作系统》笔记-day4
本文最后更新于 2026年7月13日 晚上
Day4
Day3中成功点亮了屏幕,但画面上没有任何内容,只有一个黑屏
C语言内存写入
前置条件
ESP:32位栈指针寄存器,永远指向栈顶的内存地址,也就是寄存器中存储的是栈顶的内存地址
参数,局部变量,返回地址都存在ESP栈内存里面,不是ESP寄存器中
[ESP + 偏移]就是去栈内存中去读取参数
图示:
程序
上图对应的是下面的一段程序
1 | |
这段程序实际就是把data存储到了内存地址中
因为如果与C语言联用只允许ECX/EAX/EDX这3个寄存器允许自由使用,所以这里使用了ECX
调用
在编译之前还需要在naskfunc.nas上添加一行
1 | |
在bootpack.c文件中调用函数
1 | |
程序解释
在asmhead.nas中我们定义了显存的起始位置,以及屏幕的尺寸
1 | |
这里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 | |
i & 0xff这里要解释一下
&:与运算,运算规则:两个位都为 1 时结果为 1,否则为 0,示例:1&1=1,1&0=0,0&1=0,0&0=0i&0xff:这个算式其实就和子网划分的原理一样
详解:
虽然地址有20位,但是进行与运算的只有后八位,所以前面的就给省略了,因为不管高位的十二位怎么变化,后八位都始终是0~255循环变化
1 | |
当前程序的颜色输出就是如上这样依次输出
因为屏幕是按行排布,所以颜色一行一行的一次排布
运行结果:
更多
其实如上程序,也可以做一个256个颜色不停闪烁的版本,也只需要修改几段代码
完整代码:
1 | |
运行就会看到不停地闪烁的屏幕,如果细看的话会看到有一些会看到横向的条纹/撕裂(扫描线逐行填充的痕迹),其实这也印证了屏幕是按行排列的

指针
write_mem8(i,i & 0xff);这一句代码如果有一点计算机基础的话就可以知道,可以用指针来做代替
但是指针怎么指明内存长度呢
如果我单纯的写一个*i = i & 0xff这个是BYTE,WORD,DWORD中的哪一个?
有一点基础的C语言基础的话就可以知道
char:1个字节,也就是BYTEshort:2个字节,也就是WORDint:4个字节,也就是DWORD
1 | |
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 | |
运行:
书中指针的应用(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 | |
上层函数实现
1 | |
void init_palette(void):初始化颜色,定义每个颜色的编号,并写入显卡
void set_palette(int start, int end,unsigned char* rgb):向显卡写入颜色
- 存储完整的中断寄存器,并清除所有所有中断
- 通过
0x03c8索引端口设置调色板编号 - 通过
0x03c9数据端口向显卡发送颜色数据(RGB) - 每次发完一个颜色数据,索引就会自动+1
- rgb指针向后移3个地址,准备填充下一个颜色
- 所有颜色发送完了之后,恢复中断状态
下层函数的实现
1 | |
中断标志IF(EFLAGS第九位):
CLI:把IF位设为0,CPU屏蔽所有外部硬件中断STI:把IF位设为1,CPU开启中断,执行中断服务
其他基本上都是寄存器的赋值,只有POPFD和PUSHFD做了两个栈的操作
因为EFLAGS和EAX之间不能直接复制,所以需要用到栈来做一个转运io_load_eflags获取当前的状态,就是要先把eflags压进栈,再弹出来,弹出来的值给EAXio_store_eflags恢复原本状态,就是要先把EAX的值压进栈,再弹出来,弹出来的值给eflags
因为现在我们的调色盘里面只有16个颜色,所以我们只能用0xf去做与运算,运行结果如下
图像绘制
计算公式
$$ 0xa0000 + x + y \times 320$$
$y \times 320$:跳过y行,从第y+1行开始画图
$x$:从第0列开始右移x列
矩形的绘制函数
1 | |
$(x_0,y_0),(x_1,y_1)$是主对角线上的两个点,根据两个点可以绘制一个矩形出来
vram[y * xsize + x] = color;就是上述公式的代码体现,从指定的地址开始填充颜色通过循环形成一个矩形
书中调用boxfill8做了三原色矩形的一个示例
绘制任务栏
1 | |
这里绘制了一个简易的桌面和一个任务栏,任务栏上有两个方格,运行结果如下
总结
Day4 从汇编正式跨入了 C 语言的领域
- C 与汇编的互调用:通过栈传递参数(
[ESP + 偏移]),C 函数调用汇编函数时参数按顺序压栈,汇编侧按偏移读取即可。 - 显存直接写入:显存起始地址
0xa0000,320×200 模式下每个像素占 1 字节(256 色),直接向该地址写入色号即可点亮对应像素,这是图形显示的最底层原理。 - 指针的本质:
char *p指向的内存操作宽度为 1 字节——指针的类型决定了读写内存时的数据宽度,这对应汇编中MOV [ECX],AL/AX/EAX的区别。 - 调色板机制:256 色模式下,色号并非直接存储 RGB,而是作为索引查表。通过
0x03c8(索引端口)和0x03c9(数据端口)向显卡 DAC 写入 RGB 映射,实现任意 256 种颜色的自定义。 - 中断控制:操作硬件(如设置调色板)时需要
CLI关中断保证原子性,完成后STI恢复,这是系统编程的基本素养。 - 图形绘制公式:
vram[y * xsize + x]—— 将二维坐标映射到一维帧缓冲,是后续所有图形绘制的基础。
从”只能点亮像素”到”能画矩形、画任务栏”,Day4 迈出了从裸机到 GUI 的第一步。