第5天 结构体、文字显示与GDT/IDT初始化

第5天 结构体、文字显示与GDT/IDT初始化

2020.3.29

1. 接受启动信息(harib02a)

  • 在harib01?中的bootpack.c中xsize、ysize屏幕分辨率等信息是直接写入程序的。正确的获取这些信息的方式是从asmhead.nas先前保存下来的值中获取。

  • 查看asmhead.nas的部分代码:

    ; 有关BOOT_INFO
    CYLS	EQU		0x0ff0			; 设置启动区,从内存中获取CYLS
    LEDS	EQU		0x0ff1
    VMODE	EQU		0x0ff2			; 关于颜色数目的信息。颜色的位数。
    SCRNX	EQU		0x0ff4			; 分辨率X(screen x)
    SCRNY	EQU		0x0ff6			; 分辨率Y(screen y)
    VRAM	EQU		0x0ff8			; 图像缓冲区的开始地址
    
            ORG		0xc200			; 设置程序装载地址。
            MOV		AL,0x13			; VGA显卡,320*200*8位彩色
            MOV		AH,0x00
            INT		0x10
            MOV		BYTE [VMODE],8	; 记录画面模式
            MOV		WORD [SCRNX],320
            MOV		WORD [SCRNY],200
            MOV		DWORD [VRAM],0x000a0000
    
  • 从上述代码红可以得知,分辨率X存放在内存地址0x0ff4中,分辨率Y存放在内存地址0x0ff6中,图像缓冲区的开始地址VRAM存放在内存地址的0x0ff8中。

  • 因此,harib02a的bootpack.c的HariMain函数应该修改成:

    void HariMain(void)
    {
        char *vram; /*注意vram是char类型的指针*/
        int xsize, ysize;
        short *binfo_scrnx, *binfo_scrny;
        int *binfo_vram;
    
        init_palette();
        binfo_scrnx = (short *) 0x0ff4; /*分辨率X*/
        binfo_scrny = (short *) 0x0ff6; /*分辨率Y*/
        binfo_vram = (int *) 0x0ff8; /*图像缓冲区的开始地址*/
        xsize = *binfo_scrnx;
        ysize = *binfo_scrny;
        vram = (char *) *binfo_vram; /*加上(char *)是为了防止出现警告*/
    
        init_screen(vram, xsize, ysize);
    
        for (;;) {
            io_hlt();
        }
    }
    
  • 其中,init_screen(vram, xsize, ysize)是把显示背景的部分独立出来的函数。

    void init_screen(char *vram, int x, int y)
    {
        boxfill8(vram, x, COL8_008484,  0,     0,      x -  1, y - 29);
        boxfill8(vram, x, COL8_C6C6C6,  0,     y - 28, x -  1, y - 28);
        boxfill8(vram, x, COL8_FFFFFF,  0,     y - 27, x -  1, y - 27);
        boxfill8(vram, x, COL8_C6C6C6,  0,     y - 26, x -  1, y -  1);
    
        boxfill8(vram, x, COL8_FFFFFF,  3,     y - 24, 59,     y - 24);
        boxfill8(vram, x, COL8_FFFFFF,  2,     y - 24,  2,     y -  4);
        boxfill8(vram, x, COL8_848484,  3,     y -  4, 59,     y -  4);
        boxfill8(vram, x, COL8_848484, 59,     y - 23, 59,     y -  5);
        boxfill8(vram, x, COL8_000000,  2,     y -  3, 59,     y -  3);
        boxfill8(vram, x, COL8_000000, 60,     y - 24, 60,     y -  3);
    
        boxfill8(vram, x, COL8_848484, x - 47, y - 24, x -  4, y - 24);
        boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y -  4);
        boxfill8(vram, x, COL8_FFFFFF, x - 47, y -  3, x -  4, y -  3);
        boxfill8(vram, x, COL8_FFFFFF, x -  3, y - 24, x -  3, y -  3);
        return;
    }
    

2. 试用结构体(harib02b)

  • 使用结构体后harib02b的bootpack.c的HariMain函数应该修改成:
    struct BOOTINFO {
        char cyls, leds, vmode, reserve;
        short scrnx, scrny;
        char *vram;
    };
    
    void HariMain(void)
    {
        char *vram;
        int xsize, ysize;
        struct BOOTINFO *binfo;
    
        init_palette();
        binfo = (struct BOOTINFO *) 0x0ff0;/*这样写视为防止出现警告*/
        xsize = (*binfo).scrnx;
        ysize = (*binfo).scrny;
        vram = (*binfo).vram;
    
        init_screen(vram, xsize, ysize);
    
        for (;;) {
            io_hlt();
        }
    }
    
  • 进一步理解
    其实,*binfo表示这个内存地址上的12字节的结构体。

