第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函数。这个函数需要遵守一些规则。
- 要执行的操作从栈中分配eax个字节的内存空间(esp -= eax)
- 不能改变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个字节按照面、区、点的方式编码,书中提供了一个表格,展示了编码的方法。然后我们就按照这个表格修改操作系统。我们显示全角字符时实际是是分左右两部分显示的,如果在显示第一个字节的时候就遇到换行那么字符就显示错误了,所以我们在命令行窗口换行的时候如果检测全角字符只显示读取一个字节,那么先不要换行,等第二个字节读取了并显示出来再换行。