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

第4天 C语言与画面显示的练习
https://weread.qq.com/web/reader/38732220718ff5cf3877215k182326e0221182be0c5ca23

1 用C语言实现内存写入(harib01a)

想要画东西的话,只要往VRAM里写点什么就可以了。但是在C语言中又没有直接写入指定内存地址的语句。嗯,真是不方便。所以,我们干脆就创建一个有这种功能的函数。下面就来修改一下naskfunc.nas。
_write_mem8:    ; void write_mem8(int addr, int data);
        MOV        ECX,[ESP+4]        ; [ESP+4]存放addr
        MOV        AL,[ESP+8]        ; [ESP+8]存放data
        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,基本上没有不能使用的寄存器。真方便。另外,在指定地址时,不光可以指定寄存器,还可以使用往寄存器加一个常数,或者减一个常数的方式。

如果与C语言联合使用的话,有的寄存器能自由使用,有的寄存器不能自由使用,能自由使用的只有EAX、ECX、EDX这3个。至于其他寄存器,只能使用其值,而不能改变其值。因为这些寄存器在C语言编译后生成的机器语言中,用于记忆非常重要的值。因此这次我们只用EAX和ECX。

这次还给naskfunc.nas增加了一行,那就是INSTRSET指令。它是用来告诉nask“这个程序是给486用的哦”, nask见了这一行之后就知道“哦,那见了EAX这个词,就解释成寄存器名”。如果什么都不指定,它就会认为那是为8086这种非常古老的、而且只有16位寄存器的CPU而写的程序,见了EAX这个词,会误解成标签(Label),或是常数。8086那时候写的程序中,曾偶尔使用EAX来做标签,当时也没想到这个单词后来会成为寄存器名而不能再随便使用。
----
现在,汇编这部分已经准备好了,下面来修改C语言吧。这次我们导入了变量。本次的bootpack.c内容
void io_hlt(void);
void write_mem8(int addr, int data);

void HariMain(void)
{
    int i;
    for(i=0xa0000; i<0xaffff; i++) {
        write_mem8(i, 15); /* MOV BYTE [i],15 */
    }
        
    for(;;){
        io_hlt();
    }
}
画面都不是黑屏,而是白屏。哦?这是怎么回事呢?因为VRAM全部都写入了15,意思是全部像素的颜色都是第15种颜色,而第15种颜色碰巧是纯白,所以画面就成了白色。还是画面上有点什么变化才好
------
2 条纹图案(harib01b)
所以,为了在印成书后能看出效果,我们就显示成有条纹的图案吧。修改也很简单,只要稍微改动一下bootpack.c就可以了。
for(i=0xa0000; i<0xaffff; i++) {
        //write_mem8(i, 15); /* MOV BYTE [i],15 */
        write_mem8(i, i & 0x0f);
    }