3. 试用箭头记号(harib02c)

  • xsize = (*binfo).scrnx 等价于 xsize = binfo->scrnx

4. 显示字符(harib02d)

  • 以前显示字符主要靠调用BIOS函数,但是现在是32位模式,不能再以来BIOS了。

  • 显示字符的方法

  • harib02d下的bootpack.c中的putfont8函数 :

    void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
    {
        int i;
        char *p, d /* data */;
        for (i = 0; i < 16; i++) {
            p = vram + (y + i) * xsize + x;
            d = font[i];
            /*从左到右以此判断d的二进制编码是否为1*/
            if ((d & 0x80) != 0) { p[0] = c; }
            if ((d & 0x40) != 0) { p[1] = c; }
            if ((d & 0x20) != 0) { p[2] = c; }
            if ((d & 0x10) != 0) { p[3] = c; }
            if ((d & 0x08) != 0) { p[4] = c; }
            if ((d & 0x04) != 0) { p[5] = c; }
            if ((d & 0x02) != 0) { p[6] = c; }
            if ((d & 0x01) != 0) { p[7] = c; }
        }
        return;
    }
    
  • harib02d下的bootpack.c中的HariMain函数 :

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
        static char font_A[16] = {
            0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
            0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
        };
    
        init_palette();
        init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
        putfont8(binfo->vram, binfo->scrnx, 10, 10, COL8_FFFFFF, font_A);
    
        for (;;) {
            io_hlt();
        }
    }
    
  • make run

    这样,就在QEMU的左上角大约(10,10)的位置显示了一个‘A’。

5. 增加字体(harib02e)

  • 沿用OSASK的字体数据。字体保存在文件hankaku.txt中,文件节选如下:
    char 0x41
    ........
    ...**...
    ...**...
    ...**...
    ...**...
    ..*..*..
    ..*..*..
    ..*..*..
    ..*..*..
    .******.
    .*....*.
    .*....*.
    .*....*.
    ***..***
    ........
    ........
    
  • 使用hankaku.txt中字体的方法:
    hankaku.txt经过makefont.exe编译器编译生成hankaku.bin文件,再使用工具bin2obj.exe来完成用hankaku.bin生成hankaku.obj文件,这样hankaku.obj就可以与bootpack.c文件进行链接了。(工具bin2obj.exe的功能是将所给的文件自动转化成为目标程序)

写到这里是中午12点多,刚吃完午饭。可能吃坏了东西,辗转厕所与书桌好几次。于是就睡下了,不知道今天的任务能不能完成。


  • 因此,Makefile文件也需要修改。具体的代码不再赘述。

  • 在C语言中使用这种字体,需要加上如下代码:

    extern char hankaku[4096]
    

    像这种在源程序以外准备的数据,都需要加上extern属性。这样C编译器就能知道它是外部数据,并在编译时做出相应调整。
    extern详解:https://baike.baidu.com/item/extern/4443005?fr=aladdin
    extern是计算机语言中的一个关键字,可置于变量或者函数前,以表示变量或者函数的定义在别的文件中。提示编译器遇到此变量或函数时,在其它模块中寻找其定义,另外,extern也可用来进行链接指定。

  • OSASK的字体数据,依照一般的ASCII字符编码,含有256个字符。A的字符编码是0x41,A的字体数据放在开始地址为hankaku + 0x41 * 1616个字节里。0x41刚好是字符A的ASCII码,那么A的字体数据的开始地址可以用C语言写成hankaku + 'A' * 16

  • harib02e下的bootpack.c的HariMain函数:

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
        extern char hankaku[4096]; /*使用外部字体数据hankaku*/
    
        init_palette();
        init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
        putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 'A' * 16); /*putfont8函数最后的参数需要字符起始地址即可*/
        putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16);
        putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16);
        putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16);
        putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16);
        putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);
    
        for (;;) {
            io_hlt();
        }
    }
    
  • make run

