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

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

本篇的内容围绕系统调用展开。为了让应用程序能够调用操作系统功能,引入了系统调用以及API的概念。首先实现了显示单个字符的API,让应用程序通过传递地址的方式进行调用;接下来又改进为通过中断的方式进行调用。在此基础上继续实现了显示字符串的API。

在这里插入图片描述

1. 显示单个字符的API:传递地址方式调用

应用程序调用操作系统功能,称为系统调用(system call);而API是指应用程序与操作系统接口(application program interface)。

首先来实现显示单个字符的API。上一篇中显示字符的程序,我们改写成一个函数cons_putchar:

void cons_putchar(struct CONSOLE *cons, int chr, char move)
{char s[2];s[0] = chr;s[1] = 0;if (s[0] == 0x09) {	/* tab */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;	}}} 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) {cons->cur_x += 8;if (cons->cur_x == 8 + 240) {cons_newline(cons);}}}return;
}

使用cons_putchar就可以输出单个字符。这样在应用程序中只需要调用cons_putchar就可以了。使用类似于下面的代码:

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

在汇编语言中,CALL指令与JMP指令类似,区别在于调用CALL指令时,为了能够在执行RET时正确返回,需要先将返回的目标地址PUSH到栈中。应用程序无法知道操作系统中函数的地址,因此这里不能在应用程序中直接调用cons_putchar函数,需要查到cons_putchar函数的地址后填写到应用程序的代码中。
又因为cons_putchar是C语言函数,将需要显示的字符通过写入寄存器的方式来进行参数传递,函数无法直接接收。因此这里还需要通过一个汇编语言函数将寄存器的值推入栈,再调用cons_putchar函数进行显示。

在这里插入图片描述调用cons_putchar函数,需要传入cons变量的地址。应用程序当然无法知道这个地址,需要操作系统传递。这里将这个地址放在0x0fec中,在asm_cons_putchar函数中从0x0fec地址中获取。

void console_task(struct SHEET *sheet, unsigned int memtotal)
{
……struct CONSOLE cons;char cmdline[30];cons.sht = sheet;cons.cur_x =  8;cons.cur_y = 28;cons.cur_c = -1;*((int *) 0x0fec) = (int) &cons;
……
_asm_cons_putchar:PUSH	1AND		EAX,0xff	; EAX高位置0,EAC置为存入字符编码的状态PUSH	EAXPUSH	DWORD [0x0fec]	;0x0fec中读取cons地址并PUSHCALL	_cons_putchar		; 调用cons_putchar显示字符ADD		ESP,12		; 丢弃栈中的数据RET

到这里还没有完成,应用程序还不知道asm_cons_putchar的地址,无法对其进行调用。先运行make,会生成bootpack.map的文件,用文本编辑器打开,可以找到如下的行:

0x00000BE3 : _asm_cons_putchar

这就是asm_cons_putchar函数的地址,可以将其填入到应用程序中:

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

这样就可以运行应用程序了。在命令行中输入hlt并回车,却发现QEMU出错关闭了。

这是什么原因呢?

原来应用程序在对API执行CALL指令时需要加上段号。我们给应用程序设置的段号为1003,而操作系统的段位2,不能使用普通的CALL,而应使用fat-CALL。与far-JMP指令一样,fat-CALL指令需要指定段号和偏移量。相应地,RET指令也需要使用RETF来代替。

[BITS 32]MOV AL,'A'CALL	2*8:0xbe3fin:HLTJMP	fin
_asm_cons_putchar:PUSH	1AND		EAX,0xff	; EAX高位置0,EAC置为存入字符编码的状态PUSH	EAXPUSH	DWORD [0x0fec]	;0x0fec中读取cons地址并PUSHCALL	_cons_putchar		; 调用cons_putchar显示字符ADD		ESP,12		; 丢弃栈中的数据RETF

这样修改之后再运行hlt,就可以正常显示了:

在这里插入图片描述
应用程序完成字符显示之后就进入了HLT循环,相当于就这样卡住了。是否能让其回到操作系统继续执行其他命令呢?

我们可以将HLT部分改成RET。前文使用JMP来运行应用程序,这里也需要改成CALL。
由于CALL调用的程序位于不同的段,其实是far-CALL,因此对应地也要使用RETF。首先编写一个函数farcall:

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

改写调用应用程序的部分如下:

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);farcall(0, 1003 * 8);// 用CALL代替JMPmemman_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;
}

不过重新编译之后,asm_cons_putchar函数的地址发生了变化,对应修改应用程序中的地址:

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

