API(application program interface)即应用系统对操作系统功能的调用,也可以称为系统调用(system call)。
今天我们就来实现系统调用的功能,我们先来思考一下WIN10系统是如何实现系统调用的呢?
下图是简化版的Windows系统架构图:
Windows计算机中处理器有两种模式,分别为用户模式(用户态、目态)和内核模式(核心态、内核态、管态、系统模式、管理模式)。在用户模式下,处理器运行用户进程,不能使用特权指令,其中特权指令是指具有特殊权限的指令,一般不直接给用户使用,比如开/关中断指令、内存清零指令、停机指令等。而在内核模式下,处理器运行内核代码,可以使用特权指令。
在WIN10系统中,用户应用程序是通过子系统DLL来调用本地Windows服务的。Windows为用户模式中的应用程序提供一个虚拟地址块,被称为应用程序的用户空间。而应用程序不能直接访问的其余大块的地址被称为系统空间(内核空间)。
实现系统调用,我们需要CPU从用户空间进入到系统空间执行。而使CPU进入系统空间执行,有三种方式:
1.中断:当有来自外部设备的中断请求到来时,CPU会自动转入系统空间执行。(被动)
2.异常:当有执行指令发生异常时,CPU会进入系统空间执行。(被动)
3.自陷(陷入、陷阱):CPU通过自陷指令进入系统空间执行,自陷指令的执行相当于子程序调用,系统调用一般都是通过自陷指令实现的。(主动)
这里我们也是使用类似自陷的方式,来实现系统调用。
我们先来通过API显示单个字符,实现这个功能我们先把需要显示字符编码存入寄存器,然后再让应用程序能够调用cons_putchar函数。这里存在两个问题,一个问题是函数没法接收存在寄存器上的字符编码,再一个问题是我们不知道cons_putchar函数的地址。所以我们先写一个函数_asm_cons_putchar,将寄存器的值推入栈中,再在这个函数中调用cons_putchar函数。
调用结构图如下:
在bootpack.map文件中我们能够查找到cons_putchar函数的地址,将它填入到代码中。
bootpack.map文件:
我们将地址填入应用程序中,需要注意的是,当应用程序通过API执行CALL指令实现函数调用时,需要加上段号。这里操作系统的段为“2*8”,使用far-CALL,同时指定段和偏移量。
hlt.nas文件:
[BITS 32]
MOV AL,'A' ;这句就是API
CALL 2*8:0xbe3 ;还有这句
fin:
HLT
JMP fin
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
AND EAX,0xff ; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态
PUSH EAX
PUSH DWORD [0x0fec] ; 读取内存并PUSH该值
CALL _cons_putchar ; 调用cons_putchar函数
ADD ESP,12 ; 将栈中的数据丢弃
RETF ; 使用RETF指令返回
其中CALL指令与JMP指令类似,是用来调用函数的指令,可以使用RET指令返回调用原先的位置。它会对当前指令的下一条指令进行压栈操作,即将要返回的目标地址PUSH到栈中,从而实现函数返回。
CLL指令相当于:
PUSH xxxxx
JMP xxxxxx。
修改完成,make run一下:
目前在命令行窗口中输入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函数的地址:
再将HLT指令改为RETF指令,同时执行一些内容。
hlt.nas节选:
[BITS 32]
MOV AL,'h'
CALL 2*8:0xbe8
MOV AL,'i'
CALL 2*8:0xbe8
MOV AL,' '
CALL 2*8:0xbe8
MOV AL,'m'
CALL 2*8:0xbe8
MOV AL,'i'
CALL 2*8:0xbe8
MOV AL,'n'
CALL 2*8:0xbe8
MOV AL,'t'
CALL 2*8:0xbe8
RETF
make run一下——
我们在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]
MOV AL,'h'
INT 0x31
MOV AL,'i'
INT 0x31
MOV AL,' '
INT 0x31
MOV AL,'m'
INT 0x31
MOV AL,'i'
INT 0x31
MOV AL,'n'
INT 0x31
MOV AL,'t'
INT 0x31
RETF
在使用INT指令调用时,CPU认为执行了中断处理,会自动执行CLI禁止中断请求,因此在函数开头加入STI指令允许中断发生。函数中的RETF指令就无法返回,这里改为IRETD指令。IRETD用于从使用32位操作数大小的中断返回,对应的有IRET,用于从使用16位操作数大小的中断返回。
naskfunc.nas节选:
_asm_cons_putchar:
STI
PUSH 1
AND EAX,0xff ; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态
PUSH EAX
PUSH DWORD [0x0fec] ; 读取内存并PUSH该值
CALL _cons_putchar
ADD ESP,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天自制操作系统》制作,感谢各位的持续关注。
另附书籍源码(本文源码20_day文件夹):
链接:https://pan.baidu.com/s/1Lb-nWIdTvU0mYDgo0njqtQ
提取码:ghm2