第4天 c语言与画面显示的练习

发布时间 2023-05-22 20:17:46作者: RainbowMagic

用c语言实现内存写入

只显示黑乎乎的窗口一点意义也没有,我们需要值写入到现存中,以此来让显示器显示一些图像,首先利用汇编语言来定义一个函数,函数名称为_write_mem8,函数接收两个四字节的变量esp+4获取第一个变量的地址,esp+8获取第二个变量的地址,因为每个传过来的变量大小都是四字节,所有+ 4即可,然后将指定地址地址中保持到ecx中,将第二个变量保存到al中,将al值赋值到ecx中。
[INSTRSET 'i386p']表示汇编解析是利用486解析的,如果不指定的,默认为8086,8086使用的16位寄存器,没办法使用eax ecx esp这些32位寄存器。

[INSTRSET 'i386p']
GLOBAL _write_mem8

_write_mem8:
    mov ecx, [esp+4]
    mov al, [esp+8]
    mov [ecx], al
    ret

使用c语言,调用我们在汇编中定义的函数,循环在地址里赋值即可。

void io_hlt(void);

void write_mem8(int addr, int data);

void HariMain(void) {
    int i = 0xa0000;    
    for (; i <= 0xaffff; i++) {
        write_mem8(i, 15);
    }

    fin:
    io_hlt();

    goto fin;
}

这里直接使用指针不调用函数也是可以实现的。

void io_hlt(void);

void write_mem8(int addr, int data);

void HariMain(void) {
    int *p;
    int i = 0xa0000;    
    for (; i <= 0xaffff; i++) {
        p = i;
        *p = 15;
    }

    fin:
    io_hlt();

    goto fin;
}

运行结果如下,整个屏幕都会显示白色的,因为我们将显存地址全部写如了15,而15就代表白色。
img

条纹图案

以下语句也很好理解,就是循环地址为i的地方保存i与0x0f进行与运算值,将i和0xf转成二进制,两边同时进行与运算,如果两边为1结果为1,两边只要有一方为0,那么结果为0.

void io_hlt(void);

void write_mem8(int addr, int data);

void HariMain(void) {
    int i = 0xa0000;    
    for (; i <= 0xaffff; i++) {
        write_mem8(i, i & 0x0f);
    }

    fin:
    io_hlt();

    goto fin;
}

执行结果如下:
img

指针

指针上面例子以及演示过了,无非就是定义指针变量,将指针地址修改为i,然后直接操作指针地址的值为指定值。需要注意的是,以下汇编指令是没办法运行的

mov [0xf212], 21

原因是[0xf212]没有指定地址中存储的数据的自己大小是多少,这样的话将21保存到地址中就没办法保存了,因为21肯定是从地位赋值到高位的,字节大小不知道,肯定就没办法进行赋值了。
指针变量大小为四字节,因为指针变量中存储的就是32位地址,而32位地址刚好就是四字节,那么指针变量大小肯定就是四字节。
char是一字节,表示byte。
short是二字节,表示word。
int是字节表示,表示dword。

色号设定

之前调用bios中断0x19 al = 0x13 ah=00的画面设置为320*200 色号8位。
img
在8位色号中可以设置2^8=255个不同的颜色到调色板中。

void init_palette(void)
{
	// vag调色盘最多只能设置16种不同的颜色
	static unsigned char table_rgb[18 * 3] = {
		0x00, 0x00, 0x00, /*  0:黑 */
		0xff, 0x00, 0x00, /*  1:梁红 */
		0x00, 0xff, 0x00, /*  2:亮绿 */
		0xff, 0xff, 0x00, /*  3:亮黄 */
		0x99, 0xdd, 0xcc, /*  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:暗灰 */
		0xb6, 0xa3, 0xbc, /* 16:dreamer鬃毛颜色 */
        0xFF, 0xC0, 0xCB,

	};

    set_palette(0, 18, table_rgb);
    return;
}

在定义RGB色号需要占用三个字节,书中如果不使用static进行赋值的话,占用内存是48 * 3 * 3个字节数,不知道是怎么得出来的,我觉得使用static和使用普通变量赋值的字节数都是相同的。。大概。上面定义了17个颜色数据,17 * 3 = 51个字节。使用static修饰的遍历在编译期就已经赋值了,
在c语言中如下定义static变量

static char a = 0x7f

那么在汇编中就会以以下方式进行赋值

a: 
    db 0x7f

