帮助发现bug
CPU的异常处理功能,还能帮助我们发现bug,比如下面的例子:
bug1.c:
void api_putchar(int c);
void api_end(void);
void HariMain(void)
{
char a[100];
a[10] = 'A'; /* 这句没有问题 */
api_putchar(a[10]);
a[102] = 'B'; /* 这句有问题 */
api_putchar(a[102]);
a[123] = 'C'; /* 这句也有问题 */
api_putchar(a[123]);
api_end();
}
由于数组保存在栈中,数组越界会产生栈异常。我们编写一个函数来处理栈异常,栈异常的中断号为0x0c。CPU说明书中,从0x00到0x1都是异常所使用的中断。IRQ的中断号都是从0x20之后开始的,其中0x00号是除零异常,当试图除以0时产生;0x06号是非法指令异常,当试图执行CPU无法理解的机器语言指令时产生。
naskfunc.nas
_asm_inthandler0c:
STI
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler0c
CMP EAX,0
JNE end_app
POP EAX
POPAD
POP DS
POP ES
ADD ESP,4 ; 在INT 0x0c 中也需要这句
IRETD
发生异常时,应该能显示引发异常的指令地址,我们在console.c程序中加上这个功能。
console.c节选:
int *inthandler0c(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
char s[30];
cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
sprintf(s, "EIP = %08X\n", esp[11]);
cons_putstr0(cons, s);
return &(task->tss.esp0); /* 强制结束程序 */
}
int *inthandler0d(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
char s[30];
cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
sprintf(s, "EIP = %08X\n", esp[11]); /* 显示esp(即栈)的11号元素EIP */
cons_putstr0(cons, s);
return &(task->tss.esp0); /* 强制结束程序 */
}
执行make run ,看一下效果:
强制结束应用程序
有这样一类应用程序,虽然执行内容没有错误,但是会一直执行,比如死循环。这样的应用程序会一直占用CPU资源,系统的整体速度就会变慢。
bug2.c:
void HariMain(void)
{
for (;;) { }
}
因此我们设置一个强制结束键,按下强制结束键就可以结束程序,这里设定为按下“Shift + F1”结束。
bootpack.c节选
if (i == 256 + 0x3b && key_shift != 0 && task_cons->tss.ss0 != 0) { /* 判断按下的是否是 Shift+F1 */
cons = (struct CONSOLE *) *((int *) 0x0fec);
cons_putstr0(cons, "\nBreak(key) :\n");
io_cli(); /* 不能在改变寄存器值时切换到其他任务 */
task_cons->tss.eax = (int) &(task_cons->tss.esp0);
task_cons->tss.eip = (int) asm_end_app;
io_sti();
}
make run 一下,执行bug2命令执行死循环,按下"Shift+F1"跳出循环。
用C语言显示字符串
现在我们来实现用C语言显示字符串,先来写一个显示字符串的API。
a_nask.nas节选:
_api_putstr0: ; void api_putstr0(char *s);
PUSH EBX
MOV EDX,2
MOV EBX,[ESP+8] ; s
INT 0x40
POP EBX
RET
我们再写一个C语言程序调用上面的API:
void api_putstr0(char *s);
void api_end(void);
void HariMain(void)
{
api_putstr0("hello, mint's world\n");
api_end();
}
这里要显示完整的字符串,有个地方需要注意。bim2hrb 生成的 .hrb文件 包括两个部分,代码部分和数据部分。
之前程序中没有使用字符串和外部变量,生成的.hrb文件中不包含数据部分。我们在启动程序时,需要先指定数据段的大小,将数据部分复制到数据段中,再启动应用程序。
根据hello4.hrb文件存放的内容,我们来修改console.c文件。
console.c节选:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
int i, segsiz, datsiz, esp, dathrb;
(略)
if (finfo != 0) {
/* 找到文件的情况 */
p = (char *) memman_alloc_4k(memman, finfo->size);
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
segsiz = *((int *) (p + 0x0000)); /* 0x0000地址存放为应用程序准备的数据段大小 */
esp = *((int *) (p + 0x000c)); /* 0x000c地址存放ESP初始值和数据部分传送目的地址 */
datsiz = *((int *) (p + 0x0010)); /* 0x0010地址存放hrb文件数据部分的大小 */
dathrb = *((int *) (p + 0x0014)); /* 0x0014地址存放hrb文件数据的开始地址 */
q = (char *) memman_alloc_4k(memman, segsiz);
*((int *) 0xfe8) = (int) q;
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
set_segmdesc(gdt + 1004, segsiz - 1, (int) q, AR_DATA32_RW + 0x60);
for (i = 0; i < datsiz; i++) { /* 将.hrb文件复制到数据段后再启动程序 */
q[esp + i] = p[dathrb + i];
}
start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));
memman_free_4k(memman, (int) q, segsiz);
} else { /* 找不到.hrb文件就报错 */
cons_putstr0(cons, ".hrb file format error.\n");
}
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
/* 没有找到文件的情况 */
return 0;
}
执行一下make run_——
显示窗口
现在我们来让应用程序显示窗口吧,显示窗口需要指定窗口名称、窗口高和宽,窗口位置、窗口缓冲区等信息,来看一下程序。
console.c节选
/*
edi = 窗口高度
esi = 窗口宽度
ebp =
esp =
ebx = 窗口缓冲区
ecx = 窗口名称
eax = 透明色
*/
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int ds_base = *((int *) 0xfe8);
struct TASK *task = task_now();
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct SHTCTL *shtctl = (struct SHTCTL *) *((int *) 0x0fe4);
struct SHEET *sht;
int *reg = &eax + 1; /* eax后面的地址 */
/* 强行改写通过PUSHAD保存的值 */
/* reg[0] : EDI, reg[1] : ESI, reg[2] : EBP, reg[3] : ESP */
/* reg[4] : EBX, reg[5] : EDX, reg[6] : ECX, reg[7] : EAX */
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + ds_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) {
return &(task->tss.esp0);
} else if (edx == 5) {
sht = sheet_alloc(shtctl);
sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
sheet_slide(sht, 100, 50); /* 窗口显示在(100,50)*/
sheet_updown(sht, 3); /* 背景层高度3位于task_a之上 */
reg[7] = (int) sht;
}
return 0;
}
这里shtctl的值从0x0fe4地址获得,reg是向应用程序返回值。窗口显示在(100,50)这个位置,背景层高度为3。
make run——
在窗口中描绘字符和方块
最后我们再尝试在窗口显示字符和方块吧,这两个功能之前实现过,把它们加在API中就可以了。
实现思路:
在窗口上显示字符:
EDX = 6
EBX = 窗口句柄
ESI = 显示位置的x坐标
EDI = 显示位置的y坐标
EAX = 色号
ECX = 字符串长度
EBP = 字符串
描绘方块:
EDX = 7
EBX = 窗口句柄
EAX = x0
ECX = y0
ESI = x1
EDI = y1
EBP = 色号
按照这个思路,在程序中实现,执行make run,看一下效果:
注:本文参照《30天自制操作系统》制作,感谢各位的持续关注,原著源码链接见第十九篇。