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

server/2024/10/18 19:34:40/

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

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


http://www.ppmy.cn/server/131497.html

相关文章

docker安装RabbitMQ,开启mqtt协议,并且SpringBoot继承mqtt

介绍 MQTT&#xff08;消息队列遥测传输&#xff09;是ISO标准&#xff08;ISO/IEC PRF 20922&#xff09;下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上&#xff0c;是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议。国内很多企业…

电脑查不到IP地址是什么原因?怎么解决

在日常使用电脑的过程中&#xff0c;有时会遇到无法查询到电脑IP地址的情况&#xff0c;这可能会影响到网络的正常使用。本文将探讨电脑查不到IP地址的可能原因&#xff0c;并提供相应的解决方案。 一、原因分析 ‌网络连接问题‌&#xff1a;首先&#xff0c;网络连接不稳定或…

vue面试题

一、keep-alive 在软件开发中&#xff0c;缓存组件是提高应用程序性能和响应速度的重要手段。特别是在前端框架如Vue.js中&#xff0c;缓存组件的技术被广泛使用。下面将详细介绍如何缓存当前组件、缓存后的更新机制&#xff0c;以及我对Vue.js中keep-alive组件的理解。 1.缓…

Leetcode 3319. K-th Largest Perfect Subtree Size in Binary Tree

Leetcode 3319. K-th Largest Perfect Subtree Size in Binary Tree 1. 解题思路2. 代码实现 题目链接&#xff1a;3319. K-th Largest Perfect Subtree Size in Binary Tree 1. 解题思路 这一题其实就是一个很常见的树的遍历&#xff0c;我们自底向上遍历每一个子树&#x…

ASP.NET MVC 下拉框的传值-foreach循环

数据表&#xff1a; -- 创建包裹分类表 CREATE TABLE PackageCategories (CategoryID INT PRIMARY KEY IDENTITY(1,1), -- 分类ID&#xff1a;整数类型&#xff0c;主键&#xff0c;自增&#xff0c;包裹分类的唯一标识CategoryName NVARCHAR(255) NOT NULL -- 包裹分类名称&a…

百度搜索引擎是如何解决用户点击率与网站排名关联度的呢?

百度搜索引擎是如何解决用户点击率与网站排名的关联度 大家好&#xff0c;我是林汉文&#xff08;SEO专家&#xff09;&#xff0c;今天我们来讨论一个非常有意义的话题&#xff1a;百度搜索引擎是如何解决用户点击率与网站排名的关联度&#xff0c;在当今搜索引擎优化&#x…

大数据毕业设计选题推荐-招聘信息数据分析系统-Python数据可视化-Hive-Hadoop-Spark

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

react hooks中在setState后输出state为啥没有变化,如何解决

在 React Hooks 中&#xff0c;setState 的概念被 useState 或 useReducer 钩子所替代。与类组件中的 setState 一样&#xff0c;这些钩子也是异步更新状态的。因此&#xff0c;如果你尝试在调用 setState&#xff08;即 setXXX 函数&#xff09;后立即读取状态值&#xff0c;你…