一个基于X86的小型中文操作系统的设计、编码与调试http://www.mcuol.com/Tech/207/1289.htm

一个基于X86的小型中文操作系统的设计、编码与调试

2006-11-22      嵌入式在线      收藏 | 打印

 

     简介:本文通过一个基于X86的小型中文操作系统的建立过程,说明了一个小型操作系统的设计原理,编码过程和实现方法。作为一个开放源代码的系统,本文通过对源代码的剖析,较为详细地说明了内存管理,进程结构等的具体实现,并最后介绍了在开发过程中对产生的系统内核的调试,对操作系统的实践具有一定的指导意义。

 

一 引言

 

操作系统是计算机的软件基础。在进行一些系统编程实验的时候,我们需要对一些系统程序进行试验,并对这些程序进行量化评测。现有的大型操作系统,如WindowsLinux过于复杂,不适合进行试验。并且由于系统本身的开销比较大,对程序的量化评测要扣除系统(如进程调度)的开销,不易把握。因此我们自行设计开发了一个基于X86的小型中文操作系统。本文介绍了在开发中采用的一些方法和技巧,探讨小型操作系统的实作过程。

二 设计

1.       微内核

微内核是内核的一种形式。在微内核中,各个独立的模块被分离出来,作为独立的实体存在(进程)。在系统运行过程中,各个模块的进程独立运行,各进程通过消息通讯机制进行通讯。由于微内核系统具有良好的结构和可移植性,且易于调试,所以在设计中我们采用了微内核作为我们的内核结构。

2.       内存布局/管理

X86提供的保护模式(Protected Mode)中,可以采用分段和分页两种方法来对内存进行管理[1]。我们可以通过对GDT或是LDT的相应的描述符表项来对内存的相应段进行设置,如段边界、段大小、特权级等属性[2]。在一个小型的系统中,可以通过将系统的整个内存空间全部设为一个段,即一个平的内存段,来达到简化系统的目的。这样做的缺点是不能有效利用到保护模式的一些优点,如段数据的保护。在NASM中,我们通过以下的代码对GDT进行段的设置:

_gdt:

     dw 0, 0, 0, 0          ; (0)

    

     ; kernel cs 0x08 (1)

     dw 0x3FFF    ; base: 0, limit: 64M

     dw 0x0000

     dw 0x9A00

     dw 0x00C0

 

     ; kernel ds 0x10 (2)

     dw 0x3FFF    ; base: 0, limit: 64M

     dw 0x0000

     dw 0x9200

     dw 0x00C0

    

     ; user cs 0x1b (3)

     dw 0x3FFF    ; base: 0, limit: 64M

     dw 0x0000

     dw 0xFA00    

     dw 0x00C0

 

     ; user ds 0x23 (4)

     dw 0x3FFF    ; base: 0, limit: 64M

     dw 0x0000

     dw 0xF200

     dw 0x00C0

3.       进程结构

多任务是一个现代操作系统所必须的一部分。在一个任务切换到另一个任务的过程中,我们必须保存现在正在运行的这个进程的上下文,以便在下次调入运行时能够在现在断下的地方继续运行,然后再调入下一个应该运行的进程的上下文,开始下一个进程的运行。在我们的系统中,简单地保存了下面的这些信息:

struct proc_struct

{

     int           pid; // 进程id

     int           parent;

 

     struct proc_struct* next_proc; // 下一个进程

     struct proc_struct* prev_proc; // 上一个进程

     // 进程状态

     long eip;

     long eax;

     long ebx;

     long ecx;

     long edx;

     long esp;

     long ebp;

     long edi;

     long esi;

     long eflags;

     // 段寄存器

     short       cs;

     short       ds;

     short       es;

     short       ss;

     short       fs;

     short       gs;

     // 进程已经运行时间

     int           total_tick;

     unsigned char stack[STACK_NUM]; // 堆栈段

};

其中pid字段记录了这个进程的IDparent记录了父进程的IDnext_proc是指向下一个要运行的进程的上下文的结构指针,以便系统调入下一个进程运行时进行快速定位,prev_proc记录了在这个进程之前运行的进程结构地址。由此可见,在我们的实现在,我们采用了双向链表对进程进行管理,既简单,又对进程的增加、删除、调序带来了方便。对于当前正在运行的进程,系统用struct proc_struct* p_proc这个指针来指向其进程结构。

在进行了以上的设置以后,我们就可以实现简单的进程管理系统了。process_schedule函数在进程双向链表中循环切换,在一个进程运行完一定的时间后,直接载入下一个进程的上下文,然后跳到下一个进程的执行点,进行执行。

int process_schedule(void)

