第20天 API

2020.4.23

1. 程序整理(harib17a)

  • 实现由应用程序对OS功能的调用,即API(系统调用)。
  • API,application program interface,应用程序(与系统之间的)接口。
  • 由应用程序来调用(操作)系统中的功能来完成某种操作。
  • 编写一个在命令行窗口中显示字符的API。(BIOS中也有这个功能,但是现在无法使用BIOS哦)
  • 现在,console.c的代码有点混乱,整理一下。
  • bootpack.h中有关console.c中函数和常量的声明:
/* console.c */
struct CONSOLE {
    struct SHEET *sht;
    int cur_x, cur_y, cur_c;
};
void console_task(struct SHEET *sheet, unsigned int memtotal);
void cons_putchar(struct CONSOLE *cons, int chr, char move);
void cons_newline(struct CONSOLE *cons);
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal);
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal);
void cmd_cls(struct CONSOLE *cons);
void cmd_dir(struct CONSOLE *cons);
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline);
void cmd_hlt(struct CONSOLE *cons, int *fat);
  • 结构体CONSOLE:
  • 用于存放命令行窗口常用的变量。
  • sht是指向命令行窗口图层的指针。
  • (cur_x,cur_y)代表指向下一个要输入位置(光标所在处)的左上角坐标。
  • cur_c代表光标颜色,-1代表不显示光标。
  • console_task
void console_task(struct SHEET *sheet, unsigned int memtotal)
{
    struct TIMER *timer;
    struct TASK *task = task_now();
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    int i, fifobuf[128], *fat = (int *) memman_alloc_4k(memman, 4 * 2880);
    struct CONSOLE cons; /*声明cons,用于存放命令行窗口的各种变量*/
    char cmdline[30];
    cons.sht = sheet;
    cons.cur_x =  8; /*初始值不再是16*/
    cons.cur_y = 28;
    cons.cur_c = -1; /*光标默认不显示*/

    fifo32_init(&task->fifo, 128, fifobuf, task);
    timer = timer_alloc();
    timer_init(timer, &task->fifo, 1);
    timer_settime(timer, 50);
    file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); /*向内存中写入解压缩后的FAT*/

    /* 显示提示符 */
    cons_putchar(&cons, '>', 1);

    for (;;) {
        io_cli();
        if (fifo32_status(&task->fifo) == 0) {
            task_sleep(task);
            io_sti();
        } else {
            i = fifo32_get(&task->fifo);
            io_sti();
            if (i <= 1) { /* 光标定时器 */
                if (i != 0) {
                    timer_init(timer, &task->fifo, 0); /* 下次置0 */
                    if (cons.cur_c >= 0) {
                        cons.cur_c = COL8_FFFFFF;
                    }
                } else {
                    timer_init(timer, &task->fifo, 1); /* 下次置1 */
                    if (cons.cur_c >= 0) {
                        cons.cur_c = COL8_000000;
                    }
                }
                timer_settime(timer, 50);
            }
            if (i == 2) {	/* 光标开启 */
                cons.cur_c = COL8_FFFFFF;
            }
            if (i == 3) {	/* 光标关闭 */
                boxfill8(sheet->buf, sheet->bxsize, COL8_000000, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);
                cons.cur_c = -1;
            }
            if (256 <= i && i <= 511) { /* 键盘数据 */
                if (i == 8 + 256) {
                    /* 退格键 */
                    if (cons.cur_x > 16) {
                        /* 用空格擦除光标后将光标前移一位 */
                        cons_putchar(&cons, ' ', 0);
                        cons.cur_x -= 8; /*cur_x更新*/
                    }
                } else if (i == 10 + 256) {
                    /* Enter */
                    /* 将光标用空格擦除后换行 */
                    cons_putchar(&cons, ' ', 0);
                    cmdline[cons.cur_x / 8 - 2] = 0;
                    cons_newline(&cons);
                    cons_runcmd(cmdline, &cons, fat, memtotal);	/* 运行命令 */
                    /* 显示提示符 */
                    cons_putchar(&cons, '>', 1);
                } else {
                    /* 一般字符 */
                    if (cons.cur_x < 240) {
                        /* 显示一个字符以后,将光标后移一位 */
                        cmdline[cons.cur_x / 8 - 2] = i - 256;
                        cons_putchar(&cons, i - 256, 1);
                    }
                }
            }
            /* 重新显示光标 */
            if (cons.cur_c >= 0) {
                boxfill8(sheet->buf, sheet->bxsize, cons.cur_c, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);
            }
            sheet_refresh(sheet, cons.cur_x, cons.cur_y, cons.cur_x + 8, cons.cur_y + 16);
        }
    }
}
  • cons_putchar函数:
