BBS水木清华站∶精华区

发信人: raner (lilo), 信区: Linux 
标  题: NACHOS论坛(5) 
发信站: BBS 水木调试站 (Thu Jun  4 16:49:32 1998) 
 
作  家: solmon (所罗门王) on board 'Unix' 
题  目: NACHOS论坛(5) 
来  源:  鼓浪听涛站 
日  期: Thu Mar  6 23:06:26 1997 
出  处: [email protected] 
 
           第五章     Nachos在8086上的实现 
 
    Nachos是一个优秀的操作系统课程设计系统,它对学生理解和学习操作系统的基本 
概念,基本原理和方法很有帮助.但是Nachos的运行环境必须是UNIX的工作站,硬件平台 
为DEC MIPS,SUN SPARC ,HP PA-RISC或Intel386.如果使用其网络部分,还必须将工作 
站联网.要让大量同学在在这样的软硬件环境上完成Nachos课程设计,对国内大部分高 
校来说,是很困难的.我们根据国内大部分高校的实际情况,将Nachos系统移植到广泛使 
用的DOS操作系统上,移植后的Nachos只要Intel386微机和DOS操作系统即可使用,如果 
要使用Nachos的网络部分,只需再加一个DOS的多任务管理器,如DOSSHELL或Windows.这 
些软硬件要求在国内大多数高校中都可以得到满足.甚至学生自己购买的微机上大部分 
也有上述环境.这样移植后的Nachos就可以被广泛地使用了. 
    在Nachos的移植过程中,我们做了以下几个工作: 
        1.在8086上实现Nachos的两个进程切换函数. 
        2.用自定义的数据类型改写Nachos中数据的数据类型,使其能够用DOS的16位 
                编译器编译. 
        3.将一些UNIX库函数移植到DOS. 
        4.Nachos的应用程序解释器原来运行的应用程序是用MIPS机器指令写的,现改 
                为运行8086机器指令的应用程序. 
 
一.进程切换函数的移植. 
 
    Nachos是一个真正的多进程系统,它的多进程不是依靠宿主机操作系统上的多进程 
机制实现的.从宿主机操作系统的角度来看,Nachos只是一个应用程序,只有一个进程在 
运行,但是Nachos内部实际上可以运行多个Nachos自己实现的进程.这些进程可以动� 
地生成和消亡,并且轮流占用处理机运行.由于Nachos的这种设计方法不依赖于UNIX的 
多进程机制,所以在将它移植到DOS环境时,不用在DOS上模拟UNIX的多进程功能,大大简 
化了移植工作. 
    Nachos进程管理系统的移植中,改动很大的有两个函数:SWITCH和ThreadRoot.这两 
个函数都是用来实现进程正文切换的.此类工作与具体的处理机结构及函数调用格式密 
切相关.Nachos虽然也为Intel386实现了这两个函数,但是它实现这两个函数所用的汇 
编语言与DOS下的汇编语言大相径庭,并且是供32位编译器生成的程序调用的,调用格式 
与DOS的16位编译器不一致,所以原有实现无法在DOS下使用,我们重新编写了这两个函数. 
    SWITCH函数是用来在进程切换时保护老进程图象和恢复新进程图象的.进程图象包 
括处理机寄存器状态和堆栈.它有两个参数:Thread* oldThread;Thread* newThread, 
分别指向老进程和新进程的对象.改写过的SWITCH函数与原来的有很大不同.首先,新的 
SWITCH函数是用C++语言中嵌入汇编语言(即行内汇编)编写的,而老的SWITCH全部由汇 
编语言编写.之所以用行内汇编,是由于在进程切换时,需要把CPU各寄存器的值保存到 
老进程对象的machineState数据成员中,而DOS下的编译器如Borland C++,MicroSoft 
C++等,对对象中数据成员的内存地址没有明确的规定,如果使反汇编或其它方法强行找 
出数据成员的位置,会带来许多问题.例如不同的编译器放置数据成员的位置并不一样, 
当系统的移植到其它编译器时,要重新找出这个位置,很不方便.即使只使用一种编译 
器,可以确定数据成员的位置,但是在稍稍改动进程对象的结构后,这个位置会不会改变 
呢?没有人能作出肯定的回答.要保证这个位置不发生变化,就必须在每次改动后重新查 
看这个数据成员的内存位置,可以想象,这将给Nachos的用户--学生带来多大的麻烦.我 
们使用行内汇编,直接用C++语言存取对象的数据成员,这个问题就迎刃而解了. 
    使用行内汇编也有其缺点,那就是程序员不能完全控制寄存器,一些C++语句,特别 