{

     // 进行一些进进程管理

     if(p_proc->next_proc == NULL)

     {

            // 只有这一个进程运行,跳回去继续运行

            jump_to_proc(p_proc);

     }

     p_proc = p_proc->next_proc; // 转到下一个进程

     jump_to_proc(p_proc); // 切换过去   

     return 1;

}

其中,jump_to_proc这个函数在取得下一个进程的上下文后,将上下文装入CPU各寄存器中,然后运行jmp指令跳到进程的执行点中进行执行。

void jump_to_proc(struct proc_struct* p)

{

     // 跳到指定的进程中去运行

     temp_ebp = p->ebp;

     temp_eip = p->eip;

     temp_eax = p->eax;

     temp_ebx = p->ebx;

     temp_ecx = p->ecx;

     temp_edx = p->edx;

     temp_esp = p->esp;

     temp_edi = p->edi;

     temp_esi = p->esi;

     temp_eflags = p->eflags;

     __asm__("movl %0,%%ebp/n/t"                

                   "movl %2,%%ebx/n/t"

                   "movl %3,%%ecx/n/t"

                   "movl %4,%%edx/n/t"

                   "movl %5,%%esp/n/t"

                   "movl %6,%%edi/n/t"

                   "movl %7,%%esi/n/t"

                   "pushl %8/n/t"

                   "popfl/n/t"                    

                   "movl %1,%%eax/n/t"

                   "sti/n/t" 

                   ::"m"(temp_ebp),                 

                   "m"(temp_eax),

                   "m"(temp_ebx),

                   "m"(temp_ecx),

                   "m"(temp_edx),

                   "m"(temp_esp),

                   "m"(temp_edi),

                   "m"(temp_esi),

                   "m"(temp_eflags));              

                   switching = 0;              

     __asm__("jmp *%0/n/t"

                   ::"m"(temp_eip));

     // 此函数不应被返回

}

4.       文件系统

FAT16FAT32EXT2等文件系统中,FAT32由于其实用性和简单性,我们选用它作为实现在文件系统。在硬盘驱动的基础上,我们简单地实现了FAT32文件系统的读功能,这样在后续的开发中,我们可以将各个功能模块作成单独的文件调入执行,增大系统的灵活性。

5.       启动设计

X86系统加电后BIOS执行完自检,将启动设备的第一个扇区读入到物理地址为0x7C00的内存单元中,然后跳到0x7C00去运行。由此可见,操作系统与BIOS的接口就在于启动设备(软驱,硬盘驱动器或是光盘驱动器)的第一个扇区。一般在这第一个扇区完成操作系统的初步载入。但我们在实践中发现,在一个扇区这么小的空间内(512个字节),将整个系统载入比较困难,且如果安装在PC上会与已经存在的操作系统发生冲突。

在这种情况下,我们想到GNUGRUB启动装载器。GRUB能够支持多个操作系统的共存,通常我们将系统内核编译成GRUB启动装载器能够识别的文件格式,GRUB便能将系统加载到指定的内存地址中去,然后跳到我们的系统中去运行。

 

三 编码过程

1.       开发环境

我们尝试了两种开发环境,LinuxWindows。在Linux环境下,程序开发工具丰富好用,但是在对中文的支持上不够。在Windows下,适合操作系统开发的程序开发工具并不是很多,但对中文支持很好,且具有更大的通用性。所以,在进行了一段时间的Linux开发后,我们选用Windows做为开发平台。

2.       C编译器

我们采用GNUgcc编译器进行C程序的开发。GccLinux下著名的C程序编译器,支持内联的汇编语句,比较适合系统程序的开发。且支持多种目标文件的格式,如coffelf等,可以将C源程序方便地编译成多种目标文件。在Windows环境下,默认情况下产生coff文件格式,而在Linux环境下,默认产生elf格式的文件。

3.       汇编器

在众多的汇编器中,我们选用nasm来进行汇编部分代码的编译。这是因为nasm非常适合操作系统的开发。如用其它的汇编器,在从实模式到保护模式的过渡过程中,因为涉及到16位与32位指令的过渡,一般要直接写二进制代码来进行jmp的步骤。而在nasm中,直接用jmp dword就可以轻松做到。可以使用nasm –f coff a.asm来产生coff格式的目标文件,-f开关用于选择要产生的目标文件类型。Nasm也支持elfcoff等多种目标文件格式。

4.       连接器

在用编译器产生C和汇编目标文件后,下一步是用连接器将它们连接成一个内核文件。我们选用GNUld连接器来进行目标文件的连接工作。

5.       二进制文件格式