void cons_putchar(struct CONSOLE *cons, int chr, char move)
{
    char s[2];
    s[0] = chr;
    s[1] = 0;
    if (s[0] == 0x09) {	/* 制表符 */
        for (;;) {
            putfonts8_asc_sht(cons->sht, cons->cur_x, cons->cur_y, COL8_FFFFFF, COL8_000000, " ", 1);
            cons->cur_x += 8;
            if (cons->cur_x == 8 + 240) {
                cons_newline(cons);
            }
            if (((cons->cur_x - 8) & 0x1f) == 0) {
                break;	/* 被32整除则break */
            }
        }
    } else if (s[0] == 0x0a) {	/* 换行 */
        cons_newline(cons);
    } else if (s[0] == 0x0d) {	/* 回车 */
        /* 暂时不做任何操作 */
    } else {	/* 一般字符 */
        putfonts8_asc_sht(cons->sht, cons->cur_x, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 1);
        if (move != 0) {
            /* move不为0,光标后移 */
            cons->cur_x += 8; /*cur_x更新*/
            if (cons->cur_x == 8 + 240) { /*到达行尾,换行*/
                cons_newline(cons);
            }
        }
    }
    return;
}
  • cons_putchar函数的作用是:将特定的字符写到命令行窗口上,并更新相关变量。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • chr:是字符的ASCII表示。将这个字符写入到图层上面。
  • move:0代表光标不后移,1代表光标后移(cur_x+=8)。
  • 无返回值
  • cons_newline:
void cons_newline(struct CONSOLE *cons)
{
    int x, y;
    struct SHEET *sheet = cons->sht;
    if (cons->cur_y < 28 + 112) {
        cons->cur_y += 16; /* 到下一行 */
    } else {
        /* 滚动 */
        for (y = 28; y < 28 + 112; y++) {
            for (x = 8; x < 8 + 240; x++) {
                sheet->buf[x + y * sheet->bxsize] = sheet->buf[x + (y + 16) * sheet->bxsize];
            }
        }
        for (y = 28 + 112; y < 28 + 128; y++) {
            for (x = 8; x < 8 + 240; x++) {
                sheet->buf[x + y * sheet->bxsize] = COL8_000000;
            }
        }
        sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
    }
    cons->cur_x = 8; /*重置cur_x*/
    return;
}
  • 这是根据新的数据结构CONSOLE而重新写的换行函数,作用是:将光标换到下一行,如果超出命令行底部,则滚动。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • 返回值:无返回值。
  • cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal)
{
    if (strcmp(cmdline, "mem") == 0) {
        cmd_mem(cons, memtotal);
    } else if (strcmp(cmdline, "cls") == 0) {
        cmd_cls(cons);
    } else if (strcmp(cmdline, "dir") == 0) {
        cmd_dir(cons);
    } else if (strncmp(cmdline, "type ", 5) == 0) {
        cmd_type(cons, fat, cmdline);
    } else if (strcmp(cmdline, "hlt") == 0) {
        cmd_hlt(cons, fat);
    } else if (cmdline[0] != 0) {
        putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12);
        cons_newline(cons);
        cons_newline(cons);
    }
    return;
}
  • cons_runcmd函数用于执行从命令行窗口输入进来的指令。
  • 传入参数:
  • cmdline:指向从命令行窗口输入的字符串。
  • cons:用于存放命令行窗口的常用变量。
  • fat:指向为FAT开辟的内存空间。
  • memtotal:用于mem命令,是总的内存空间。
  • 返回值:无返回值。
  • cmd_mem:
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    char s[30];
    sprintf(s, "total   %dMB", memtotal / (1024 * 1024));
    putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
    cons_newline(cons);
    sprintf(s, "free %dKB", memman_total(memman) / 1024);
    putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
    cons_newline(cons);
    cons_newline(cons);
    return;
}
  • cmd_mem函数的作用是:执行mem命令。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • memtotal:总的内存空间。
  • 返回值:无。
  • cmd_cls:
