30天开发操作系统 第 20 天 -- API

前言

大家早上好,今天我们继续努力哦。 昨天我们已经实现了应用程序的运行, 今天我们来实现由应用程序对操作系统功能的调用(即API, 也叫系统调用)。
为什么这样的功能称为“系统调用”(system call)呢?因为它是由应用程序来调用(操作)系统中的功能来完成某种操作, 这个名字很直白吧。
“API” 这个名字就稍微复杂些,是“application program interface" 的缩写, 即“应用程序(与系统之间的)接口”的意思。请大家把这两个名字记住哦,考试题目中会有的哦.……开玩笑啦,这些其实用不着记啦。 有记这些单词的工夫,还不如多享受一下制作操作系统的乐趣呢。
这值得纪念的第一次,我们就来做个在命令行窗口中显示字符的API吧。BIOS中也有这个功能哦,如果忘了的话请重新看看第二天的内容。怎么样,找到了吧?无论什么样的操作系统, 都会有功能类似的API,这可以说是必需的。

一、程序整理

现在这程序是怎么回事!下面来改造一下我们操作系统, 让它可以使用API吧…
尤其是console task,简直太不像样了。看着如此混乱的程序代码,真是提不起任何干劲来进行改造, 我们还是先把程序整理一下吧。
由于只是改变了程序的写法,并没有改变程序处理的内容,因此这里就不讲解了。 从249行改到了85行的console_task, 哦耶!

console.c
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;char cmdline[30];cons.sht = sheet;cons.cur_x =  8;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));/* 显示提示符 */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) {	/* 光标ON */cons.cur_c = COL8_FFFFFF;}if (i == 3) {	/* 光标 OFF */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) { /* 键盘数据(通过任务A) */if (i == 8 + 256) {/* 退格键 */if (cons.cur_x > 16) {/* 用空格擦除光标后将光标前移一位 */cons_putchar(&cons, ' ', 0);cons.cur_x -= 8;}} 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);}}
}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;if (cons->cur_x == 8 + 240) {cons_newline(cons);}}}return;
}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;return;
}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;
}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;
}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;return;
}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;
}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;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;
}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;
}
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; /* 没有找到 */
}

嗯嗯,比之前的代码易读多了。你看,只要想把代码写得清爽些就一定能做到的,连笔者都 做到了嘛(笑)。这个例子说明,如果持续增加新的功能,一个函数的代码就会变得很长,像这 样定期整理一下还是很有帮助的。 好了,我们来“make run”,输人一些命令试试看。和之前运行的情况一样,很好。

二、显示单个字符的API

现在我们要开始做显示单个字符的API了哦。 说起来其实也不是很难, 只要应用程序能用某 种方法调用cons putchar就可以了。
首先我们做一个测试用的应用程序, 将要显示的字符编码存人AL寄存器, 然后调用操作系统的函数, 字符就显示出来了。

[BITS 32]MOV AL,'A’CALL (cons_putchar的地址)
fin:HLTJMP fin

就是这个样子。CALL是一个用来调用丽数的指令。在C语言中,goto和函数调用的处理方式 完全不同,不过在汇编语言中,CALL指令和JMP指令其实差不多是一码事,它们的区别仅仅在 当执行CALL指令时,为了能够在接下来执行RET指令时正确返回,会先将要返回的目标地 于, 址PUSH到栈中。
关于CALL指令这里想再讲一下。有人可能会想,直接写CALLcons putchar不就好了吗?然 而,hlt.nas这个应用程序在汇编时并不包含操作系统本身的代码,因此汇编器无法得知要调用的 函数地址,汇编就会出错。要解决这个问题,必须人工查好地址后直接写到代码中。在对haribote.sys进行make的时候, 通过一定的方法我们可以查出cons putchar的地址, 没有问题, 那S 么我们就来查一下地址… 且慢!

