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

第27天

前一天讲到为什么用ncst命令之后应用程序就无法关闭了。现在看一下程序ncst到底干了什么事情。如果在命令行窗口输入ncst a命令,那么会就会新打开一个窗口,然后向新的命令行任务发送a,命令行任务就会运行a应用程序,之后命令行任务就休眠了。之后我们点击窗口的关闭按扭,将命令行任务的eax和eip休改成应用程序结束的状态,但是由于再没有消息发送给命令行任务了,所以一直没有执行,这就是BUG产生的原因。所以我们的解决办法就是设置好寄存器的值之后,调用task_run函数唤醒命令行任务就可以了。

下面来看一段恶意程序:

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "crack7.nas"]
  GLOBAL    _HariMain
[SECTION .text]
_HariMain:
  MOV       AX,1005*8
  MOV       DS,AX
  CMP       DWORD [DS:0x0004],'Hari'
  JNE       fin                 ; 不是应用程序因此不执行任何操作
  MOV       ECX,[DS:0x0000]     ; 读取应用程序数据段的大小
  MOV       AX,2005*8
  MOV       DS,AX
crackloop:                          ; 将应用程序的数据段用123填充
  ADD       ECX,-1
  MOV       BYTE [DS:ECX],123
  CMP       ECX,0
  JNE       crackloop
fin:                                ; �结束
  MOV       EDX,4
  INT       0x40

这段程序从GDT的第1005段开始搜索是否有应用程序在运行,如果有则将应用程序的数据段用123填充,对于CPU来说应用程序只是访问应用程序的数据段,并没有访问操作系统的数据,所以并不会出现异常。如果我们能禁止应用程序访问其他应用程序的数据段就好了。

实际上CPU也提供了这种功能,这种机制就是LDT。之前说的GDT是global descriptor table;LDT表示local descriptor table。前者表示全局,GDT中的段设置提供所有任务通用;后者表示局部,LDT中的段设置只对某些应用程序有效。

LDT也和GDT一样容量也是64KB,但是LDT却不能和GDT一样有一个专门的寄存器表示其初使地址。因为每个应用程序的LDT都是独立的不可能给每个应用程序分配专门的寄存器,所以LDT的初始地址只能放到内存中。而且LDT也必须和GDT相关联,问题就是如何关联。

如果我们将GDT中的项设置为tss那么CPU就自动运行多任务。所以CPU在设计的时候就为tss数据段设立了一项LDTR的字段,CPU会根据TSS中的这个字段确定每个任务的LDT的起始位置。所以如果我们想要让应用程序使用LDT,就需要在任务创建的时候在内存中保留LDT,而且需要将LDT初始化。

struct TASK {
  int sel, flags; 
  int level, priority;
  struct FIFO32 fifo;
  struct TSS32 tss;
  struct SEGMENT_DESCRIPTOR ldt[2];
  struct CONSOLE *cons;
  int ds_base, cons_stack;
};

我们的应用程序只需要两个段,所以只需要定义2个。然后在初始化多任务的时候将tss中的ldtr字段赋值;再给LDT赋值。

taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;

set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int) taskctl->tasks0[i].ldt, AR_LDT);

在运行应用程序的时候再将TASK结构体中的ldt字符赋值就行了。

实际在我在看到这里的时候思考了很久,最后终于想通了,这里面实际上有很多曲曲绕绕的东西。真正的应用程序数据段和代码段的地址保存在ldt中。但是由于ldt依附于gdt,所以又要先对gdt赋值给ldt(真的很难讲清楚)。

我们的每个应用程序都很大,因为我们每个应用程序都要把所有的API链接进应用程序中,不管用到或都没有用到。因为我们所有的API程序段都写在一个文件中,这个源文件编译之后生成一 个obj文件。找到问题的原因,解决办法就是为每个API定一个源文件,也就是将API函数分开,然后有多少个API就生成多少个OBJ文件,链接器只把使用到的API链接进可执行文件中,这样就是减少应用程序的大小了。

以上的解决方法虽然能解决问题,因为我们现在的API还少,才20个,如果像windows这样的操作系统有几千个API那就是一场灾难。为此,前人发明的库这个概念。本书的作都为我们提供了库管理器,如果要创建库文件只要输入下面的命令:

apilib.lib : Makefile $(OBJS_API)
  $(GOLIB) $(OBJS_API) out:apilib.lib

这样就生成了alilib.lib文件。再写一个apilib.h文件把API函数的声明写进去,我们以后写应用程序的时候只要包含这个文件只可以使用API了。

第28天

一开始我们写一个将1000以内的质数打印出来的程序。

#include <stdio.h>
#include "apilib.h"

#define MAX     1000

void HariMain(void)
{
  char flag[MAX], s[8];
  int i, j;
  for (i = 0; i < MAX; i++) {
    flag[i] = 0;
  }
  for (i = 2; i < MAX; i++) {
    if (flag[i] == 0) {/* 没有标记为质数 */
      sprintf(s, "%d ", i);
      api_putstr0(s);
      for (j = i * 2; j < MAX; j += i) {
        flag[j] = 1;    /* 给他的倍数上标记 */
      }
    }
  }
  api_end();
}