哪儿变了呢?是write_mem8那里。地址部分虽然和之前一样,但写入的值由15变成了i &0x0f。        
这次我们用的是“与”(AND)运算。将地址值与0x0f进行“与”运算会怎么样呢?低4位原封保留,而高4位全部都变成0。
就像这样,每隔16个像素,色号就反复一次.出现了下面这种条纹图案。
------
3 挑战指针(harib01c)
这次我们是一个字节一个字节地写入,所以使用了char。既然说到这里,那我们再介绍点相关知识,“char i; ”是类似AL的1字节变量,“short i; ”是类似AX的2字节变量,“int i; ”是类似EAX的4字节变量。而不管是“char *p”,还是“short *p”,还是“int *p”,变量p都是4字节。这是因为p是用于记录地址的变量。在汇编语言中,地址也像ECX一样,用4字节的寄存器来指定,所以也是4字节。
void HariMain(void)
{
    int i;
    char *p; // use for byte
    
    for(i=0xa0000; i<0xaffff; i++) {
        //write_mem8(i, 15); /* MOV BYTE [i],15 */
        //write_mem8(i, i & 0x0f);
        p = i;
        *p = i & 0x0f;
    }
        
    for(;;){
        io_hlt();
    }
}
bootpack.c: In function `HariMain':
bootpack.c:12: warning: assignment makes pointer from integer without a cast
bootpack.c:19:2: warning: no newline at end of file
------
嗯?且慢!仔细看看画面,发现有一行警告。warning: assignment makes pointer from integer without a cast这个警告的意思是说,“赋值语句没有经过类型转换,由整数生成了指针”。其中有两个单词的意思不太明白。类型转换是什么?指针又是什么?
类型转换是改变数值类型的命令。一般不必每次都注意类型转换,但像这次的语句中,如果不明确进行类型转换,C编译器就会每次都发出警告:“喂,是不是写错了?”顺便说一下,cast在英文中的原意是压入模具,让材料成为某种特定的形状。
指针是表示内存地址的数值。C语言中不用“内存地址”这个词,而是用“指针”。在C语言中,普通数值和表示内存地址的数值被认为是两种不同的东西,虽然笔者也觉得它们没什么不同,但也只能接受这种设计思想了。基于这种设计思想,如果将普通整数值赋给内存地址变量,就会有警告。为了避免这种情况的发生,可以这样写:
p = (char *)i;
这就对i进行了类型转换,使之成为表示内存地址的整数。(其实这样转换以后,数值一点都没变,但对于C编译器来说,类型的不同有着很大的差别。)以后再进行这样的赋值时,就不会出现这种讨厌的警告了。于是我们这样修改一下。

p不是指针,而是地址变量。不要使用“p是指针”这种模棱两可的说法,“p是地址变量”这种说法比较好
--------------------
4 指针的应用(1)(harib01d)
char *p = 0xa0000;
    for(i=0;i<0xffff;i++)
        *(p+i) = i & 0x0f;

------------------
5 指针的应用(2)(harib01e)
C语言中,*(p + i)还可以改写成p[i]这种形式,所以以上片段也可以写成这样:
    char *p = 0xa0000;
    for(i=0;i<0xffff;i++){
        //*(p+i) = i & 0x0f;
        p[i] = i & 0x0f;
    }
p[i]与*(p + i )意思完全相同。要是嫌后者太长太麻烦,或者是为了看起来好看就会使用这种写法。在这个例子里,*(p + i )是6个字符,而p[i]只有4个字符。区别只有这一点,所以大家可以根据喜好使用。p[i]不过是一个看起来像数列的使用了地址变量的省略写法而已。

反过来说,也可以将p[0]写成*p,写成指针的形式反倒是节省了2个字符。总之,根据情况,按自己喜欢的方式写就行了。

不是说改变一下写法,地址变量就变成数组了。大家不要被那些劣质的教科书骗了。编译器生成的机器语言也完全一样。这比什么都更能证明,意思没有变化,只是写法不同。

说个题外话,加法运算可以交换顺序,所以将*(p + i)写成*(i + p) 也是可以的。同理,将p[i]写成i[p]也是可以的(可能你会不相信,但这样写既不会出错,也能正常运行)
------------------------
6 色号设定(harib01f)
这次使用的是320× 200的8位颜色模式,色号使用8位(二进制)数,也就是只能使用0~255的数。我想熟悉电脑颜色的人都会知道,这是非常少的。一般说起指定颜色,都是用#ffffff一类的数。这就是RGB(红绿蓝)方式,用6位十六进制数,也就是24位(二进制)来指定颜色。8位数完全不够。那么,该怎么指定#ffffff方式的颜色呢?

这个8位彩色模式,是由程序员随意指定0~255的数字所对应的颜色的。比如说25号颜色对应#ffffff,26号颜色对应#123456等。这种方式就叫做调色板(palette)。
要想描绘一个操作系统模样的画面,只要有以下这16种颜色就足够了。所以这次我们也使用这16种颜色,并给它们编上号码0-15。
所以我们要给bootpack.c添加很多代码。
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;

    /* static char like assembly DB命令相当 */
}

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();    /* 割り込み許可フラグの値を記録する */
    io_cli();                     /* set interrupt 0, clean interrupts */
    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;
}

程序的头部罗列了很多的外部函数名,这些函数必须在naskfunc.nas中写。这有点麻烦,但也没办法。先跳过这一部分,我们来看看主函数HariMain。函数里只是增加了一行调用调色板置置的函数,变更并不是太大。我们接着往下看

正如为了区别不同的内存要使用内存地址一样,在OUT指令和IN指令中,为了区别不同的设备,也要使用设备号码。设备号码在英文中称为port(端口)。port原意为“港口”,这里形象地将CPU与各个设备交换电信号的行为比作了船舶的出港和进港。

在C语言中,没有与IN或OUT指令相当的语句,所以我们只好拿汇编语言来做了。唉,汇编真是关键时刻显身手的语言呀。

如果我们读一读程序的话,就会发现突然蹦出了0x03c8、0x03c9之类的设备号码,这些设备号码到底是如何获得的呢?随意写几个数字行不行呢?这些号码当然不是能随便乱写的。否则,别的什么设备胡乱动作一下,会带来很严重的问题。所以事先必须仔细调查。

调色板的访问步骤。
首先在一连串的访问中屏蔽中断(比如CLI)。
将想要设定的调色板号码写入0x03c8,紧接着,按R, G, B的顺序写入0x03c9。如果还想继续设定下一个调色板,则省略调色板号码,再按照RGB的顺序写入0x03c9就行了。
如果想要读出当前调色板的状态,首先要将调色板的号码写入0x03c7,再从0x03c9读取3次。读出的顺序就是R, G, B。如果要继续读出下一个调色板,同样也是省略调色板号码的设定,按RGB的顺序读出。
如果最初执行了CLI,那么最后要执行STI

首先是CLI和STI。所谓CLI,是将中断标志(interrupt flag)置为0的指令(clearinterrupt flag)。STI是要将这个中断标志置为1的指令(set interrupt flag)。而标志,是指像以前曾出现过的进位标志一样的各种标志,也就是说在CPU中有多种多样的标志。更改中断标志有什么好处呢?正如其名所示,它与CPU的中断处理有关系。当CPU遇到中断请求时,是立即处理中断请求(中断标志为1),还是忽略中断请求(中断标志为0),就由这个中断标志位来设定。        

下面再来介绍一下EFLAGS这一特别的寄存器。这是由名为FLAGS的16位寄存器扩展而来的32位寄存器。FLAGS是存储进位标志和中断标志等标志的寄存器。进位标志可以通过JC或JNC等跳转指令来简单地判断到底是0还是1。但对于中断标志,没有类似的JI或JNI命令,所以只能读入EFLAGS,再检查第9位是0还是1。顺便说一下,进位标志是EFLAGS的第0位。

set_palette中想要做的事情是在设定调色板之前首先执行CLI,但处理结束以后一定要恢复中断标志,因此需要记住最开始的中断标志是什么。所以我们制作了一个函数io_load_eflags,读取最初的eflags值。处理结束以后,可以先看看eflags的内容,再决定是否执行STI,但仔细想一想,也没必要搞得那么复杂,干脆将eflags的值代入EFLAGS,中断标志位就恢复为原来的值了。函数io_store_eflags就是完成这个处理的。

估计不说大家也知道了,CLI也好,STI也好,EFLAGS的读取也好,EFLAGS的写入也好,都不能用C语言来完成。所以我们就努力一下,用汇编语言来写吧。
------------------
我们已经解释了bootpack.c程序,那么现在就来说说naskfunc.nas。
_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

到现在为止的说明,想必大家都已经懂了,尚且需要说明的只有与EFLAGS相关的部分了。如果有“MOV EAX, EFLAGS”之类的指令就简单了,但CPU没有这种指令。能够用来读写EFLAGS的,只有PUSHFD和POPFD指令。

PUSHFD是“push flags double-word”的缩写,意思是将标志位的值按双字长压入栈。其实它所做的,无非就是“PUSH EFLAGS”。POPFD是“pop flagsdouble-word”的缩写,意思是按双字长将标志位从栈弹出。它所做的,就是“POP EFLAGS”。

栈是数据结构的一种,大家暂时只要理解到这个程度就够了。往栈登录数据的动作称为push(推),请想象一下往烤箱里放面包的情景。从栈里取出数据的动作称为pop(弹出)。
也就是说,“PUSHFD POP EAX”,是指首先将EFLAGS压入栈,再将弹出的值代入EAX。所以说它代替了“MOV EAX, EFLAGS”。另一方面,PUSH EAX POPFD正与此相反,它相当于“MOV EFLAGS, EAX”。

最后要讲的是io_load_eflags。它对我们而言,是第一个有返回值的函数的例子,但根据C语言的规约,执行RET语句时,EAX中的值就被看作是函数的返回值,所以这样就可以。

-------------
7 绘制矩形(harib01g)
颜色备齐了,下面我们来画“画”吧。首先从VRAM与画面上的“点”的关系开始说起。在当前画面模式中,画面上有320×200(=64000)个像素。假设左上点的坐标是(0,0),右下点的坐标是(319,199),那么像素坐标(x, y)对应的VRAM地址应按下式计算。
0xa0000+x+y*320
其他画面模式也基本相同,只是0xa0000这个起始地址和y的系数320有些不同。
根据上式计算像素的地址,往该地址的内存里存放某种颜色的号码,那么画面上该像素的位置就出现相应的颜色。这样就画出了一个点。继续增加x的值,循环以上操作,就能画一条长长的水平直线。再向下循环这条直线,就能够画很多的直线,组成一个有填充色的长方形。
根据这种思路,我们制作了函数boxfill8。源程序就是bootpack.c。并且在程序HariMain中,我们不再画条纹图案,而是使用这个函数3次,画3个矩形
#define COL8_000000        0
#define COL8_FF0000        1
#define COL8_00FF00        2
#define COL8_FFFF00        3
......
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;
        }
    }
}

这次新出现了#define声明方式,它用来表示常数声明。要记住哪种色号对应哪种颜色实在太麻烦了,所以为了便于理解,做了以上声明。
----------------------------
8 今天的成果(harib01h)
我们已经努力到现在了,再加最后一把劲儿。这次我们只修改HariMain程序。让我们看看执行结果会是什么样呢?
    vram = (char *)0xa0000;
    xsize = 320;
    ysize = 200;


任务条(task bar)有点大了,这是因为像素数太少的缘故吧。但很有进步,已经有点操作系统的样子了。
现在的haribote.sys是1216字节,大概是1.2KB吧。虽然这个操作系统很小,但已经有这么多功能了

在本机调试下面的任务条显示不正确。< change <=, bug fixed
    int x, y;
    for(y = y0; y <= y1; y++){
        for(x = x0; x <= x1; x++) {
            vram[y*xsize+x] = c;
        }
    }


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值