普通局部变量存储在栈空间中的,函数调用完毕就会被释放掉了,而使用static修饰的变量是存储在静态存储区的,即便是函数执行完毕,变量依旧不会被释放。
如下所示,每次调用static都会修改变量内容
img
普通变量则是分配在栈中,每次调用函数都会重新分配
img

接着说设置调色盘的函数,如果cpu只连接内存而不连接其他设备的话,那么这台电脑只能由计算和存储功能,联网、展示图片什么的功能都没有,我们的电脑肯定不是这样的,因此cpu除了需要与存储设备连接还与其他设备连接,设备与cpu交互通过设备编码来进行交互,例如下面代码的0x03c9和0x03c8,cpu向设备发信息被称为out,cpu拉取设备发送的信息被称为in。
VGA文档信息:https://wiki.osdev.org/VGA_Hardware#VGA_Registers
我们根据设备编码向指定的设备发消息便可以自定义我们的调色盘。如下图所示,0x03c8是我们设置调色盘需要的设备编码。
img

设置调色盘的流程:

  1. 关中断,为了避免在初始化调色盘的过程中被其他服务将程序中断,首先将中断进行屏蔽。
    cli命令是关中断,将中断标志位设置成0.
  2. 以R、G、B的顺序将颜色写入到0x03c8端口中,如下所示这个函数就是在端口中写入调试盘序列,也就是第一个。
	io_out8(0x03c8, start);

接着按R、G、B的顺序将颜色写入到端口0x03c9中,如果想要接着继续设定调色盘,那么可以忽略0x03c8,直接设置0x03c9即可。
文档:https://moddingwiki.shikadi.net/wiki/VGA_Palette
img
io_out8(0x03c9, rgb[0] / 4);的原因是VGA红绿蓝三种颜色,每种颜色各占一个字节,且每个字节只能使用后低6位来表示颜色,值 / 4就相当于右移两位,虽然不知道为什么VGA只能使用6位来设置颜色,网上找资料也没找到,但是确实就是这么设置的。
如下FF二进制是 1111 1111
img
除4则是:
img
3. 设定调色盘完毕后打开中断。
sti命令是开中断,将中断标志位设置成1。当cpu遇到中断时会不会断下来是根据这个标志位来设定的。
io_load_eflags()函数的作用是在关中断前获取下中断标志位的值,在对调色盘设置完毕后再使用io_store_eflags()函数将中断位初始化回去。

void set_palette(int start, int end, unsigned char *rgb) 
{
	int i, eflags;
	// 记录中断寄存器中的值
	eflags = io_load_eflags();
	// 关中断
	io_cli();
	io_out8(0x03c8, start);

	for (i = start; i <= end; i++)
	{
		// 将调色板中记录的RGB值存储到0x03c9地址中
		// 为什么要 / 4 rgb 红绿蓝(R、G、B)都是一个具有 6 位值(从 0 到 63)的字节+ 两个 0)
		// 前两位0不识别 所以要右移两位使得前两位识别
		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;
}

PUSHFD是将标志位寄存器进行压栈
POPFD则是将标志位寄存器出栈
由于不能直接将标志位寄存器的值直接睡得着到eax寄存器中,所有需要先将标志位寄存器进行压栈,在出栈的时候在赋值给eax寄存器中。
stroe也是一样,先将eax寄存器进行压栈,出栈的时候再将标志位寄存器进行出栈操作。

; 
_io_cli:
    cli
    ret

_io_out8:
    mov edx, [esp+4]
    mov al, [esp+8]
    out dx, al
    ret

_io_store_eflags:
    mov eax, [esp + 4]
    push eax
    popfd
    ret

_io_load_eflags:
    pushfd
    pop eax
    ret

画矩形

之前设置的显示模式是320 * 200,横为320是像素,宽为200像素,左上角为0,0的像素的0,下一行1,0则是320,因为每一行320哥像素,Vrarm的算法就是0xa0000 + 320 * y + x。如下面这个方法。

void boxfill8(unsigned char *vram, int xSize, unsigned int color, int x0, int y0, int x1, int y1) {
	int x, y;

	for (x = x0; x <= x1; x++) {
		for (y = y0; y <= y1; y++) {
			*(vram + xSize * y + x) = color;
		}
	}
}

这个方法遍历x到x0,y到y0所有的像素点,并设置我们调色盘设置的指定颜色进行展示。
调用展示如下:

void HariMain(void)
{
    int j = 0;
    int i = 0xa0000;
    init_palette();

	char *p;

	p = (char *) 0xa0000;

	boxfill8(i, 320, 0xf, 70, 150, 120, 172);


fin:
    io_hlt();

    goto fin;
}

img