这样做有个问题, 因为cons_putchar是用C语言写的函数, 即便我们将字符编码存入寄存器 函数也无法接收, 因此我们必须在CALL之前将文字编码推入栈才行, 但这样做也太麻烦了。
没办法,我们只好用汇编语言写一个用来将寄 存器的值推入栈的函数了。这个函数不是应用 程序的一部分, 而是写在操作系统的代码中, 因此我们要改写的是naskfunc.nas。 另一方面, 在 应用程序中, 我们CALL的地址不再是cons_putchar, 而是变成了新写的 asm cons_putchar:

_asm_cons_putchar:PUSH	1AND		EAX,0xff	; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态PUSH	EAXPUSH	(cons的地址)CALL	_cons_putcharADD		ESP,12		; 丢弃栈中数据RET

PUSH的特点是后进先出,因此这个程序的顺序没问题。(这个12,是因为我们push了三次4个字节(32位),运行时栈的增长是从高地址向低地址,大家还记得吧😄)
栈传递(Stack Passing):

调用函数最常见的方法是将参数依次压入堆栈。调用函数后,函数通过堆栈访问这些参数。

push arg3
push arg2
push arg1
call function_name
add esp, 12 ; 清理堆栈(假设3个参数,每个4字节)
大家按照这个理解

这段程序的问题在于 “cons的地址" 到底是多少。应用程序是不知道这个地址的, 唔,那么只能让操作系统把这个地址事先保存在内存中的某个地方 用程序来指定地址难以实现。哪里比较好呢?对了, 就保存在BOOTINFO之前的0x0fec这个地址吧。
现在操作系统这边的工作已经完成了,因此我们先来“ make”一下, 注意这里不是"make run"。因为应用程序还没有准备好呢,所以我们先make。
make完成后, 除了haribote.sys之外, 还会生成一个叫bootpack.map的文件 首然 之前我们一直忽略这个文件的, 不过这次它要派上用场了。
这是一个文本文件,用文本编辑器打开:其中应该可以找到这样一行:
0x00000BE3 :_asm_cons putchar
这就是 _asm_cons putchar 的地址了,我们将地址填在应用程序中:

_asm_cons_putchar:PUSH	1AND		EAX,0xff	; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态PUSH	EAXPUSH	(cons的地址)CALL	_cons_putcharADD		ESP,12		; 丢弃栈中数据RET
void console_task(struct SHEET *sheet, unsigned int memtotal)
{...char cmdline[30];cons.sht = sheet;cons.cur_x =  8;cons.cur_y = 28;cons.cur_c = -1;*((int *) 0x0fec) = (int) &cons; /*这里!*/...
}

现在操作系统这边的工作已经完成了,因此我们先来“ make”一下, 注意这里不是"make run"。因为应用程序还没有准备好呢,所以我们先make。
make完成后, 除了haribote.sys之外, 还会生成一个叫bootpack.map的文件 首然 之前我们一直忽略这个文件的, 不过这次它要派上用场了。
这是一个文本文件,用文本编辑器打开:其中应该可以找到这样一行:
0x00000BE3 :_asm_cons putchar
这就是 _asm_cons putchar 的地址了,我们将地址填在应用程序中:

[BITS 32]MOV		AL,'A'CALL    0xbe3
fin:HLTJMP		fin

然后再进行汇编就可以了,很简单吧。

说起来, 我们写的这些代码里面, 哪个部分是API呢?“MOVE AL, A’”和“CALL 0xbe3” ,就是API了,因为API就是由应用程序来使用操作系统所提供的服务。当然,我们这个是否达到“服务” 的程度就另当别论了。
现在我们的应用程序也已经完成了, “make run”嘿!然后在命令行窗口里面运行 “hlt" 就可以了。
啊! qemu.exe出错关闭了!看来我们遇了一个不得了的大bug。 在真机环境下无法预料会造成什么后果, 因此请大家不要尝试。下面我们来解决这个bug。

