Advertisement

《30天自制操作系统》 day4 小结

阅读量:

1. 用C语言往内存写入

Naskfunc.nas中增加了一个函数可供C语言调用的函数_write_mem8,用于实现直接写入指定内存地址的语句。
_write_mem8
如果C语言中write_mem8(0x1234,0x56);语句,
则动作上相当于汇编的MOV BYTE[0x1234],0x56

第一个数字在内存里的存放地址[ESP+4] 下一个数字的存放就依次累加4

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

这段代码中还增加了INSTRSET指令,是用来告诉nask这个程序是给486使用的,不然会被默认解释成8086机器使用(偶尔使用)的标签(label)或者常数。最后代码如下:

复制代码
    ; 用彙編語言寫了一個名叫io_hlt的函數,因為之後要與bootpack.obj鏈接,所以也需要編譯成目標文件。因此將輸出格式設定為WCOFF模式,且設定成32位機器語言模式。
    
    ; naskfunc
    ; TAB=4
    
    [FORMAT "WCOFF"]                ; 製作目標文件的模式
    [INSTRSET "i486p"]       ; 告诉nask这个程序是给486使用的
    [BITS 32]                       ; 製作32位模式用的機械語言
    
    ; 製作目標文件的信息
    [FILE "naskfunc.nas"]           ; 源文件名信息
        GLOBAL      _io_hlt,_write_mem8     ; 程序中包含的函數名
    
    ; 以下是實際的函數
    [SECTION .text]                 ; 目標文件中寫了這些之後再寫程序
    
    ; C语言对汇编函数HLT的调用
    _io_hlt:    ; void io_hlt(void);
        HLT
        RET
    
    ; 写入指定内存地址的语句  C语言实现的汇编接口
    ; 这个函数类似于C语言中"write_mem8(0x1234,0x56);"语句,动作上相当于"MOV BYTE[0x1234],0x56"
    _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

话说[INSTRSET "i486p"]这一句的添加位置作者没有详细说明,同时要记得在bootpack.c中的GLOBAL声明中加上新写的函数_write_mem8如下:

复制代码
    GLOBAL      _io_hlt,_write_mem8     ; 程序中包含的函數名

CPU家谱:
8086→80186→286(16)→386(32)→486→Pentium→PentiumPro→PentiumⅡ→PentiumⅢPentium4→……

然后修改bootpack.c里的代码:

复制代码
    /*在下面使用函数前需要先声明函数,相当于告訴C編譯器,有一個函數在別的文件里*/
    void io_hlt(void);
    void write_mem8(int addr, int data);
    
    /*是函數聲明卻不用{ },而用;,這表示的意思是:函數在別的文件中,你自己找一下吧!*/
    
    void HariMain(void)
    {
    int i;              /*变量声明:i是一个32位整数*/
    
    for (int i = 0xa0000; i <= 0xaffff; i++)
    {
        write_mem8(i, 15);  /*MOV BYTE [i],15*/
    }
    
    for(;;) {
        io_hlt();    /*執行naskfunc里的_io_hlt*/
    }
    }

2. 条纹图案

只需要在bootpack.c中修改写入值”15”为”i&0x0f”:

复制代码
    write_mem8(i, i & 0x0f);

对图形来说,0和1并不是作为数字来使用,重点是0和1 的排列方式。对于0和1的互相变化,有位运算”或”(OR)运算、”与”(AND)运算和”异或”(XOR)运算。
简单来说:

1 2 3
OR 有1得1 如 0100 OR 0010 → 0110
AND(&) 同1为1 如 0100 AND 1101 → 0100
XOR 不同得1 如 1010 XOR 0010 → 1000

将写入内存的数值经过&之后每隔16个像素,色号就反复一次,屏幕就能显示条纹了。
4-22

这个效果有点丧病啊,表示眼睛已经花了= =、

3. 挑战指针

