第4天 C语言与画面显示的练习
1 用C语言实现内存写入(harib01a)
昨天我们成功地让画面显示黑屏了,但只做到这一步没什么意思,还是往画面上画点儿什么东西比较有趣。想要画东西的话,只要往VRAM里写点什么就可以了。但是在C语言中又没有直接写入指定内存地址的语句。嗯,真是不方便。所以,我们干脆就创建一个有这种功能的函数。下面就来修改一下naskfunc.nas。
naskfunc.nas里添加的部分
_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
这个函数类似于C语言中的“write_mem8(0x1234,0x56); ”语句,动作上相当于“MOV BYTE[0x1234],0x56”。顺便说一下,addr是address的缩写,在这里用它来表示地址。
■■■■■
在C语言中如果用到了write_mem8函数,就会跳转到_write_mem8。此时参数指定的数字就存放在内存里,分别是:
第一个数字的存放地址:[ESP + 4]
第二个数字的存放地址:[ESP + 8]
第三个数字的存放地址:[ESP + 12]
第四个数字的存放地址:[ESP + 16]
(以下略)
我们想取得用参数指定的数字0x1234或0x56的内容,就用MOV指令读入寄存器。因为CPU已经是32位模式,所以我们积极使用32位寄存器。16位寄存器也不是不能用,但如果用了的话,不只机器语言的字节数会增加,而且执行速度也会变慢,没什么好处。
在指定内存地址的地方,如果使用16位寄存器指定[CX]或[SP]之类的就会出错,但使用32位寄存器,连[ECX]、[ESP]等都OK,基本上没有不能使用的寄存器。真方便。另外,在指定地址时,不光可以指定寄存器,还可以使用往寄存器加一个常数,或者减一个常数的方式。另外说一下,在16位模式下,也能使用这种方式指定,但那时候没有什么地方用得上,所以没有使用。
如果与C语言联合使用的话,有的寄存器能自由使用,有的寄存器不能自由使用,能自由使用的只有EAX、ECX、EDX这3个。至于其他寄存器,只能使用其值,而不能改变其值。因为这些寄存器在C语言编译后生成的机器语言中,用于记忆非常重要的值。因此这次我们只用EAX和ECX。
■■■■■
这次还给naskfunc.nas增加了一行,那就是INSTRSET指令。它是用来告诉nask“这个程序是给486用的哦”, nask见了这一行之后就知道“哦,那见了EAX这个词,就解释成寄存器名”。如果什么都不指定,它就会认为那是为8086这种非常古老的、而且只有16位寄存器的CPU而写的程序,见了EAX这个词,会误解成标签(Label),或是常数。8086那时候写的程序中,曾偶尔使用EAX来做标签,当时也没想到这个单词后来会成为寄存器名而不能再随便使用。
上面虽然写着486用,但并不是说会出现仅能在486中执行的机器语言,这只是单纯的词语解释的问题。所以486用的模式下,如果只使用16位寄存器,也能成为在8086中亦可执行的机器语言。“纸娃娃操作系统”也支持386,所以虽然这里指定的是486,但并不是386中就不能用。可能会有人问,这里的386,486都是什么意思啊?我们来简单介绍一下电脑的CPU(英特尔系列)家谱。
8086→80186→286→386→486→Pentium→PentiumPro→PentiumII→PentiumIII→Pentium4→…
从上面的家谱来看,386已经是非常古老的CPU了。到286为止CPU是16位,而386以后CPU是32位。
■■■■■
现在,汇编这部分已经准备好了,下面来修改C语言吧。这次我们导入了变量。
本次的bootpack.c内容
void io_hlt(void); void write_mem8(int addr, int data); void HariMain(void) { int i; /*变量声明:i是一个32位整数*/ for (i = 0xa0000; i <= 0xaffff; i++) { write_mem8(i, 15); /* MOV BYTE [i],15 */ } for (; ; ) { io_hlt(); } }
for语句是初次登场它是循环语句,会循环执行花括号({})括起来的部分。圆括号(())中写的是循环执行的条件。共有3个条件,各个条件之间以分号(;)隔开,最初一个条件是初始值。所以上文第一个for语句中,把0xa0000赋值给i。任何for语句的初始值设定语句总是要执行,这是C语言的规定。
下一个部分“i <= 0xaffff”是循环条件。for语句会判断是否满足这个条件,如果不满足,就跳出“{}”括起来的循环体部分。另外,这个部分在第一次执行时就要判断,所以,有时候循环体部分有可能一次都得不到执行。不过这次的for语句中,最初的i值是0xa0000,满足条件,所以循环体部分能够被执行。
最后一个部分是“i++”,这是“i=i+1 ”的省略形式,也就是i的值增加1。这个语句在循环体执行完以后肯定要执行一次,然后判断循环条件。
只看文字说明不易于理解,我们写成代码形式来辅助说明。
for(A ; B ; C){D; } 与以下程序等价
A; label: if (B) { D; C; goto label; }
for语句的3个条件,全都可以省略。这种情况下,不做任何初值设定,循环条件永远成立,“{}”内的循环体部分执行完以后,不做任何处理。也就是单纯的无限循环。我们在“io_hlt(); ”处使用了这种循环。
■■■■■
下一步是运行“make run ”还是“make install”呢?两个都可以,但不管执行哪个,画面都不是黑屏,而是白屏。哦?这是怎么回事呢?因为VRAM全部都写入了15,意思是全部像素的颜色都是第15种颜色,而第15种颜色碰巧是纯白,所以画面就成了白色。还是画面上有点什么变化才好。
太好了,成功了!但看不出来……
最初做成的时候,还挺高兴的,但在写这本书的时候,才发觉这是一次失败。纯白的截图放到书里还是一片白,什么都看不出来。