void cmd_cls(struct CONSOLE *cons)
{
    int x, y;
    struct SHEET *sheet = cons->sht;
    for (y = 28; y < 28 + 128; y++) {
        for (x = 8; x < 8 + 240; x++) {
            sheet->buf[x + y * sheet->bxsize] = COL8_000000;
        }
    }
    sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
    cons->cur_y = 28; /*重置cur_y,因为输入cls必会输入回车,回车会产生新行,新行的cur_x恒为8*/
    return;
}
  • cmd_cls函数:用于执行cls命令。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • 返回值:无。
void cmd_dir(struct CONSOLE *cons)
{
    struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600); /*获取文件名存放地址的开始地址*/
    int i, j;
    char s[30];
    for (i = 0; i < 224; i++) {
        if (finfo[i].name[0] == 0x00) {
            break;
        }
        if (finfo[i].name[0] != 0xe5) {
            if ((finfo[i].type & 0x18) == 0) {
                sprintf(s, "filename.ext   %7d", finfo[i].size);
                for (j = 0; j < 8; j++) {
                    s[j] = finfo[i].name[j];
                }
                s[ 9] = finfo[i].ext[0];
                s[10] = finfo[i].ext[1];
                s[11] = finfo[i].ext[2];
                putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
                cons_newline(cons);
            }
        }
    }
    cons_newline(cons);
    return;
}
  • cmd_dir函数:用于执行dir命令。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • 返回值:无。
  • cmd_type
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); /*调用file_search函数*/
    char *p;
    int i;
    if (finfo != 0) {
        /* 找到文件的情况 */
        p = (char *) memman_alloc_4k(memman, finfo->size);
        file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
        for (i = 0; i < finfo->size; i++) {
            cons_putchar(cons, p[i], 1);
        }
        memman_free_4k(memman, (int) p, finfo->size);
    } else {
        /* 没找到 */
        putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "File not found.", 15);
        cons_newline(cons);
    }
    cons_newline(cons);
    return;
}
  • cmd_type函数:用于执行type命令。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • fat: FAT的开始地址。
  • 返回值:无。
  • cmd_hlt:
void cmd_hlt(struct CONSOLE *cons, int *fat)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct FILEINFO *finfo = file_search("HLT.HRB", (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
    char *p;
    if (finfo != 0) {
        /* 找到了文件 */
        p = (char *) memman_alloc_4k(memman, finfo->size);
        file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
        set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
        farjmp(0, 1003 * 8);
        memman_free_4k(memman, (int) p, finfo->size);
    } else {
        /* 没找到文件 */
        putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "File not found.", 15);
        cons_newline(cons);
    }
    cons_newline(cons);
    return;
}
  • cmd_hlt函数:执行可执行文件hlt.hrb。
  • 传入参数:
  • cons:用于存放命令行窗口的常用变量。
  • fat: FAT的开始地址。
  • 返回值:无
  • file_search:(file.c中)
struct FILEINFO *file_search(char *name, struct FILEINFO *finfo, int max)
{
    int i, j;
    char s[12];
    for (j = 0; j < 11; j++) {
        s[j] = ' ';
    }
    j = 0;
    for (i = 0; name[i] != 0; i++) {
        if (j >= 11) { return 0; /* 没找到 */ }
        if (name[i] == '.' && j <= 8) {
            j = 8;
        } else {
            s[j] = name[i];
            if ('a' <= s[j] && s[j] <= 'z') {
                /* 将小写转化成大写 */
                s[j] -= 0x20;
            } 
            j++;
        }
    }
    for (i = 0; i < max; ) {
        if (finfo[i].name[0] == 0x00) {
            break;
        }
        if ((finfo[i].type & 0x18) == 0) {
            for (j = 0; j < 11; j++) {
                if (finfo[i].name[j] != s[j]) {
                    goto next;
                }
            }
            return finfo + i; /* 找到文件 */
        }
next:
        i++;
    }
    return 0; /* 没有找到 */
}
  • file_search函数:搜索文件是否存在。
  • 传入参数:
  • name:指向文件名的指针。
  • finfo: 文件名存放的开始地址。
  • max:最大的文件数,224。
  • 返回值:找到文件了,返回文件名的地址;否则返回0。
  • 这样修改以后,代码清爽了很多。
  • make后用VMware顺利运行。