前面提到的“C语言中没有直接写入指定内存地址的语句”是因为C语言中有替代这种命令的语句,也就是使用指针。
指针符号是”*”,p中的p是地址,而p是p指向地址的内容。
使用*i = i * 0x0f可直接将i*0x0f写入i指向的内存地址中。
*i = i * 0x0f对应汇编的MOV [i], ( i * 0x0f),但如果直接这样写就不清楚[i]到底是BYTE还是WORD还是DWORD。
由于MOV指令的两个对象必须是相同字节长度,即同类型(BYTE/WORD/DWORD),除非另一方是寄存器才可以省略。同理,在使用指针时需要事先声明它的类型,即指针所指向内容的类型。
char i是类似AL的1字节变量,short i是类似于AX的2字节变量,int i是类似于EAX的4字节变量。

char p ; / 用于BYTE类地址 * /
short p; / 用于 WORD 类 地 址* /
int p ; / 用于DWORD 类 地 址 * /

以上指针中的p都是4字节,因为p是用于记录地址的变量。在汇编语言中,地址也像ECX一样,用4字节的寄存器来指定,所以也是4字节。

p = i; /带入地址 /
_p = i & 0x0f; /_这可以替代write_mem8(i, i&0x0f)*/

在执行make run之后出现了“warning: assignment makes pointer from integer without a cast ”这句话
类型错误

在C语言中,不用“内存地址”这个词,而是用“指针”。并且在C语言中,普通数值和表示内存地址的数值被认为是两种不同的东西。如果将普通整数值赋给内存地址变量就会有警告,可以在赋值的时候使用强制类型转换:
p = (char * ) i; /*注意i的类型要和p类型一样*/

作者在接下来的篇幅讲解了指针,解释了指针和汇编语言的对应关系,着重强调了*p不是变量,只有p是变量,所以在变量声明的时候char *p声明的是p。在书中作者希望我们不要把p理解成指针,而要理解成p是地址变量。

4/5. 指针的应用

原代码

复制代码
    char *p;            /*变量p,用于BYTE型地址*/
    for (i = 0xa0000; i <= 0xaffff; i++)
    {
       p = (char *)i;  /*带入地址*/
       *p = i & 0x0f;   /*这可以替代write_mem8(i, i&0x0f)*/
    }

在声明p的时候并没有赋值,它所指的地址实际上是i的值,由i来指定写入内存的地址。

(1)

复制代码
    p = (char *) 0xa0000;  /*给地址变量赋值*/
    for (int i = 0; i <= 0xffff; ++i)
    {
    *(p + i) = i & 0x0f;
    }

在声明p的时候给它赋值为写入内存的起始地址,之后i作为地址增量,由p+i来指定写入内存的地址。

(2)

C语言中,*(p+i)还可以改写成p[i]这种形式:

复制代码
    p = (char *) 0xa0000;  /*给地址变量赋值*/
    for (int i = 0; i <= 0xffff; ++i)
    {
    p[i] = i & 0x0f;
    }

p[i]与*(p + i)意思相同 ,这两者的差距只有前者4个字符,后者6个字符。但是p[i]并不能说是数组,只是一个看起来像是数列的使用了地址变量的省略写法而已。

小惊讶 :加法运算可以交换顺序,于是** (p+i)和(i+p)**,p[i]和i[p]a[2]和2[a] 都是一个意思,这更能说明它们与数组没有关系。

6. 色号设定

接下来要给操作系统化妆啦。16色编号如下:
色号

再根据作者讲解修改完bootpack.c中的代码后,作者以汇编的角度解说table_rgb的声明部分。

RESB指令是“reserve byte”的略写预约字节,如果想要从当前位置向后空出3个字节来,并且填0,就可以用

复制代码
    RESB 3

在RESB 3前面加上地址就变成了:

复制代码
    a:
    RESB 3

与C语言中的char a[3]一个意思。

但是汇编中RESB的内容能够保证是0,但是C语言不能保证,因此需要在这个声明后加上“={…}”,还可以写上数据的初始值。
char a[3] = {1, 2, 3};

复制代码
    Char a[3];
    a[0] = 1;
    a[1] = 2;
    a[2] = 3;

a是表示最初地址的数字,也就是说它被认为是指针。