6. 显示字符串(harib02f)

  • 显然,上述显示六个字符的代码略显冗长。因此,函数putfont8_asc便是用来显示字符串的。

  • harib02f下的bootpack.c中的putfont8_asc函数:

    void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
    {
        extern char hankaku[4096];
        for (; *s != 0x00; s++) {/*C字符串以'0x00'结尾,结束符\0*/
            putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
            x += 8;
        }
        return;
    }
    

    所谓字符串,就是字符按顺序排列在内存里,并在末尾加上0x00的一段数据。

  • harib02f下的bootpack.c中的HariMain函数

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
    
        init_palette();
        init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
        putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");
        putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
        putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
    
        for (;;) {
            io_hlt();
        }
    }
    
  • make run

    PS:Haribote OS有阴影效果哦。(笑)

7. 显示变量值(harib02g)

  • 显示变量值可以调试我们写的程序。在没有写出我们自己的调试器之前,只能使用这种方式来确认程序的正确性。

  • 使用C语言中sprintf函数可以实现将输出内容作为字符串直接写入内存中。
    但是我们不能使用printf函数,因为printf是指定格式输出,不可避免的需要使用操作系统的功能(我们现在正在做操作系统,因此我们现在还没有操作系统)。
    sprintf函数,是本次使用的名为GO的C语言编译器附带的函数。它可以不使用操作系统的任何功能。sprintf只对内存进行操作,所以适合于任何操作系统。

  • sprintf函数语法 https://baike.baidu.com/item/sprintf/9703430?fr=aladdin

    • 函数声明
      int sprintf(char *string, char *format [,argument,...]);
    • 参数列表
      string-- 这是指向一个字符数组的指针,该数组存储了 C 字符串。
      format-- 这是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是%[flags][width][.precision][length] pecifier
      [argument]…:根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
    • 功能
      把格式化的数据写入某个字符串缓冲区。
    • 返回值
      如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数。
      sprintf 返回以format为格式argument为内容组成的结果被写入string的字节数,结束字符‘\0’不计入内。即,如果“Hello”被写入空间足够大的string后,函数sprintf 返回5。
  • 简单来讲,sprintf和printf差不多。使用方法是:sprintf(内存地址, 格式, 格式对应的值)。这个格式和printf的格式差不多。注意,sprintf可不认识’\n’。

  • 显示变量scrnx的值

    sprintf(s, "scrnx = %d", binfo->scrnx);/*此时,s是指向字符串的指针*/
    putfont8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
    
  • harib02g下的bootpack.c中的HariMain函数:

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
        char s[40];/*注意s的大小要合适*/
    
        init_palette();
        init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
        putfonts8_asc(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, "ABC 123");
        putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS.");
        putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS.");
        /*新添加的代码*/
        sprintf(s, "scrnx = %d", binfo->scrnx);
        putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);
    
        for (;;) {
            io_hlt();
        }
    }
    
  • make run

8. 显示鼠标指针(harib02h)

  • 鼠标的大小为16*16。先准备16*16=256字节的内存,然后往里面写入鼠标指针的数据即可。

  • harib02h下的bootpack.c中的init_mouse_cursor8函数:

    void init_mouse_cursor8(char *mouse, char bc)
    /* 初始化鼠标指针 */
    {
        static char cursor[16][16] = {
            "**************..",
            "*OOOOOOOOOOO*...",
            "*OOOOOOOOOO*....",
            "*OOOOOOOOO*.....",
            "*OOOOOOOO*......",
            "*OOOOOOO*.......",
            "*OOOOOOO*.......",
            "*OOOOOOOO*......",
            "*OOOO**OOO*.....",
            "*OOO*..*OOO*....",
            "*OO*....*OOO*...",
            "*O*......*OOO*..",
            "**........*OOO*.",
            "*..........*OOO*",
            "............*OO*",
            ".............***"
        };
        int x, y;
    
        for (y = 0; y < 16; y++) {
            for (x = 0; x < 16; x++) {
                if (cursor[y][x] == '*') {
                    mouse[y * 16 + x] = COL8_000000; /*黑色*/
                }
                if (cursor[y][x] == 'O') {
                    mouse[y * 16 + x] = COL8_FFFFFF; /*白色*/
                }
                if (cursor[y][x] == '.') {
                    mouse[y * 16 + x] = bc; /*背景色*/
                }
            }
        }
        return;
    }
    

    bc是背景色,mouse是存放鼠标颜色的数组。

  • harib02h下的bootpack.c中的putblock8_8函数,用于将背景色显示出来。

    void putblock8_8(char *vram, int vxsize, int pxsize,
    int pysize, int px0, int py0, char *buf, int bxsize)
    {
        int x, y;
        for (y = 0; y < pysize; y++) {
            for (x = 0; x < pxsize; x++) {
                vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
            }
        }
        return;
    }
    

    其中,vxsize是屏幕分辨率X的大小;pxsize和pysize是显示图形的大小,这里是鼠标的大小16*16;px0和py0是图形左上角相对于屏幕的位置;buf是图形存放地址;bxsize是图形一行含有的像素数。

  • harib02h下的bootpack.c中的HariMain函数:

    void HariMain(void)
    {
        struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
        char s[40], mcursor[256];
        int mx, my;
    
        init_palette();
        init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);
    
        mx = (binfo->scrnx - 16) / 2; /* 设置鼠标的位置 */
        my = (binfo->scrny - 28 - 16) / 2;
        init_mouse_cursor8(mcursor, COL8_008484); /*初始化鼠标*/
        putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /*显示鼠标*/
        sprintf(s, "(%d, %d)", mx, my);/*显示鼠标的位置*/
        putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);
    
        for (;;) {
            io_hlt();
        }
    }
    

    这里,(mx, my) = (152, 78)。

  • make run