像这样会造成模拟器出错关闭的bug, 果然只有在开发操作系统时才能碰到。 如果不用模拟器进行开发的话,不经意间产生的bug有时可能会造成电脑损坏、硬盘被格式化等严重问题, 也许好几天都无法恢复过来。 开发操作系统就是这么刺激。如果通过这次的bug, 大家能够瞥见这种刺激的冰山一角,那么这个bug也算是有点用的吧(苦笑)。

不过,光扯刺激啦什么的也无济于事, 我们还得仔细寻找原因。哦,原来如此,找到了!
原因其实很简单。应用程序对API执行CALL的时候,千万不能忘记加上段号。因此我们不能使用普通的CALL,应用程序所在的段为 “1003 * 8", 而操作系统所在的段为“2*8”,而应该使用far-CALL。
far-CALL实际上和far-JMP一样,只要同时指定段和偏移量即可。

[BITS 32]MOV		AL,'A'CALL    2*8:0xbe3
fin:HLTJMP		fin

好,完工了,这样bug应该就解决了, 我们来试试看。 make run然后运行“hlt"。还是不行。这次虽然没有出错关闭,但qemu.exe停止响应了。
这个问题是由于_asm_cons_putchar的RET指令所造成的。普通的RET指令是用于普通的 CALL 的返回,而不能用于far-CALL的返回,既然我们用了far-CALL,就必须相应地使用far-RET, 也就是RETF指令。因此我们将程序修改一下。

asm_cons_putchar:
(中略)
RETF ;这里!

好啦, 这次应该没问题了吧。
在这里插入图片描述

3.结束应用程序

照现在这个样子,应用程序结束之后会执行HLT,我们就无法在命令行窗口中继续输入命令 了,这多无聊啊。如果应用程序结束后不执行HLT,而是能返回操作系统就好了。
怎样才能实现这样的设想呢?没错,只要将应用程序中的HLT改成RET, 就可以返回了。相应地,操作系统这边也需要用CALL来代替JMP启动应用程序才对。虽说是CALL,不过因为要调用的程序位于不同的段, 所以实际上应该使用far-CALL, 因此应用程序那边也应该使用RETF。 我们的方针已经明确了。

C语言中没有用来执行far-CALL的命令,我们来创建一个farcall函数,这个函数和farjmp大同小异。

_farcall:		; void farcall(int eip, int cs);CALL	FAR	[ESP+4]				; eip, csRET

我们还要把hlt命令的处理改为调用farcall。

void cmd_hlt(struct CONSOLE *cons, int *fat)
{	...if (finfo != 0) {...farcall(0, 1003 * 8); /* 这里 */} else {}...
}

最后我们还要改写一下应用程序hlt.nas, 把HLT换成RETF就可以了。

[BITS 32]MOV		AL,'A'CALL    2*8:0xbe3RETF

完工了哦。好, 我们来 make run 然后运行“hlt”。貌似是有bug (今天我们碰了好几个钉子了嘛)。 qemu.exe又停止响应了, 明白了。由于我们改写了操作系统的代码, 怎么回事呢?导致asm_cons_putchar的地址 发生了变化。重新查看bootpack.map,我们发现地址变成了这样:
0x0000BE8 : asm_cons putchar
因此, 我们把应用程序的地址修改一下:

[BITS 32]MOV		AL,'A'CALL    2*8:0xbe8RETF

'make run", “hlt",怎么样?好了!成功了!
在这里插入图片描述

趁热打铁, 我们再来个新的尝试: hello” 显示

貌似用循环比较好呢?算了,实在太麻烦(笑)。我们运行一下试试看, 结果如下。
在这里插入图片描述

话说回来,现在这个应用程序已经和当初“hlt"这个名字完全对不上号了, 看来我们得赶快给它改改名字了哦。

4.不随操作系统版本而改变的API

所以说我们又要改写console.c。等等,如果修改了操作系统的代码,岂不是asm_cons_ putchar的地址也会像上次那样发生变化?难道说每次我们修改操作系统的代码,都得把应用程序的代码也改一遍?这也太麻烦了。

