GeekOS是一个基于x86架构的PC机上运行的微操作系统内核...
Geekos简介:
GeekOS是一个基于x86架构的PC机上运行的微操作系统内核,是美国马理兰大学的教师开发的用于操作系统课程设计,目的是使学生能够实际动手参与到一个操作系统的开发工作中,基于这个目的,该系统设计的既简单又实用。到目前为止GeekOS的版本由原来的0.0.0已经发展到0.3.0,
尽管GeekOS可以在linux下也可以在WINDOWS下用虚拟机进行开发,但是我个人觉得还是在linux下开发比较好,因为在开发过程中老有同学来问,说这里或是那里出问题,有时有的问题真的不是很懂,因为可能是配置的问题,我又没有在WINDOWS底下实际配置过、而且操作起来也比较麻烦。
关于Bochs
Bochs 是用 C++ 开发的可移植的 IA-32 (x86) PC 模拟器,几乎可以运行在所有流行的平台上。它包括对 Intel x86 CPU 、通用 I/O 设备和可定制的 BIOS 的模拟。目前, Bochs 可以模拟 386, 486, Pentium Pro 或者 AMD64 CPU ,包括可选的 MMX, SSE, SSE2 和 3DNow 指令。Bochs 的模拟环境中可以运行大部
Linux, Windows 95, DOS, Windows NT 4, FreeBSD, MINIX 分的操作系统,包括
下载Bochs 2.2.1版本,在linux下你可以选择二进制代码包等。从Bochs网站
或者rpm的版本, 在WINDOWS下选用exe版本即可。
WINDOWS下的.exe和linux下的rpm版本都比较容易安装,直接双击就可以了。如果是选择二进制代码包则在安装的时候要用到下面三条命令:在该目录底下按以下顺序运行命令:./configure , make ,makefile就可以了。Linux下的rpm格式的不能更改安装路径,但是在安装的时候可以通过“细节”那一项来观察安装到那个目录底下。而在WINDOWS底下安装的时候通过完全安装,也就是把dlxlinux这一项选上,于是在安装后有一个例子。可不要小看这个例子,对于刚开始学的同学来说可是非常重要的,理由有两个:
1、可以让刚学的新手对它有一个感性上的认识,知道自己要做什么。
2、里面的那个配置文件稍加修改,就变成我们的了,省了自己写配置文件
的痛苦。
Bochs文件的配置:
Bochs文件的配置是使用Bochs的关键,而我们这次课设其实只要配置下面
这几项也就差不多了:
romimage: file=../BIOS-bochs-latest, address=0xf0000
#(file=后面是BIOS-bochs-latest存在路径,由于我把配置文件放在同个目录底下,所以用../,如果你不是的话就把它的路径写上去就可以了)
vgaromimage: file=../VGABIOS-lgpl-latest #(这里跟romimage其实是同个道理)
floppya: 1_44=fd.img, status=inserted #(这里1_44=后面填的是你编译运行得到的映象文件的路径,如果不是在同个文件夹底下则要记得把路径添完整,如果使用dlxlinux里面的那个配置文件,也就是例子的那个,则记得要把floppyb那一行用#注释掉)
boot: a
#(确定是从软驱启动,如果是要从硬盘启动则是boot: c,这里之所以要从设置软驱启动是因为我们geekos的编译得到的就是一个软盘映象fd.img文件,通过这个软盘映象文件就可以在Bochs下启动Geekos了) 做Project 1的时候,需要添加一个磁盘镜像 ata0-master:type=disk,mode=flat,path=“diskc.img“,cylinders=615, heads=6, spt=17
编译后产生的硬盘镜象。Cylinders
示柱面数是615、heads表# diskc.img是
示磁头数是6、spt表示每磁道扇区数是17。
然后我们就可以开始运行仿真器了,在弹出窗口中,选择[2]Read
Opinion From ,选取配置文件的路径,如果bochsr.txt文件是和bochs.exe
在同一个文件夹下面的话就不用设定,然后选[5]Begin simulation,就可以
了。另外可以同过模拟器的日志文件,来查看所有的输出信息,包括错误信
息。
实验任务
Project 0
目的 具体任务和
要求对GeekOS进行基本的编译,并实现一个用于键盘输入的内核进程。
任务的设计原理
首先我们要知道怎样来创建一个内核级线程,然后才是去实现对键盘的
输入处理及显示。
1、如何创建一个内核级线程
首先我们弄清楚内核线程结构,它是这么定义的: struct Kernel_Thread {
unsigned long esp;
volatile unsigned long numTicks;
int priority;
DEFINE_LINK( Thread_Queue, Kernel_Thread );
void* stackPage;
struct User_Context* userContext;
struct Kernel_Thread* owner;
int refCount;
Boolean alive;
struct Mutex joinLock;
struct Condition joinCond;
};
esp字段用来存放一个线程挂起的堆栈指针;stackPage字段指向内核线程的堆栈页面。numTicks和priority分别被调度程序用来实现基于先占权和基于优先权的时间片调度。DEFINE_LINK宏定义一个内核线程在线程队列上时的前一个和后一个字段。userContext字段如果不为空,则指向一个线程用户环境,它是一
个允许线程执行用户模式的代码和数据的组合段。其余字段用来实现Join()函数,该函数允许父线程等待子线程退出。
了解完内核线程结构我们就可以开始来创建一个内核级线程,通过调用Start_Kernel_Thread()可以生成一个内核线程,它的形式如下:
Start_Kernel_Thread(
Thread_Start_Func startFunc,
ulong_t arg,
int prority,
bool detached
);
其中startFunc是一个函数指针,指向内核线程入口的函数体;arg是传递给入口函数的参数;prority是此线程运行的特权级别,detached表示是否将当前的线程与生成的新线程之间建立父子关系,这里我们要创建的是一个内核级的线程,所以应该要为true,这样就不会去Main线程与其余内核线程间的父子关系。
下面我们就可以开始来了解关于键盘处理的一些函数了,然后在调用它就
以完成Project 0 的任务了。
Geekos是如何处理键盘代码的呢,,在keyboard.c里面提供了这样一个函
然后返回一个16数Keycode Wait_For_Key(void),它是作用等待一个键盘事件,位的Keycode型的数据。这个数据也就是用户所按下的键的键值。
但是要注意的是当你去敲键盘的某个按键是其实是产生了两个事件即按下和弹起,所以Wait_For_Key()这个函数也相应的会返回两个值,当然,它们的Keycode值肯定是相同的,但是这个就要求我们在写代码的时候一定要加以处理。
还有就是Keycode值到底是什么呢,关于这个我们可以在keyboard.h里面看到它已经定义了许多键盘代码(也就是说其实还有一些按键没有定义如小键盘,当然这个没什么难度),从网上查得原来它是这么定义的:低10位表示键盘值,通过s_scanTableNoShift和s_scanTableWithShift这两个数组来转换相应的键盘码为所表示字符的ASCII码,高六位分别是: KEY_SPECIAL_FLAG (特殊键,比如F1,F2) 用返回的值key&0x0100 就可以判断是否按下特殊健,1为有效,说明是特殊健,0则不是,以下的几种情况也是同样处理
KEY_KEYPAD_FLAG (小键盘键) 0x0200 KEY_SHIFT_FLAG (左,右SHIFT) 0x1000 KEY_ALT_FLAG (左,右ALT) 0x2000 KEY_CTRL_FLAG (左,右CTRL) 0x4000 KEY_RELEASE_FLAG (键弹起来标志位) 0x8000 任务的具体实现:
下面给出Project 0要添加的代码: Start_Kernel_Thread(
&key_handle_Function,
0,
PRIORITY_NORMAL,
true
);
这个就是在mian()函数里面调用的函数,里面的参数的意思在上面已经给出了,这里再结合实际的参数说一下:& key_handle_Function是指向我添加的一个函数的函数制指针;0表示没有参数;PRIORITY_NORMAL表示优先级是normal;
true表示这不与Main线程建立父子关系,这样即使Main线程运行结束,我们创建的线程也不会退出。
下面给出key_handle_Function即键盘处理及显示的函数的代码:
static void key_handle_Function(ulong_t arg)
{
Keycode Key_Press;
Print("Waiting for the Keyborad press...........\n");
while(1)
{
Key_Press = Wait_For_Key();
Print("%c",Key_Press); //显示按键
Wait_For_Key(); //等待按键的弹起
}
}
这里之所以要在按键显示完之后还要加上一个 Wait_For_Key()就是为了处理掉按键弹起这个事件,不要让用户敲一个按键结果显示了两个字符。下面给出运行是的图象:
在项目 0中要注意的:
在开始做项目0的时候就是摸着石头过河,添加代码本身不是很多,主要是环境的搭建,技术上唯一要注意的就是上面反复提到的对一个按键的按下和弹起的处理。
Project 1
具体任务和目的
主要是分析ELF文件的格式,了解操作系统执行一个应用程序的工作原
理,然后在Parse_ELF_Executable()中添加代码,实现对ELF文件的解释,
装入到内存运行,得到指定的结果。 任务的设计原理
首先要说一下ELF的文件格式简介
Executable and linking format(ELF)文件是x86 Linux系统 下的一种常用目标文件(object file)格式,有三种主要类型: (1)适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
(2)适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
(3)共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。 其次是说一下ELF文件头 象bmp、exe等文件一样,ELF的文件头包含整个文件的控制结构。它的定义如下:
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
其中E_ident的16个字节标明是个ELF文件(7F+'E'+'L'+'F'+class
+data+version+pad)。E_type表示文件类型,2表示可执行文件。E_machine说明机器类别,3表示386机器,8表示MIPS机器。E_entry给出进程开始的虚地
址,即系统将控制转移的位置。E_phoff指出program header table的文件偏移,e_phentsize表示一个program header表中的入口的长度(字节数表示),e_phnum给出program header表中的入口数目。类似的,e_shoff,e_shentsize,e_shnum 分别表示section header表的文件偏移,表中每个入口的的字节数和入口数目。E_flags给出与处理器相关的标志,e_ehsize给出ELF文件头的长度(字节数表示)。E_shstrndx表示section名表的位置,指出在section header表中的索引。
.h里面定义的两个结构体。 另外在elf
struct Exe_Format {
struct Exe_Segment segmentList[EXE_MAX_SEGMENTS]; /* 段的定义*/
int numSegments; /* 可执行文件中段的个数*/
ulong_t entryAddr; /* 代码入口 */ };
struct Exe_Segment {
ulong_t offsetInFile; /*段在可执行文件中的偏移 */
ulong_t lengthInFile; /*段在可执行文件中的长度 */
ulong_t startAddress; /* 段在内存中的起始地址*/
ulong_t sizeInMemory; /* 段在内存中的大小 */
int protFlags; /* VM保护标志*/ };
一个object文件的section header table可以让我们定位所有的sections。section header table是个elfHeader结构的数组(下面描述)。一个section报头表(section header table)索引是这个数组的下标。ELF header table的shoff成员给出了section报头表的偏移量(从文件开始的计数)。shnum告诉我们section报头表中包含了多少个入口;shentsize 给出了每个入口的大小。具体定义如下:
typedef struct {
unsigned char ident[16];
unsigned short type; /*可执行文件的类型*/
unsigned short machine;/*机器的类型*/
unsigned int version;/*值为1*/
unsigned int entry;/*虚拟地址*/
unsigned int phoff;/*program header 的偏移*/
unsigned int sphoff;/*section header的偏移*/
unsigned int flags;/*指示具体的进程*/
unsigned short ehsize;/*elf头部的大小*/
unsigned short phentsize;/*program header的大小*/
unsigned short phnum;
unsigned short shentsize; /*section header的大小*/
unsigned short shnum; /*section header的个数*/
unsigned short shstrndx;
} elfHeader;
因为我们只需要在elf.c里面的函数Parse_ELF_Executable中添加代码,入口参数:
char *exeFileData 可执行文件在内存中的缓冲区指针 ulong_t exeFileLength 可执行文件的长度(单位是byte)
struct Exe_Format *exeFormat 描述可执行代码段和入口地址
所以我们要做的工作就是,要明白ELF文件在内存中各个数据代表什么,这部分可以看ELF文件的构造和相关数据结构,然后看exeFormat这个结构体里面定义的变量,比如说:
exeFormat->segmentList[0].offsetInFile对应于ELF中的offset,我们需要做的就是exeFormat->segmentList[0].offsetInFile=proHeader->offset ,把内存中分析出来的值赋给exeFormat相关变量,当完成把exeFormat所有变量赋值完成,就相当于可以执行a.exe这个程序了 我们需要用到的就是这几个参数:
该成员保存着在程序头表中入口的个数。因此, phentsize和phnum 的phnum
乘积就是表的大小(以字节计数)。假如没有程序头表(program header table),phnum变量为0。对应Exe_Format里面的成员numSegments。
phoff 该成员保持着程序头表(program header table)在文件中的偏移量(以字节计数)。 假如该文件没有程序头表的的话,该成员就保持为0。
entry该成员是系统第一个传输控制的虚拟地址,在那启动进程。假如文件没有如何关联的入口点,该成员就保持为0。 sphoff 该成员保持着section头表(section header table)在文件中的偏移量(以字节计数)。假如该文件没有section头表的的话,该成员就保持为0。
然后就是程序头了, 一个可执行的或共享的 object file 的程序头表是一个结构数组,每一个结构描述一个段或其他系统准备执行该程序所需要的信息。一个 object file段包含一个或多个部分(就象下面的“段目录”所描述的那样)。程序头仅仅对于可执行或共享的 object file 有意义。一个文件使用 ELF 头的 phentsize 和 phnum 成员来指定其拥有的程序头大小。
typedef struct {
unsigned int type;
unsigned int offset;/*从文件开始的偏移*/
unsigned int vaddr;/*在内存中的第一个字节*/
unsigned int paddr;/*段的物理地址*/
unsigned int fileSize;/*段在文件中的大小*/
unsigned int memSize;/*内存镜象中段的大小*/
unsigned int flags;/*段的标志*/
unsigned int alignment;
} programHeader;
offset 该成员给出了该段的驻留位置相对于文件开始处的偏移。
该成员给出了文件映像中该段的字节数;它可能是 0 。fileSize
memSize 该成员给出了内存映像中该段的字节数;它可能是 0 。
vaddr 该成员给出了该段在内存中的首字节地址。 flags 该成员给出了和该段相关的标志。
假如在flags中的一个标记位被设置,该section相应的属性也被打开。否则,该属性没有被应用。未明的属性就设为0。
Name Value
===== ====
0x1 F_WRITE
F_ALLOC 0x2
F_EXECINSTR 0x4
0xf0000000 F_MASKPROC
F_WRITE 该section包含了在进程执行过程中可被写的数据。
F_ALLOC 该section在进程执行过程中占据着内存。一些控制section不存在一个 object文件的内存映象中;对于这些sections,这个属性应该关掉。
F_EXECINSTR 该section包含了可执行的机器指令。 F_MASKPROC 所有的包括在这掩码中的位为特定处理语意保留的。
任务的具体实现:
下面给出Project 1要添加的代码:
int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength,
struct Exe_Format *exeFormat)
{
int seg_count;
elfHeader* head = (elfHeader*)exeFileData;
programHeader *proHeader=(programHeader
*)(exeFileData+head->phoff);
for(seg_count =0; seg_count
phnum; seg_count++)
{
exeFormat->segmentList[seg_count].offsetInFile=proHeader->offset;
exeFormat->segmentList[seg_count].lengthInFile=proHeader->fileSize;
exeFormat->segmentList[seg_count].startAddress=proHeader->vaddr; exeFormat->segmentList[seg_count].sizeInMemory=proHeader->memSize;
exeFormat->segmentList[seg_count].protFlags=proHeader->flags; proHeader++;
}
exeFormat->numSegments=head->phnum;
exeFormat->entryAddr=head->entry;
return 0;
}
下面给出运行是的图象:
完成情况:
在本次课程设计中我负责的是0、1两个项目,相对来说,这两个项目要比
另外3个来的简单的多,为了让同组的同学有更多的时间来完成后面的任务,我
也是用了最快的时间完成了这两个项目,但是后面的项目的确有点难度,我们花
了许多时间,也就只是把项目2基本完成了而已。
存在问题:
在项目0里还没有完全实现所有键值的识别,如小键盘,但是没有去完成它
是因为那是个重复性的工作,没什么技术含量,所以我也就没去弄了。我想这也
是存在的唯一的问题吧,因为要求添加的也就那几行代码。
如何优化:
只要按照相应的键值进行编码就可以实现对所有按键的识别。
收获及建议:
虽然这次的课程设计我们只完成一半的任务,但是却让我对整个的操作系统有了一个感性认识,而不再像刚学完理论课一样,就知道它是个什么东西,每个部分是怎么工作的而已。同时也感受到了Bochs这个虚拟机的功能的强大,对它有了一定的认识。
同时也让我又都了一次团队开发的经验,虽然我们并没有依照软件工程的思想来做,但是有许多东西却是在一次次的集体讨论和不断尝试中完成的。
还有就是一点点的建议。因为后面的几个项目的确有点难度,老师可以在任务布置后的一个多星期左右,也就是同学们已经摸索过一段时间了之后,可以有针对性给同学们讲解一下。这样的话同学们可能能够学到更多的东西。
最后就是要感谢老师们在这次课程设计中对我们的指导。
参考文献:
[1] 黄廷辉(《操作系统课程设计指导书》,2006 [2] 陈莉君(《深入分析Linux内核源代码》[M](北京:人民邮电出版社,2002