【操作系统】30天自制操作系统--(19)API

        本章实现由应用程序对操作系统功能的调用(即API,也叫系统调用)。简单来说,就是操作系统提前准备好接口,供外部的应用程序调用并实现相应的功能(前一章中,应用程序只是一个简单的HLT,并没有调用操作系统的接口,这一章就是操作系统准备好一个字符打印的接口,供应用程序使用)。

一 调用API显示单个字符

         我们先来通过API显示单个字符,实现这个功能我们先把需要显示字符编码存入寄存器,然后再让应用程序能够调用cons_putchar函数。

        这里存在两个问题,一个问题是函数没法接收存在寄存器上的字符编码,再一个问题是我们不知道cons_putchar函数的地址。所以我们先写一个函数_asm_cons_putchar,将寄存器的值推入栈中,再在这个函数中调用cons_putchar函数。如下图所示:

【1】首先,编写 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) {	/* 制表符 */
		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;
}

【2】在console_task中直接指定 cons_putchar 的地址

*((int *) 0x0fec) = (int) &cons; 

【3】在 naskfunc.nas 中写 _asm_cons_putchar ,由他调用 cons_putchar 的地址

_asm_cons_putchar:
		PUSH 	1
		AND		EAX,0xff
		PUSH	EAX
		PUSH    DWORD [0x0fec]		; cons 地址,直接指定
		CALL 	_cons_putchar
		ADD		ESP,12
		RETF                        ; 因为是跨段调用,所以需要使用RETF

【4】_asm_cons_putchar 的地址编译之后可以看到,为0xbe3,在应用程序中调用 _asm_cons_putchar 的地址即可实现字符打印,这边先在之前HLT应用程序的基础上改动

[BITS 32]
		MOV		AL,'A'
		CALL    2*8:0xbe3     ; 应用程序所在段为“1003*8”,而操作系统所在段为“2*8”,
                              ; 所以这边是跨段调用,需要加段号far-CALL。
fin:
		HLT
		JMP		fin

        在命令行中输入hlt,执行应用程序,效果如下,成功打印一个字符:

 

二 结束应用程序

        目前的程序,执行hlt之后,打印完“A”就直接挂死了,这不行,得让它在操作系统调用之后就返回。这就需要在操作系统这边执行完 cmd_hlt 中,也使用CALL而非JMP。

        首先,naskfunc.nas中,创建一个farcall函数,这个函数和farjump大同小异:

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

        然后,还要把hlt命令的处理改为调用farcall:

void cmd_hlt(struct CONSOLE *cons, int *fat)
{
    /* 中略 */
    if (finfo != 0) {
        /*找到文件的情况*/
        farcall(0, 1003 * 8); /*这里!*/
        } else {
        /*没有找到文件的情况*/
    }
	// ...
}

        最后,改写应用程序,把HLT换成RETF:

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

        这样,在命令行中执行完hlt之后,能够正常返回,并再次执行命令:

三 打造不随操作系统版本改变的API

        回顾第一章里面的四个步骤,发现有一个地方不是很靠谱,就是 _asm_cons_putchar 的地址0xbe3。因为 cons_putchar 的地址0x0fec是在程序里面写死的,不会改变,而这个_asm_cons_putchar 的地址0xbe3会随着程序的修改编译而改变。

        为了解决这个问题,我们得把_asm_cons_putchar 的地址固定下来。这边作者使用了类似于之前鼠标键盘操作的IDT。将_asm_cons_putchar 注册到IDT的空闲项,就可以将他的地址固定下来

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 + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x40, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32); / *这里!*/
    return;
}

        与此同时,还需要特别注意,之前用far-CALL调用的话,返回是RETF,现在用中断INT执行调用,返回需要改成IRETD。这边可以参考之前的文章【操作系统】30天自制操作系统--(5)分割编译与中断处理,另外再附一段关于JUMP、CALL、RET、RETF的描述:

JMP、CALL和RET指令的近转移形式只是在当前代码段中执行程序控制转移,因此不会执行特权级检查。JMP、CALL或RET指令的远转移形式会把控制转移到另外一个代码段中,因此处理器一定会执行特权级检查。
1、jmp指令紧紧进行执行流程的跳转,不会保存返回地址
2、call指令在进行流程跳转前会保存返回地址,以便在跳转目标代码中可以使用ret指令返回到call指令的下一条指令处继续执行。执行段内跳转时,只保存EIP;如果是段间跳转,还保存CS。
3、ret和retf:这两个指令的功能都是调用返回。
    (1)ret在返回时只从堆栈中取得EIP;retf中的字母f表示far,即段间转移返回,要从堆栈中取出EIP和CS。
    (2)两个指令都可以带参数,表示发生过程调用时参数的个数,返回时要从堆栈中退出相应个数的参数。
    (3)恢复CS时,如果发现将 发生特权级变化(当前CS的低2位不等于从堆栈中取得的新的CS值的低2位。由跳转的相关理论可知,只有跳转到非一致代码段时才会发生特权级变化,那么, 也只有从非一致代码段返回时才会发生特权级变化的返回),则还要从调用者堆栈中取得ESP和SS恢复到相应寄存器中,也即恢复调用者堆栈。

        根据上面的描述,就需要对进行改写:

_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                       ; 中断返回

        这样,就可以在应用程序中使用INT 0X40来代替原来的CALL    2*8:0xbe3进行对_asm_cons_putchar的调用:

[BITS 32]
		MOV		AL,'h'
		INT		0x40
		RETF

        至此,便通过IDT,把_asm_cons_putchar 的地址固定下来,使其不随操作系统版本的改变而改变。

四 为应用程序自由命名

        至此,操作系统执行hlt时单独的分支,如果想执行其他的应用程序,还得专门再适配,这样就很麻烦。所以这边考虑做一个兼容处理,使得除了常用的命令行命令(mem、cls、dir、type)之外,系统可以根据命令行的内容搜索该文件,如果找到该应用程序(文件),则执行。反之则认为是“Bad Command!”

        创建 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];
	}
	// 将文件后面置为 0
	name[i] = 0;
	// 寻找文件
	finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
	if (finfo == 0 && name[i - 1] != '.') {
		name[i    ] = '.';
		name[i + 1] = 'H';
		name[i + 2] = 'R';
		name[i + 3] = 'B';
		name[i + 4] = 0;
		// 在后面加上 .hrb 继续寻找
		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;
}

        在 cons_runcmd 中调用cmd_app即可:

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) {  /* 调用cmd_app检查 */
            /* 没有找到对应的应用程序,则报错 */
			putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12);
			cons_newline(cons);
			cons_newline(cons);
		}
	}
	return;
}

五 调用API显示字符串

        目前已经具备了输出单个字符的能力,那么要显示字符串,首先想到的就是用循环来显示字符串:

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

        这边还需要注意,_asm_cons_putchar中,在调用INT 0X40之后,寄存器ECX的值会发生变化,所以需要加上PUSHAD和POPAD,确保可以将全部寄存器的值还原:

_asm_cons_putchar:
		STI
		PUSHAD                      ; 新增入栈
		PUSH 	1
		AND		EAX,0xff
		PUSH	EAX
		PUSH    DWORD [0x0fec]		; cons 地址,直接指定
		CALL 	_cons_putchar
		ADD		ESP,12
		POPAD                       ; 新增出栈
		IRETD

        这样,就可以正常打印出字符串了:

         

        但上面显示字符串,其本质还是在应用程序中循环调用的打印单个字符的API,这与实际的需求不符。为此,我们还需要提供API来直接打印字符串,这样应用程序调用一次就可以了。

        采用两种方法来显示字符串:一种是遇到0之后结束、一种是打印指定长度。类似之前设计显示单个字符的API一样,显示字符串的API步骤如下:

【1】首先编写cons_putstr0、cons_putstr1函数,功能是显示字符串(区别在于指定长度与否):

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

【2】之前单独绑定INT号的方式已经不适用了,因为如果API多的话,空闲的INT就不够用了。作者采用的方式是,将一类API绑定某个INT号,另外再通过一个特殊的功能参数,选择不同的API:

        那么可以通过参数,将之前的cons_putchar与新加入的cons_putstr0、cons_putstr1绑在一起,得到一个新的 API ---- 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;
}

【3】将之前的 _asm_cons_putchar 改写为一个新的函数 _asm_hrb_api ,并在其中调用hrb_api:

_asm_hrb_api:
		STI
		PUSHAD						; 用于保存寄存器的值

		PUSHAD						; 由于向 hri_api 传值
		CALL	_hrb_api
		ADD		ESP,32
		POPAD
		IRETD

【4】设置中断注册表:

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 + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
    set_gatedesc(idt + 0x40, (int) asm_hrb_api,      2 * 8, AR_INTGATE32);  / *这里!*/
    return;
}

【5】应用程序中调用这个新的API,需要附加功能号的选择:

[INSTRSET "i486p"]
[BITS 32]
		MOV		EDX,1     /* 附加功能号 */
		MOV		EBX,msg
		INT		0x40
		RETF
msg:
		DB	"hello",0

【特别注意】:上面附加功能号1是可以使用的,但是附加2会有问题。原因如下,这边需要特别注意一个问题,在显示单个字符的时候,我们使用了[CS:ECS]的方式特别指定了CS,因此可以成功读取msg的内容。

        但在显示字符串时,由于无法指定段地址,程序误以为是DS而从完全错误的内存地址中读取了内容, 碰巧读出的内容是0,于是就什么都没有显示出来。

        因此,我们需要在 API 中做改动,使其能将应用程序传递的地址转化成代码段内的地址
当初设置代码段的是 cmd_app,所以需要在此中向 hrb_api 传递代码段的起始位置的信息
我们无法从 cmd_app 直接向 hrb_api 传递信息,因此只好在内存中找个位置存放一下,之前用过0x0fec了,这边就用它前一个0x0fe8:

*((int*) 0x0fe8) = (int) p;

        将原来的 ebx 改为从基地址出发的 cs_base+ebx:

void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	int cs_base = *((int *) 0xfe8);
	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 + cs_base);
	} else if (edx == 3) {
		cons_putstr1(cons, (char *) ebx + cs_base, ecx);
	}
	return;
}

        这样,在应用程序中附加功能码2,即调用API cons_putstr0可以正常显示了。

        hello.nas中:

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

        hello2.nas中:

[INSTRSET "i486p"]
[BITS 32]
		MOV		EDX,2
		MOV		EBX,msg
		INT		0x40
		RETF
msg:
		DB	"hello",0

         分别运行应用程序hello、hello2结果:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值