《30天自制操作系统》 day4 小结
1. 用C语言往内存写入
Naskfunc.nas中增加了一个函数可供C语言调用的函数_write_mem8,用于实现直接写入指定内存地址的语句。

如果C语言中write_mem8(0x1234,0x56);语句,
则动作上相当于汇编的MOV BYTE[0x1234],0x56
第一个数字在内存里的存放地址[ESP+4] 下一个数字的存放就依次累加4
值得注意的是,如果和C语言联合使用的话,有底寄存器能自由使用,有的寄存器不能自由使用。能自由使用的只有EAX、ECX、EDX这三个。至于其他寄存器,只能使用其值而不能改变其值。因为这些寄存器在C语言编译后生成的机器语言中,用于记忆非常重要的值。
这段代码中还增加了INSTRSET指令,是用来告诉nask这个程序是给486使用的,不然会被默认解释成8086机器使用(偶尔使用)的标签(label)或者常数。最后代码如下:
; 用彙編語言寫了一個名叫io_hlt的函數,因為之後要與bootpack.obj鏈接,所以也需要編譯成目標文件。因此將輸出格式設定為WCOFF模式,且設定成32位機器語言模式。
; naskfunc
; TAB=4
[FORMAT "WCOFF"] ; 製作目標文件的模式
[INSTRSET "i486p"] ; 告诉nask这个程序是给486使用的
[BITS 32] ; 製作32位模式用的機械語言
; 製作目標文件的信息
[FILE "naskfunc.nas"] ; 源文件名信息
GLOBAL _io_hlt,_write_mem8 ; 程序中包含的函數名
; 以下是實際的函數
[SECTION .text] ; 目標文件中寫了這些之後再寫程序
; C语言对汇编函数HLT的调用
_io_hlt: ; void io_hlt(void);
HLT
RET
; 写入指定内存地址的语句 C语言实现的汇编接口
; 这个函数类似于C语言中"write_mem8(0x1234,0x56);"语句,动作上相当于"MOV BYTE[0x1234],0x56"
_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
话说[INSTRSET "i486p"]这一句的添加位置作者没有详细说明,同时要记得在bootpack.c中的GLOBAL声明中加上新写的函数_write_mem8如下:
GLOBAL _io_hlt,_write_mem8 ; 程序中包含的函數名
CPU家谱:
8086→80186→286(16)→386(32)→486→Pentium→PentiumPro→PentiumⅡ→PentiumⅢPentium4→……
然后修改bootpack.c里的代码:
/*在下面使用函数前需要先声明函数,相当于告訴C編譯器,有一個函數在別的文件里*/
void io_hlt(void);
void write_mem8(int addr, int data);
/*是函數聲明卻不用{ },而用;,這表示的意思是:函數在別的文件中,你自己找一下吧!*/
void HariMain(void)
{
int i; /*变量声明:i是一个32位整数*/
for (int i = 0xa0000; i <= 0xaffff; i++)
{
write_mem8(i, 15); /*MOV BYTE [i],15*/
}
for(;;) {
io_hlt(); /*執行naskfunc里的_io_hlt*/
}
}
2. 条纹图案
只需要在bootpack.c中修改写入值”15”为”i&0x0f”:
write_mem8(i, i & 0x0f);
对图形来说,0和1并不是作为数字来使用,重点是0和1 的排列方式。对于0和1的互相变化,有位运算”或”(OR)运算、”与”(AND)运算和”异或”(XOR)运算。
简单来说:
| 1 | 2 | 3 |
|---|---|---|
| OR | 有1得1 | 如 0100 OR 0010 → 0110 |
| AND(&) | 同1为1 | 如 0100 AND 1101 → 0100 |
| XOR | 不同得1 | 如 1010 XOR 0010 → 1000 |
将写入内存的数值经过&之后每隔16个像素,色号就反复一次,屏幕就能显示条纹了。

