本篇的内容不少,主要围绕着文件操作与文字显示展开。
1. alloca函数
在开发文件操作与文字显示之前,需要先做一些准备,引入alloca函数。首先看下面的代码:
#include <stdio.h>
#include "apilib.h"
#define MAX 1000
void 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 10000
void 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,-4
SUB ESP,EAX
JMP 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 EBX
MOV EDX,21
MOV EBX,[ESP+8] ; fname
INT 0x40
POP EBX
RET
_api_fclose: ; void api_fclose(int fhandle);
MOV EDX,22
MOV EAX,[ESP+4] ; fhandle
INT 0x40
RET
_api_fseek: ; void api_fseek(int fhandle, int offset, int mode);
PUSH EBX
MOV EDX,23
MOV EAX,[ESP+8] ; fhandle
MOV ECX,[ESP+16] ; mode
MOV EBX,[ESP+12] ; offset
INT 0x40
POP EBX
RET
_api_fsize: ; int api_fsize(int fhandle, int mode);
MOV EDX,24
MOV EAX,[ESP+4] ; fhandle
MOV ECX,[ESP+8] ; mode
INT 0x40
RET
_api_fread: ; int api_fread(char *buf, int maxsize, int fhandle);
PUSH EBX
MOV EDX,25
MOV EAX,[ESP+16] ; fhandle
MOV ECX,[ESP+12] ; maxsize
MOV EBX,[ESP+8] ; buf
INT 0x40
POP EBX
RET
代码内容比较简单,与上文的设计相符合。
最后再编写一个用于测试的应用程序,该程序实现的功能是将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 EBX
MOV EDX,26
MOV ECX,[ESP+12] ; maxsize
MOV EBX,[ESP+8] ; buf
INT 0x40
POP EBX
RET
最后是编写应用程序,命名为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命令就可以显示出来了。