8 用API显示字符串(harib17h)
从实际的应用程序开发角度来说,能显示字符串的API远比只能显示单个字符的API要来的方便,因为一次显示一串字符的情况比一次只显示一个字符的情况多得多。
从其他操作系统的显示字符串的API来看,一般有两种方式:一种是显示一串字符,遇到字符编码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;
}
■■■■■
哦,对了,有了这个函数,就可以简化mem、dir、type这几个命令的代码,趁着还没忘记,赶紧改良一下。
本次的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) {
/*不是命令,不是应用程序,也不是空行*/
cons_putstr0(cons, "Bad command.\n\n"); /*这里!*/
}
}
return;
}
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal)
{
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
char s[60]; /*从此开始*/
sprintf(s, "total %dMB\nfree %dKB\n\n", memtotal / (1024 * 1024), memman_total(memman) / 1024);
cons_putstr0(cons, s); /*到此结束*/
return;
}
void cmd_dir(struct CONSOLE *cons)
{
struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);
int i, j;
char s[30];
for (i = 0; i < 224; i++) {
if (finfo[i].name[0] == 0x00) {
break;
}
if (finfo[i].name[0] != 0xe5) {
if ((finfo[i].type & 0x18) == 0) {
sprintf(s, "filename.ext %7d\n", finfo[i].size);
for (j = 0; j < 8; j++) {
s[j] = finfo[i].name[j];
}
s[ 9] = finfo[i].ext[0];
s[10] = finfo[i].ext[1];
s[11] = finfo[i].ext[2];
cons_putstr0(cons, s); /*这里!*/
}
}
}
cons_newline(cons);
return;
}
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline)
{
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
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));
cons_putstr1(cons, p, finfo->size); /*这里!*/
memman_free_4k(memman, (int) p, finfo->size);
} else {
/*没有找到文件的情况*/
cons_putstr0(cons, "File not found.\n"); /*这里!*/
}
cons_newline(cons);
return;
}
代码缩减了12行,什么嘛!一开始就这样写不就好了吗?不过不管怎么说也算是个值得高兴的事吧。
在上面字符串中我们使用了“\n”这个新的符号,这里来讲解一下。在C语言中,“\”这个字符有特殊的含义,用来表示一些特殊字符。这里出现的“\n”代表换行符,即0x0a,也就是说用2个字符来表示1个字节的信息,有点怪吧。此外还有“\t”,它代表制表符,即0x09。顺便说一下,换行符“\n”之所以用“n”,是因为它是“new line”的缩写。
■■■■■
好,我们已经有了cons_putstr0和cons_putstr1,那么怎样把它们变成API呢?最简单的方法就是像显示单个字符的API那样,分配INT 0x41和INT 0x42来调用这两个函数。不过这样一来,只能设置256个项目的IDT很快就会被用光。
既然如此,我们就借鉴BIOS的调用方式,在寄存器中存入功能号,使得只用1个INT就可以选择调用不同的函数。在BIOS中,用来存放功能号的寄存器一般是AH,我们也可以照搬,但这样最多只能设置256个API函数。而如果我们改用EDX来存放功能号,就可以设置多达42亿个API函数,这样总不会不够用了吧。
功能号暂时按下面那样划分,寄存器的用法也是随意设定的,如果不喜欢的话尽管修改就好哦。
功能号1……显示单个字符(AL = 字符编码)
功能号2……显示字符串0(EBX = 字符串地址)
功能号3……显示字符串1(EBX = 字符串地址,ECX = 字符串长度)
接下来我们将_asm_cons_putchar改写成一个新的函数。
本次的naskfunc.nas节选
_asm_hrb_api:
STI
PUSHAD ; 用于保存寄存器值的PUSH
PUSHAD ; 用于向hrb_api传值的PUSH
CALL _hrb_api
ADD ESP,32
POPAD
IRETD
这个函数非常短,因为我们想尽量用C语言来编写API处理程序,而且这样各位读者也更容易理解。
用C语言编写的API处理程序如下。
本次的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;
}
嗯,还是挺好理解的吧。开头的寄存器顺序是按照PUSHAD的顺序写的,如果在_asm_hrb_api中不用PUSHAD,而是一个一个分别去PUSH的话,那当然可以按照自己喜欢的顺序来。
啊,对了对了,我们还得改一下IDT的设置,将INT 0x40改为调用_asm_hrb_api。
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;
}
■■■■■
这样改写之后,现在的hello.nas就无法正常运行了,因为我们需要往EDX里面存入1才能调用相应的API。虽说我们加上一条向EDX中存入1的指令就可以,不过既然已经写好了cons_putstr0,那就干脆用这个新的API写一个hello2.nas吧。
本次的hello2.nas
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2
MOV EBX,msg
INT 0x40
RETF
msg:
DB "hello",0
哇,这样貌似短多了,make了一下,只有19个字节,创造了最小文件纪录哦。
好,完工了,赶紧“make run”,运行“hello2”试试看。
咦?什么都没显示出来?
呃……貌似失败了,怎么回事呢……?今天已经很累了,脑子都不转了,我们还是明天再来找原因吧。总之,我们先将这个放在一边,在以前的hello.nas中加一条EDX = 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",0
看看这次怎么样。“make run”试验一下。
嗯嗯,这次貌似成功了
成功了,总算稍稍松了口气。
今天我们在最后的最后碰了个大钉子(就是hello2),心情有点不爽,不过已经困得不行了,就先到这吧!大家明天见。