这个效果有点丧病啊,表示眼睛已经花了= =、
3. 挑战指针
前面提到的“C语言中没有直接写入指定内存地址的语句”是因为C语言中有替代这种命令的语句,也就是使用指针。
指针符号是”*”,p中的p是地址,而p是p指向地址的内容。
使用*i = i * 0x0f可直接将i*0x0f写入i指向的内存地址中。
*i = i * 0x0f对应汇编的MOV [i], ( i * 0x0f),但如果直接这样写就不清楚[i]到底是BYTE还是WORD还是DWORD。
由于MOV指令的两个对象必须是相同字节长度,即同类型(BYTE/WORD/DWORD),除非另一方是寄存器才可以省略。同理,在使用指针时需要事先声明它的类型,即指针所指向内容的类型。
char i是类似AL的1字节变量,short i是类似于AX的2字节变量,int i是类似于EAX的4字节变量。
char p ; / 用于BYTE类地址 * /
short p; / 用于 WORD 类 地 址* /
int p ; / 用于DWORD 类 地 址 * /
以上指针中的p都是4字节,因为p是用于记录地址的变量。在汇编语言中,地址也像ECX一样,用4字节的寄存器来指定,所以也是4字节。
p = i; /带入地址 /
_p = i & 0x0f; /_这可以替代write_mem8(i, i&0x0f)*/
在执行make run之后出现了“warning: assignment makes pointer from integer without a cast ”这句话

在C语言中,不用“内存地址”这个词,而是用“指针”。并且在C语言中,普通数值和表示内存地址的数值被认为是两种不同的东西。如果将普通整数值赋给内存地址变量就会有警告,可以在赋值的时候使用强制类型转换:
p = (char * ) i; /*注意i的类型要和p类型一样*/
作者在接下来的篇幅讲解了指针,解释了指针和汇编语言的对应关系,着重强调了*p不是变量,只有p是变量,所以在变量声明的时候char *p声明的是p。在书中作者希望我们不要把p理解成指针,而要理解成p是地址变量。
4/5. 指针的应用
原代码
char *p; /*变量p,用于BYTE型地址*/
for (i = 0xa0000; i <= 0xaffff; i++)
{
p = (char *)i; /*带入地址*/
*p = i & 0x0f; /*这可以替代write_mem8(i, i&0x0f)*/
}
在声明p的时候并没有赋值,它所指的地址实际上是i的值,由i来指定写入内存的地址。
(1)
p = (char *) 0xa0000; /*给地址变量赋值*/
for (int i = 0; i <= 0xffff; ++i)
{
*(p + i) = i & 0x0f;
}
在声明p的时候给它赋值为写入内存的起始地址,之后i作为地址增量,由p+i来指定写入内存的地址。
(2)
C语言中,*(p+i)还可以改写成p[i]这种形式:
p = (char *) 0xa0000; /*给地址变量赋值*/
for (int i = 0; i <= 0xffff; ++i)
{
p[i] = i & 0x0f;
}
p[i]与*(p + i)意思相同 ,这两者的差距只有前者4个字符,后者6个字符。但是p[i]并不能说是数组,只是一个看起来像是数列的使用了地址变量的省略写法而已。
小惊讶 :加法运算可以交换顺序,于是** (p+i)和(i+p)**,p[i]和i[p] ,a[2]和2[a] 都是一个意思,这更能说明它们与数组没有关系。
6. 色号设定
接下来要给操作系统化妆啦。16色编号如下:

再根据作者讲解修改完bootpack.c中的代码后,作者以汇编的角度解说table_rgb的声明部分。
RESB指令是“reserve byte”的略写预约字节,如果想要从当前位置向后空出3个字节来,并且填0,就可以用
RESB 3
在RESB 3前面加上地址就变成了:
a:
RESB 3
与C语言中的char a[3]一个意思。
但是汇编中RESB的内容能够保证是0,但是C语言不能保证,因此需要在这个声明后加上“={…}”,还可以写上数据的初始值。
如char a[3] = {1, 2, 3};
即
Char a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
a是表示最初地址的数字,也就是说它被认为是指针。
在之后,作者对几种数组声明以及对它的初始化的方式进行了对比分析。
情况一:
char a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
这里a表谁最初地址的数字,被认为是指针。
情况二:
char a[3] = {0x01, 0x02, 0x03};
情况三:
static char a[3] = {0x01, 0x02, 0x03};
这三者消耗的空间依次减少
情况一翻译成汇编语言如下:
a:
RESB 3
之后是赋值语句
情况三翻译成汇编语言如下:
a:
DB 0x01, 0x02, 0x03
而作者在后文讲的
用DB代替RESB指令在C语言中也有类似指令,就是在声明是加上static。
说法容易让人误会,如果按上面那样进行对比就会好理解得多。并且可以将三种情况解析出来的汇编语言进行对比,会发现数组声明加了static和未加时的汇编语言也是有差别的。因为变量声明前面有static之后它在内存中的存储位置就变了,并且未初始化的全局静态变量会被程序自动初始化为0。而且在程序运行之前,static变量就会被初始化或者赋值。
CPU如果只与内存相连,则只能完成计算和存储的功能。但CPU还要对键盘输入有响应,要通过网卡从网络取得信息,等等。这些设备会和CPU胡同电信号,为了区别这些设备,要使用设备号码(port)。
向设备发送电信号的是OUT指令;从设备取得电信号的是IN指令。但在C语言中没有与IN和OUT相当的语句,所以需要用汇编语言来做。
关于设备号在http://community.osdev.info/?VGA中有详解,其中的vedio DA converter解释如下:

最后代码如下:
void set_palette(int static, 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;
}
在调色板的访问步骤中的CLI指将中断标志置为0的指令,STI是将这个终端标志置为1的指令。
EFLAGS是一个特别的寄存器,它是由名为FLAGS的16位寄存器扩展而来的32位寄存器。FLAGS是存储进位标志和中断标志等标志的寄存器。进位标志可以通过JC或JNC灯跳转指令来简单判断到底是0还是1.单对于中断标志,没有类似JL或JNI命令,所以只能读入EFLAGS,再检查第九位是0还是1.进位标志是EFLASG的第0位。

中断处理结束之后需要恢复中断现场,所以需要记住最开始的中断标志,所以io_load_eflags读取最初的eflags值,io_store_eflags恢复原来的值。这些都需要用汇编语言来实现。
而CPU中并没有MOV EAX, EFLAGS之类的指令,能够用来读写EFLAGS的只有PUSHFD(push flags double-word,将标志位的值按双字压入栈) 和POPFD(pop flags double-word,按双字长将标志位从栈弹出) 指令
_io_load_eflags: ; int io_load_eflags(void);
PUSHFD ; 指PUSH EFLAGS
POP EAX
RET
_io_store_eflags: ; void io_store_eflags(int eflags);
MOV EAX, [ESP+4]
PUSH EAX
POPFD ; 指POP EFLAGS
RET
运行之后条纹的颜色会有所变化:

7. 绘制矩形
颜色备齐之后可以开始画画了。在当前画面模式中有320x200(=64000)个像素。假设左上点的坐标是(0,0),右下点的坐标是(319319),那么像素坐标(x, y)对应的VRAM地址应按下式计算:
0xa0000 + x + y*320
注意0xa0000这个起始位置和y的系数320。
#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 *p; /*变量p,用于BYTE型地址*/
init_palette(); /*设定调色板*/
p = (char *) 0xa0000; /*将地址赋值进去*/
boxfill8(p, 320, COL8_FF0000, 20, 20, 120, 120);
boxfill8(p, 320, COL8_00FF00, 70, 50, 170, 150);
boxfill8(p, 320, COL8_0000FF, 120, 80, 220, 180);
for(;;) {
io_hlt(); /*執行naskfunc里的_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;
}
出现的#define声明方式是用来表示常数声明,将色号用简单的数字记性标记,便于记忆。
给界面加上任务条
最终bootpack.c的内容如下:
/*在下面使用函数前需要先声明函数,相当于告訴C編譯器,有一個函數在別的文件里*/
// void write_mem8(int addr, int data);
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);
/*是函數聲明卻不用{ },而用;,這表示的意思是:函數在別的文件中,你自己找一下吧!*/
#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;
init_palette(); /*设定调色板*/
vram = (char *) 0xa0000;
xsize = 320;
ysize = 200;
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);
for(;;) {
io_hlt(); /*執行naskfunc里的_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)
{
/*table_rgb的声明*/
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;
}
运行结果为:

