第20天 API
2020.4.23
1. 程序整理(harib17a)
- 实现由应用程序对OS功能的调用,即API(系统调用)。
- API,application program interface,应用程序(与系统之间的)接口。
- 由应用程序来调用(操作)系统中的功能来完成某种操作。
- 编写一个在命令行窗口中显示字符的API。(BIOS中也有这个功能,但是现在无法使用BIOS哦)
- 现在,console.c的代码有点混乱,整理一下。
- bootpack.h中有关console.c中函数和常量的声明:
/* console.c */
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);
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);
- 结构体CONSOLE:
- 用于存放命令行窗口常用的变量。
- sht是指向命令行窗口图层的指针。
- (cur_x,cur_y)代表指向下一个要输入位置(光标所在处)的左上角坐标。
- cur_c代表光标颜色,-1代表不显示光标。
- console_task
void console_task(struct SHEET *sheet, unsigned int memtotal)
{
struct TIMER *timer;
struct TASK *task = task_now();
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
int i, fifobuf[128], *fat = (int *) memman_alloc_4k(memman, 4 * 2880);
struct CONSOLE cons; /*声明cons,用于存放命令行窗口的各种变量*/
char cmdline[30];
cons.sht = sheet;
cons.cur_x = 8; /*初始值不再是16*/
cons.cur_y = 28;
cons.cur_c = -1; /*光标默认不显示*/
fifo32_init(&task->fifo, 128, fifobuf, task);
timer = timer_alloc();
timer_init(timer, &task->fifo, 1);
timer_settime(timer, 50);
file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); /*向内存中写入解压缩后的FAT*/
/* 显示提示符 */
cons_putchar(&cons, '>', 1);
for (;;) {
io_cli();
if (fifo32_status(&task->fifo) == 0) {
task_sleep(task);
io_sti();
} else {
i = fifo32_get(&task->fifo);
io_sti();
if (i <= 1) { /* 光标定时器 */
if (i != 0) {
timer_init(timer, &task->fifo, 0); /* 下次置0 */
if (cons.cur_c >= 0) {
cons.cur_c = COL8_FFFFFF;
}
} else {
timer_init(timer, &task->fifo, 1); /* 下次置1 */
if (cons.cur_c >= 0) {
cons.cur_c = COL8_000000;
}
}
timer_settime(timer, 50);
}
if (i == 2) { /* 光标开启 */
cons.cur_c = COL8_FFFFFF;
}
if (i == 3) { /* 光标关闭 */
boxfill8(sheet->buf, sheet->bxsize, COL8_000000, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);
cons.cur_c = -1;
}
if (256 <= i && i <= 511) { /* 键盘数据 */
if (i == 8 + 256) {
/* 退格键 */
if (cons.cur_x > 16) {
/* 用空格擦除光标后将光标前移一位 */
cons_putchar(&cons, ' ', 0);
cons.cur_x -= 8; /*cur_x更新*/
}
} else if (i == 10 + 256) {
/* Enter */
/* 将光标用空格擦除后换行 */
cons_putchar(&cons, ' ', 0);
cmdline[cons.cur_x / 8 - 2] = 0;
cons_newline(&cons);
cons_runcmd(cmdline, &cons, fat, memtotal); /* 运行命令 */
/* 显示提示符 */
cons_putchar(&cons, '>', 1);
} else {
/* 一般字符 */
if (cons.cur_x < 240) {
/* 显示一个字符以后,将光标后移一位 */
cmdline[cons.cur_x / 8 - 2] = i - 256;
cons_putchar(&cons, i - 256, 1);
}
}
}
/* 重新显示光标 */
if (cons.cur_c >= 0) {
boxfill8(sheet->buf, sheet->bxsize, cons.cur_c, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);
}
sheet_refresh(sheet, cons.cur_x, cons.cur_y, cons.cur_x + 8, cons.cur_y + 16);
}
}
}
- 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; /*cur_x更新*/
if (cons->cur_x == 8 + 240) { /*到达行尾,换行*/
cons_newline(cons);
}
}
}
return;
}
- cons_putchar函数的作用是:将特定的字符写到命令行窗口上,并更新相关变量。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- chr:是字符的ASCII表示。将这个字符写入到图层上面。
- move:0代表光标不后移,1代表光标后移(cur_x+=8)。
- 无返回值
- cons_newline:
void cons_newline(struct CONSOLE *cons)
{
int x, y;
struct SHEET *sheet = cons->sht;
if (cons->cur_y < 28 + 112) {
cons->cur_y += 16; /* 到下一行 */
} else {
/* 滚动 */
for (y = 28; y < 28 + 112; y++) {
for (x = 8; x < 8 + 240; x++) {
sheet->buf[x + y * sheet->bxsize] = sheet->buf[x + (y + 16) * sheet->bxsize];
}
}
for (y = 28 + 112; y < 28 + 128; y++) {
for (x = 8; x < 8 + 240; x++) {
sheet->buf[x + y * sheet->bxsize] = COL8_000000;
}
}
sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
}
cons->cur_x = 8; /*重置cur_x*/
return;
}
- 这是根据新的数据结构CONSOLE而重新写的换行函数,作用是:将光标换到下一行,如果超出命令行底部,则滚动。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- 返回值:无返回值。
- cons_runcmd:
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 (strcmp(cmdline, "hlt") == 0) {
cmd_hlt(cons, fat);
} else if (cmdline[0] != 0) {
putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12);
cons_newline(cons);
cons_newline(cons);
}
return;
}
- cons_runcmd函数用于执行从命令行窗口输入进来的指令。
- 传入参数:
- cmdline:指向从命令行窗口输入的字符串。
- cons:用于存放命令行窗口的常用变量。
- fat:指向为FAT开辟的内存空间。
- memtotal:用于mem命令,是总的内存空间。
- 返回值:无返回值。
- cmd_mem:
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal)
{
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
char s[30];
sprintf(s, "total %dMB", memtotal / (1024 * 1024));
putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
cons_newline(cons);
sprintf(s, "free %dKB", memman_total(memman) / 1024);
putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
cons_newline(cons);
cons_newline(cons);
return;
}
- cmd_mem函数的作用是:执行mem命令。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- memtotal:总的内存空间。
- 返回值:无。
- cmd_cls:
void cmd_cls(struct CONSOLE *cons)
{
int x, y;
struct SHEET *sheet = cons->sht;
for (y = 28; y < 28 + 128; y++) {
for (x = 8; x < 8 + 240; x++) {
sheet->buf[x + y * sheet->bxsize] = COL8_000000;
}
}
sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128);
cons->cur_y = 28; /*重置cur_y,因为输入cls必会输入回车,回车会产生新行,新行的cur_x恒为8*/
return;
}
- cmd_cls函数:用于执行cls命令。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- 返回值:无。
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", 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];
putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30);
cons_newline(cons);
}
}
}
cons_newline(cons);
return;
}
- cmd_dir函数:用于执行dir命令。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- 返回值:无。
- cmd_type
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); /*调用file_search函数*/
char *p;
int i;
if (finfo != 0) {
/* 找到文件的情况 */
p = (char *) memman_alloc_4k(memman, finfo->size);
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
for (i = 0; i < finfo->size; i++) {
cons_putchar(cons, p[i], 1);
}
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;
}
- cmd_type函数:用于执行type命令。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- fat: FAT的开始地址。
- 返回值:无。
- cmd_hlt:
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);
farjmp(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;
}
- cmd_hlt函数:执行可执行文件hlt.hrb。
- 传入参数:
- cons:用于存放命令行窗口的常用变量。
- fat: FAT的开始地址。
- 返回值:无
- file_search:(file.c中)
struct FILEINFO *file_search(char *name, struct FILEINFO *finfo, int max)
{
int i, j;
char s[12];
for (j = 0; j < 11; j++) {
s[j] = ' ';
}
j = 0;
for (i = 0; name[i] != 0; i++) {
if (j >= 11) { return 0; /* 没找到 */ }
if (name[i] == '.' && j <= 8) {
j = 8;
} else {
s[j] = name[i];
if ('a' <= s[j] && s[j] <= 'z') {
/* 将小写转化成大写 */
s[j] -= 0x20;
}
j++;
}
}
for (i = 0; i < max; ) {
if (finfo[i].name[0] == 0x00) {
break;
}
if ((finfo[i].type & 0x18) == 0) {
for (j = 0; j < 11; j++) {
if (finfo[i].name[j] != s[j]) {
goto next;
}
}
return finfo + i; /* 找到文件 */
}
next:
i++;
}
return 0; /* 没有找到 */
}
- file_search函数:搜索文件是否存在。
- 传入参数:
- name:指向文件名的指针。
- finfo: 文件名存放的开始地址。
- max:最大的文件数,224。
- 返回值:找到文件了,返回文件名的地址;否则返回0。
- 这样修改以后,代码清爽了很多。
make
后用VMware顺利运行。
2. 显示单个字符的API(1)(harib17b)
- 让应用程序能够显示单个字符的方法很简单:只要应用程序能够调用cons_putchar就行了。
- 首先,做一个用于测试的应用程序。将要显示的字符编码存入AL寄存器,然后调用操作系统的函数,字符就显示出来了。
- hlt.nas设想
[BITS 32]
MOV AL,'A'
CALL (cons_putchar的地址)
fin:
HLT
JMP fin
- CALL是一个用来调用函数的指令。在C语言中,goto和函数调用的处理方式完全不同。但是在汇编语言中,CALL指令和JMP指令其实差不多。他们的区别只是:在执行CALL指令时,为了能够在接下来执行RET指令时能够正确返回,会先将要返回的目标地址PUSH到栈中。
- 这里有个问题,cons_putchar函数的地址我们应用程序是不知道的。应用程序编译时,并不会包含OS的代码。因此,要想知道cons_putchar的地址,只能编写好OS后编译,通过人工查看文件得到cons_putchar的地址,然后再写入到应用程序的代码中。
- 注意:cons_putchar函数是用C语言写的函数,即便将字符编码写入寄存器,函数也无法接收,因此,必须在CALL之前将字符编码写入栈中才行。
- 解决思路:添加一个中间层,应用程序先调用这个中间层,把相应的数据存放到栈中,然后这个中间层调用cons_putchar。
- 中间层:汇编语言编写的用来将寄存器的值存入栈的函数。 这个中间层是OS的一部分,因此要写在naskfunc.nas中。 在应用程序hlt.nas中,CALL的不再是cons_putchar的地址,而是这个中间层的地址。
- 修改naskfunc.nas:
_asm_cons_putchar:
PUSH 1
AND EAX,0xff ; 将AH和EAX的高位置0,将EAX置为已存入字符编码的状态
PUSH EAX
PUSH DWORD [0x0fec] ; 将cons的地址压栈
CALL _cons_putchar
ADD ESP,12 ; 将栈中的数据丢弃
RET
- 详解:
-
PUSH 1
:因为现在是32位模式,所以PUSH一次向栈中压入32位数据,也就是4字节数据。 - cons的地址,应用程序是不知道的。因此,将这个地址事先保存在内存的特定地址上,这里保存在BOOTINFO之前的0x0fec这个地址。
- 因此,还得修改console_task:
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; /*写入指定内存*/
……
}
make
将编写好的OS编译。注意一个叫bootpack.map的文件,这是一个文本文件,打开,找到_asm_cons_putchar的位置:
- 这个
0x00000BE3
就是asm_cons_putchar的地址了。
- 将
0x0be3
写入hlt.nas:
[BITS 32]
MOV AL,'A'
CALL 0xbe3
fin:
HLT
JMP fin
- 应用程序中的API是:
MOV AL,'A'
和
CALL 0xbe3
- 重新make,用VMware运行试试:
- 不幸的是,出现BUG了。
- 不要轻易在真机上运行harib17b,因为无法预料会产生什么后果!
3. 显示单个字符的API(2)(harib17c)
- 开发OS真刺激!一不小心就产生严重问题!
- 产生上述问题的原因:
- 应用程序对API执行CALL的时候,千万不能忘记加上段号!!。
- 应用程序所在的段是
1003 * 8
;OS所在的段是2 * 8
。 - 不能使用普通的CALL,应该使用far-CALL指令。
- far-CALL指令和far-JMP一样,只要同时指定段和偏移量即可。
- 修改hlt.nas:
[BITS 32]
MOV AL,'A'
CALL 2*8:0xbe3
fin:
HLT
JMP fin
- 相应地,还需要修改asm_cons_putchar:
_asm_cons_putchar:
PUSH 1
AND EAX,0xff ;
PUSH EAX
PUSH DWORD [0x0fec] ;
CALL _cons_putchar
ADD ESP,12 ;
RETF
- 将最后一句RET修改成RETF。
- RET是用于普通的CALL的。而RETF设计用于far-CALL的。
make
后用VMware运行:
- 字符成功显示!
4. 结束应用程序(harib17d)
- 让应用程序结束后不执行HLT,而是返回操作系统。
- 只要将应用程序中的HLT改成RET,就可以返回了,因为需要返回,所以OS调用应用程序的地方也应该由JMP改成CALL。
- 由于调用的应用程序位于不同的段,因此应该调用far-CALL,应用程序那边也不应该是RET,而是RETF。
- C语言中没有用来执行far-CALL的指令,因此在naskfunc.nas中创建farcall函数:
_farcall: ; void farcall(int eip, int cs);
CALL FAR [ESP+4] ; eip, cs
RET
- 和farjmp类似。
- 修改console.c中的cmd_hlt函数:
void cmd_hlt(struct CONSOLE *cons, int *fat)
{
……
if (finfo != 0) {
/* 找到文件的情况 */
……
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
} else {
……
}
……
}
- 就是将farjmp(0, 1003*8)改成了farcall(0, 1003*8)。
- 注意,我们修改了OS的代码,因此asm_cons_putchar的地址也就变化了,需要重新查看bootpack.map,经过查看:
0x00000BE8 : _asm_cons_putchar
- 修改hlt.nas:
[BITS 32]
MOV AL,'A'
CALL 2*8:0xbe8
RETF
- 其实就是把HLT改成RETF。
- 把0xbe3改成0xbe8。
- 重新
make
后用VMware运行:
- 成功显示字符’A’!
- 重新修改hlt.nas,输入hlt显示字符串"hello"
[BITS 32]
MOV AL,'h'
CALL 2*8:0xbe8
MOV AL,'e'
CALL 2*8:0xbe8
MOV AL,'l'
CALL 2*8:0xbe8
MOV AL,'l'
CALL 2*8:0xbe8
MOV AL,'o'
CALL 2*8:0xbe8
RETF
- 重新make后用VMware运行:
- 成功显示字符串“hello”。
5. 不随操作系统版本而改变的API(harib17e)
- harib17d暴露了一个问题,每次修改OS的代码,asm_cons_putchar的地址都会变化,从而导致应用程序的代码必须改变。这是不友好的。
- 解决上述问题的方式有很多。这里采用如下这一种:
- CPU中有个专门用来注册函数的地方,这个地方就是中断处理程序注册的地方。
- 当发生IRQ-1的时候调用某个函数f,这个函数f是在IDT中注册的。
- IRQ只有0~15,而CPU用于通知异常状态的中断最多也只有32种(查阅资料)。
- IDT最多可以设置256个函数,因此还有剩下很多位置没有使用。
- 从IDT中选一个空闲的项0x40号(0x30~0xff都是空闲的),将asm_cons_putchar注册在这里。
- 这样,我们只要调用INT 0x40产生一个中断就可以调用asm_cons_putchar函数了。
- 修改init_gdtidt(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); /*0x40号注册asm_cons_putchar*/
return;
}
- 修改hlt.nas:
[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
- 将
CALL 2\*8:0xbe8
改成了INT 0x40
。
- 修改naskfunc.nas中的asm_cons_putchar:
_asm_cons_putchar:
STI
PUSH 1
AND EAX,0xff
PUSH EAX
PUSH DWORD [0x0fec]
CALL _cons_putchar
ADD ESP,12
IRETD
- 由于INT会产生中断,因此RETF是无法返回的,需要使用IRETD指令。
- INT调用时,对于CPU来讲相当于执行了中断处理,因此,在调用的同时CPU会自动执行CLI指令来禁止中断请求。。
- 这里,我们只是用INT来代替CALL,我们不想看到“API处理过程中键盘无法输入”这种情形,因此在开头添加了STI指令。
- 还有一种方式:在将函数注册到CPU的时候修改设置来禁止CPU擅自执行CLI,这里不再深入。
make
后用VMware运行:
- 顺利运行。
- 应用程序比之前的还小了:
- harib17d的hlt.hrb:46字节。
- harib17e的hlt.hrb:21字节。
- 这是因为:far-CALL指令需要7个字节,而INT指令还需要2个字节。
6. 为应用程序自由命名(harib17f)
- 修改cons_runcmd:
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;
}
- 去掉了cmd_hlt函数,创建了新的函数cmd_app。
- 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];
}
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);
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
/* 没有找到文件 */
return 0;
}
- cmd_app用来根据命令行的内容判断文件名,并运行相应的应用程序,如果找到了文件则返回1,如果没有找到文件则返回0。
- cmd_app支持输入
可执行文件
和可执行文件.hrb
的形式。(这一点模仿了Windows) - 现在的工作流程是:当输入的命令不是mem、cls、dir、type其中之一是,则调用cmd_api,如果返回0则作为错误处理。
- 将hlt.nas改成hello.nas。
make
后在VMware中运行(Makefile已修改,将hello.nas添加到磁盘映像文件中了)。
- 输入dir确认磁盘文件情况,输入hello.hrb:
- 输入hlt(hlt已经被删除,因此会提示Bad command):
7. 当心寄存器(harib17g)
- 修改hello.hrb:
[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
- DB: 伪指令,是定义一字节变量的意思。字符h用0x68表示,0x68一字节。
- msg是一个标签,标签是一个数字,这个数字的值就是内存地址。MOV ECX,msg 相当于把一个数字写入ECX中,相当于把一个内存地址写入ECX中。
- 虽然ECX是32位寄存器,一个内存地址占8位,源和目的的位数不同,但是msg是一个标签,是一个数字,这个数字的含义是一个内存地址。
- 使用循环,这样在写入长字符串时有优势。
make
后用VMware运行。结果执行hello时,只输出了一个字母h。很是奇怪啊。- 应用程序肯定没问题,问题出现在OS上,那就是asm_cons_putchar。
- 修改asm_cons_putchar:
_asm_cons_putchar:
STI
PUSHAD
PUSH 1
AND EAX,0xff
PUSH EAX
PUSH DWORD [0x0fec]
CALL _cons_putchar
ADD ESP,12
POPAD
IRETD
- 添加了两行代码:
PUSHAD
和POPAD
。 - INT40之后ECX寄存器的值发生了变化,应该是cons_putchar改动了ECX的值,加上这两句代码,将寄存器的值全部还原。
make
后用VMware运行,顺利运行(这里修改了字符串“hello”改成了“Hi,stranger!”)。
8. 用API显示字符串(harib17h)
- 显示单个字符的API在实际中比显示一串字符串的API的情况少得多。
- 参考其他OS显示字符串的API,一般有两种方式:
- 显示一串字符串,遇到字符编码0结束。
- 指定要显示字符串的长度,然后显示。
- 由于两种方式的实现都不困难,因此,二者均在次OS中实现。
- 修改console.c添加函数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;
}
- 有了这两个函数,便可以简化cons_runcmd、cons_mem、cons_dir、cons_type这几个函数。
-
\n
代表换行符,即0x0a。 -
\t
代表制表符,即0x09。
- 修改cons_runcmd:
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"); /*\n代表换行*/
}
}
return;
}
- 这样,cons_newline(cons)可以使用
\n
代替。
- 修改cmd_mem:
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;
}
- 修改cmd_dir:
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;
}
- 修改cmd_type:
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;
}
- 现在的问题是:如何将cons_putstr0和cons_putstr1变成API。
- 最简单的方法是:像调用单个字符cons_putchar的API一样,为cons_putstr0和cons_putstr1分配INT 41和INT 42来调用这两个函数。不幸的是,这样IDT很快就会被用光。
- 借鉴BIOS的调用方式:
- 在寄存器中存入功能号,使得只使用1个INT就可以选择调用不同的函数。
- 用来存放功能号的寄存器一般是AH(8位寄存器),这样最多只能设置256个API函数。
- 这里,改用寄存器EDX(32位寄存器)来存放功能号,这样可以设置大约42亿API函数(够用了吧)。
- 功能号暂时按如下划分(寄存器的用法也是随意设定的):
- 功能号1:显示单个字符(AL=字符编码)
- 功能号2:显示字符串0(EBX=字符串地址(msg))
- 功能号3:显示字符串1(EBX=字符串地址(msg),ECX=字符串长度)
- 小知识点:内存地址占一个字节(8位,内存是一个字节一个字节拼接在一起的);32位OS中,指针占四个字节(32位)。指针不等于地址,指针有类型,地址没有类型。指针指向地址。
- 将asm_cons_putchar改成一个新的函数asm_hrb_api:
_asm_hrb_api:
STI
PUSHAD ; 用于保存寄存器的值
PUSHAD ; 用于向hrb_api传递参数
CALL _hrb_api
ADD ESP,32
POPAD
IRETD
- C语言编写的API处理函数hrb_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); /*只取eax的低8位*/
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx, ecx);
}
return;
}
- 其实传进来的ebx就是一个数字,不过这个数字的含义是一个地址。
- 修改IDT的设置,将asm_hrb_api注册成INT 40:
void init_gdtidt(void)
{
……
/* INDT设置 */
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_hrb_api, 2 * 8, AR_INTGATE32); /*asm_hrb_api注册*/
return;
}
- 此时,需要修改应用程序hello.nas,需要向寄存器EDX中写入1,才能调用cons_putchar。
[INSTRSET "i486p"]
[BITS 32]
MOV ECX,msg
MOV EDX,1 ;EDX写入1,调用1号功能的API。
putloop:
MOV AL,[CS:ECX]
CMP AL,0
JE fin
INT 0x40
ADD ECX,1
JMP putloop
fin:
RETF
msg:
DB "hello",0
make
后用VMware运行:
- 成功显示。
- 现在使用功能号2或3来显示字符串。编写新的应用程序hello2.nas:
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2
MOV EBX,msg
INT 0x40
RETF
msg:
DB "hello",0
make
后在VMware上运行:
- 很不幸,出BUG了!(明天再研究吧!)