这个程序运行良好。但是只要我们把MAX改成10000的时候就出题了。出现了一条警告:can't link _alloca。计算机所使用的C语言编绎器规定,如果栈中的变量超过4K,就需要调用_alloca这个函数。这个函数的主要功能是根据操作系统的规格来获取栈中的空间。在windows和linux中如果不调用这个函数,需仅对esp进行减法运算的话是无法获取内存空间的。

为了让我们的操作系统支持运行栈空间超过4K的程序,我们需要写一个allloca函数。这个函数需要遵守一些规则。

  1. 要执行的操作从栈中分配eax个字节的内存空间(esp -= eax)
  2. 不能改变ecx,edx, ebx, ebp, esi, edi的值
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "alloca.nas"]

  GLOBAL    __alloca

[SECTION .text]

__alloca:
  ADD       EAX,-4
  SUB       ESP,EAX
  JMP       DWORD [ESP+EAX]

接下来我们为操作系统写一些文件操作的API。文件操 作的API包括打开、定位、读取、写入、关闭。我们设计API。

  • 打开文件

  • edx = 21

  • ebx = 文件中

  • eax = 文件句柄(0表示打开失败)

  • 关闭文件

  • edx = 22

  • eax = 文件句柄

  • 文件定位

  • edx = 23

  • eax = 文件句柄

  • ecx = 定位模式:0表示定位起点为文件开头;1表示定位起点为当前位置;2表示定位起点为文件末尾

  • ebx = 定位偏移量

  • 获取文件大小

  • edx = 24

  • eax = 文件句柄

  • ecx = 文件大小获取模式:0表示普通文件大小;1表示当前位置从开头算起的偏移量;2表示当前位置从文件末尾算起的偏移量

  • eax = 文件大小

  • 文件读取

  • edx = 25

  • eax = 文件句柄

  • ebx = 缓冲区地址

  • ecx = 最大读取字节

  • eax = 本次读取到的字节数

我们先来定义一个结构体,表示文件句柄的结构体。

struct FILEHANDLE {
  char *buf;
  int size;
  int pos;
};

我们规定每个命令行任务最多可以打开8个文件。并且把文件句柄保存在task结构体中,为了传递参数方便。

for (i = 0; i < 8; i++) {   
  if (task->fhandle[i].buf != 0) {
    memman_free_4k(memman, (int) task->fhandle[i].buf, task->fhandle[i].size);
    task->fhandle[i].buf = 0;
  }
}

在每个应用程序运行完之后,加入上面这段程序,就算应用程序没有关闭文件,操作系统也会关闭已打开 的文件。

接下来实现具体的API。

else if (edx == 21) {
  for (i = 0; i < 8; i++) {
    if (task->fhandle[i].buf == 0) {
      break;
    }
  }
  fh = &task->fhandle[i];
  reg[7] = 0;
  if (i < 8) {
    finfo = file_search((char *) ebx + ds_base, (struct FILEINFO *) (ADR_DISKIMG + 0x002600),224);
    if (finfo != 0) {
      reg[7] = (int) fh;
      fh->buf = (char *) memman_alloc_4k(memman, finfo->size);
      fh->size = finfo->size;
      fh->pos = 0;
      file_loadfile(finfo->clustno, finfo->size, fh->buf, task->fat, (char *) (ADR_DISKIMG + 0x003e00));
    }
  }
} else if (edx == 22) {
  fh = (struct FILEHANDLE *) eax;
  memman_free_4k(memman, (int) fh->buf, fh->size);
  fh->buf = 0;
} else if (edx == 23) {
  fh = (struct FILEHANDLE *) eax;
  if (ecx == 0) {
    fh->pos = ebx;
  } else if (ecx == 1) {
    fh->pos += ebx;
  } else if (ecx == 2) {
    fh->pos = fh->size + ebx;
  }
  if (fh->pos < 0) {
    fh->pos = 0;
  }
  if (fh->pos > fh->size) {
    fh->pos = fh->size;
  }
} else if (edx == 24) {
  fh = (struct FILEHANDLE *) eax;
  if (ecx == 0) {
    reg[7] = fh->size;
  } else if (ecx == 1) {
    reg[7] = fh->pos;
  } else if (ecx == 2) {
    reg[7] = fh->pos - fh->size;
  }
} else if (edx == 25) {
  fh = (struct FILEHANDLE *) eax;
  for (i = 0; i < ecx; i++) {
    if (fh->pos == fh->size) {
      break;
    }
    *((char *) ebx + ds_base + i) = fh->buf[fh->pos];
    fh->pos++;
  }
  reg[7] = i;
}

我们写一个应用程序测试一下。

#include "apilib.h"

void HariMain(void)
{
  int fh;
  char c;
  fh = api_fopen("ipl10.nas");
  if (fh != 0) {
    for (;;) {
      if (api_fread(&c, 1, fh) == 0) {
        break;
      }
      api_putchar(c);
    }
  }
  api_end();
}