9. GDT与IDT的初始化(harib02i)

  • 要想移动鼠标,就必须将GDT和IDT初始化。

    • 为了让操作系统能够使用32位模式,需要对CPU进行各种设定。GDT和IDT都是与CPU有关的设定。

    • 这就需要修改asmhead.nas。

    • 原先的asmhead.nas只是做了一些能够运行bootpack.c所必需的一些设定。

  • 分段

    • ORG:声明程序要读入的内存地址。

    • 操作系统需要能够同时运行多个程序。这时,多个程序执行ORG,可能发生内存使用范围重叠现象。解决这个现象的方法就是分段

    • 分段:将内存分成很多块(大小不同),每一块的起始地址都看成是0.

    • 分页:将内存分成很多块(大小相同),且也将程序分割成等大的多块。

    • 段寄存器就是用来分段的。

      • 16位模式时,如果计算地址,重要将地址乘以16即可。
      • 32位模式,代码MOV AL,[DS:EBX]CPU会往EBX里加上DS所表示的段的起始地址,而不再是DS的16倍了。
      • 如果省略段寄存器的地址,16位和32位默认都是DS。
    • 表示一个段

      • 段的大小
      • 段的起始地址
      • 段的管理属性(禁止写入,禁止执行,系统专用等)
    • CPU用8个字节(64位) 来表示一个段。 注意,用于指定段的寄存器只有16位。即段寄存器(默认DS)只有16位。

    • 内存被分成很多段。因此需要段号来区分这些段。段号和段一一对应

    • 段寄存器只有16位,由于CPU设计的原因,段寄存器的低3位不能使用,因此,我们可以使用13位,那么就可以表示2^13=8192个不同的段,即段号范围0~8191.

    • 我们可以设定8192个段,一个段占8字节,那么共需要8192*8字节=65536字节(64KB)

    • 显然这64KB只能放在内存里。

  • GDT

    • 这64KB的数据就叫做GDT。GDT,global (segment) descriptor table,意思是 全局段号记录表

    • 将这些数据整齐地排列在内存的某个地方,然后将这个地方的内存地址和有效设定个数(最多8192个段,不一定需要这么多)放在CPU内被称作GDTR的特殊寄存器中,这样GDT的设定就OK了。

    • 图解

  • IDT

    • IDT,interrupt descriptor table,中断记录表。中断,当CPU遇到外部状况发生变化或者内部偶然错误,会临时切换过去处理这种突发情况。

    • 如果想使用鼠标,就必须使用中断机制。因此,我们必须设定IDT。

    • IDT记录了0~255的终端号码与调用函数的对应关系。 比如,发生了123号中断,那么CPU就去调用指定123号指定的函数。

    • IDT的设定方法和GDT的设定方法类似(上图)。

    • 注意,需要先设定GDT,再设定IDT。

  • harib02i下的bootpack.c代码节选:

    struct SEGMENT_DESCRIPTOR { /*存放GDT的8字节内容,依据是CPU的资料*/
        short limit_low, base_low;
        char base_mid, access_right;
        char limit_high, base_high;
    };
    
    struct GATE_DESCRIPTOR { /*存放IDT的8字节内容*/
        short offset_low, selector;
        char dw_count, access_right;
        short offset_high;
    };
    
    void init_gdtidt(void) /*初始化GDT和IDT*/
    {
        struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; /*0x270000~0x27ffff设置为GDT*/
        struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800;
        int i;
    
        /* GDT初始化 */
        for (i = 0; i < 8192; i++) {
            set_segmdesc(gdt + i, 0, 0, 0);
        }
        set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
        set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
        load_gdtr(0xffff, 0x00270000);
    
        /* IDT初始化 */
        for (i = 0; i < 256; i++) {
            set_gatedesc(idt + i, 0, 0, 0);
        }
        load_idtr(0x7ff, 0x0026f800);
    
        return;
    }
    
    void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
    {   
        /*存疑1*/
        if (limit > 0xfffff) {/*从GDT图可以看出,limit占20位,所以最大是0xfffff。下面的代码因该是越界重置,但是没看懂为什么。*/
            ar |= 0x8000; /* G_bit = 1 */
            limit /= 0x1000;
        }
        sd->limit_low    = limit & 0xffff; /*取低16位*/
        sd->base_low     = base & 0xffff; /*取低16位*/
        sd->base_mid     = (base >> 16) & 0xff; /*先右移16位再取低8位*/
        sd->access_right = ar & 0xff; /*取低8位*/
        /*存疑2*/
        sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); /*前面半句看懂了,后面的| ((ar >> 8) & 0xf0)有点莫名其妙。*/
        sd->base_high    = (base >> 24) & 0xff; /*右移24位,再取8位*/
        return;
    }
    
    void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
    {
        gd->offset_low   = offset & 0xffff; /*取低16位*/
        gd->selector     = selector; /*我觉得应该还得加上&0xffff*/
        /*存疑3*/
        gd->dw_count     = (ar >> 8) & 0xff; /*从这里看dw_count是在高位*/
        gd->access_right = ar & 0xff;
        gd->offset_high  = (offset >> 16) & 0xffff; 
        return;
    } 
    
  • CPU有关GDT的设定

    (reference:https://www.cnblogs.com/bajdcc/p/8972946.html

  • GDT结构体中变量与GDT设定的对应关系

  • CPU有关IDT的设定

    (reference:https://blog.csdn.net/fwqcuc/article/details/5855460

  • IDT结构体中变量与IDT设定的对应关系

  • 为什么设定0x270000~0x27ffff这65536个字节为GDT,或者说为什么GDT的其实地址是0x270000?
    原因:内存分布图上,显示这一块儿没有被使用。

  • IDT被设定成为0x26f800~0x26ffff。共256*8字节=2048字节(2KB)。

  • 顺便提一下,内存0x280000~0x2fffff共524288字节(512KB)已经被bootpack.h使用了。这件事情是asmhead.nas干的。

  • GDT的初始化:
    将8192个段的上限(limit,指段的字节数-1),基址(base),访问权限都设置成为了0.

  • 两句代码

    set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
    set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
    
    • 上述代码是对段号1和段号2进行设置。
    • 段号为1的段,上限值是0xffffffff,大小正好是‭42949672956字节(4GB),地址是0,那么段号为1的段表示的是CPU能够管理的全部内存本身。 至于权限为什么是0x4092,这个留到第6天解释。
    • 段号位2的段,上限值是0x0007ffff,大小正好是524288字节(512KB),地址为0x280000,这就是位bootpack.hrb准备的,用这个段就可以执行bootpack.hrb,因为bootpack.hrb是以ORG 0为前提翻译成的机器语言。
  • load_gdtr(0xffff, 0x00270000);

    • 这句话是把特殊寄存器GDTR内容加载到内存里面。

    • 寄存器GDTR

      (reference:https://www.cnblogs.com/wanghj-dz/p/3975107.html

      • GDTR可以存储6字节。
      • 高4字节存储GDT的基址;低2字节存储GDT的最大长度(字节数)
      • 显然,第一个参数0xffff是GDT的限长65536字节。第二个参数是GDT的内存起始地址。
    • naskfunc.nas添加了两个函数load_gdtr和load_idtr.

      _load_gdtr:		; void load_gdtr(int limit, int addr);
              MOV		AX,[ESP+4]		; limit
              MOV		[ESP+6],AX
              LGDT	[ESP+6]
              RET
      
      _load_idtr:		; void load_idtr(int limit, int addr);
              MOV		AX,[ESP+4]		; limit
              MOV		[ESP+6],AX
              LIDT	[ESP+6]
              RET
      

2020.3.30傍晚,在读上述代码的时候突然卡住了,摆在我面前的有三个问题:1.WIN32汇编;2.函数调用过程栈的变化。3. load_gdtr(0xffff, 0x00270000)中参数传递。

1. WIN32汇编
  • 代码含义?

    MOV		AX,[ESP+4]	
    MOV		[ESP+6],AX
    
  • 我深知,一时半会儿是不能掌握WIN32汇编的。同时花太多时间在学习WIN32汇编也不是制作操作系统的重点。我的重心应该在C语言编程上。

  • 于是,我重新读了第2天有关汇编的相关知识(P36-37)。瞬间觉得豁然开朗了许多。

  • MOV AX,[ESP+4]:AX是16位寄存器,由于WIN32汇编中源数据和目的数据必须位数相同,因此上述代码相当于MOV AX,WORD[ESP+4], WORD[ESP+4]是指以内存地址ESP+4为起始地址的连续2字节(WORD)的内容。 那么这句话的意思也就显而易见了。

  • MOV [ESP+6],AX:类比上面,相当于MOV WORD[ESP+6],AX。这句话的意思是:把以内存地址为ESP+6为起始地址的连续2字节的内容赋值给寄存器AX

  • 现在只剩一个问题了,为什么是[ESP+4],[ESP+6]而不是其他呢? 解决这个问题,我们需要知道函数调用过程栈的变化

2. 函数调用过程栈的变化
  • 这个问题暴露了我大学四年学习:没学到点啥。这绝不是贬低中国每羊大学,而是批判自己的学习态度不端正和治学不严谨。

  • 首先,了解一下程序对内存使用的分区情况:

    • 栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似与数据结构中的栈。
    • 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。
    • 静态区(static):全局变量和静态变量存放于此。
    • 文字常量区:常量字符串放在此,程序结束后由系统释放。
    • 程序代码区:存放函数体的二进制代码。
  • 栈区

    • 栈是从高地址向低地址延伸的。
    • 每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。
    • 寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。
    • 认识几个寄存器。
  • 函数调用过程栈的变化

    • Reference:https://www.zhihu.com/question/22444939/answer/22200552

    • 假设有如下代码:

      void func_A(arg_A1, arg_A2);
      void func_B(arg_B1, arg_B2);
      
      int main(int argc, char *argv[], char **envp){
          func_A(arg_A1, arg_A2);
      }
      
      void func_A(arg_A1, arg_A2){
          var_A;
          func_B(arg_B1, arg_B2);
      }
      
      void func_B(arg_B1, arg_B2){
          var_B1;
          var_B2;
      }
      
    • 总体过程:

      • 在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈
      • 在func_A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈
      • 在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行
      • 在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行
      • 在实际运行中,main函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作
    • ESP、EBP、EIP进一步理解

      • ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
      • EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部
      • 函数栈帧:ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP标识了当前栈帧的顶部
      • EIP:指令寄存器(extended instruction pointer), 其内存放着一个指针,该指针永远指向下一条待执行的指令地址。
    • 函数调用大致包括以下几个步骤:

      • 参数入栈:将参数从右向左依次压入系统栈(当前栈帧)

      • 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行

      • 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处

      • 栈帧调整:具体包括

        • 保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)

        • 将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)

        • 给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶,ESP抬到新栈帧的顶部。)

    • 下图也许更形象一点儿。

