Day20 API
文章目录
- 1. 显示单个字符的API(harib17c)
- 2. 结束应用程序(harib17d)
- 3. 不随操作系统改变的API(harib17e)
- 4. 为应用程序自由命名(harib17g)
- 5. 用API显示字符串(harib17h)
1. 显示单个字符的API(harib17c)
重新整理console.c文件的代码之后,产出了比较内聚的函数,这些函数接口都属于操作系统内部所使用的函数,并不对外提供。
// 文件 bootpack.h
void cons_putchar(struct CONSOLE *cons, int chr, char move); // console打印一般字符
void cons_newline(struct CONSOLE *cons); // 换行
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal); // 运行指令
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal); // 打印内存信息
void cmd_cls(struct CONSOLE *cons); // 清屏
void cmd_dir(struct CONSOLE *cons); // 显示文件信息
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline); // 打印文件内容
void cmd_hlt(struct CONSOLE *cons, int *fat); // 休眠
如果期望在应用程序(以Day19章最后一节的hlt.hrb为例)中打印单个字符,就需要使用相关cons_putchar函数的系统调用,即API。
由于cons_putchar本身包含3个参数,因此需要在naskfunc.nas文件中创建一个_asm_cons_putchar
函数。
_asm_cons_putchar:PUSH 1 # move参数赋值AND EAX,0xff # 低8bits于0xff进行按位与运算,保留EAX的低8bits(输入的字符)PUSH EAX # chr参数赋值PUSH DWORD [0x0fec] # cons参数赋值CALL _cons_putchar # 调用cons_putcharADD ESP,12 # 将栈中的数据丢弃(升高ESP,认为已push的3个参数可以被覆盖)RETF # far-RET
其中0x0fec是在操作系统的console_task中将cons的地址放在了0x0fec位置。
void console_task(struct SHEET *sheet, unsigned int memtotal)
{/* 省略 */struct CONSOLE cons;/* 省略 */ *((int *) 0x0fec) = (int) &cons; // 取出cons地址赋值给一个本行有效的临时的全局int变量,该全局int变量的地址为0x0fec/* 省略 */
}
如此,只是操作系统的代码完成了,并且封装了一个名为_asm_cons_putchar的API。需要先make编译完操作系统后,查看map文件中_asm_cons_putchar的地址,然后在应用程序中调用这个地址。
那么为啥不直接在CALL指令后跟_asm_cons_putchar函数名呢?
因为操作系统和应用程序并不在一起编译。使用汇编器对应用程序汇编时,并不包含操作系统本身的代码,因此汇编器无法得知要调用的函数地址,汇编时就会出错。
# 文件 hlt.nas
[BITS 32]MOV AL,'A'CALL 2*8:0xbe3 # far-CALL
fin:HLTJMP fin
在C语言中,goto语句和函数调用的处理方式完全不同,但是在汇编语言中CALL指令和JMP指令本质上差不多,只是在执行CALL时,会自动将函数执行结束后的返回地址PUSH到栈中。
2. 结束应用程序(harib17d)
此时的hlt.hrb被farjmp之后,console窗口就卡在那里了。如果期望调用完可以返回,则需要修改应用程序hlt.nas中的HLT为RETF,并且修改操作系统中farjmp为farcall。
# 文件 naskfunc.nas
_farcall: # void farcall(int eip, int cs);CALL FAR [ESP+4] ; eip, csRET
# 文件hlt.nas, 执行后会打印hello
[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
3. 不随操作系统改变的API(harib17e)
当前的缺点很明显,就是如果修改系统代码,就可能导致_asm_cons_putchar函数的地址发生改变,进而导致应用软件需要重新编译。
其中一个解决该问题的办法就是将函数符号注册给IDT,类似于中断服务函数的注册。IDT中最多可以256个函数,从其中选择一个空闲的拿来用就好。
// dsctbl.c文件
void init_gdtidt(void)
{struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT;/* 省略 *//* idt的设置 */set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x40, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32); // 注册asm_cons_putchar函数到idt的0x40/* 省略 */
}
还需要修改应用程序hlt.nas的CALL为INT,MS-DOS的API据说也是这么做的。
# hlt.nas 文件
[BITS 32]MOV AL,'h'INT 0x40 # CALL 修改为 INTMOV AL,'e'INT 0x40MOV AL,'l'INT 0x40MOV AL,'l'INT 0x40MOV AL,'o'INT 0x40RETF
使用INT指令调用函数的时候,会被视作中断来处理,因此在被调用的函数内部,无法使用RET进行返回,需要替换为IRETD指令。另外,当INT调用的时候,CPU会自动执行CLI指令来禁止中断请求,为了防止这种情况,在应用函数_asm_cons_putchar的开头首先执行STI用来取消禁止中断。
# naskfunc.nas文件
_asm_cons_putchar:STI # 取消禁止中断PUSH 1AND EAX,0xffPUSH EAXPUSH DWORD [0x0fec]CALL _cons_putcharADD ESP,12IRETD # 替换为IRETD
4. 为应用程序自由命名(harib17g)
为了代码的扩展性,将cmd_hlt修改为cmd_app,它会根据命令行的内容判断文件名,并运行相应的应用程序。如果找到文件则返回1,未找到则返回0。如果返回0,则直接做错误处理。
// 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, "Bad command.", 12);cons_newline(cons);cons_newline(cons);}}return;
}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; /* 暂且将文件名后面一个元素空间清空 *//* 寻找文件 */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);farcall(0, 1003 * 8);memman_free_4k(memman, (int) p, finfo->size);cons_newline(cons);return 1;}/* 没有找到文件的情况 */return 0;
}
此时,可修改hlt.nas文件名为hello.nas,顺便加上循环。汇编之后会生成hello.hrb。
# hello.nas 文件
[INSTRSET "i486p"]
[BITS 32]MOV ECX,msg
putloop:MOV AL,[CS:ECX]CMP AL,0JE finINT 0x40ADD ECX,1JMP putloop
fin:RETF
msg:DB "hello",0
5. 用API显示字符串(harib17h)
常用的显示字符串策略有两种:显示字符,直到遇到0;预先指定显示的长度。
// console.c 文件
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,可以分配两个空闲的INT号给IDT。也可以借鉴BIOS的调用方式,在寄存器中存入功能号,使得只用一个INT就可以调用不同的函数。当前使用EDX存放功能号。
暂时这样划分功能号:
- 1 -> 显示单个字符(AL=字符编码)
- 2 -> 显示字符串func0(EBX=字符串地址)
- 3 -> 显示字符串func1(EBX=字符串地址,ECX=字符串长度)
// console.c 文件
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;
}
# naskfunc.nas文件
_asm_hrb_api:STIPUSHAD # 用于保存寄存器值的PUSHPUSHAD # 用于向hrp_api传值的PUSHCALL _hrb_apiADD ESP,32POPADIRETD
PUSHAD: 一次性将所有 32 位通用寄存器的值压入栈(push),用于在函数调用或中断处理前保存现场(保存寄存器状态)。
作用就是下面这个顺序的集合:
PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP ; 注意:压入的是当前 ESP(在 PUSH EBX 之后的值)
PUSH EBP
PUSH ESI
PUSH EDI
此外需要修改init_gdtidt函数中对idt的初始化,将idt + 0x40
配置为hrb_api
。
set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32);
重新创建一个调用显示字符串的函数:
# hello2.nas 文件
[INSTRSET "i486p"]
[BITS 32]MOV EDX,2MOV EBX,msg # 把msg地址赋值给EBX, 其实此处只是一个偏移INT 0x40RETF
msg:DB "hello",0
在hello.nas中,单字符打印时会用[CS:ECX]
的方式指定CS代码段基地址,才可以成功读取到字符。
但是在IDT触发调用hello2应用程序时,由于无法指定段地址,所以hrb_api并不知道“hello”这个字符串的具体在哪个地方。可以在调用cmd_app的时候将申请的内存地址(表示应用程序代码段的基地址)通过全局变量传递出去,使hrb_api可以访问到该地址。
// console.c 文件
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{char *p = NULL;/* 省略 */if (finfo != 0) {/* 找到文件的情况 */p = (char *) memman_alloc_4k(memman, finfo->size);*((int *) 0xfe8) = (int) p; // 代码段地址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;}/* 省略 */
}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); // msg偏移 + 代码段地址} else if (edx == 3) {cons_putstr1(cons, (char *) ebx + cs_base, ecx);}return;
}