操作系统|30天自制操作系统——第十九天系统调用(API)

系统调用 API(application program interface)即应用系统对操作系统功能的调用,也可以称为系统调用(system call)。
今天我们就来实现系统调用的功能,我们先来思考一下WIN10系统是如何实现系统调用的呢?
下图是简化版的Windows系统架构图:
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

?
Windows计算机中处理器有两种模式,分别为用户模式(用户态、目态)和内核模式(核心态、内核态、管态、系统模式、管理模式)。在用户模式下,处理器运行用户进程,不能使用特权指令,其中特权指令是指具有特殊权限的指令,一般不直接给用户使用,比如开/关中断指令、内存清零指令、停机指令等。而在内核模式下,处理器运行内核代码,可以使用特权指令。
在WIN10系统中,用户应用程序是通过子系统DLL来调用本地Windows服务的。Windows为用户模式中的应用程序提供一个虚拟地址块,被称为应用程序的用户空间。而应用程序不能直接访问的其余大块的地址被称为系统空间(内核空间)。
实现系统调用,我们需要CPU从用户空间进入到系统空间执行。而使CPU进入系统空间执行,有三种方式:
1.中断:当有来自外部设备的中断请求到来时,CPU会自动转入系统空间执行。(被动)
2.异常:当有执行指令发生异常时,CPU会进入系统空间执行。(被动)
3.自陷(陷入、陷阱):CPU通过自陷指令进入系统空间执行,自陷指令的执行相当于子程序调用,系统调用一般都是通过自陷指令实现的。(主动)
这里我们也是使用类似自陷的方式,来实现系统调用。
显示单个字符的API 我们先来通过API显示单个字符,实现这个功能我们先把需要显示字符编码存入寄存器,然后再让应用程序能够调用cons_putchar函数。这里存在两个问题,一个问题是函数没法接收存在寄存器上的字符编码,再一个问题是我们不知道cons_putchar函数的地址。所以我们先写一个函数_asm_cons_putchar,将寄存器的值推入栈中,再在这个函数中调用cons_putchar函数。
调用结构图如下:
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

在bootpack.map文件中我们能够查找到cons_putchar函数的地址,将它填入到代码中。
bootpack.map文件:
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

我们将地址填入应用程序中,需要注意的是,当应用程序通过API执行CALL指令实现函数调用时,需要加上段号。这里操作系统的段为“2*8”,使用far-CALL,同时指定段和偏移量。
hlt.nas文件:

[BITS 32] MOVAL,'A'; 这句就是API CALL2*8:0xbe3; 还有这句 fin: HLT JMPfin

cons_putchar函数的地址保存在内存中,这里保存在BOOTINFO之前的0x0fec。
console.c节选:
void console_task(struct SHEET *sheet, unsigned int memtotal) { (略) cons.sht = sheet; cons.cur_x =8; cons.cur_y = 28; cons.cur_c = -1; *((int *) 0x0fec) = (int) &cons; /*cons_putchar函数的地址保存在0x0fec*/ }

我们使用了far-CALL调用_asm_cons_putchar函数,因此需要使用对应的far-RET返回,即最后使用RETF指令返回。
_asm_cons_putchar函数:
_asm_cons_putchar: PUSH 1 ANDEAX,0xff ; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态 PUSH EAX PUSH DWORD [0x0fec] ; 读取内存并PUSH该值 CALL _cons_putchar; 调用cons_putchar函数 ADDESP,12; 将栈中的数据丢弃 RETF; 使用RETF指令返回

其中CALL指令与JMP指令类似,是用来调用函数的指令,可以使用RET指令返回调用原先的位置。它会对当前指令的下一条指令进行压栈操作,即将要返回的目标地址PUSH到栈中,从而实现函数返回。
CLL指令相当于:
PUSH xxxxx
JMP xxxxxx。
修改完成,make run一下:
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