虽说确实有的操作系统版本一改变,应用程序 也得重新编译,不过还有些系统即便版本改变,应用程序也照样可以运行,大家觉得哪种更好呢?

把这个搞定之后, 我们再考虑命名的事。
解决这个问题的方法其实有很多,这里先为大家介绍其中一种。
CPU中有个专门用来注册函数的地方,也许大家一下子想不起来,其实是中断处理 程序。在前面我们曾经做过“当发生IRQ-1的时候调用这个函数”这样的设置, 大家还记得吗? 这是在IDT中设置的。
而CPU用于通知异常状态的中断最多也只有32种,这些都在CPU规格说 反正IRQ只有0~15, 明书中有明确记载。 不过,IDT中却最多可以设置256个函数,因此还剩下很多没有使用的项。
我们的操作系统从这些项里面借用一个的话, CPU应该也不会有什么意见的吧。所以我们就从IDT中找一个空闲的项来用一下。好,我们就选0x40号(其实0x30~0xff都是空闲的,只要在这个范围内任意一个都可以),并将_asm_ cons_putchar注册在这里。

void init_gdtidt(void)
{...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);return;
}

我们只要用INT 0x40来代原来的CALL 2*8:0xbd1就可以调用_asm_ cons_putchar了。这样一来,很方便吧?我们来修改一下应用程序吧。

[BITS 32]MOV		AL,'h'INT		0x40MOV		AL,'e'INT		0x40MOV		AL,'l'INT		0x40MOV		AL,'l'INT		0x40MOV		AL,'o'INT		0x40RETF

于是程序变成了这个样子。看到这里,直觉敏锐的你也许已经发现了“跟调用BIOS的时候差不多嘛….” 虽然INT号不同,但通过INT方式调用这一点的确是非常类似。说起来, 没错,MS-DOS的API采用的也是这种INT方式。
另外,使用INT指令来调用的时候会被视作中断来处理, 需要使用 IRETD指令,用RETF是无法返回的, 我们还要改写_asm_ cons_putchar。

_asm_cons_putchar:STIPUSH	1AND		EAX,0xff	; 将AH和EAX的高位置0,将EAX置为已存入字待编码的状态PUSH	EAXPUSH	DWORD [0x0fec]	; 读取内存并PUSH该值CALL	_cons_putcharADD		ESP,12		; 丢弃栈中的数据IRETD	;这里!

用INT调用时,对于CPU来说相当于执行了中 断处理程序,因此在调用的同时CPU会自动执 但我们只是用它来代替CALL使用,这种做法就显得画蛇添足了。 行CLI指令来禁止中断请求。 我们可不想看到“API处理时键盘无法输入”这样的情况, 因此需要在开头添加一条STI指令。
对于这种问题,一般来说可以通过在注册到IDT时修改设置来禁止CPU擅自执行CLI, 其实, 最近貌似懒到家了,得反省一下。 不过这个有点麻烦, 还是算了吧(笑)。

make run → 结果如下:

你看,用这种方法还能把应用程序缩小。这是因为far-CALL指令需要7个字节而INT指令只需要2个字节的缘故。 这次修改还真是一箭双雕呢。
在这里插入图片描述

5.应用程序自由命名

现在我们的应用程序只能用hit这个名字,下面我们来让系统支持其他应用程序名,这次我们就用hello吧。 将console.c中的 “hlt” 改成“hello", 好啦, 这样我们就可以用hello这个应用程序 …··.!别生气别生气,开个玩笑而已(😆)。
好吧, 我们先来改写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。
这个函数用来根据命令行的内容判断文件名,并运行相应的应用程序 ,如果找到文件则返回1,没有找到文件则返回0。 现在程序的工作过程是:当输入的命令不是me m、cls、dir、type其中之一时,则调用cmd_app, 如果返回0则作为错误处理。 这样应该能行。
我们在cmd_hlt的基础上稍作修改后得到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;
}

