4.1 用C语言实现内存写入
想要在屏幕上画点东西的话,只要在VRAM里写点什么,但是C语言没有直接写入内存地址的语句(其实有),我们创建一个这种函数。
修改一下前面的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]
在指定内存地址的地方,如果使用16位寄存器指定[CX] 或 [SP] 之类的就会出错,但使用32位寄存器,[ECX]、[ESP]等都OK。基本上没有不能用的寄存器。指定地址时还可以往寄存器里加一个或减一个常数的方式,如上面的 [ESP + 4] 。
与C语言联合使用的话,能自由使用的只有EAX、ECX、EDX这3个,至于其它寄存器只能使用其值,而不能改变其值,因为这些寄存器在C语言编译后生成的机器语言中,用于记忆非常重要的值。
所以上面代码中读取第一个参数(即 MOV ECX, [ESP+4] )中用的是ECX,ECX就是与C语言联合使用时可以自由使用的寄存器,而这句中的ESP则只是使用(读取)其值。
---------------------------------------------------------------------------------
naskfunc.nas 还加了一行 INSTRSET指令,告诉nask这个程序是给486用的,也就是说,EAX会被解释成寄存器,否则会把EAX理解为标签或常数。
[FORMAT "WCOFF"] ; 创建一个目标文件 [INSTRSET "i486p"] ; 要使用486的指令(即这是32位CPU的程序),我猜i486p代表intel 486 program [BITS 32] ; 制作32位的机器码 [FILE "naskfunc.nas"] ; 源文件名
这里虽然写着i486p,但并不是说386不能用,但必须是32位以上CPU,286以下的是16位CPU。
----------------------------------------------------------------------
接下来修改C语言代码,这次导入变量:
void io_hlt(void); void write_mem8(int addr, int data); void HariMain(void) { int i; /* 变量声明,i是一个32位整数*/ //0x0000是显示内存的开始地址,见第二章的内存分布图, for (i = 0xa0000; i <= 0xaffff; i++) { //一共是65536个点,320*200 = 64000
// 1111b = 15 (b 表示 binary 即二进制),这应该是4位色,一共只能表示16种颜色 write_mem8(i, 15); /* MOV BYTE [i],15 */ } for (;;) { io_hlt(); } }
for (;;) {}就是无限循环
上面利用for循环,将内存的0xA0000到0xAFFFF,全部设为了15(白色),其实在asmhead.nas中,分辨率设的是320*200,也就是64000,十六进制也就是FA00, 所以将上面代码上的0xaffff改为0xafa00,启动后也一样是全白屏。如果改为0xa7d00( 0x7D00 = 3200 = 64000/2),则启动画面是半个白屏:如下:
-----------------------------------------------------
-------------------------------------------------------
2.2 条纹图案
上面的代码会显示为全屏白色,修改bootpack.c,显示为垂直条纹图案:
for (i = 0xa0000; i <= 0xaffff; i++) { write_mem8(i, i & 0x0f); /* MOV BYTE [i],15 */ }
& 表示按位与算 ,可将指定位转为0, A & B ,B中的要转换为0的位要设为0,不变的位设为1。IP地址中子网掩码用的就是这个特性。
| 表示按位或运算, 可将指定位转为1,A | B,B中的要转换为1的位要设为1,不变的为设为0。
异或,XOR, 可将指定位反转, A XOR B,B中的要转换位要设为1,不变的位设为0.
i & 0x0f 就是 i & 00001111与运算,也就是 i 的低4位不变,高4位变0,(再高的位因为没有值,也是按0处理的,我理解)。所以,所有的 i 经过运算后:
0xa0000 & 0x0f,就变成了 0x00000 (即0)
0xa0001 & 0x0f ,就变成了 0x00001 (即1)
0xa0002 & 0x0f ,就变成了 0x00002,(即2)
...
0xa000f & 0x0f,就变成了 0x0000f,(即15)
0xa0010 & 0x0f,就变成了 0x00000,(即0)
也就是说都是只有最后一位不变,(这里说的是十六进制,其实0x0f二进制是00001111,对于二进制其实是低4位不变)。最后的结果就是:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 ...
每16个数就循环一次,效果就是:
4.3 挑战指针
write_mem8(i, i & 0x0f);
这句可以换成:
*i = i & 0x0f; //这里i是个地址值,*i在C语言中表示这个地址指向的内存空间,*i=x 就是往这个地址中写入x这个值。
但这样写会报错。因为上面这句C语句与下面这句汇编语句 编译成“机器码”后是等价的:
MOV [i], (i & 0x0f)
MOV [x],y 这样写会报错,因为指定内存[x]时不知道要写入目标内存的数据类型是BYTE、WORD、还是DWORD,只有在后一个操作数y也是寄存器时才能省略类型。
因为不知道[i]是BYTE、WORD、还是DWORD,所以就会出错。
char *p; //变量p是用于内存地址的变量,也就是指针
p里放入与i相同的值,然后执行:
*p = i & 0x0f;
这样C编译器认为 “p是地址专用变量,用于存放char类型的,所以是BYTE,另外:
char i 是BYTE 1字节
short i 是 WORD 2字节
int i 是 DWORD 4字节
因为我们是一个字节一个字节写入内存,所以使用char,也就是BYTE。
但是
char *p 、short *p、int *p 变量p都是4字节,因为这里p是记录地址的变量,在汇编语言中地址像ECX一样用4字节的寄存器来指定。
------------------------------------------------------------
bootpack.c:
void io_hlt(void); void HariMain(void) { int i; /* 变量声明,i是一个32位整数 */ char *p; /* 变量p用于 BYTE型 地址 */ for (i = 0xa0000; i <= 0xaffff; i++) { p = i; /* 代入地址 */ *p = i & 0x0f; /* 这可以替代 write_mem8(i, i & 0x0f); */ } for (;;) { io_hlt(); } }
执行make run 后可以正常运行,但是命令行 中有个警告:
指针是表示内存地址的数值,类型转换是改变数值类型的命令。C语言中不用内存地址,而是用”指针“。在C语言中如果将普通整数值赋给内存地址变量(指针),就会警告,所以可以这样写:
p = (char *) i;
这就对i 进行了类型转换,使之成为了表示内存地址的整数,(其实数值没变,但对C编译器来说,类型不同差别很大),这样就不会再警告了。
现在实现了用C语言写内存功能。write_mem8没用可以删了。(naskfunc.nas)。
学习到72页 to continue...