三十天自制操作系统(10)

第22天

CPU中的IRQ中断是从0x20号开始的,0x20也就是定时器中断。0x0~0x1f都是CPU异常所使用的中断。0x00是除零异常;0x06是非法指令异常;之前介绍的0x0d是一般保护异常;0x0c是栈异常。

我们一开始先讲栈异常。所谓的栈异常,就是定义一个数组比如 a[100],但是却在程序中访问a[101],这就是栈异常。CPU不会因为应用程序访问超出定义的栈发出警告,但是如果应用程序访问超出操作系统定义的数据段或者代码段就会产生0x0c号中断。

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

_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
  IRETD

0x0c异常的处理程序写好,再把asm_inthandler0c注册进idt里就可以了。下面的汇编代码中把esp的值push到栈里,当作参数传进inthandler0c函数中。在这个函数中把传进去的esp作为int数据类型的指针,并加上了11,现在看看加上11后表示什么值。esp[07]为_asm_inthandler0c中pushad压进去的值;esp[89]为_asm_inthandler0c中push进去的ds和es。esp[10~15]为异常产生时CPU自动push的结果,其中esp[11]为eip。

我们的操用系统已经能处理2种操作系统的异常了。为了完善操作系统我们再增加一种功能:用户按下某个按键时候,强制执行当前正在运行的应用程序。

我们现在有2个任务同时在运行,1个是一开机就运行的task_a,还有一个就是命令行任务task_console。如果task_console启一个应用程序,这个应用程序很复杂,要运行很久,但是过了一段时间之后,用户后悔了,不想再运行了,然后按下ctrl+f1结束应用程序。那么处理ctrl+f1就不能在task_console里执行,因为应用程序运行的时候,task_console就没有办法处理消息队列里的数据了,所以只能在task_a里处理。当a_task任务接收到ctrl+f1的时候察看task_console里是不是运行应用程序,如果在运行应用程序那么就把寄存器强制转换为task_console的寄存器就可以了。

我们已经实现了用C语言调用显示字符的api,接下来可以写显示字符串的api了。

_api_putstr0:   ; void api_putstr0(char *s);
PUSH    EBX
MOV     EDX,2
MOV     EBX,[ESP+8]     ; s
INT     0x40
POP     EBX
RET

但是编绎执行的时候程序无法正常运行。这里牵涉到可执行文件格式的问题,接下来讲讲编绎,链接之后可执行文件的格式了。

  • 0x0000:请求操作系统为应用程序准备的数据段的大小
  • 0x0004:“Hari”字符串
  • 0x0008:数据段内预备空间的大小
  • 0x000c:esp初始值和数据部分传送目的的地址
  • 0x0010:hrb文件内数据部分的大小
  • 0x0014:hrb文件内数据部分从哪里开始
  • 0x0018:0xe9000000
  • 0x001c:应用程序入口地址 - 0x20
  • 0x0020:malloc空间的起始地址

重点看0x000c中存放的esp的值,esp为栈地址,也就是说esp之前部分被认为是栈空间,要显示的字符串也会被放到栈里。0x0018这个地址中的数据是按双字存放的,以intel8086CPU存放数据的规定是高高低低,也就是说0x0018~0x0001b的内存分别是00,00,00,e9。而e9就是jmp指令的机器码。e9后面跟着的就是应用程序入口地址-0x20的值。如果我们把这个文件读入内存然后从0x001b开始执行的话,就可以正常jmp到程序的入口地址,然后就可以正常执行了,在正常执行之前把ss和sp,ds之类的寄存器设置好,使能正常指向程序的数据段就行了。下在是代码段。

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));
  esp    = *((int *) (p + 0x000c));
  datsiz = *((int *) (p + 0x0010));
  dathrb = *((int *) (p + 0x0014));
  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++) {
    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 {
    cons_putstr0(cons, ".hrb file format error.\n");
  }
  memman_free_4k(memman, (int) p, finfo->size);
  cons_newline(cons);
  return 1;
}

这段代码先判断文件中的0x0004开始的4个字节是不是"Hari”字符串,如果不就认为不是可执行文件。一个可执行文件至少有36个字节,因为文件头就是36字节了,如果文件小于36字节肯定不是可执行文件。由于链接程序自动将数据段调整为4K的倍数 ,所以文件第1个字节肯定是0x00,所以也判断一下第1个字节,如果不为0的话肯定不是可执行文件。

接下来写显示窗口的应用程序。

  • edx = 5
  • ebx = 窗口缓冲区
  • esi = 窗口宽度
  • edi = 窗口高度
  • eax = 透明色
  • ecx = 窗口名称

返回值eax = 用于操作窗口的句柄。

我们先写应用程序

int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_end(void);
char buf[150 * 50];
void HariMain(void)
{
  int win;
  win = api_openwin(buf, 150, 50, -1, "hello");
  api_end();
 }

这里api_openwin函数返回的是用于操作窗口的句柄,其实就是SHEET类型的指针。

再来写api_openwin函数

_api_openwin:   ; int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
  PUSH  EDI
  PUSH  ESI
  PUSH  EBX
  MOV       EDX,5
  MOV       EBX,[ESP+16]    ; buf
  MOV       ESI,[ESP+20]    ; xsiz
  MOV       EDI,[ESP+24]    ; ysiz
  MOV       EAX,[ESP+28]    ; col_inv
  MOV       ECX,[ESP+32]    ; title
  INT       0x40
  POP       EBX
  POP       ESI
  POP       EDI
  RET

这程序很简单,就是根据我们之前设计的寄存器的功能,赋值,然后调用0x40中断

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);
  sheet_updown(sht, 3); 
  reg[7] = (int) sht;
}

我们默认把窗口放到(100,50)的位置。reg[7]就是中断返回时候eax的值。

设计在窗口上写字符串和画方块的api

显示字符串api

  • edx = 6
  • ebx = 窗口句柄
  • esi = x座标
  • edi = y座标
  • eax = 色号
  • ecx = 字符串长度
  • ebp = 字符串

描绘方块api

  • edx = 7
  • ebx = 窗口句柄
  • eax = x0
  • ecx = y0
  • esi = x1
  • edi = y1
  • ebp = 色号

接下来跟之前显示窗口的功能差不多。

写好之后应用程序就可以这么调用;

int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_putstrwin(int win, int x, int y, int col, int len, char *str);
void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
void api_end(void);

char buf[150 * 50];

void HariMain(void)
{
  int win;
  win = api_openwin(buf, 150, 50, -1, "hello");
  api_boxfilwin(win,  8, 36, 141, 43, 3 /*黄色 */);
  api_putstrwin(win, 28, 28, 0 /* �黑色 */, 12, "hello, world");
  api_end();
}

你可能感兴趣的:(三十天自制操作系统(10))