是有关存取数组的语句会使用和改变寄存器的内容.我们解决这个问题的方法是,在寄 
存器的值未被修改时将其压入堆栈,要用时再从堆栈中弹出,这样就万无一失了. 
    SWITCH函数的执行过程为: 
        1.将SP,SS,AX,BX,CX,DX,SI,DI,BP寄存器的值顺序压入堆栈.因为在压栈的过 
                程中,栈顶寄存器SP会改变,所以首先将SP压入堆栈. 
        2.C语言调用函数的压栈规则是:先将参数从右向左压入堆栈,再把函数的返回 
                地址压入堆栈.根据这个规则,可以从栈上找到SWITCH函数的返回地 
                址,取出这个地址放入一个临时变量中保存. 
        3.从栈中分别弹出先前保存的各寄存器值,放入老进程的machineState中,以 
                达到保护现场的目的. 
        4.将SWITCH函数的返回地址也保存到老进程中. 
        5.将newThread的machineState中保存的栈顶寄存器SP和栈段寄存器SS读入DX, 
                CX寄存器,供下面切换堆栈时使用. 
        6.将newThread的值读入AX,BX寄存器(高16位放入AX,低16位放入BX).这是因 
                为newThread是SWITCH函数的参数,它是放在堆栈上的,切换堆栈后就 
                找不到它了,所以先把它读入寄存器中. 
        7.将CX的值赋给栈段寄存器SS,DX的值赋给栈顶寄存器SP,达到切换堆栈的目 
                的.现在进程在新的堆栈上运行了. 
        8.用AX,BX寄存器的值重新装配newThread指针. 
        9.将新进程保存的SWITCH函数的返回地址放入新堆栈.这样SWITCH函数结束返 
                回后,会从新进程的断点继续运行下去. 
        10.从newThread的machineState中取出以前保存的BP,DI,SI,DX,CX,BX,AX寄 
                存器的值,并把它们压入堆栈. 
        11.从堆栈中分别弹出第10步压入的值,并放入相应的寄存器.不直接在第10步 
                中将newThread中保存的值放入寄存器的原因与做第1步和第3部的原 
                因一样,都是因为存取进程对象中保存的寄存器内容时,要使用一些 
                通用寄存器,所以先得把寄存器的内容压入堆栈保存. 
    我们在移植进程切换的另一个函数ThreadRoot时,对它做了一些简化.ThreadRoot 
代表进程运行的全过程,既包括用户定义的进程函数,又包括进程的初始化函数和进程 
的结束函数.ThreadRoot有四个参数,它们是: 
        InitialPC,InitialArg,用户定义的工作函数及其参数; 
        StartupPC,进程的初始化函数; 
        WhenDonePC进程的结束函数. 
    经过分析,我们发现在Nachos中只需有一个初始化函数和一个结束函数,所以没有 
必要通过参数传入这两个函数,我们直接在ThreadRoot函数中调用这两个函数. 
    ThreadRoot的另两个参数并不是由堆栈传入的,而是放在AX,BX,CX,DX四个寄存器 
中传入的(这两个参数都是32位数,AX等通用寄存器只有16位,所以用4个寄存器传2个参 
数).ThreadRoot的执行过程如下: 
        1.将AX,BX,CX,DX寄存器的值压入堆栈. 
        2.运行进程初始化函数. 
        3.从堆栈中弹出寄存器的值,装配成两个参数. 
        4.运行第3步装配的进程工作函数. 
        5.运行进程结束函数. 
 
    ThreadRoot函数的特殊的参数传入方法,将要求其它程序在调用它时要做一些额外 