我们在程序上动了一点脑筋,使得无论输入“hlt” 还是”“hlt.hrb” 都可以启动。因为在Windows命令行窗口中:不管加不加后面的 .exe都可以运行程序,所以我们就借鉴了这个设计。

差不多完工了,我们将hlt.nas改名为hello.nas, 然后汇编生成hello.hrb。 接下来 make run 用dir命令确认一下磁盘中的内容,再输入“hello”。 ha!出来了!成功了!
在这里插入图片描述

不错!我们再来输人“hlt”试一下,这个文件现在已经没有了, 会不会报错呢?另外, 嗯, 如果输入"hello.hrb” 能否正常运行呢?我们来试试看。
在这里插入图片描述

出现错误信息了, 加上扩展名的情况也可以,太完美了。

6.小心寄存器

hello.hrb的大小现在是21个字节,能不能再让它变小点呢?我们做了如下修改,用了一个循环。

[INSTRSET "i486p"]
[BITS 32]MOV		ECX,msg
putloop:MOV		AL,[CS:ECX]CMP		AL,0JE		finINT		0x40ADD		ECX,1JMP		putloop
fin:RETF
msg:DB	"hello",0

改成这样后make一下,hello.hrb变成了26个字节,居然还大了5个字节,哎,好失望。不过, 这样改也有好处,即便以后要显示很长的字符符,程序也不会变得太大。
在这里插入图片描述

为啥只显示出一个呢? 再把hello.nas仔细检查一遍,也没发现什么不对劲的地方啊···
那问题肯定出在操作系统身上。 既然应用程序没问题, 不过,到底是哪里有问题呢?刚刚找到了点眉目,我们给_asm_cons_putchar添上2行代码,就是PUSHAD和POPAD。

_asm_cons_putchar:STIPUSHAD	; 这里!PUSH	1AND		EAX,0xff	; PUSH	EAXPUSH	DWORD [0x0fec]	; CALL	_cons_putcharADD		ESP,12		; POPAD	; 这里!IRETD

为什么要这么改我们待会儿再讲,先来试验一下。
在这里插入图片描述

果然是这个问题呀。 那为什么会想到加上PUSHAD和POPAD呢?因为推测这有可能是 INT 0x40之后ECX寄存器的值发生了变化所导致的,应该是_cons_putchar改动了ECX的值。因此,我们加上了PUSHAD和POPAD确保可以将全部寄存器的值还原,这样程序就能正常运行了。

7.用API显示字符串

能显示字符申的API远比只能显示单个字符的API要来的方便, 从实际的应用程序开发角度来说,因为一次显示一串字符的情况比一次只显示个字符的情况多得多。从其他操作系统的显示字符申的API来看,一般有两种方式:一种是显示一串字符,遇到字符编码0则结束;另一种是先指定好要显示的字串的长度再显示。我们到底要用哪一种呢? 再三考虑之后,我们打算同时实现两种方式(笑)。

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;
}

哦,对了, 有了这个函数,就可以简化mem、 dir、type这几个命令的代码,趁着还没忘记, 赶紧改良一下。

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");/* 这里 */}}return;
}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;
}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;
}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;
}

代码缩减了12行,什么嘛!一开始就这样写不就好了吗?不过不管怎么说也算是个值得高兴 的事吧。
在上面字符串中我们使用了“\n” 这个新的符号, 这里来讲解一下。在C语言中, “\”这个 字符有特殊的含义,用来表示一些特殊字符。 这里出现的“\n” 代表换行符,即0x0a,也就是说用2个字符来表示1个字节的信息, 有点怪吧。此外还有 “\t”, 它代表制表符, 即0x09。顺便说一 下, 换行符“\n” 之所以用“n”,是因为它是“new line" 的缩写。