2. 显示单个字符的API(1)(harib17b)

  • 让应用程序能够显示单个字符的方法很简单:只要应用程序能够调用cons_putchar就行了。
  • 首先,做一个用于测试的应用程序。将要显示的字符编码存入AL寄存器,然后调用操作系统的函数,字符就显示出来了
  • hlt.nas设想
[BITS 32]
        MOV		AL,'A'
        CALL    (cons_putchar的地址)
fin:
        HLT
        JMP		fin
  • CALL是一个用来调用函数的指令。在C语言中,goto和函数调用的处理方式完全不同。但是在汇编语言中,CALL指令和JMP指令其实差不多。他们的区别只是:在执行CALL指令时,为了能够在接下来执行RET指令时能够正确返回,会先将要返回的目标地址PUSH到栈中。
  • 这里有个问题,cons_putchar函数的地址我们应用程序是不知道的。应用程序编译时,并不会包含OS的代码。因此,要想知道cons_putchar的地址,只能编写好OS后编译,通过人工查看文件得到cons_putchar的地址,然后再写入到应用程序的代码中。
  • 注意:cons_putchar函数是用C语言写的函数,即便将字符编码写入寄存器,函数也无法接收,因此,必须在CALL之前将字符编码写入栈中才行。
  • 解决思路:添加一个中间层,应用程序先调用这个中间层,把相应的数据存放到栈中,然后这个中间层调用cons_putchar。
  • 中间层:汇编语言编写的用来将寄存器的值存入栈的函数。 这个中间层是OS的一部分,因此要写在naskfunc.nas中。 在应用程序hlt.nas中,CALL的不再是cons_putchar的地址,而是这个中间层的地址。
  • 修改naskfunc.nas:
_asm_cons_putchar:
        PUSH	1
        AND		EAX,0xff	; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态
        PUSH	EAX
        PUSH	DWORD [0x0fec]	; 将cons的地址压栈
        CALL	_cons_putchar
        ADD		ESP,12		; 将栈中的数据丢弃
        RET
  • 详解:
  • PUSH 1:因为现在是32位模式,所以PUSH一次向栈中压入32位数据,也就是4字节数据。
  • cons的地址,应用程序是不知道的。因此,将这个地址事先保存在内存的特定地址上,这里保存在BOOTINFO之前的0x0fec这个地址。
  • 因此,还得修改console_task:
void console_task(struct SHEET *sheet, unsigned int memtotal)
{
    ……
    cons.sht = sheet;
    cons.cur_x =  8;
    cons.cur_y = 28;
    cons.cur_c = -1;
    *((int *) 0x0fec) = (int) &cons; /*写入指定内存*/
    ……
}
  • make将编写好的OS编译。注意一个叫bootpack.map的文件,这是一个文本文件,打开,找到_asm_cons_putchar的位置:
  • 这个0x00000BE3就是asm_cons_putchar的地址了。
  • 0x0be3写入hlt.nas:
[BITS 32]
        MOV		AL,'A'
        CALL    0xbe3
fin:
        HLT
        JMP		fin
  • 应用程序中的API是:
MOV		AL,'A'

CALL    0xbe3
  • 重新make,用VMware运行试试:
  • 不幸的是,出现BUG了。
  • 不要轻易在真机上运行harib17b,因为无法预料会产生什么后果!