的工作,这不是太多余了吗?事实上,系统中没有一个地方显式地调用ThreadRoot函数, 
而且ThreadRoot函数根本就不能结束返回(这一点在进程管理一章中已作了详细介绍). 
那么ThreadRoot函数有什么用处呢?从前面的介绍中我们知道在进程切换时SWITCH函数 
会返回到新进程以前的断点处,但是新进程首次被调度上CPU运行时,并没有以前的断 
点,那么它将返回到哪里呢?为了解决这个问题,我们在生成一个新进程的时候,将 
ThreadRoot函数的地址放入新进程堆栈中放返回地址的位置,并且在存放AX,BX,CX,DX 
寄存器值的machineState数组中放入ThreadRoot需要的两个参数.这样,当新进程首次 
占用处理机时,就可以运行ThreatRoot函数了. 
 
  二.在16位编译器上编译Nachos. 
    Nachos原来使用GNU C++编译器编译的,它是一个32位的编译器,而DOS下常用的编 
译器,如MicroSoft C++,Borland C++都是16位编译器.32位编译器与16位编译器的主要 
不同在于整型(int)的长度不一样,32位编译器中整型长度为32位,占4个字节.而16位编 
译器中整型长度为16位,占2个字节.Nachos中大量整型数据的值不能用2个字节存放.例 
如在Nachos代码中,向一个函数传递未知类型的参数时,都将参数定义为整型,然后在函 
数内部再根据具体需要,对参数进行类型转换.这些参数往往被转换为指向一个对象的 
内存指针,内存指针有32位,需要用4个字节表示,这就要求整型必须有4个字节.为了在 
DOS下正确地编译Nachos代码,我们不得不手工改写这些整型数据的类型.此外,我们还 
考虑到将来各方面条件改善了,Nachos又会用32位编译器重新编译,以便移植到更高级 
的硬件上.尽管现有的Nachos已经是32位的,似乎不需要再把Nachos for DOS移植到32 
位,但是我们今后大量工作都是在Nachos for DOS上进行的,Nachos for DOS将比现有 
的Nachos更加完善,如果我们不想放弃这些宝贵的工作成果, 就只有把Nachos for DOS 
移植到32位或更高的系统上.那么如何使Nachos的数据类型转换工作同时满足上述两方 
面的要求呢?我们发现Windows的API(应用程序接口)也遇到与我们同样的问题,即需要 
尽可能方便地移植到不同的编译器上,Windows的API在设计的时候就注意到这个问题, 
所以它没有使用常规的C语言的基本数据类型来定义数据,而是自己定义了一套数据类 
型,然后用C语言的define和typedef两种语句将自定义的数据类型对应到C语言的数据 
类型.这样,如果C语言的数据类型因编译器的不同而改变时,只要改动少量的define和 
typedef就可以保持不变.这种方法非常方便灵活.我们借鉴了Windows API的方法,定义 
了下列几种数据类型: 
        1.BYTE ,占1个字节. 
        2.WORD ,占2个字节. 
        3.DWORD,占4个字节. 
    因为C语言标准库函数的参数和返回值的类型为C语言的标准数据类型,所以我们定 
义了INT和CHAR两种类型,分别与C语言中的int和char相兼容. 
 
    这些自定义的数据类型放在PUBLIC.H中,PUBLIC.H被Nachos原有的一个头文件 
COPYRIGH.H包含,因为COPYRIGH.H被每一个Nachos的C++文件包含,所以PUBLIC.H也就被 
全部C++文件包含了.我们在通读Nachos源代码的基础上,将Nachos原有的char,int类型 
的数据根据其所需空间的大小和用途,改为自定义的类型.在改写的过程中,我们尽可能 
用DWORD来改写int 类型的变量,给将来扩展系统带来方便. 
 
  三.UNIX库向DOS的移植. 
    DOS中没有的UNIX库函数可分为两类,第一类可以用其它库函数直接来实现相同的功 
能.如bcopy可以用memcpy来代替,移植时只要重新编写这些函数即可. 
 
    另一类无法直接移植到DOS下,它们主要集中在网络部分.Nachos使用UNIX的套接口 