我们已经有了cons_putstr0 和 cons_putstr1,那么怎样把它们变成API呢?最简单的方法就是像显示单个字符的API那样, 分配INT0x41和INT0x42来调用这两个函数。 不过这样一来, 只能设置256个项目的IDT很快就会被用光。
既然如此,我们就借鉴BIOS的调用方式, 在寄存器中存人功能号, 使得只用1个INT就可以用来选择调用不同的函数。 在BIOS中, 存放功能号的寄存器一般是AH, 我们也可以照搬, 但这样最多只能设置256个API函数。而如果我们改用EDX来存放功能号, 就可以设置多达42亿个API函数。这样总不会不够用了吧。

功能号暂时按下面那样划分, 寄存器的用法也是随意设定的, 如果不喜欢的话尽管修改就好哦。

功能号1………….显示单个字符(AL=字符编码)
功能号2… 显示字符串0(EBX=字符串地址)
功能号3………显示字符串1(EBX=字符串地址,ECX=字符串长度)
接下来我们将_asm_cons_putchar改写成一个新的函数。

_asm_hrb_api:STIPUSHAD	; 用于保存寄存器值的PUSHPUSHAD	; 用于向hrb_api传值的PUSHCALL	_hrb_apiADD		ESP,32POPADIRETD

这个函数非常短, 因为我们想尽量用C语言来编写API处理程序, 而且这样大家也更容易理解。
用C语言编写的API处理程序如下:

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);} else if (edx == 2) {cons_putstr0(cons, (char *) ebx);} else if (edx == 3) {cons_putstr1(cons, (char *) ebx, ecx);}return;
}

嗯,还是挺好理解的吧。开头的寄存器顺序是按照PUSHAD的顺序写的,如果在_asm_hrb_api 中不用PUSHAD,而是个一个分别去PUSH的话,那当然可以按照自己喜欢的顺序来。
啊,对了对了,我们还得改一下IDT的设置,将INT 0x40改为调_asm_hrb_api。

void init_gdtidt(void)
{...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);return;
}

这样改写之后, 现在的hello.nas就无法正常运行了, 因为我们需要往EDX里面存人1才能调用相应的API。虽说我们加上一条向EDX中存入1的指令就可以, 不过既然已经写好了cons_putstr0, 那就于脆用这个新的API写一个hello2.nas吧。

[INSTRSET "i486p"]
[BITS 32]MOV		EDX,2MOV		EBX,msgINT		0x40RETF
msg:DB	"hello",0

完工了, 好, 赶紧 运行 "hello2” 试试看。 make run
在这里插入图片描述

……貌似失败了,怎么回事昵?今天已经很累了, 脑子都不转了,我们还是明天再来找原因吧。总之,我们先将这个放在一边,在以前的hello.nas中加一条EDX= 1;试试看吧。

[INSTRSET "i486p"]
[BITS 32]MOV		ECX,msgMOV		EDX,1 ;这里!
putloop:MOV		AL,[CS:ECX]CMP		AL,0JE		finINT		0x40ADD		ECX,1JMP		putloop
fin:RETF
msg:DB	"hello",0

在这里插入图片描述

成功了, 总算稍稍松了口气。 今天我们在最后的最后碰了个大钉子(就是hello2),心情有点不爽, 不过已经困得不行了, 就先到这吧!大家明天见。

总结

今天我们在最后的最后碰了个大钉子(就是hello2),心情有点不爽, 不过已经困得不行了, 就先到这吧!
祝大家元宵节快乐,团团圆圆,巳巳如意!
我们明天见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/18187.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UE5.2后 Bake Out Materials失效

这个问题出现在5.3&#xff0c;5.4&#xff0c;5.5没有测试 烘焙贴图后会找不到贴图位置&#xff0c; 这个是5.2的正常状态 默认是生成在模型当前目录里&#xff0c;包括新的材质 但是这个bug会让材质和贴图都消失&#xff0c;无法定位 暂时没有办法解决&#xff0c;等官方 …

macOS部署DeepSeek-r1

