• 自制操作系统日志——第二十天


    自制操作系统日志——第二十天

    今天我们主要的任务就是制作API。然后利用api来调用系统进行显示字符。话不多说,一起加油吧。



    一、显示单个字符的api

    再正式开始之前我把之前的console.c代码进行了优化的设置,详情请查看代码。

    API 是应用程序对操作系统功能的调用,也可以称之为系统调用。即应用程序调用操作系统的来完成某些操作系统级别的某种操作。

    目前我们实现API的基本思路就是:将要显示的字符编码存入AL寄存器中,然后就直接调用操作系统的函数就可以显现了。也就是说,我们可以在之前的那个程序里使用call命令进行调用我们系统里缩写的cons_putchar()这个函数即可!但是由于cons_putchar是用c写的,该函数无法接收,因此我们还需建立一个函数进行调用,即大致思路如下:
    在这里插入图片描述

    我们建立asm_cons_putchar函数将寄存器的值进行保存到栈中,然后再调用cons_putchar函数即可,以下是修改的代码:
    naskfunc.nas:

    _farcall: ;void farcall(int eip, int cs)
        CALL FAR [ESP+4]
    	RET
    	
    _asm_cons_putchar:
        PUSH 1
    	AND  EAX,0xff				;将AH和EAX的高位置都置为0,将EAX置为已存入字符编码的状态
    	PUSH EAX
    	PUSH DWORD [0x0fec]			;读取内存并push该值
    	CALL _cons_putchar
    	ADD ESP,12 					;将栈栈中数据丢弃
    	RETF
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    console.c:

    void console_task(struct SHEET *sheet, unsigned int memtotal)
    {*((int *) 0x0fec) = (int) &cons;//将对应的cons值保存到bootinfo事先预留好的地址处}
    	
    //启动应用程序hlt.hrb
    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);
    		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;
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    以上,我们增加了farcall这个函数,是因为我们想要再应用程序中显示完字符后还能返回到原先的地址,call命令是将跳转前的信息进行保存的!!

    然后,修改前一天的那个程序:
    hlt.nas

    [BITS 32]
        MOV AL,'Y'
        CALL 2*8:0xD6C
    	MOV AL,'U'
        CALL 2*8:0xD6C
    	MOV AL,'A'
        CALL 2*8:0xD6C
    	MOV AL,'N'
        CALL 2*8:0xD6C
    	MOV AL,'-'
        CALL 2*8:0xD6C
    	MOV AL,'O'
        CALL 2*8:0xD6C
    	MOV AL,'S'
        CALL 2*8:0xD6C
    	RETF
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里由两点:
    第一是我们的 CALL 2*8:0xD6C 这个地址是我们再程序中想要跳转到_asm_cons_putchar地址的所在处,具体的我们是通过bootpack.map中查询到的:
    在这里插入图片描述

    其二,我们选择跳转到地址,一定要加上段地址,这是因为该程序所在的地址是1003 * 8 而代码的程序这是在 2 * 8这个地址处!

    最后,让我们看看运行后的效果怎么样:
    在这里插入图片描述

    利用中断的API

    在前面叙述中,我们是事先找到了对应的asm_cons_putchar函数的地址。但是,如果又添加了新函数,或者操作系统版本更改了,那么对应的地址就会发送变化。因此,我们需要设计一个能够不随版本改变而变化的API。

    在前面的中断处理里,我们写了一个专门用于注册函数的IDT表。对于CPU而言,用于通知异常中断的最多只有32种(目前最新技术的话不太确定,需要查一下),但是呢,对于IDT表而言,却可以注册256种函数。因此我们可以借用一下IDT表来注册一下函数,一遍我们在程序里可以直接利用int进行调用该函数。

    dsctbl.c:

        set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 << 3, AR_INTGATE32);
        set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 << 3, AR_INTGATE32);
        set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 << 3, AR_INTGATE32);
        set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 << 3, AR_INTGATE32);
    	set_gatedesc(idt + 0x40, (int) asm_cons_putchar, 2 << 3, AR_INTGATE32);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    hlt.nas:

    [BITS 32]
        MOV AL,'Y'
        int 0x40
    	MOV AL,'U'
        int 0x40
    	MOV AL,'A'
        int 0x40
    	MOV AL,'N'
        int 0x40
    	MOV AL,'-'
        int 0x40
    	MOV AL,'O'
        int 0x40
    	MOV AL,'S'
        int 0x40
    	RETF
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    由于,我们使用的是中断进行调用(此时,cpu会设置关闭中断),因此这里我们还需要使用利用IRTED进行返回,且在程序里开放中断的权限。
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后,运行!你会发现一点即hlt.hrb这个程序会比之前的大小要小很多。嘿嘿嘿,这是因为far-call指令主要7字节,而int指令只需要2字节。

    二、为自由程序命名并优化程序

    以下,我们将让系统支持其他程序的应用名。
    首先在cmd_run中将判断是否是程序名的任务交给新建的函数cmd_app:
    cmd_run:

    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);
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    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);
    		farjmp(0, 1003 * 8);
    		memman_free_4k(memman, (int) p, finfo->size);
    		cons_newline(cons);
    		return 1;
    	}
    	/*没有找到文件的情况*/
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    然后,进行修改名称即可!
    在这里插入图片描述

    这里,我们进一步的对程序进行优化一下,直接采用循环进行处理:
    hello.nas

    [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-yuan-os",0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里,我们需要使用ecx寄存器进行累加计数,以确保所有的单个字符都可以遍历到。然后,还需要修改一下_asm_cons_putchar这个函数,需要在进入时,先保存一下所有的寄存器的值,以防止在调用_cons_putchar这个函数时,将我们之前设置的ECX的值打乱:

    _asm_cons_putchar:
        STI
    	PUSHAD
        PUSH 1
    	AND  EAX,0xff				;将AH和EAX的高位置都置为0,将EAX置为已存入字符编码的状态
    	PUSH EAX
    	PUSH DWORD [0x0fec]			;读取内存并push该值
    	CALL _cons_putchar
    	ADD ESP,12 					;将栈栈中数据丢弃
    	POPAD
    	IRETD
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后,make run一下估计没啥问题!

    用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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    至此,我们就可以把前面的都更改成cons-putstr0这个函数进行显示了!!!更加的简洁便利。当然,我们还需要能够让应用程序能够调用我们这两个字符串显示的函数,我们这里决定效仿bios的调用,通过设置0x40函数的子功能进行调用,代码中我们讲利用dx寄存器进行存放子功能的函数:
    naskfunc.nas:

    _asm_hrb_api:
        STI
    	PUSHAD						;用于保存寄存器值的push
    	PUSHAD						;用于向hrb_api传值的push
    	CALL _hrb_api
    	ADD ESP,32
    	POPAD
    	IRETD
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    console.c:

    //用edx来指向是哪一号的子功能
    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);//前面讲cons的内容保存到了0xfec当中
    	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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后还要修改IDT的设设置:

    set_gatedesc(idt + 0x40, (int) asm_hrb_api,      2 << 3, AR_INTGATE32);
    
    • 1

    最后,我们来制作一下程序,看看能否显示成功:
    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-yuan-os",0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    hello2.nas

    [INSTRSET "i486p"]
    [BITS 32]
    		MOV		EDX,2
    		MOV		EBX,msg
    		INT		0x40
    		RETF
    msg:
    		DB	"hello-yuan-os",0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    咦?这里的hello2并未显示。让我们来分析看看吧。

    首先,在hello中我们利用的是[cs:ecx]的方式进行指定了msg里的内容。但是我们在显示字符串那边却无法指定对应的段地址,这就会让cpu默认段地址是ds,从而从错误的地方取出了数据。

    因此,我们需要做的事情,就是将完整的内存地址传递给显示字符串的函数中。在整个代码里,我们主要是在cmd_app里设置了程序的段地址,因此我们需要将该段地址的数据保存在一个空闲的地方。emm…就放在之前bootinfo里的0fe8里好了。

    int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
    {if (finfo != 0) {
    		/*找到文件的情况*/
    		p = (char *) memman_alloc_4k(memman, finfo->size);
    		*((int *) 0xfe8) = (int) p;}
    
    //用edx来指向是哪一号的子功能
    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);//前面讲cons的内容保存到了0xfec当中
    	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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    噢噢噢噢,成功了!嘿嘿嘿。


    总结

    以上就是今天的全部内容,虽然有点小累了,但是看着这一天天的逐渐成型还是很高兴的。

  • 相关阅读:
    【BP数据预测】基于matlab供需算法优化BP神经网络数据预测(含前后对比)【含Matlab源码 2032期】
    详解单例模式+代码实现
    【Git】Git 分支
    Linux常用操作命令(拷贝、登录)介绍
    Git详解及常用命令
    跨平台编译GSL(Windows、Linux、MacOS环境下编译与安装)
    什么是电源高压测试标准?如何测试?测试时要注意什么?
    BFC(边距重叠解决方案)
    【Java 基础篇】Java反射:深入了解Class对象
    类型组合——数组、结构、指针
  • 原文地址:https://blog.csdn.net/qq_43696276/article/details/126148981