(socket)机制来实现不同工作站上的Nachos系统之间的网络通讯.DOS中没有类似功能, 
而且国内部分高校尚未实现联网,考虑到这些实际情况,我们决定在单机上用一个文件 
来模拟网络.具体的方法是在Windows下启动两个DOS仿真程序(运行Windows 的Program 
Manager中的DOS PROMPT程序),这两个DOS仿真程序分别运行一个Nachos,这两个Nachos 
程序一个把报文写入报文文件,另一个把报文从报文文件中读出.这样就可以模拟网络 
的信息交换功能了.这种方法不仅适用于模拟单机上,两个Nachos程序之间的网络通讯, 
还可以扩展到多个Nachos程序之间的网络通讯.只要内存足够大,Windows就可以运行多 
个DOS仿真程序,每个DOS仿真程序上运行一个Nachos,每个Nachos系统都有一个标志号. 
网络报文中含有发送方和接收方的标志号,Nachos程序只接收发给自己的报文.这样就 
可以模拟一个多节点的网络了.此外,如果您是在一个真正的网络上工作,也可以把报文 
文件放在网络的文件服务器上,让工作组的成员可以在不同的客户机上存取这个文件, 
这样就实现了不同客户机上运行的Nacho之间模拟网络通讯. 
 
    网络模拟的具体实现为:在Nachos开始运行时,用共享方式打开报文文件,其共享属 
性设为SH_DENYNO,它允许在报文文件打开时,其它程序对它进行读写操作.在Nachos运 
行期间报文文件一直处于打开状态.Nachos结束时,才关闭报文文件.我们知道,如果有 
两个程序同时对报文文件进行读写会引起读写的混乱.为了避免这个问题,在单机运行 
Nachos时先运行DOS提供的SHARE.EXE程序,它可以控制文件的读写,避免多个程序之间 
的读写冲突.在网络运行Nachos时,文件服务器会提供文件读写互斥的服务.所以无论在 
单机上,还是在网络上实现网络模拟时,都不会出现读写文件混乱的情况. 
    用户通过模拟网络发送报文的过程为: 
        1.保存报文文件的当前文件读写指针位置 
        2.将报文文件的当前文件读写指针移到文件尾. 
        3.把报文写入报文文件. 
        4.把报文文件的当前文件读写指针移回原先的位置. 
    用户查看报文文件中是否有发给自己的报文的过程为: 
 
                  ↓ 
            ━━━━━━ 
             报文已收到       Y 
              标志为真     ━━━━┓ 
            ━━━━━━           ┃ 
                 ┃                ↓ 
  ┏━━━━━→ ┃ N      ┏━━━━━━━━┓ 
  ┃             ↓        ┃   返回TRUE     ┃ 
  ┃        ━━━━━━   ┗━━━━━━━━┛ 
  ┃        已读到报文          Y 
  ┃         文件尾部       ━━━━┓ 
  ┃        ━━━━━━            ┃ 
  ┃             ┃                 ↓ 
  ┃             ┃N        ┏━━━━━━━━┓ 
  ┃             ↓         ┃   返回FALSE    ┃ 
  ┃    ┏━━━━━━━━┓┗━━━━━━━━┛ 
  ┃    ┃  把一条报文读  ┃ 
  ┃    ┃  入报文缓冲区  ┃ 
  ┃    ┗━━━━━━━━┛ 
  ┃             ┃ 
  ┃             ┃ 
  ┃             ↓ 
  ┃       ━━━━━━━ 
  ┃ N      此报文的接收 
  ┗━━     方为本节点 
           ━━━━━━━ 
                 ┃ 
                 ┃Y 
                 ↓ 
       ┏━━━━━━━━━┓ 
       ┃ 置报文已收到标志 ┃ 
       ┗━━━━━━━━━┛ 
                 ┃ 
                 ┃ 
                 ↓ 
        ┏━━━━━━━━┓ 
        ┃   返回TRUE     ┃ 
        ┗━━━━━━━━┛ 
 
    用户读取报文的过程为: 
        1.调用报文查看函数,如果它的返回值为假,则返回. 
        2.将报文从报文缓冲区中拷贝到调用者提供的内存区. 
        3.清报文已收到标志. 
 