3. 显示单个字符的API(2)(harib17c)

  • 开发OS真刺激!一不小心就产生严重问题!
  • 产生上述问题的原因:
  • 应用程序对API执行CALL的时候,千万不能忘记加上段号!!
  • 应用程序所在的段是1003 * 8;OS所在的段是2 * 8
  • 不能使用普通的CALL,应该使用far-CALL指令。
  • far-CALL指令和far-JMP一样,只要同时指定段和偏移量即可。
  • 修改hlt.nas:
[BITS 32]
        MOV		AL,'A'
        CALL    2*8:0xbe3
fin:
        HLT
        JMP		fin
  • 相应地,还需要修改asm_cons_putchar:
_asm_cons_putchar:
        PUSH	1
        AND		EAX,0xff	; 
        PUSH	EAX
        PUSH	DWORD [0x0fec]	; 
        CALL	_cons_putchar
        ADD		ESP,12		; 
        RETF
  • 将最后一句RET修改成RETF。
  • RET是用于普通的CALL的。而RETF设计用于far-CALL的。
  • make后用VMware运行:
  • 字符成功显示!

4. 结束应用程序(harib17d)

  • 让应用程序结束后不执行HLT,而是返回操作系统。
  • 只要将应用程序中的HLT改成RET,就可以返回了,因为需要返回,所以OS调用应用程序的地方也应该由JMP改成CALL。
  • 由于调用的应用程序位于不同的段,因此应该调用far-CALL,应用程序那边也不应该是RET,而是RETF。
  • C语言中没有用来执行far-CALL的指令,因此在naskfunc.nas中创建farcall函数:
_farcall:		; void farcall(int eip, int cs);
        CALL	FAR	[ESP+4]				; eip, cs
        RET
  • 和farjmp类似。
  • 修改console.c中的cmd_hlt函数:
void cmd_hlt(struct CONSOLE *cons, int *fat)
{
    ……
    if (finfo != 0) {
        /* 找到文件的情况 */
        ……
        farcall(0, 1003 * 8);
        memman_free_4k(memman, (int) p, finfo->size);
    } else {
       ……
    }
    ……
}
  • 就是将farjmp(0, 1003*8)改成了farcall(0, 1003*8)。
  • 注意,我们修改了OS的代码,因此asm_cons_putchar的地址也就变化了,需要重新查看bootpack.map,经过查看:
0x00000BE8 : _asm_cons_putchar
  • 修改hlt.nas:
[BITS 32]
        MOV AL,'A'
        CALL 2*8:0xbe8
	    RETF
  • 其实就是把HLT改成RETF。
  • 把0xbe3改成0xbe8。
  • 重新make后用VMware运行:
  • 成功显示字符’A’!
  • 重新修改hlt.nas,输入hlt显示字符串"hello"
[BITS 32]
        MOV		AL,'h'
        CALL    2*8:0xbe8
        MOV		AL,'e'
        CALL    2*8:0xbe8
        MOV		AL,'l'
        CALL    2*8:0xbe8
        MOV		AL,'l'
        CALL    2*8:0xbe8
        MOV		AL,'o'
        CALL    2*8:0xbe8
        RETF
  • 重新make后用VMware运行:
  • 成功显示字符串“hello”。

5. 不随操作系统版本而改变的API(harib17e)

  • harib17d暴露了一个问题,每次修改OS的代码,asm_cons_putchar的地址都会变化,从而导致应用程序的代码必须改变。这是不友好的。
  • 解决上述问题的方式有很多。这里采用如下这一种
  • CPU中有个专门用来注册函数的地方,这个地方就是中断处理程序注册的地方。
  • 当发生IRQ-1的时候调用某个函数f,这个函数f是在IDT中注册的。
  • IRQ只有0~15,而CPU用于通知异常状态的中断最多也只有32种(查阅资料)。
  • IDT最多可以设置256个函数,因此还有剩下很多位置没有使用。
  • 从IDT中选一个空闲的项0x40号(0x30~0xff都是空闲的),将asm_cons_putchar注册在这里。
  • 这样,我们只要调用INT 0x40产生一个中断就可以调用asm_cons_putchar函数了。
  • 修改init_gdtidt(dsctbl.c):
void init_gdtidt(void)
{
    ……
    /* IDT的设置 */
    set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x40, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32); /*0x40号注册asm_cons_putchar*/

    return;
}
  • 修改hlt.nas:
[BITS 32]
	MOV		AL,'h'
	INT		0x40
	MOV		AL,'e'
	INT		0x40
	MOV		AL,'l'
	INT		0x40
	MOV		AL,'l'
	INT		0x40
	MOV		AL,'o'
	INT		0x40
	RETF
  • CALL 2\*8:0xbe8改成了INT 0x40
  • 修改naskfunc.nas中的asm_cons_putchar:
_asm_cons_putchar:
        STI
        PUSH	1
        AND		EAX,0xff	
        PUSH	EAX
        PUSH	DWORD [0x0fec]	
        CALL	_cons_putchar
        ADD		ESP,12		
        IRETD
  • 由于INT会产生中断,因此RETF是无法返回的,需要使用IRETD指令。
  • INT调用时,对于CPU来讲相当于执行了中断处理,因此,在调用的同时CPU会自动执行CLI指令来禁止中断请求。
  • 这里,我们只是用INT来代替CALL,我们不想看到“API处理过程中键盘无法输入”这种情形,因此在开头添加了STI指令。
  • 还有一种方式:在将函数注册到CPU的时候修改设置来禁止CPU擅自执行CLI,这里不再深入。
  • make后用VMware运行:
  • 顺利运行。
  • 应用程序比之前的还小了:
  • harib17d的hlt.hrb:46字节。
  • harib17e的hlt.hrb:21字节。
  • 这是因为:far-CALL指令需要7个字节,而INT指令还需要2个字节。

6. 为应用程序自由命名(harib17f)

  • 修改cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal)
{
    if (strcmp(cmdline, "mem") == 0) {
        cmd_mem(cons, memtotal);
    } else if (strcmp(cmdline, "cls") == 0) {
        cmd_cls(cons);
    } else if (strcmp(cmdline, "dir") == 0) {
        cmd_dir(cons);
    } else if (strncmp(cmdline, "type ", 5) == 0) {
        cmd_type(cons, fat, cmdline);
    } else if (cmdline[0] != 0) {
        if (cmd_app(cons, fat, cmdline) == 0) {
            /* 不是命令,不是应用程序,也不是空行 */
            putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12);
            cons_newline(cons);
            cons_newline(cons);
        }
    }
    return;
}
  • 去掉了cmd_hlt函数,创建了新的函数cmd_app。
  • cmd_app函数:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct FILEINFO *finfo;
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
    char name[18], *p;
    int i;

    /* 根据命令行生成文件名 */
    for (i = 0; i < 13; i++) {
        if (cmdline[i] <= ' ') {
            break;
        }
        name[i] = cmdline[i];
    }
    name[i] = 0; /* 暂且将文件名的后面置为0 */

    /* 寻找文件 */
    finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
    if (finfo == 0 && name[i - 1] != '.') {
        /* 找不到文件,在文件名后面加上.hrb后重新寻找 */
        name[i    ] = '.';
        name[i + 1] = 'H';
        name[i + 2] = 'R';
        name[i + 3] = 'B';
        name[i + 4] = 0;
        finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
    }

    if (finfo != 0) {
        /* 找到文件 */
        p = (char *) memman_alloc_4k(memman, finfo->size);
        file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
        set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
        farcall(0, 1003 * 8); 
        memman_free_4k(memman, (int) p, finfo->size);
        cons_newline(cons);
        return 1;
    }
    /* 没有找到文件 */
    return 0;
}
  • cmd_app用来根据命令行的内容判断文件名,并运行相应的应用程序,如果找到了文件则返回1,如果没有找到文件则返回0。
  • cmd_app支持输入可执行文件可执行文件.hrb的形式。(这一点模仿了Windows)
  • 现在的工作流程是:当输入的命令不是mem、cls、dir、type其中之一是,则调用cmd_api,如果返回0则作为错误处理。
  • 将hlt.nas改成hello.nas。make后在VMware中运行(Makefile已修改,将hello.nas添加到磁盘映像文件中了)。
  • 输入dir确认磁盘文件情况,输入hello.hrb:
  • 输入hlt(hlt已经被删除,因此会提示Bad command):

