当前位置: 首页 > article >正文

【读书笔记-《30天自制操作系统》-27】Day28

本篇的内容不少,主要围绕着文件操作与文字显示展开。
在这里插入图片描述

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命令就可以显示出来了。


http://www.kler.cn/news/357476.html

相关文章:

  • 【分立元件】方形贴片固定电阻器制造流程
  • 机器学习数据标准化与归一化:提升模型精度的关键
  • RabbitMQ 持久化与不公平分发
  • sqli-labs less-25a
  • 单片机的寻址方式有哪些?
  • 创建虚拟机并安装操作系统
  • 贪心day4
  • 【人工智能-初级】第9章 神经网络的基础:理解感知器与激活函数
  • qt项目使用其他项目的ui之单继承之成员变量
  • Cookie与Session的区别(特别详细)
  • C++学习路线(十六)
  • [论文阅读]: Detecting Copyrighted Content in Language Models Training Data
  • 【python】OpenCV—Fourier Transform
  • 十一、SQL 优化:提升数据库性能的关键技巧与实例讲解
  • MongoDB 的优点和缺点
  • 探索YOLO v11:3D人工智能的RGB-D视觉革命
  • 深度解析 Redis 存储结构及其高效性背后的机制
  • 放眼全球:在竞争激烈的当下,海外媒体发稿何以备受关注?
  • UDP协议和TCP协议
  • 常见网络钓鱼类型