四.Nachos应用程序解释器的移植. 
 
    大多数模拟操作系统的应用程序使用的机器指令是模拟操作系统自己设计的,不是 
宿主机的机器指令.这就要求应用程序必须用模拟系统特有的汇编语言编写,为了避免 
给学生布置太多的工作,这些应用程序都是很简单,很短小的,很难用来充分测试操作系 
统的功能.Nachos突破了这一限制,它的应用程序可以用C或C++语言编写,通过编译生成 
宿主机的机器指令,Nachos能够解释执行这些机器指令,这样就大大减轻了编写应用程 
序的工作强度. 
    Nachos原有的编译器是GNU C++,它生成的是MIPS的机器指令,但是同学们对MIPS的 
机器指令不太熟悉,在跟踪应用程序执行过程中会遇到困难,另一方面,DOS环境下也不 
能运行GNU C++编译器,所以我们决定用Borland C++ 或MicroSoft C++编译应用程序. 
这样就有三项移植工作要做: 
        1.DOS编译器生成的是8086的机器指令,需要把原来的MIPS指令解码和解释执 
                行部分改写为8086指令的解码和解释执行. 
        2.将DOS编译器生成的可执行文件中的调用DOS系统功能的指令去掉.因为应用 
                程序是在Nachos操作系统上运行的,不应调用DOS系统功能. 
        3.将DOS编译器生成的可执行文件,按Nachos要求的格式进行改写,并移到 
                Nachos的文件系统中去. 
    对8086机器指令的分析执行分为两步,第一步读出机器指令进行解码,将解码结果 
放入指令信息数据结构中,供下一步使用. 
 
    8086中可供程序员使用的有14个16位寄存器,它们是8个通用寄存器AX,BX,CX,DX, 
SP,BP,SI,DI,其中AX,BX,CX,DX的高8位和低8位还可以分别作为一个寄存器使用.例如 
AX就可以分为AH和AL.指令寄存器IP指向指令地址的段内地址偏移量.标志寄存器FR中 
有6个状态位,3个控制位.状态位分别为进位标志,奇偶标志,辅助进位标志,零标志,符 
号标志,溢出标志.这些标志反映算术或逻辑运算结果的某些性质.控制位有:方向标志, 
中断允许标志,跟踪标志.它们用来控制程序的运行.8086还有4个段寄存器,分别指向各 
段的基址.它们是:代码段寄存器CS,堆栈段寄存器SS,数据段寄存器DS,附加段寄存器ES. 
8086的寻址方式比较复杂,它分为立即寻址,寄存器寻址,存储器寻址,串操作寻址,外设 
I/O寻址和程序转移操作寻址.其中变化最大的是存储器寻址.这种寻址方式又可分为直 
接寻址,寄存器间接寻址,基址寻址,变址寻址,以及基址变址寻址.根据寻址方式计算而 
得的地址只是段内偏移地址,还需与所在段的段基址组合才是20位的物理地址. 
    8086采用变字节长指令,由1到6个字节组成,其格式如下: 
 
      第一字节         第二字节         第三到六字节 
 ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━┓ 
 ┃ 7 6 5 4 3 2 1 0┃7 6 5 4 3 2 1 0 ┃              ┃ 
 ┗━━━━━━━━┻━━━━━━━━┻━━━━━━━┛ 
      操作码    D W  MOD  REG   R/M   地址位移量或立即数 
 
    第一字节的高6位为操作码,指出指令的操作类型.D为方向位,当D=0时,表示REG字 
段指出的寄存器为源操作数寄存器;当D=1时,表示REG字段指出的寄存器为目的操作数 
寄存器,用来存放运算结果.W为操作位,当W=0时,表示参加运算的操作数为字节操作数; 
当W=1时,为字操作数. 
 
    第二字节指出所用操作数存放在何处(存储器还是寄存器),以及存储器操作数有效 
地址的计算方法.7,6两位为MOD(方式)字段,指出是存储器操作数还是寄存器操作数; 
5~3位为REG(寄存器)字段,指出寄存器的编号;2~0位为R/M(寄存器/存储器)字段,受MOD 
字段控制,指出第二个操作数所在的寄存器的编号(当MOD=11时),或指出存储器操作数 
的有效地址的计算方法. 
    第三到六字节根据指令的不同而决定取舍,一般由其指出存储器操作数地址的位移 
