第4天 C语言与画面显示的练习
2020.3.28
1. 用C语言实现内存写入
-
projects/04_day下的harib01a下的naskfunc.nas
; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式 [INSTRSET "i486p"] ; 告诉nask,这个程序是给486用的 [BITS 32] ; 制作32位模式用的机器语言 [FILE "naskfunc.nas"] ; 源文件名信息 GLOBAL _io_hlt,_write_mem8 [SECTION .text] _io_hlt: ; void io_hlt(void); HLT RET _write_mem8: ; void write_mem8(int addr, int data); MOV ECX,[ESP+4] ; [ESP+4]中存放的是地址addr,将其读入ECX MOV AL,[ESP+8] ; [ESP+8]中存放的是数据data,将其读入AL MOV [ECX],AL RET
-
wrire_mem8(int addr, int data)
是用于将一个数据data直接写入内存地址addr的函数。(似乎,C语言中的memcpy等函数或指针亦可实现) -
比如,代码
write_mem8(0x1234, 0x56)
就相当于MOV BYTE[0x1234], 0x56
-
调用函数时,参数指定的数字存放在内存中,分别是:
第1个数字的存放地址:
[ESP + 4]
第2个数字存放的地址:[ESP + 8]
……
第n个数字存放的地址:[ESP + 4*n]
-
我们现在是32位模式,因此要尽可能地使用32位的寄存器,因为使用16位的寄存器可能出错。如果和C语言联合使用的话,只有
EAX、ECX、EDX
三个寄存器能自由使用,其余的只能使用其值,但是不能改变其值。 -
[INSTRSET "i486p"]
这句代码是告诉nask,这个程序是给486用的。 -
CPU(英特尔系列家谱):
8086 -> 80186 -> 286 -> 386 -> **486** -> Pentium -> PentiumPro -> PentiumII -> PentiumIII -> Pentium4 -> ……
到286为止的CPU是16位,386以后的是32位。 -
projects/04_day下的harib01a下的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(); } }
-
0xa0000
是图像缓冲区开始的地址,0xaffff
是结束地址。其实页面大小为320 * 200 = 0xfa00
。那么,结束地址其实可以是0xaf9ff
。(至于为什么是0xaffff
,可能是作者预留出来的防止出错吧) -
15的含义:画面的全部像素的颜色都是第15种颜色,第15种颜色是白色。
-
make run
之后QEMU显示纯白的画面。
2. 条形图案(harib01b)
- 将代码
write_mem8(i, 15);
改成write_mem8(i, i & 0x0f);
3. 挑战指针(harib01c)
-
write_mem8(i, i & 0x0f)
相当于代码*i = i & 0x0f
?
会报类型错误。
因为,如果我们在写汇编代码MOV [0x1234], 0x56
时,会出错。这是因为,在指定内存时,不知道是BYTE、WORD还是DWORD。只有在另一方也是寄存器时才能省略。 -
其实,代码
write_mem8(i, i & 0x0f)
相当于汇编代码MOV [i], (i & 0x0f)
,显然我们未指定[i]
是BYTE、WORD还是DWORD。 -
此时,我们只需要声明一个变量p,便可以实现:
char *p; /*变量p是用于内存地址的专用变量*/ *p = i & 0x0f;
上述代码便相当于
write_mem8(i, i & 0x0f)
。这样,C语言编译器便会认为p是地址专用变量,而且时用于存放字符(char)的,所以就是BYTE。 -
拓展
char *p; /*用于BYTE类的地址*/ short *p; /*用于WORD类的地址*/ int *p; /*用于DWORD类的地址*/
-
为什么使用char?
因为我们是一个字节一个字节地写入,所以是char。char i;
是类似于AL的1字节变量;
short i;
是类似于AX的2字节变量;
int i;
是类似于EAX的4字节变量;
不管是char *p
、short *p
、int *p
,变量p都是4字节变量。因为p是用于记录地址的变量,在汇编语言中,地址向ECX一样,用4字节的寄存器来指定。 -
修改projects/04_day下的harib01b的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 = (char *) i;还可以防止C语言编译器报警告。*/ *p = i & 0x0f; /* 上述代码可以代替 write_mem8(i, i & 0x0f); */ } for (;;) { io_hlt(); } }
这样我们便实现了只用C语言写入内存的功能了。
-
只使用类型转换的写法
*((char *) i) = i & 0x0f;
-
深入理解两句代码
p = (char *) i; *p = i & 0x0f;
- 假设p相当于ECX,那么上述代码相当于:
MOV ECX, i MOV BYTE[ECX], (i & 0x0f)
- 它们的区别很明显,一个是给ECX寄存器赋值,一个是给ECX号内存地址赋值。从存储的半导体来看,一个发生在CPU,一个发生在内存芯片。
- *p不是一个变量, p才是变量。
- 假设p相当于ECX,那么上述代码相当于:
-
C语言小知识
char *p
声明的是p,而不是*p。char *p
和char* p
的区别:
在C语言编译时二者没有差别。但是建议写成char *p
。char* p, q;
这样声明的话,p会被认为是指针,而q只会被认为是一个1字节的变量。因而,声明两个地址变量的方式:char *p, *q;
。- 这里,地址变量=指针。
4. 指针的应用(1)(harib01d)
- 其实,bootpack.c还可以修改成如下:
…… p = (char *) 0xa0000; /* 给地址变量赋值 */ for (i = 0; i <= 0xffff; i++) { *(p + i) = i & 0x0f; } ……
5. 指针的应用(2)(harib01e)
-
*(p + i)
等价于p[i]
-
注意:p[i]不是数组!!
6. 色号设定(harib01f)
-
我们使用的是320*200的8位颜色模式,色号使用8位(2进制)数,也就是只能使用0~255的数。一般来讲,我们指定颜色是使用RGB方式,用6位16进制数,也就是24位(2进制)数来指定。
-
这个8位彩色模式,是由程序员随意指定0~255的数字所对应的颜色。比如,25号颜色对应#ffffff。这种方式叫做调色板(palette)。
-
倘若程序员不做任何设定,那么默认的是:0号颜色是#000000, 15号颜色是#ffffff。其余的不清楚了。
-
我们使用OSAKA描绘一个OS画面需要的16种颜色:
-
projects/04_day下的harib01f的rootpack.c:
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); 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_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; }
-
io_hlt、io_cli、io_out8、io_load_eflags、io_store_eflags这些都需要在naskfunc.nas中编写代码。
-
init_palette很简单。我们来看一下set_palette。
-
中间的for循环可以看成是借助函数io_out8往设备里传输数据即可。(io_out8函数的详解先跳过。)
-
调色板的访问步骤
-
CLI和STI
CLI:clear interrupt flag, 将中断标志置为0的指令
STI:set interrupt flag, 将中断标志置为1的指令
当CPU遇到中断请求时,是立即处理中断请求(中断标志为1),还是忽略中断请求(中断标志位0)。 -
EFLAGS寄存器
- 是由名为FLAGS的16位寄存器拓展而来的32位寄存器。
- 存储进位标志和中断标志等标志。
- 进位标志可以通过JC或者JNC等跳转指令来简单地判断到底是0还是1.
- 中断标志没有类似JC或者JNC的指令。所以只能**读入EFLAGS,再检查第9位上是0还是1.(By the way,进位标志是EFLAGS的第0位)
- io_load_eflags的作用就是读取最初的eflags值;o_store_eflags是把最初的eflags值带入到EFLAGS中,这样中断标志位就恢复成原来的样子了。
-
-
projects/04_day下的harib01f的naskfunc.nas:
; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式 [INSTRSET "i486p"] ; 使用486为止的指令 [BITS 32] ; 制作32位模式用的机器语言 [FILE "naskfunc.nas"] ; 源程序文件名 GLOBAL _io_hlt, _io_cli, _io_sti, _io_stihlt GLOBAL _io_in8, _io_in16, _io_in32 GLOBAL _io_out8, _io_out16, _io_out32 GLOBAL _io_load_eflags, _io_store_eflags [SECTION .text] _io_hlt: ; void io_hlt(void); HLT RET _io_cli: ; void io_cli(void); CLI RET _io_sti: ; void io_sti(void); STI RET _io_stihlt: ; void io_stihlt(void); STI HLT RET _io_in8: ; int io_in8(int port); MOV EDX,[ESP+4] ; port MOV EAX,0 IN AL,DX RET _io_in16: ; int io_in16(int port); MOV EDX,[ESP+4] ; port MOV EAX,0 IN AX,DX RET _io_in32: ; int io_in32(int port); MOV EDX,[ESP+4] ; port IN EAX,DX RET _io_out8: ; void io_out8(int port, int data); MOV EDX,[ESP+4] ; port MOV AL,[ESP+8] ; data OUT DX,AL RET _io_out16: ; void io_out16(int port, int data); MOV EDX,[ESP+4] ; port MOV EAX,[ESP+8] ; data OUT DX,AX RET _io_out32: ; void io_out32(int port, int data); MOV EDX,[ESP+4] ; port MOV EAX,[ESP+8] ; data OUT DX,EAX RET _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
-
IN: CPU从设备取得电气信号的指令
-
OUT: CPU向设备发送电信号的指令
-
没有
MOV EAX, EFLAGS
的命令,所以,只能借助栈来实现,PUSHFD和POPFD。 -
PUSHFD: push flags double-word,将标志位按双字长压入栈。
其实,它所做的就是PUSH EFLAGS
。 -
POPFD: push flags double-word,将标志位按双字长弹出栈。
其实,它所做的就是POP EFLAGS
。 -
io_load_eflags代码含义
PUSHFD
:将标志位压入栈;POP EAX
,将栈中的数据(标志位)弹出给EAX。
注意到:这个函数是有返回值的。
根据C语言的规约,执行RET时,EAX中的值就被看做成函数的返回值。 -
io_store_eflags代码含义
MOV EAX,[ESP+4]
:将eflags放入EAX中;PUSH EAX
:将EAX弹入栈中;POPFD
:将栈中的数据弹出,给EFLAGS -
make run
:
条纹的颜色改变了。这证明程序正确。
7. 绘制矩形(harib01g)
-
VRAM画面大小为320*200,那么就有64000个像素点,左上角的坐标为(0, 0),右下角的坐标为(319, 199) .
-
bootpack.c节选
#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; 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(); } } 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; }
-
vram[y * xsize + x] = c;
其中,c代表第几种颜色,调色板我们已经设置好了。 -
make run
:
8. 今天的成果(harib01f)
- harib01f的bootpack.c中的HariMain:
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(); } }
make run
:
有点操作系统的样子了。(不过显得缺少美感)
9. 拓展:画心(harib01i_ex)
- bootpack.c节选
void HariMain(void) { char *vram; int xsize, ysize; init_palette(); vram = (char *) 0xa0000; xsize = 320; ysize = 200; int rand[32] = {254,53,238,59,291,14,43,114,131,184,131,146,76,156,74,42,58,54,263,36,58,55,293,129,246,119,71,34,75,120,147,154}; int i; for (i = 0; i < 16; i ++){ drawheart(vram, xsize, ysize, i, rand[i*2], rand[i*2+1]); } int rand2[32] ={68,149,229,45,213,71,13,12,97,171,299,114,76,41,32,88,99,39,205,0,143,146,277,177,139,177,32,112,221,139,167,31}; for (i = 0; i < 16; i ++){ drawheart(vram, xsize, ysize, i, rand2[i*2], rand2[i*2+1]); } for (;;) { io_hlt(); } } void drawheart(unsigned char *vram, int xsize, int ysize, unsigned char c, int sx, int sy){ const int SIZE_ = 11; int Heart[11][11]= { {0,0,1,0,0,0,0,0,1,0,0}, {0,1,1,1,0,0,0,1,1,1,0}, {1,1,1,1,1,0,1,1,1,1,1}, {1,1,1,1,1,1,1,1,1,1,1}, {0,1,1,1,1,1,1,1,1,1,0}, {0,0,1,1,1,1,1,1,1,0,0}, {0,0,0,1,1,1,1,1,0,0,0}, {0,0,0,0,1,1,1,0,0,0,0}, {0,0,0,0,0,1,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0} }; if (sx > xsize - SIZE_ || sy > ysize - SIZE_){ return ;// do noting } int i, j; for (i = 0; i < SIZE_; i++){ for (j = 0; j < SIZE_; j++){ if (Heart[i][j] == 1){ vram[(sy + i) * xsize + (sx + j)] = c; } else{ } } } return ; }`
make run
10. 题外话:使用MFC画心(harib01i_ex)
-
总进度:87/710, 12%. 2020/3/23 20:46