好奇&#xff0c;跟着网友们的操作试了一下 网上方案很多&#xff0c;主要参考的是这篇 DeepSeek 接入 PyCharm&#xff0c;轻松助力编程_pycharm deepseek-CSDN博客 方案是&#xff1a;PyCharm CodeGPT插件 DeepSeek-r1:1.5b 假设已经安装好了PyCharm PyCharm: the Pyth…

架构设计系列(二):CI/CD

一、概述 CI/CD 是 持续集成&#xff08;Continuous Integration&#xff09; 和 持续交付/持续部署&#xff08;Continuous Delivery/Continuous Deployment&#xff09; 的缩写&#xff0c;是现代软件开发中的一套核心实践和工具链&#xff0c;旨在提高软件交付的效率、质量…

Windows 11 搭建私有知识库(docker、dify、deepseek、ollama)

一、操作系统信息 版本 Windows 11 家庭中文版 版本号 23H2 安装日期 ‎2023/‎8/‎21 操作系统版本 22631.4460二、搭建思路 ollama拉取deepseek、bge-m3模型docker拉取dify的镜像dify链接ollama使用模型&#xff0c;并上传文件搭建知识库&#xff0c;创建应用 三、搭建步骤…

本地部署DeepSeek摆脱服务器繁忙

由于图片和格式解析问题&#xff0c;可前往 阅读原文 最近DeepSeek简直太火了&#xff0c;频频霸榜热搜打破春节的平静&#xff0c;大模型直接开源让全球科技圈都为之震撼&#xff01;再次证明了中国AI的换道超车与崛起 DeepSeek已经成了全民ai&#xff0c;使用量也迅速上去了…

‌CBA认证‌(业务架构师认证)简介---适用人群、考试内容与形式、含金量与职业前景,以及‌CBA、TOGAF认证对比表格

‌CBA认证‌&#xff0c;即业务架构师认证&#xff08;Certified Business Architect&#xff0c;CBA&#xff09;&#xff0c;是由业务架构师协会&#xff08;Business Architecture Institute&#xff09;推出的一项国际认证计划。该认证旨在评估和认证业务架构师的专业能力和…

保姆级GitHub大文件(100mb-2gb)上传教程

GLF&#xff08;Git Large File Storage&#xff09;安装使用 使用GitHub desktop上传大于100mb的文件时报错 The following files are over 100MB. lf you commit these files, you will no longer beable to push this repository to GitHub.com.term.rarWe recommend you a…

使用 Visual Studio Code (VS Code) 开发 Python 图形界面程序

安装Python、VS Code Documentation for Visual Studio Code Python Releases for Windows | Python.org 更新pip >python.exe -m pip install --upgrade pip Requirement already satisfied: pip in c:\users\xxx\appdata\local\programs\python\python312\lib\site-pa…

Python的那些事第二十一篇:Python Web开发的“秘密武器”Flask

基于 Flask 框架的 Python Web 开发研究 摘要 在 Web 开发的江湖里,Python 是一位武林高手,而 Flask 则是它手中那把小巧却锋利的匕首。本文以 Flask 框架为核心,深入探讨了它在 Python Web 开发中的应用。通过幽默风趣的笔触,结合实例和表格,分析了 Flask 的特性、优势以…

Qt开发①Qt的概念+发展+优点+应用+使用

目录 1. Qt的概念和发展 1.1 Qt的概念 1.2 Qt 的发展史&#xff1a; 1.3 Qt 的版本 2. Qt 的优点和应用 2.1 Qt 的优点&#xff1a; 2.2 Qt 的应用场景 2.3 Qt 的应用案例 3. 搭建 Qt 开发环境 3.1 Qt 的开发工具 3.2 Qt SDK 的下载和安装 3.3 Qt 环境变量配置和使…

【第4章:循环神经网络(RNN)与长短时记忆网络(LSTM)— 4.3 RNN与LSTM在自然语言处理中的应用案例】