3. load_gdtr(0xffff, 0x00270000)中参数传递。
  • 首先明白一点:数据存放在内存中,我们采用的是小端模式(高地址高位)

  • 调用load_gdtr时:

    • 参数入栈
    • 返回地址入栈
    • EBP入栈
    • ESP=EBP
    • 给新栈帧分配空间
  • 指令LGDT 地址addr:将内存地址addr开始的6个字节读入GDTR寄存器中。

  • 我所能理解的 存疑四

  • load_gdtr函数时用汇编写的,且没有局部变量,只使用了CPU的寄存器AX。所以我猜(笑)ESP=EBP,他们俩都指向返回地址。【暂时,这是我所能做出的最合适的解释了。希望看到这篇文档的大佬能帮忙解惑。】

  • load_idtr和load_gdtr十分相似,这里不再赘述。


  • 重新回到这里的时候已经是2020.03.30 23:22了。

  • 接下来的IDT的初始化和load_idtr(0x7ff, 0x0026f800)和GDT的类似。

  • 访问权属性和IDT的详细说明还是留到明天吧,大言不惭的讲,我和当时川合秀实一样,有点累了。

  • make run

    只是进行了简单地初期设定,鼠标依旧不能移动。
    不要灰心,但我们已经成功了一大半了(笑)。

10. 再次刷新两项记录

  • 此文档的markdown代码行数大约是720行。达成成就超长文档
  • 终于读完100页了,截止现在是107页。达成成就100页超人
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值