本篇的内容不少,主要围绕着文件操作与文字显示展开。
1. alloca函数
在开发文件操作与文字显示之前,需要先做一些准备,引入alloca函数。首先看下面的代码:
#include <stdio.h>
#include "apilib.h"#define MAX 1000void HariMain(void)
{char flag[MAX], s[8];int i, j;for (i = 0; i < MAX; i++) {flag[i] = 0;}for (i = 2; i < MAX; i++) {if (flag[i] == 0) {/* 没有标记的为质数 */sprintf(s, "%d ", i);api_putstr0(s);for (j = i * 2; j < MAX; j += i) {flag[j] = 1; /* 给它的倍数做上标记 */}}}api_end();
}
运行该程序,可以展示1000以内的质数。
接下来将MAX修改为10000,使程序能够展示1-10000以内的质数。由于flags[10000]需要大概10k的空间,因此在Makefile中需要将栈的大小指定为11k。
但是在编译过程中,会出现一条告警“Warning: can’t link __alloca”。忽略告警,运行程序也会出现问题。
这与使用的C语言编译有关。编译器规定,如果栈中的变量超过4KB,则需要调用__alloca函数,该函数的作用是根据操作系统的规格来获取栈中的空间。对于Windows和Linux系统,如果不调用__alloca函数,会无法正常获取内存空间。虽然本操作系统不存在这个问题,但是为了适应编译器,我们也需要编写一个__alloca函数,只对ESP进行减法运算,而不进行其他操作。
不过其实我们也可以换一种方式,通过malloc获取所需要的内存:
#include <stdio.h>
#include "apilib.h"#define MAX 10000void HariMain(void)
{char *flag, s[8];int i, j;api_initmalloc();flag = api_malloc(MAX);for (i = 0; i < MAX; i++) {flag[i] = 0;}for (i = 2; i < MAX; i++) {if (flag[i] == 0) {sprintf(s, "%d ", i);api_putstr0(s);for (j = i * 2; j < MAX; j += i) {flag[j] = 1; }}}api_end();
}
这样就可以避开栈空间的问题了。但是栈空间的问题还是需要解决。
alloca函数的代码如下:
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "alloca.nas"]GLOBAL __alloca[SECTION .text]__alloca:ADD EAX,-4SUB ESP,EAXJMP DWORD [ESP+EAX] ; 代替RET
__alloca函数会在以下情况中被C语言程序调用:
- 要执行的操作从栈中分配EAX个字节的内存地址(ESP -= EAX)
- 不能改变ECX,EDX,EBX,EBP,ESI,EDI的值(可以临时改变,但需要通过PUSH/POP恢复)
据此,我们来看alloca函数的代码是怎么来的。
首先会想到如下的代码:
SUB ESP,EAX
RET
但这样不行,因为RET的返回地址保存在ESP中,而我们又对ESP进行了操作,导致通过RET无法正确返回。
于是改进为如下的代码:
SUB ESP,EAX
JMP DWORD [ESP + EAX]
这里通过JMP来代替RET指令,但还是有问题。
RET指令相当于POP EIP指令,而POP EIP指令又相当于如下的两条指令:
MOV EIP, [ESP]
ADD ESP, 4
除了ESP-EAX外,由于POP EIP操作,ESP的值又增加了4,因此还需要将这一点纳入考虑。最终修改成以下的程序:
SUB ESP, EAX
ADD ESP, 4
JMP DWORD [ESP + EAX -4]
这样就既保证了ESP寄存器值得正确,又使程序能够正确返回。
在C语言中,在函数外部声明得变量和带static的变量一样,都会被解释为DB和RESB,而在函数内部不带static声明的变量则会从栈中分配空间。因此将变量设置在函数内部,可以减少编译出来的应用程序的大小。
2. 文件操作API
完成了上面的准备工作,接下来就来完成文件操作API的开发。
所谓文件操作API,就是指定文件并能自由读写文件内容的API。一般的操作系统中,输入输出文件的API基本具有以下几种功能:
- 打开…………open
- 定位…………seek
- 读取…………read
- 写入…………write
- 关闭…………close
当前操作系统还不能实现写入文件,因此先完成其他四种操作的API设计:
(1) 打开文件
- EDX = 21
- EBX = 文件名
- EAX = 文件句柄(为0时表示打开失败,由操作系统返回)
(2) 关闭文件
- EDX = 22
- EAX = 文件句柄
(3) 文件定位
- EDX = 23
- EAX = 文件句柄
- ECX = 定位模式 0:定位的起点为文件开头 1: 定位的起点为当前的访问位置 2:定位的起点为文件末尾
- EDX = 定位偏移量
(4) 获取文件大小
- EDX = 24
- EAX = 文件句柄
- ECX = 文件大小获取模式 0:普通文件大小 1:当前读取位置从文件开头起算的偏移量 2:当前读取位置从文件末尾起算的偏移量
- EAX= 文件大小(由操作系统返回)
(5) 文件读取
- EDX = 25
- EAX = 文件句柄
- EBX = 缓冲区地址
- ECX = 最大读取字节数
- EAX = 本次读取到的字节数(由操作系统返回)
接下来来编程实现这些API。首先修改TASK结构体,在其中增加文件句柄
struct FILEHANDLE {char *buf;int size;int pos;
};struct TASK {int sel, flags; /* selはGDTの番号のこと */int level, priority;struct FIFO32 fifo;struct TSS32 tss;struct SEGMENT_DESCRIPTOR ldt[2];struct CONSOLE *cons;int ds_base, cons_stack;struct FILEHANDLE *fhandle;int *fat;
};
在console_task与cmd_app中需要相应地增加对文件的操作:
void console_task(struct SHEET *sheet, int memtotal)
{……struct FILEHANDLE fhandle[8];……for (i = 0; i < 8; i++) {fhandle[i].buf = 0; /* 未使用标记 */}task->fhandle = fhandle;task->fat = fat;……
}int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{……if (finfo != 0) {/* 找到文件的情况 */……if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {……start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));……for (i = 0; i < 8; i++) { /* 将未关闭的文件关闭 */if (task->fhandle[i].buf != 0) {memman_free_4k(memman, (int) task->fhandle[i].buf, task->fhandle[i].size);task->fhandle[i].buf = 0;}}timer_cancelall(&task->fifo);memman_free_4k(memman, (int) q, segsiz);} else {cons_putstr0(cons, ".hrb file format error.\n");}memman_free_4k(memman, (int) p, finfo->size);cons_newline(cons);return 1;}return 0;
}
在hrb_api中增加对于这些api的处理:
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{struct FILEINFO *finfo;struct FILEHANDLE *fh;struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;…………} else if (edx == 21) {for (i = 0; i < 8; i++) {if (task->fhandle[i].buf == 0) {break;}}fh = &task->fhandle[i];reg[7] = 0;if (i < 8) {finfo = file_search((char *) ebx + ds_base,(struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);if (finfo != 0) {reg[7] = (int) fh;fh->buf = (char *) memman_alloc_4k(memman, finfo->size);fh->size = finfo->size;fh->pos = 0;file_loadfile(finfo->clustno, finfo->size, fh->buf, task->fat, (char *) (ADR_DISKIMG + 0x003e00));}}} else if (edx == 22) {fh = (struct FILEHANDLE *) eax;memman_free_4k(memman, (int) fh->buf, fh->size);fh->buf = 0;} else if (edx == 23) {fh = (struct FILEHANDLE *) eax;if (ecx == 0) {fh->pos = ebx;} else if (ecx == 1) {fh->pos += ebx;} else if (ecx == 2) {fh->pos = fh->size + ebx;}if (fh->pos < 0) {fh->pos = 0;}if (fh->pos > fh->size) {fh->pos = fh->size;}} else if (edx == 24) {fh = (struct FILEHANDLE *) eax;if (ecx == 0) {reg[7] = fh->size;} else if (ecx == 1) {reg[7] = fh->pos;} else if (ecx == 2) {reg[7] = fh->pos - fh->size;}} else if (edx == 25) {fh = (struct FILEHANDLE *) eax;for (i = 0; i < ecx; i++) {if (fh->pos == fh->size) {break;}*((char *) ebx + ds_base + i) = fh->buf[fh->pos];fh->pos++;}reg[7] = i;}return 0;
}
增加汇编语言中的api函数:
_api_fopen: ; int api_fopen(char *fname);PUSH EBXMOV EDX,21MOV EBX,[ESP+8] ; fnameINT 0x40POP EBXRET_api_fclose: ; void api_fclose(int fhandle);MOV EDX,22MOV EAX,[ESP+4] ; fhandleINT 0x40RET
_api_fseek: ; void api_fseek(int fhandle, int offset, int mode);PUSH EBXMOV EDX,23MOV EAX,[ESP+8] ; fhandleMOV ECX,[ESP+16] ; modeMOV EBX,[ESP+12] ; offsetINT 0x40POP EBXRET_api_fsize: ; int api_fsize(int fhandle, int mode);MOV EDX,24MOV EAX,[ESP+4] ; fhandleMOV ECX,[ESP+8] ; modeINT 0x40RET_api_fread: ; int api_fread(char *buf, int maxsize, int fhandle);PUSH EBXMOV EDX,25MOV EAX,[ESP+16] ; fhandleMOV ECX,[ESP+12] ; maxsizeMOV EBX,[ESP+8] ; bufINT 0x40POP EBXRET
代码内容比较简单,与上文的设计相符合。
最后再编写一个用于测试的应用程序,该程序实现的功能是将ipl10.nas的内容展示出来:
#include "apilib.h"void HariMain(void)
{int fh;char c;fh = api_fopen("ipl10.nas");if (fh != 0) {for (;;) {if (api_fread(&c, 1, fh) == 0) {break;}api_putchar(c);}}api_end();
}
应用程序命名为typeipl,运行该应用程序,结果如下:
该应用程序看起来还是比较好用的,接下来用它来替换掉之前命令行中的type命令。当前的应用程序只能用来显示ipl10.nas的内容,要使其能够显示任意的文件,还需要运行时获取文件名,这个功能称为获取命令行。下面通过编写一个API来实现。将该API编写为可以返回完整的命令行内容,即包含应用程序的内容和文件名的完整内容。
获取命令行
- EDX = 26
- EBX = 存放命令行内容的地址
- ECX = 最多可存放多少字节
- EAX = 实际存放了多少字节(由操作系统返回)
对程序做一些修改:
struct TASK {int sel, flags; int level, priority;struct FIFO32 fifo;struct TSS32 tss;struct SEGMENT_DESCRIPTOR ldt[2];struct CONSOLE *cons;int ds_base, cons_stack;struct FILEHANDLE *fhandle;int *fat;char *cmdline;// 增加获取的命令行
};void console_task(struct SHEET *sheet, int memtotal)
{……task->cons = &cons;task->cmdline = cmdline; // 初始化……
}int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{…………} else if (edx == 26) {i = 0;for (;;) {*((char *) ebx + ds_base + i) = task->cmdline[i];if (task->cmdline[i] == 0) {break;}if (i >= ecx) {break;}i++;}reg[7] = i;}return 0;
}
添加汇编语言API:
_api_cmdline: ; int api_cmdline(char *buf, int maxsize);PUSH EBXMOV EDX,26MOV ECX,[ESP+12] ; maxsizeMOV EBX,[ESP+8] ; bufINT 0x40POP EBXRET
最后是编写应用程序,命名为type.c:
#include "apilib.h"void HariMain(void)
{int fh;char c, cmdline[30], *p;api_cmdline(cmdline, 30);for (p = cmdline; *p > ' '; p++) { } /* 跳过之前的内容,直到遇见空格 */for (; *p == ' '; p++) { } /* 跳过空格 */fh = api_fopen(p);if (fh != 0) {for (;;) {if (api_fread(&c, 1, fh) == 0) {break;}api_putchar(c);}} else {api_putstr0("File not found.\n");}api_end();
}
前面在实现type命令时,程序代码中直接跳过了5个字符(“type” + 空格),来读取后面的文件名。而这里修改为从空格跳过,这样读取到的命令行中即使命令的长度不同,如“cat”,“type”等,也能准确地分离出后面的文件名了。
运行命令type ipl10.nas,运行结果与上面相同:
3. 日文文字显示
作者主要面对的是日本读者,因此这里引入的是日文文字显示。但一方面这部分功能可谓牵一发动全身,另一方面日文显示中也有很多汉字,因此这里译者保留了原书日文显示的内容,并补充了一些中文显示的内容。
其实归根结底,显示日文和显示中文都一样,只是要准备好相应的字库就可以了。如果将字库文件内置到操作系统核心中,会导致操作系统很大,更换字体时还需要重新make。因此这里单独生成一个字库文件nihongo.fnt,在操作系统启动时检查到存在该文件,则自动将其读入内存。
日文的字符是采用全角模式显示的,一个全角字符为16 x 16点阵,需要32字节存储。根据JIS的汉字编码表,将所有的汉字都加入到字库中,共需要276KB的容量,这个容量对于本操作系统来说过大了,在启动时会变得很慢。因此这里选取了部分常用的汉字来生成简化版的字库。最终nihongo.fnt的内容如下:
- 000000 - 000FFF: 显示日文用半角字模,共256个字符(4096字节)
- 001000 - 02383F: 显示日文用全角字模,共4418个字符(141376字节)
接下来修改主程序,增加自动装载字库的功能:
/* 载入nihongo.fnt */nihongo = (unsigned char *) memman_alloc_4k(memman, 16 * 256 + 32 * 94 * 47);fat = (int *) memman_alloc_4k(memman, 4 * 2880);file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200));finfo = file_search("nihongo.fnt", (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);if (finfo != 0) {file_loadfile(finfo->clustno, finfo->size, nihongo, fat, (char *) (ADR_DISKIMG + 0x003e00));} else {for (i = 0; i < 16 * 256; i++) {nihongo[i] = hankaku[i]; /* 未找到字库时,半角部分直接复制英文字库 */}for (i = 16 * 256; i < 16 * 256 + 32 * 94 * 47; i++) {nihongo[i] = 0xff; /* 未找到字库,全角部分以0xff填充 */}}*((int *) 0x0fe8) = (int) nihongo;memman_free_4k(memman, (int) fat, 4 * 2880);
实现用日文字库来显示字符,首先在struct TASK中添加了一个langmode变量,用于指定一个任务使用内置的英文字库还是使用nihongo.fnt的日文字库。在字符显示时,根据langmode进行不同的处理。
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{extern char hankaku[4096];struct TASK *task = task_now();char *nihongo = (char *) *((int *) 0x0fe8);if (task->langmode == 0) {for (; *s != 0x00; s++) {putfont8(vram, xsize, x, y, c, hankaku + *s * 16);x += 8;}}if (task->langmode == 1) {for (; *s != 0x00; s++) {putfont8(vram, xsize, x, y, c, nihongo + *s * 16);x += 8;}}return;
}
此外我们还需要一个命令来对langmode的值进行设置:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, int memtotal)
{……} else if (strncmp(cmdline, "langmode ", 9) == 0) {cmd_langmode(cons, cmdline);} else if (cmdline[0] != 0) {……
}void cmd_langmode(struct CONSOLE *cons, char *cmdline)
{struct TASK *task = task_now();unsigned char mode = cmdline[9] - '0';if (mode <= 1) {task->langmode = mode;} else {cons_putstr0(cons, "mode number error.\n");}cons_newline(cons);return;
}
这样输入langmode 0就设置为英文模式,langmode 1就设置为日文模式。
接下来显示全角字符。根据JIS规格,全角字符的编码以"点,区,面"为单位来进行定义:
- 1个点对应1个全角字符
- 1个区中包含94个点
- 1个面中包含94个区
这是用来确定字符在字库中的位置的。比如对于字符0x82, 0xa0,根据表可知该字符位于04区02点,根据这个编号,就可以计算得到字模的内存地址用于显示。
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{extern char hankaku[4096];struct TASK *task = task_now();char *nihongo = (char *) *((int *) 0x0fe8), *font;int k, t;if (task->langmode == 0) {for (; *s != 0x00; s++) {putfont8(vram, xsize, x, y, c, hankaku + *s * 16);x += 8;}}if (task->langmode == 1) {for (; *s != 0x00; s++) {if (task->langbyte1 == 0) {if ((0x81 <= *s && *s <= 0x9f) || (0xe0 <= *s && *s <= 0xfc)) {task->langbyte1 = *s;} else {putfont8(vram, xsize, x, y, c, nihongo + *s * 16);}} else {if (0x81 <= task->langbyte1 && task->langbyte1 <= 0x9f) {k = (task->langbyte1 - 0x81) * 2;} else {k = (task->langbyte1 - 0xe0) * 2 + 62;}if (0x40 <= *s && *s <= 0x7e) {t = *s - 0x40;} else if (0x80 <= *s && *s <= 0x9e) {t = *s - 0x80 + 63;} else {t = *s - 0x9f;k++;}task->langbyte1 = 0;font = nihongo + 256 * 16 + (k * 94 + t) * 32;putfont8(vram, xsize, x - 8, y, c, font ); /* 左半部分 */putfont8(vram, xsize, x , y, c, font + 16); /* 右半部分 */}x += 8;}}return;
}
putfonts8_asc函数中,每接受到一个字节就会执行一次x += 8,当显示全角字符时,需要在接收到第2个字节之后,再往左回移8个像素并绘制字模的左半部分。
对于换行,当字符串很长时,可能在全角字符的第1个字节处就需要自动换行,这样接收到第2个字节时,字模的左半部分就会画到命令行窗口外面去。所以在遇到第1个字节换行时,将cur_x再右移8个像素。
void cons_newline(struct CONSOLE *cons)
{int x, y;struct SHEET *sheet = cons->sht;struct TASK *task = task_now();if (cons->cur_y < 28 + 112) {cons->cur_y += 16;} else {if (sheet != 0) {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;if (task->langmode == 1 && task->langbyte1 != 0) {cons->cur_x = 16;}return;
}
运行程序,显示结果:
对于显示中文汉字,可以对以上nihongo.fnt做如下的修改:
- 000000 - 000FFF: 英文半角字模,共256个字符(4096字节),来自系统内置字库数据
- 001000 - 02963F: 中文全角字模,共5170个字符(165440字节),来自HZK16或其他符合GB2312标准的汉字点阵字库
测试中文显示,可以用记事本等文本编辑器编写一个包含中文的文本文件,然后用GB2312编码进行保存。将文本文件装入磁盘映像,用type命令就可以显示出来了。