结束应用程序 目前在命令行窗口中输入hlt之后,执行HLT指令就没法继续输入指令了。我们来让应用程序结束后,能返回操作系统。由于需要调用的程序位于不同的段,应该使用far-CALL调用函数,相对的应用程序使用far-RET返回调用,这里只要把HLT指令改为RETF就可以了。
创建一个farcall函数,这个函数与far jmp类似:
naskfunc.nas节选:
_farcall:; void farcall(int eip, int cs); CALL FAR [ESP+4]; eip, cs RET

再将执行HLT指令的地方改为调用farcall,这里应用程序所在的段为“1003*8”。
console.c节选:
void cmd_hlt(struct CONSOLE *cons, int *fat) { (略) if (finfo != 0) {/* 找到文件的情况 */ (略) farcall(0, 1003 * 8); /* 调用farcall */ memman_free_4k(memman, (int) p, finfo->size); } else {/* 没有找到文件的情况 */ (略) } (略) }

由于修改了操作系统代码,要重新查找一下_asm_cons_putchar函数的地址:
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

再将HLT指令改为RETF指令,同时执行一些内容。
hlt.nas节选:
[BITS 32] MOVAL,'h' CALL2*8:0xbe8 MOVAL,'i' CALL2*8:0xbe8 MOVAL,' ' CALL2*8:0xbe8 MOVAL,'m' CALL2*8:0xbe8 MOVAL,'i' CALL2*8:0xbe8 MOVAL,'n' CALL2*8:0xbe8 MOVAL,'t' CALL2*8:0xbe8 RETF

make run一下——
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

不随操作系统版本而改变的API 我们在IDT中找一个空闲的项,这里选择使用0x31号(0x30 ~ 0xff都是空闲的,随便选一个),再将_asm_cons_putchar函数注册到这里:
dsctbl.c节选:
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 + 0x31, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32); /* 注册asm_cons_putchar函数*/ return; }

再将hlt.nas文件中的“CALL 2*8:0xbe8 ”更改为“ INT 0x31”。
hlt.nas文件:
[BITS 32] MOVAL,'h' INT0x31 MOVAL,'i' INT0x31 MOVAL,' ' INT0x31 MOVAL,'m' INT0x31 MOVAL,'i' INT0x31 MOVAL,'n' INT0x31 MOVAL,'t' INT0x31 RETF

在使用INT指令调用时,CPU认为执行了中断处理,会自动执行CLI禁止中断请求,因此在函数开头加入STI指令允许中断发生。函数中的RETF指令就无法返回,这里改为IRETD指令。IRETD用于从使用32位操作数大小的中断返回,对应的有IRET,用于从使用16位操作数大小的中断返回。
naskfunc.nas节选:
_asm_cons_putchar: STI PUSH 1 ANDEAX,0xff ; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态 PUSH EAX PUSH DWORD [0x0fec] ; 读取内存并PUSH该值 CALL _cons_putchar ADDESP,12; 丢弃栈中的数据 IRETD; 这里改为IRETD指令

更改应用程序名称 现在应用程序的名字叫hlt已经不合适了,我们来实现这样的功能:先根据应用程序的名称来寻找对应的文件,如果找到就执行,找不到就提示输入错误”Input error“。
console.c节选:
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, "Input error.", 12); cons_newline(cons); cons_newline(cons); } } return; }

修改cmd_hlt函数,得到cmd_app函数,令输入hi.hre和hi都能够运行程序。再将hlt.nas文件更名为hi.nas,然后汇编生成hi.hrb。
修改完成,执行make run——
操作系统|30天自制操作系统——第十九天系统调用(API)
文章图片

太久没更,有些生疏了。技术也要时时勤拂拭,勿使惹尘埃!
注:本文参照《30天自制操作系统》制作,感谢各位的持续关注。
另附书籍源码(本文源码20_day文件夹):
【操作系统|30天自制操作系统——第十九天系统调用(API)】链接:https://pan.baidu.com/s/1Lb-nWIdTvU0mYDgo0njqtQ
提取码:ghm2

    推荐阅读