7. 当心寄存器(harib17g)

  • 修改hello.hrb:
[INSTRSET "i486p"]
[BITS 32]
        MOV		ECX,msg
putloop:
        MOV		AL,[CS:ECX]
        CMP		AL,0
        JE		fin
        INT		0x40
        ADD		ECX,1
        JMP		putloop
fin:
        RETF
msg:
        DB	"hello",0
  • DB: 伪指令,是定义一字节变量的意思。字符h用0x68表示,0x68一字节。
  • msg是一个标签,标签是一个数字,这个数字的值就是内存地址。MOV ECX,msg 相当于把一个数字写入ECX中,相当于把一个内存地址写入ECX中。
  • 虽然ECX是32位寄存器,一个内存地址占8位,源和目的的位数不同,但是msg是一个标签,是一个数字,这个数字的含义是一个内存地址。
  • 使用循环,这样在写入长字符串时有优势。
  • make后用VMware运行。结果执行hello时,只输出了一个字母h。很是奇怪啊。
  • 应用程序肯定没问题,问题出现在OS上,那就是asm_cons_putchar。
  • 修改asm_cons_putchar:
_asm_cons_putchar:
        STI
        PUSHAD
        PUSH	1
        AND		EAX,0xff	
        PUSH	EAX
        PUSH	DWORD [0x0fec]	
        CALL	_cons_putchar
        ADD		ESP,12		
        POPAD
        IRETD
  • 添加了两行代码:PUSHADPOPAD
  • INT40之后ECX寄存器的值发生了变化,应该是cons_putchar改动了ECX的值,加上这两句代码,将寄存器的值全部还原。
  • make后用VMware运行,顺利运行(这里修改了字符串“hello”改成了“Hi,stranger!”)。

8. 用API显示字符串(harib17h)

  • 显示单个字符的API在实际中比显示一串字符串的API的情况少得多。
  • 参考其他OS显示字符串的API,一般有两种方式:
  • 显示一串字符串,遇到字符编码0结束。
  • 指定要显示字符串的长度,然后显示。
  • 由于两种方式的实现都不困难,因此,二者均在次OS中实现。
  • 修改console.c添加函数cons_putstr0和cons_putstr1:
void cons_putstr0(struct CONSOLE *cons, char *s)
{
    for (; *s != 0; s++) {
        cons_putchar(cons, *s, 1);
    }
    return;
}

void cons_putstr1(struct CONSOLE *cons, char *s, int l)
{
    int i;
    for (i = 0; i < l; i++) {
        cons_putchar(cons, s[i], 1);
    }
    return;
}
  • 有了这两个函数,便可以简化cons_runcmd、cons_mem、cons_dir、cons_type这几个函数。
  • \n代表换行符,即0x0a。
  • \t代表制表符,即0x09。
  • 修改cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal)
{
    if (strcmp(cmdline, "mem") == 0) {
        cmd_mem(cons, memtotal);
    } else if (strcmp(cmdline, "cls") == 0) {
        cmd_cls(cons);
    } else if (strcmp(cmdline, "dir") == 0) {
        cmd_dir(cons);
    } else if (strncmp(cmdline, "type ", 5) == 0) {
        cmd_type(cons, fat, cmdline);
    } else if (cmdline[0] != 0) {
        if (cmd_app(cons, fat, cmdline) == 0) {
            /* 不是命令,不是应用程序,也不是空行 */
            cons_putstr0(cons, "Bad command.\n\n"); /*\n代表换行*/
        }
    }
    return;
}
  • 这样,cons_newline(cons)可以使用\n代替。
  • 修改cmd_mem:
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    char s[60];
    sprintf(s, "total   %dMB\nfree %dKB\n\n", memtotal / (1024 * 1024), memman_total(memman) / 1024);
    cons_putstr0(cons, s);
    return;
}
  • 修改cmd_dir:
void cmd_dir(struct CONSOLE *cons)
{
    struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);
    int i, j;
    char s[30];
    for (i = 0; i < 224; i++) {
        if (finfo[i].name[0] == 0x00) {
            break;
        }
        if (finfo[i].name[0] != 0xe5) {
            if ((finfo[i].type & 0x18) == 0) {
                sprintf(s, "filename.ext   %7d\n", finfo[i].size);
                for (j = 0; j < 8; j++) {
                    s[j] = finfo[i].name[j];
                }
                s[ 9] = finfo[i].ext[0];
                s[10] = finfo[i].ext[1];
                s[11] = finfo[i].ext[2];
                cons_putstr0(cons, s);
            }
        }
    }
    cons_newline(cons);
    return;
}
  • 修改cmd_type:
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline)
{
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
    char *p;
    if (finfo != 0) {
        /* 找到文件 */
        p = (char *) memman_alloc_4k(memman, finfo->size);
        file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
        cons_putstr1(cons, p, finfo->size);
        memman_free_4k(memman, (int) p, finfo->size);
    } else {
        /* 没找到文件 */
        cons_putstr0(cons, "File not found.\n");
    }
    cons_newline(cons);
    return;
}
  • 现在的问题是:如何将cons_putstr0和cons_putstr1变成API。
  • 最简单的方法是:像调用单个字符cons_putchar的API一样,为cons_putstr0和cons_putstr1分配INT 41和INT 42来调用这两个函数。不幸的是,这样IDT很快就会被用光
  • 借鉴BIOS的调用方式:
  • 在寄存器中存入功能号,使得只使用1个INT就可以选择调用不同的函数。
  • 用来存放功能号的寄存器一般是AH(8位寄存器),这样最多只能设置256个API函数。
  • 这里,改用寄存器EDX(32位寄存器)来存放功能号,这样可以设置大约42亿API函数(够用了吧)。
  • 功能号暂时按如下划分(寄存器的用法也是随意设定的):
  • 功能号1:显示单个字符(AL=字符编码)
  • 功能号2:显示字符串0(EBX=字符串地址(msg))
  • 功能号3:显示字符串1(EBX=字符串地址(msg),ECX=字符串长度)
  • 小知识点:内存地址占一个字节(8位,内存是一个字节一个字节拼接在一起的);32位OS中,指针占四个字节(32位)。指针不等于地址,指针有类型,地址没有类型。指针指向地址。
  • 将asm_cons_putchar改成一个新的函数asm_hrb_api:
_asm_hrb_api:
        STI
        PUSHAD	; 用于保存寄存器的值
        PUSHAD	; 用于向hrb_api传递参数
        CALL	_hrb_api
        ADD		ESP,32
        POPAD
        IRETD
  • C语言编写的API处理函数hrb_api(console.c中):
void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    if (edx == 1) {
        cons_putchar(cons, eax & 0xff, 1); /*只取eax的低8位*/
    } else if (edx == 2) {
        cons_putstr0(cons, (char *) ebx); 
    } else if (edx == 3) {
        cons_putstr1(cons, (char *) ebx, ecx); 
    }
    return;
}
  • 其实传进来的ebx就是一个数字,不过这个数字的含义是一个地址。
  • 修改IDT的设置,将asm_hrb_api注册成INT 40:
void init_gdtidt(void)
{
    ……
    /* INDT设置 */
    set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x40, (int) asm_hrb_api,      2 * 8, AR_INTGATE32); /*asm_hrb_api注册*/

    return;
}
  • 此时,需要修改应用程序hello.nas,需要向寄存器EDX中写入1,才能调用cons_putchar。
[INSTRSET "i486p"]
[BITS 32]
        MOV		ECX,msg
        MOV		EDX,1   ;EDX写入1,调用1号功能的API。
putloop:
        MOV		AL,[CS:ECX]
        CMP		AL,0
        JE		fin
        INT		0x40
        ADD		ECX,1
        JMP		putloop
fin:
        RETF
msg:
        DB	"hello",0
  • make后用VMware运行:
  • 成功显示。
  • 现在使用功能号2或3来显示字符串。编写新的应用程序hello2.nas:
[INSTRSET "i486p"]
[BITS 32]
        MOV		EDX,2
        MOV		EBX,msg
        INT		0x40
        RETF
msg:
        DB	"hello",0
  • make后在VMware上运行:
  • 很不幸,出BUG了!(明天再研究吧!)