这样运行结束后就可以退回到操作系统了。还可以显示一个简单的字符串:

[BITS 32]MOV		AL,'h'CALL    2*8:0xbe8MOV		AL,'e'CALL    2*8:0xbe8MOV		AL,'l'CALL    2*8:0xbe8MOV		AL,'l'CALL    2*8:0xbe8MOV		AL,'o'CALL    2*8:0xbe8RETF

在这里插入图片描述
2. 显示单个字符的API:中断方式调用

如上面所展示的,如果修改了操作系统的代码,相应函数的地址发生了变化,用这种通过地址调用的方式就总要修改应用程序的代码,这样会很麻烦。

为了解决这个问题,我们回想一下前面介绍过的中断处理函数。IDT中最多可以注册256个函数,出了IRQ0-15以外,还有很多空闲,我们可以从中找一个注册asm_cons_putchar函数。比如选择0x40号:

void init_gdtidt(void)
{
……/* IDT设置 */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);……
}

这样只需要使用INT 40指令就可以调用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

使用INT指令来调用asm_cons_putchar函数,会被视为中断处理,会自动调用CLI来禁止中断请求,而实际上这里并非中断,我们应该允许此时接收中断,因此在开头使用STI允许中断请求;返回指令也需要使用IRETD:

_asm_cons_putchar:STIPUSH	1AND		EAX,0xff	PUSH	EAXPUSH	DWORD [0x0fec]	CALL	_cons_putcharADD		ESP,12		; IRETD

这样修改下来,应用程序就不用总是随着操作系统的修改而修改了。

3. 显示字符串的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;
}

这样前文显示字符串的部分都可以用以上的函数进行替换了。

有了显示字符串的函数,如何变成API呢?还是采用注册中断函数的方法。不过中断号毕竟还是有限的,如果每个函数都注册一个中断号,中断号还是很容易就被占满的。这里我们可以仿照BIOS的调用,通过传入不同的功能号,只用一个INT就可以选择调用多个不同的函数了。

  • 功能号1: 显示单个字符(AL = 字符编码)
  • 功能号2: 显示字符串函数0(EBX = 字符串地址)
  • 功能号3: 显示字符串1(EBX = 字符串地址,ECX = 字符串长度)

asm_cons_putcha修改为新的函数:

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

处理函数hrb_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;
}

这样应用程序通过传入不同的功能号调用asm_hrb_api函数,hrb_api就会根据传入参数来选择不同的函数。再把IDT中的0x40号函数修改成asm_hrb_api函数就可以了。

参数变了,应用程序也需要进行改写一下:

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

运行结果如下:
在这里插入图片描述
并没有显示出任何内容?这是什么原因呢?下一篇我们来继续解决,敬请期待。

http://www.lryc.cn/news/435785.html

相关文章:

  • Kubernetes服务注册与发现
  • 【 html+css 绚丽Loading 】000047 玄武流转盘
  • 线程池原理及改造
  • 彻底理解mysql Buffer Pool (拓展)
  • 信号量(二值信号量和计数信号量)和互斥量
  • 结构型模式-python版
  • Java重修笔记 第五十四天 坦克大战(二)常用的绘图方法、画出坦克图形
  • OpenAI澄清:“GPT Next”不是新模型。
  • <<编码>> 第 10 章 逻辑与开关(Logic and Switches) 示例电路
  • 深入浅出 Ansible 自动化运维:从入门到实战
  • 一句话描述设计模式
  • 【Linux】Ubuntu 22.04 shell实现MySQL5.7 tar 一键安装
  • SQL Server开启网络访问
  • el-input设置type=‘number‘和v-model.number的区别
  • 6.第二阶段x86游戏实战2-理解程序流程
  • Netty笔记01-Netty的基本概念与用法
  • OpenHarmony鸿蒙( Beta5.0)RTSPServer实现播放视频详解
  • QT使用事件事件和绘制事件实现简易时钟
  • kubeadm方式安装k8s
  • 如何使用go生成可执行文件
  • 手写Promise
  • 深度学习云服务器免费使用教程
  • 使用ansible的剧本制作salt-master与salt-minion的安装与启动服务过程
  • 数据库sqlite3
  • 开发基础之Python 函数(Basic Python Functions for Development)
  • Django_Vue3_ElementUI_Release_001_项目初始化
  • MySQL之安装与基础知识
  • 前端基础 | HTML基础:HTML结构,HTML常见标签
  • 宏任务和微任务+超全面试真题
  • 针对SVM算法初步研究