这个应用程序名称为typeipl。这个应用程序的功能就是将ipl10.nas文件的内容打印到命令行窗口。这个功能和我们之前做的命令行的type命令是一样的。而且运和地应用程序可以强制执行,如果想增加什么功能修改也方便。我们想办法实现这个想法。为了用应用程序能替代type命令功能,我们需要让应用程序获取命令行参数的功能。

我们写一个API用于让应用程序获取操作系统命令行窗口输入的命令。

  • 获取命令行
  • edx = 26
  • ebx = 存放命令行内容的地址
  • ecx = 最多可存放多少字节
  • eax = 实际存放了多少字节

我们在命令行窗口输入的每个字符都会存到命令行函数的cmdline[30]数组中。我们把这个数组指针也存放到TASK结构体中,为了传递参数方便。

else if (edx == 26) {
  i = 0;
  for (;;) {
    *((char *) ebx + ds_base + i) =  task->cmdline[i];
    if (task->cmdline[i] == 0) {
      break;
    }
    if (i >= ecx) {
      break;
    }
    i++;
  }
  reg[7] = i;
}

然后写apilib函数

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api026.nas"]
  GLOBAL    _api_cmdline
[SECTION .text]

_api_cmdline:       ; int api_cmdline(char *buf, int maxsize);
  PUSH  EBX
  MOV       EDX,26
  MOV       ECX,[ESP+12]        ; maxsize
  MOV       EBX,[ESP+8]         ; buf
  INT       0x40
  POP       EBX
  RET

然后写一个应用程序测试一下

#include "apilib.h"
 void HariMain(void)
{
  int fh;
  char c, cmdline[30], *p;
  api_cmdline(cmdline, 30);
  for (p = cmdline; *p > ' '; p++) { }  
  for (; *p == ' '; p++) { }    
  fh = api_fopen(p);
  if (fh != 0) {
    for (;;) {
      if (api_fread(&c, 1, fh) == 0) {
        break;
      }
      api_putchar(c);
    }
  } else {
    api_putstr0("File not found.\n");
  }
  api_end();
}

编绎运行之后一切正常。

将下来要实现显示日文的功能。这本书在翻译的时候原原本本得呈现了日文显示,但是因为中文和日文一样也是象形文件,应该会有很多共通之处。

我们在日常使用计算机过程中经常会碰到半角字符和全角字符。所谓半角就是用816点阵显示的字符,而全角字符就是1616点阵显示的字符。日文的字符基本上是用全角字符的,如果一个半角字符的字库需要16字节的话,那么一个全角字符就需要32字节。

日文中的汉字根据使用频率分为第一~第四水准。根据JIS规格,全角字符编码以点、区、面为单位区分,一个点表示一个全角字符,一个区包含94个点,一个面包含94个区。

为了显示日文,首先肯定要像之前显示ASCII字符一样先制作一个字库。本书作者作好的字库是这样的:000000000fff显示日文用半角字模,共256个字符;00100002383f显示日文用全角字模,共4418个字符。

接下来修改主程序让操作系统自动装载日文字库。首先寻找nihongo.fnt文件,如果找到的话装入内存空间,如果没有找到那就用内置的半角字库代替日文半角字库,并用方块填 充全角字库的部分。并且把存放nihongo.fnt内容的内存地址写入0x0fe8。

为了让操 作系统支持英文和日文模式转换,我们在 TASK结构体中增加一个变更char langmode,langmode如果为0表示英文,为1表示日文。再新增一个命令用于转换输入模式。

else if (strncmp(cmdline, "langmode ", 9) == 0) {
  cmd_langmode(cons, cmdline);
}


void cmd_langmode(struct CONSOLE *cons, char *cmdline)
{
  struct TASK *task = task_now();
  unsigned char mode = cmdline[9] - '0';
  if (mode <= 1) {
    task->langmode = mode;
  } else {
    cons_putstr0(cons, "mode number error.\n");
  }
  cons_newline(cons);
  return;
}

再修改操作系统显示字符串的函数:

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
  extern char hankaku[4096];
  struct TASK *task = task_now();
  char *nihongo = (char *) *((int *) 0x0fe8);
  if (task->langmode == 0) {
    for (; *s != 0x00; s++) {
      putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
      x += 8;
    }
  }
  if (task->langmode == 1) {
    for (; *s != 0x00; s++) {
      putfont8(vram, xsize, x, y, c, nihongo + *s * 16);
      x += 8;
    }
  }
  return;
}

上面这段函数可以看出,实际上操作系统不是不能支持全角字符的显示。汉字需要有和2个字节编码,这2个字节按照面、区、点的方式编码,书中提供了一个表格,展示了编码的方法。然后我们就按照这个表格修改操作系统。我们显示全角字符时实际是是分左右两部分显示的,如果在显示第一个字节的时候就遇到换行那么字符就显示错误了,所以我们在命令行窗口换行的时候如果检测全角字符只显示读取一个字节,那么先不要换行,等第二个字节读取了并显示出来再换行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容