首先用C语言实现内存写入:
光是成功的让画面黑屏是不够的,还是要往画面上画点什么。首先修改naskfunc.nas。写成这样:
; naskfunc
; TAB=4
[FORMAT "WCOFF"] ; 创建对象文件的模式
[INSTRSET "i486p"] ; 想使用486的命令的记述
[BITS 32] ; 制作32位模式用的机器语言
[FILE "naskfunc.nas"] ; 源文件名称信息
GLOBAL _io_hlt,_write_mem8
[SECTION .text] ; 目标文件中写了这些之后再写程序
_io_hlt: ; void io_hlt(void)
HLT
RET ; 这个相当于C语言中的return,意思是"函数的处理到此结束,返回吧”
_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
按照作者的说法,只要往VRAM里面写点什么就可以了。翻一下书的前面可以知道这个代表了图像缓冲区的开始地址。
不仅如此,还要改变bootpack.c:
void io_hlt(void);
void write_mem8(int addr,int data);
void HariMain(void)
{
int i; //变量声明: 是一个32位整数
for(i=0xa0000;i<=0xaffff;i++)
{
write_mem8(i,15); //MOV BYTE [i],15
}
for(;;)io_hlt();
}
运行起来后出现了一片白:
只是显示出一篇白色并不能让人满意,那就尝试显示一下条纹吧。办法便是修改一下bootpack.c:
void io_hlt(void);
void write_mem8(int addr,int data);
void HariMain(void)
{
int i; //变量声明: 是一个32位整数
for(i=0xa0000;i<=0xaffff;i++)
{
write_mem8(i,i&0x0f); //MOV BYTE [i],15
}
for(;;)io_hlt();
}
虽然没用到,但还是说一下异或运算“A XOR B":
对于某一位,A和B该位的值如果不相同,那么异或运算的结果该位为1,否则就为0
作者认为针对下面这三种语句:
char *p; //用于BYTE类地址
short *p; //用于WORD类地址
int *p; //用于DWORD类地址
在以上三个语句中,指针变量p都是4字节,在汇编语言中,地址是用4字节的变量来指定的,所以也是4字节。
再次改变bootpack.c:
void io_hlt(void);
void HariMain(void)
{
int i; //变量声明: 是一个32位整数
char *p; //变量p,用于BYTE型地址
for(i=0xa0000;i<=0xaffff;i++)
{
p=(char*)i; //代入地址
*p=i&0x0f; //MOV BYTE [i],15
//这可以代替write_mem8(i,i&0x0f)
}
for(;;)io_hlt();
}
运行后能出现彩色条纹:
作者在书中讲解了C语言的指针,以下面两个语句为例:
p=(char*)i;
*p=i & 0x0f;
转化成汇编语言,假设p相当于ECX,写出来就是:
MOV ECX,i
MOV BYTE [ECX],(i & 0x0f)
这两个汇编语句是不一样的,一个是给ECX寄存器赋值,而它的下一句则是给ECX号的内存地址赋值。存储他们的半导体也不一样,一个在CPU里,一个在内存芯片里。
如果把上面的两个C语言语句颠倒过来,也就是写成这样:
*p=i&0x0f
p=(char*)i;
如果照汇编语言来看,这相当于是:
MOV BYTE [ECX],(i&0x0f)
MOV ECX,i
在做第一个MOV的时候,ECX的值不确定,这会导致i&0x0f的结果写入内存的某个未知地址中。
顺带说句,作者说将p[i]写成i[p]也是可以的,试了下,惊了
接下来继续做操作系统的画面,要使用16种颜色,然后,修改bootpack.c:
void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflegs(void);
void io_store_eflegs(int eflegs);
// 就算写在同一个源文件里,如果想在定义前使用,还是必须事先声明一下。
void init_palette(void);
void set_palette(int start, int end, unsigned char *rgh);
void HariMain(void)
{
int i; // 声明变量,变量i是32位整型
char *p; // 变量p是BYTE [...]用的地址
init_palette(); // 设定调色板
p=(char*)0xa0000; // 指定地址
for(i=0;i<=0xffff;i++)p[i]=i&0x0f;
for(;;)io_hlt();
}
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_eflegs(); // 记录中断许可标志的值
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;
}
这个C语言的声明:
char a[3];
其中的a以汇编语言来讲就是标志符,标志符的值就意味着地址。还准备了“RESB 3"(从地址a开始空出3个字节)。也就是上述声明相当于:
a:
RESB 3
在nask中RESB的内容能保证是0,但 C语言中不能保证里面是否有垃圾数据。
char型的变量有3种模式,分别是signed型、unsigned型和未指定型。其中signed型用于处理-128——127的整数,unsigned型能够处理0——255的整数。未指定型是指没有特别指定时,可由编译器决定是unsigned还是signed。
0xff会被误解成-1.
既然CPU与设备相连,那么就有向这些设备发送信号,或者从这些设备取得信号的指令。像设备发送电信号的是OUT指令,从设备获取电信号的是IN指令。在这两种指令中为了区分不同的设备,也要使用设备号码((英文中称为port)。
指令CLI:将中断标志置为0;
指令STI:将中断标志置为1;
CPU遇到中断请求时,如果中断标志为1则立即处理中断请求,如果中断标志为0则忽略中断请求。
书中介绍了EFLAGS这一特别的寄存器。这是由名为FLAGS的16位寄存器扩展而来的32位寄存器。FLAGS是存储进位标志和中断标志等标志的寄存器。对于中断标志,只能读入EFLAGS,再检查第9位是0还是1。顺便说一下,进位标志是EFLAGS的第0位
能够用来读写寄存器EFLAGS的,只有PUSHFD和POPFD指令。前者的意思是将标志位的值按双字长压入栈。后者的意思是按照双字长将标志位从栈弹出。
那么这样一来,
PUSHFD POP EAX
指的是首先将EFLAGS压入栈,再将弹出的值压入EAX。
实质上是取代了下前面这句:
MOV EAX,EFLAGS
另一方面,
PUSH EAX POPFD
相当于:
MOV EFLAGS,EAX
。。。。。。
经过一系列修改后,呈现出了这个样子:
好,进入Day 5部分。
如何实现对字符的显示是一个问题,作者认为字符可以用8*16的长方形像素点阵来表示。8位是一个字节,而一个字符是16个字节。如下图所示:
修改了bootpack.c之后,效果如下:
显示汉字的事情放后面,我们先要能显示字母和数字,照作者所说沿用OSASK的字体。
在C语言中,像这种在源程序以外准备的数据,都要加上extern属性,这样编译器就能够知道他们是外部数据,并在编译时做出相应调整,比如:
extern char hankakul.txt
所谓的字符串是指按顺序排列在内存里,末尾加上0x00而组成的字符编码
关于操作系统开发,作者还说:
能不能显示变量值,对于操作系统的开发影响很大,这是因为程序运行与想象中不一致时,将可以变量的值显示出来是最好的方法。就想Windows的调试器不能对Linux的程序进行调试一样,Windows的调试器也不能对我们的自制操作系统进行调试。
自制操作系统中不能随便用printf函数,但是sprintf函数可以使用,因为sprintf不是按指定格式输出,只是将输出内容作为字符串写在内存中,它能够不使用操作系统的任何功能,只对内存进行操作,所以可以应用于所有操作系统。
关于sprintf函数的格式记号及其含义:
%d: 单纯的十进制数
%5d: 5位十进制数,如果是123,则在前面加上两个空格,变成“ 123”强行达到5位
%05d: 5位十进制数,如果是123,则在前面加上两个0,变成“00123”强行达到5位
%x: 单纯的十六进制数,字母部分用小写
%X: 单纯的十六进制数,字母部分用大写
%5x: 5位十六进制数,如果是456(十进制),则在前面加上两个空格变成“ 1c8",强行达到5位。还有%5X的形式。
%05x: 5位十六进制数,如果是456(十进制),则在前面加上两个0变成“001c8",强行达到5位。还有%05X的形式。