咱今天来聊聊在人工智能领域里,特别重要的两个神经网络:循环神经网络(RNN)和长短时记忆网络(LSTM),主要讲讲它们在自然语言处理里的应用。你想想,平常咱们用手机和别人聊天、看新闻、听语音助手说话,背后说不定就有 RNN 和 LSTM 在帮忙呢! 二、RNN 是什么? (一)…

DeepSeek应用——与PyCharm的配套使用

目录 一、配置方法 二、使用方法 三、注意事项 1、插件市场无continue插件 2、无结果返回&#xff0c;且在本地模型报错 记录自己学习应用DeepSeek的过程&#xff0c;使用的是自己电脑本地部署的私有化蒸馏模型...... &#xff08;举一反三&#xff0c;这个不单单是可以用…

国自然地区基金|影像组学联合病理组学预测进展期胃癌术后预后的研究|基金申请·25-02-13

小罗碎碎念 今天和大家分享一个国自然地区科学项目&#xff0c;执行年限为2020.01&#xff5e;2023.12&#xff0c;直接费用为34万元。 胃癌在我国发病形势严峻&#xff0c;现有TNM分期预后评估存在局限&#xff0c;难以满足精准医疗需求。本项目运用“医工结合&#xff0c;学科…

【Java集合一】集合概述

一、集合简介 Java 集合框架&#xff08;Collection Framework&#xff09;是 Java 提供的一组用于存储和操作对象的类和接口集合。这些集合类提供了不同的数据结构&#xff0c;使得数据的管理和操作更加方便和高效。 Java 集合框架提供了各种类型的数据结构&#xff0c;如列…

k8s集群搭建参考(by lqw)

文章目录 声明配置yum源安装docker安装 kubeadm&#xff0c;kubelet 和 kubectl部署主节点其他节点加入集群安装网络插件 声明 由于看了几个k8s的教程&#xff0c;都存在各种问题&#xff0c;自己搭建的时候&#xff0c;踩了不少坑&#xff0c;最后还是靠百度csdnchatGPT才搭建…

MySQL 插入替换语句(replace into statement)

我们日常使用 insert into 语句向表中插入数据时&#xff0c;一定遇到过主键或唯一索引冲突的情况&#xff0c;MySQL的反应是报错并停止执行后续的语句&#xff0c;而replace into语句可以实现强制插入。 文章目录 一、replace into 语句简介1.1 基本用法1.2 使用set语句 二、注…

日语发音的节拍

短音 每个假名&#xff08;包括清音、浊音、半浊音&#xff09;都占 1 拍。 长音 长音占 2 拍&#xff0c;发音时间比短音长 不同母音的长音形式不同&#xff08;あ段あ&#xff0c;い段い&#xff0c;う段う&#xff0c;え段い/え&#xff0c;お段う/お&#xff09; 促音 …

[AI]从零开始的llama.cpp部署与DeepSeek格式转换、量化、运行教程

一、前言 在上一次的DeepSeek的部署教程中&#xff0c;我们使用Ollama与LM Studio很轻松的部署了DeepSeek并且也完成了相关API的调用&#xff0c;如果还有不会的小伙伴请看下面的教程&#xff1a; DeepSeek本地部署&#xff1a;[AI]从零开始的DeepSeek本地部署及本地API调用教…

基于SSM+uniapp的数学辅导小程序+LW示例参考

1.项目介绍 系统角色&#xff1a;管理员、普通用户功能模块&#xff1a;用户管理、学习中心、知识分类管理、学习周报管理、口算练习管理、试题管理、考试管理、错题本等技术选型&#xff1a;SSM&#xff0c;Vue&#xff08;后端管理web&#xff09;&#xff0c;uniapp等测试环…

解决DeepSeek服务器繁忙问题

目录 解决DeepSeek服务器繁忙问题 一、用户端即时优化方案 二、高级技术方案 三、替代方案与平替工具&#xff08;最推荐简单好用&#xff09; 四、系统层建议与官方动态 用加速器本地部署DeepSeek 使用加速器本地部署DeepSeek的完整指南 一、核心原理与工具选择 二、…