量或立即数. 
    第二字节中MOD字段的编码表如下: 
    ┏━━━━┳━━━━━━━━━━━━━━━┓ 
    ┃   MOD  ┃          方    式            ┃ 
    ┣━━━━╋━━━━━━━━━━━━━━━┫ 
    ┃   00   ┃    存储器方式,无位移量       ┃ 
    ┃   01   ┃    存储器方式,有8位位移量    ┃ 
    ┃   10   ┃    存储器方式,有16位位移量   ┃ 
    ┃   11   ┃    寄存器方式,无位移量       ┃ 
    ┗━━━━┻━━━━━━━━━━━━━━━┛ 
 
    REG字段的编码表如下: 
    ┏━━━┳━━━━━━━━┳━━━━━━━━┓ 
    ┃ REG  ┃  W=0(字节操作) ┃  W=1(字操作)   ┃ 
    ┣━━━╋━━━━━━━━╋━━━━━━━━┫ 
    ┃ 000  ┃      AL        ┃       AX       ┃ 
    ┃ 001  ┃      CL        ┃       CX       ┃ 
    ┃ 010  ┃      DL        ┃       DX       ┃ 
    ┃ 011  ┃      BL        ┃       BX       ┃ 
    ┃ 100  ┃      AH        ┃       SP       ┃ 
    ┃ 101  ┃      CH        ┃       BP       ┃ 
    ┃ 110  ┃      DH        ┃       SI       ┃ 
    ┃ 111  ┃      BH        ┃       DI       ┃ 
    ┗━━━┻━━━━━━━━┻━━━━━━━━┛ 
 
    解码过程分析二进制的机器指令,将指令所占字节数,操作码类型,源目操作数的寻 
址方式放入指令信息结构中.第二步根据指令信息结构的内容,从仿真8086机器的寄存 
器或内存中取出操作数,根据操作码指明的功能运算后,将结果放回仿真机器中. 
 
    8086的汇编级指令有106条(以助记符计),可分为数据传送指令.算术指令,位处理 
指令,字符串指令,程序转移指令以及处理机控制指令等6大类.其中有5条指令不能模 
拟.一类是对外设端口进行读写的指令IN,OUT.这是因为仿真机上并没有模拟外设端口. 
另一类是中断指令INT,INTO,IRET,中断指令是用来调用DOS提供的系统功能的,所以 
Nachos应用程序不应使用这些指令. 
 
    在应用程序中出现I/O端口指令和中断指令的原因有两个,一是用户调用了一些包 
含这些指令的C语言库函数.所以在编写应用程序时,只能使用一部分C语言库函数,如 
strcpy,需要调用操作系统功能时,必须使用我们提供的库函数,而不能用C语言库函数, 
例如,打开文件时,用我们提供的Open函数,而不能用标准库中的open函数. 
 
    在应用程序中出现I/O端口指令和中断指令的另一个原因是编译器链接的时候会在 
可执行文件中加入一些初始化程序以帮助应用程序装入内存.这些程序大量调用了DOS 
中断,我们移植的时候要去掉这些程序.编译器的文档写明,这些初始化程序放在C0S.OBJ 
文件里,编译时被链入可执行程序,于是我们重新改写了C0S程序,去掉了所有的DOS中断 
调用. 
 
    Nachos不能直接运行编译器生成的可执行程序.必须将可执行程序的头部转换成 
Nachos要求的格式.然后把它拷入Nachos的文件系统.这样Nachos才能运行这些程序. 
Nachos可执行文件的头部包含的信息有程序中有几个数据段,代码段和栈段,每段的长 
度及在文件中的位置,指令指针和代码段寄存器的初值.这些信息都可以在编译器生成 
的可执行文件中找到. 
 
 
-- 
m※ 来源:.鼓浪听涛站 bbs.xmu.edu.cn.[FROM: [email protected]] m 
 
-- 
※ 来源:·BBS 水木调试站 Leeward.lib.tsinghua.edu.cn·[FROM: 166.111.68.98] 

BBS水木清华站∶精华区