在之后,作者对几种数组声明以及对它的初始化的方式进行了对比分析。

情况一:

复制代码
    char a[3];
    a[0] = 1;
    a[1] = 2;
    a[2] = 3;

这里a表谁最初地址的数字,被认为是指针。

情况二:

复制代码
    char a[3] = {0x01, 0x02, 0x03};

情况三:

复制代码
    static char a[3] = {0x01, 0x02, 0x03};

这三者消耗的空间依次减少

情况一翻译成汇编语言如下:

复制代码
    a:
    RESB 3
    之后是赋值语句

情况三翻译成汇编语言如下:

复制代码
    a:
    DB 0x01, 0x02, 0x03

而作者在后文讲的

用DB代替RESB指令在C语言中也有类似指令,就是在声明是加上static。

说法容易让人误会,如果按上面那样进行对比就会好理解得多。并且可以将三种情况解析出来的汇编语言进行对比,会发现数组声明加了static和未加时的汇编语言也是有差别的。因为变量声明前面有static之后它在内存中的存储位置就变了,并且未初始化的全局静态变量会被程序自动初始化为0。而且在程序运行之前,static变量就会被初始化或者赋值。

CPU如果只与内存相连,则只能完成计算和存储的功能。但CPU还要对键盘输入有响应,要通过网卡从网络取得信息,等等。这些设备会和CPU胡同电信号,为了区别这些设备,要使用设备号码(port)。

向设备发送电信号的是OUT指令;从设备取得电信号的是IN指令。但在C语言中没有与IN和OUT相当的语句,所以需要用汇编语言来做。

关于设备号在http://community.osdev.info/?VGA中有详解,其中的vedio DA converter解释如下:
vedio DA converter

最后代码如下:

复制代码
    void set_palette(int static, 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;
    }

在调色板的访问步骤中的CLI指将中断标志置为0的指令,STI是将这个终端标志置为1的指令。

EFLAGS是一个特别的寄存器,它是由名为FLAGS的16位寄存器扩展而来的32位寄存器。FLAGS是存储进位标志和中断标志等标志的寄存器。进位标志可以通过JC或JNC灯跳转指令来简单判断到底是0还是1.单对于中断标志,没有类似JL或JNI命令,所以只能读入EFLAGS,再检查第九位是0还是1.进位标志是EFLASG的第0位。
EFLAGS

中断处理结束之后需要恢复中断现场,所以需要记住最开始的中断标志,所以io_load_eflags读取最初的eflags值,io_store_eflags恢复原来的值。这些都需要用汇编语言来实现。

而CPU中并没有MOV EAX, EFLAGS之类的指令,能够用来读写EFLAGS的只有PUSHFD(push flags double-word,将标志位的值按双字压入栈)POPFD(pop flags double-word,按双字长将标志位从栈弹出) 指令

复制代码
    _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

运行之后条纹的颜色会有所变化:
确定色号的条纹

7. 绘制矩形

颜色备齐之后可以开始画画了。在当前画面模式中有320x200(=64000)个像素。假设左上点的坐标是(0,0),右下点的坐标是(319319),那么像素坐标(x, y)对应的VRAM地址应按下式计算:

0xa0000 + x + y*320

注意0xa0000这个起始位置和y的系数320。

复制代码
    #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;            /*变量p,用于BYTE型地址*/
    
    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();    /*執行naskfunc里的_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;
    }

出现的#define声明方式是用来表示常数声明,将色号用简单的数字记性标记,便于记忆。

复制代码
 给界面加上任务条

最终bootpack.c的内容如下:

复制代码
    /*在下面使用函数前需要先声明函数,相当于告訴C編譯器,有一個函數在別的文件里*/
    // void write_mem8(int addr, int data);
    
    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);
    
    /*是函數聲明卻不用{ },而用;,這表示的意思是:函數在別的文件中,你自己找一下吧!*/
    
    #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 *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();    /*執行naskfunc里的_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;
    }
    void init_palette(void)
    {
    /*table_rgb的声明*/
    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;
    }

运行结果为:
任务条

全部评论 (0)

还没有任何评论哟~