Windows环境下,coff文件格式是得到编译器和连接器较为广泛支持的一种目标文件格式。在coff目标文件中,C与汇编产生的标号(Symbol)略有不同,在开发中应予以重视。即gcc在对C源程序中的标号进行处理时,在前面添加了一个下划线。如果我们在汇编程序中要引用这个标号,就要注意增加一个下划线。如有一个C程序里面有一个void maind(void)函数,如在汇编中引用,则要这样定义:

extern  _maind

如果编译成elf文件格式,则没有此问题。

 

四 调试过程

1.       使用X86模拟器

作为计算机的基础软件,在编译后的操作系统一定要在真实的机器上运行才能知道程序是否正确。这样,如果要进行操作系统的调试,要么把本机重启,要么找一台专门的机器用于调试。这样做都不是很方便。

我们还有一个选择就是使用X86系统的模拟器。模拟器运行于现有的操作系统之上,其将操作系统内核读入,在其内部摸拟X86系统。这样,我们运用摸拟器就可以直接在开发机上进行操作系统的运行,非常方便快速。

常见的X86模拟器有VMWareBochs等。VMWare是商业软件,有WindowsLinux两种版本,易于使用,但其是为一般用户开发的,功能较少。Bochs是开源的优秀模拟器,免费使用,而且它体积小巧,功能强大。在我们的开发中,使用Bochs进行系统的模拟环境,并借用Bochs进行二进制级别的调试。

2.       使用Bochs进行二进制级别的调试

在操作系统的开发中,调试是一件比较困难的事情。因为在开发的初期无法移植一些知名的调试软件之前,调试一般只能依靠二进制级的工具来进行。前面提到的模拟器Bochs,在除了模拟功能以外,还具有一定的调试功能。在Bochs目录下,BOCHSDBG.EXE即为集成了调试功能的Bochs可执行文件。在Bochs运行后,出现了如下的提示后,就可以输入一些进行诸如断点设置,汇编/反汇编,查看CPU状态等调试命令。

Next at t=0

(0) context not implemented because BX_HAVE_HASH_MAP=0

[0x000ffff0] f000:fff0 (unk. ctxt): jmp f000:e05b

<bochs:1>

下面是用Bochs进行调试的一些常用命令,熟练掌握这些命令,对调试系统是非常有帮助的。

命令

说明

c

继续执行(continue)

step  [count]

单步执行,count为步数,默认为1(execute count instructions)

Ctrl-C

停止运行

quit

退出bochs

vbreak seg:off

虚拟地址断点,seg为段地址,off为偏移,用于在指定的虚拟地址处设置断点(virtual address break)

pbreak addr            

物理地址断点,addr为物理地址。用于在指定的物理地址处设置断点(physical address break)

info break

显示已经设置的断点情况

delete n

删除断点(Delete a breakpoint)

x  /nuf addr

查看在线性地址处的内存单元内容(Examine memory at linear address addr)
其中:n表示要显示的单元数,u表示单元大小,f表示打印格式

xp /nuf addr

查看在物理地址处的内存单元内容(Examine memory at physical address addr)其中nuf的意义同上

setpmem addr datasize val

设置在物理内存地址addr上的内存单元内容。Datasize为要设置的单元大小,val为要设置的值。

info registers

打印出CPU寄存器的当前值。

set $reg = val

将一个寄存器赋值,其中reg可以换为eax等寄存,如set $eax = 0xA

dump_cpu

CPU的整个状态全部打印出来

disassemble start end

反汇编指定地址的程序。Start为开始的线性地址,end为结束的线性地址。

 

参考文献:

1.        IntelIA-32 Intel® Architecture Software Developer’s Manual Volume 3: System Programming GuideIntel Corporation2002

2.        杨季文,80x86汇编语言程序设计教程,清华大学出版社,1998

3.       Peter AbelIBM PC Assembly Language And Programming(Fourth Edition)Prentice Hall1998

4.       W.Richard StevensAdvanced Programming in the UNIX EnvironmentAddison-Wesley1993

5.       M.Morris ManoComputer System Architecture(Third Edition)Prentice Hall1993

6.       Barry B.BreyThe Intel Microprocessors(Fifth Edition)Pearson Education2000

7.       Maurice.BachThe Design of The UNIX Operating SystemPrentice Hall1986

8.       Andrew S.Tanenbaum  Albert S.WooddhullOperating System:Design and Implementation(Second Edition) Prentice Hall1997

本文来源:互联网    作者:华中师范大学城市与环境科学学院 陈斌
评一评 已有 0 位网友对此文发表了看法。  我也来评一下
  •  

我要登录 >> 提示:请用嵌入式在线帐号登录,以方便您与此处网友进行交流。

验证码:  看不清?换一张

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值