第20天:API

20.1、程序整理

// 结构体保存了光标位置
struct CONSOLE {
	struct SHEET *sht;
	int cur_x, cur_y, cur_c;
};
// 命令行任务主程序
void console_task(struct SHEET *sheet, unsigned int memtotal);
// 打印文件内的每个字符
void cons_putchar(struct CONSOLE *cons, int chr, char move);
// 换行
void cons_newline(struct CONSOLE *cons);
// 保存cmd命令
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal);
// men命令
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal);
// cls命令
void cmd_cls(struct CONSOLE *cons);
// dir命令
void cmd_dir(struct CONSOLE *cons);
// type命令
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline);
// hlt命令
void cmd_hlt(struct CONSOLE *cons, int *fat);

20.2、显示单个字符的API

cons_putchar函数可以打印字符,如果能过调用到这个函数就可以打印字符了。

1、把要打印的字符放到栈里(这就是系统留的API)
2、调用cons_putchar函数(使用CALL指令)

// naskfunc.c
_asm_cons_putchar:
		PUSH	1
		AND		EAX,0xff	; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态
		PUSH	EAX
		PUSH	DWORD [0x0fec]	; 读取内存并压入栈
		CALL	_cons_putchar
		ADD		ESP,12		; 将栈中数据丢弃
		RET

bootinfo之前的0x0fec地址存cons地址:

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;
	(略)
}

这样操作系统API做好了,开发的应用程序调用 asm_cons_putchar 函数就好了。
但是这个函数地址是多少呢?make之后有个bootpack.map文件,里面有该函数的地址。

API的地址

来看我们开发应用,打印一个字符:

[BITS 32]
		MOV		AL,'A'
		CALL    0xbe3
fin:
		HLT
		JMP		fin

运行发现会闪退。CALL的时候需要指定段号(far-CALL),这个API属于操作系统,操作系统代码在2号段,所以:

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

运行之后模拟器停止运行。是因为 far-CALL 返回要用 RETF。

_asm_cons_putchar:
		(略)
		RETF

20.2.1、返回到操作系统

应用程序执行完打印任务后,就HLT了,如果继续返回操作系统就好了。

只要在应用程序打印完字符后,return就好了。但是,之前输入 hlt 指令后,cmd_htl函数通过far-jmp去1003号段执行的应用程序,没法使用return返回到调用的地方。所以使用far-CALL来调用1003号段的程序,因为使用CALL指令,会让CALL下一条指令地址入栈,return时能找到回来的地址。

// console.c
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;
}
// naskfunc.c
_farcall:		; void farcall(int eip, int cs);
		CALL	FAR	[ESP+4]				; eip, cs
		RET

↑这就使用call指令跳到1003号段执行程序了。
↓准备好的应用程序:

[BITS 32]
		MOV		AL,'A'
		CALL    2*8:0xbe8 ;因为程序有改动,API接口函数地址也变了
		RETF

20.3、不随操作系统改变的API地址

每次改变代码,API函数地址都变化,怎么让它不变化呢?作者提供了一个办法——注册到中断描述符表中。
之前说过,IRQ的0~15对应中断号是 0x20 ~ 0x2f ,从0x30 ~ 0xff都空闲了。

// 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 + 0x40, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32);

	return;
}

下次些应用程序就这样,使用 INT 0x40:

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

因为使用中断执行API,那么API返回指令也得改:

_asm_cons_putchar:
		STI
		PUSH	1
		AND		EAX,0xff
		PUSH	EAX
		PUSH	DWORD [0x0fec]
		CALL	_cons_putchar
		ADD		ESP,12		
		IRETD ; 使用这个指令返回

20.4、为应用程序自由命名

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; /* 把输入的命令的最后设置为0 */

	/* 寻找文件 */
	finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
	if (finfo == 0 && name[i - 1] != '.') {
		/* 如果没找到于命令相同的文件,在命令后面加上.hrb,相当于windows的 .exe */
		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;
	}
	/* 妹找到文件的情况,返回0 */
	return 0;
}

20.5、寄存器问题

之前自己写的应用程序打印“hello”,太麻烦了,搞个循环:

[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

这里使用ECX来控制循环。
但是执行的时候发现,只打印一个 “h” 就停止了,这是因为使用寄存器之前要保存寄存器的值:

_asm_cons_putchar:
		STI
		PUSHAD // 保存寄存器值
		PUSH	1
		AND		EAX,0xff	
		PUSH	EAX
		PUSH	DWORD [0x0fec]	
		CALL	_cons_putchar
		ADD		ESP,12		
		POPAD // 恢复寄存器值
		IRETD

20.6、用API显示字符串

显示字符串有两种方法:

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呢?当然可以注册到IDT,使用INT中断来调用,但是这样IDT很快会被消耗光,采用往寄存器里保存功能号的方式,一个INT就能调用不同的函数。使用EDX来存功能号。

功能号1——显示单个字符(AL=字符编码)
功能号2——显示字符串0(EBX=字符串地址)
功能号3——显示字符串1(EBX=字符串地址,ECX=字符串长度)

_asm_hrb_api:
		STI
		PUSHAD	; 保存寄存器的值
		PUSHAD	; hrb_api使用寄存器
		CALL	_hrb_api
		ADD		ESP,32
		POPAD
		IRETD
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;
}

asm_hrb_api 函数注册到IDT 0x40。

当我们想打印字符串或者字符时:

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

这存在问题,EBX存放的字符串的相对地址,没有指定段地址,hrb_api默认段地址是DS,就会打印错误。

段地址不能传给C语言,所以把段地址先保存在0xfe8:

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; // 把段地址保存在0xfe8
		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;
	}
	/* 没找到文件,返回0 */
	return 0;
}

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

20.7、问题

在 hello2 中,为什么不能在应用程序中,把段地址保存在EBX,hrb_api使用,必须在C语言保存。

段地址

在应用程序中使用该过程能取到 “h”。

在这里插入图片描述

在这怎么不能把字符串的地址保存在EBX传给 hrb_api 函数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值