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文件,里面有该函数的地址。
来看我们开发应用,打印一个字符:
[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 函数。