首页
Web开发
Windows程序
编程语言
数据库
移动开发
系统相关
微信
其他好文
会员
首页
>
其他好文
> 详细
实现操作系统TOS lab1
时间:
2016-06-12 03:15:11
阅读:
581
评论:
0
收藏:
0
[点我收藏+]
标签:
实验目的:
操作系统是一个软件,也需要通过某种机制加载并运行它。
在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作,为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统TOS来做准备。
lab1提供了一个非常小的boot loader和 TOS ,真个bootloader执行代码小于512字节(一个扇区),这样才能放到硬盘的主引导扇区中。
通过分析和实现这个boot loader和TOS ,可以了解到:
计算机原理
cpu的编码地址与寻址:基于分段机制的内存管理
cpu的中断机制
外设:串口/并口/CGA,时钟,硬盘
Bootloader软件
编译运行boot loader的过程
调式boot loader的方法
PC启动boot loader的过程
ELF执行文件的格式和加载
外设访问:读硬盘,在CGA上显示字符串
TOS软件
编译运行TOS 的过程
TOS 的启动过程
调式TOS的方法
函数调用关系:在汇编级别了解函数调用栈的结构和处理过程
中断管理:与软件相关的中断处理
外设管理:时钟
实验内容:
说明
lab1中包含了一个boot loader和一个OS kernel。
这个boot loader可以切换到x86保护模式,能够读取磁盘并加载ELF执行文件格式,并显示字符。
而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别 OS。
练习
为了实现lab1的目标,lab1提供了6个基本练习和一个扩展练习,要求完成实验报告。
对实验报告的要求:
基于markdown格式来完成,以文本方式为主。
填写各个基本练习中要求完成的报告内容
完成实验后,请分析tos_lab中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别;
列出你认为本实验中的知识点,以及与对应的原理知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点);
列出你认为OS 原理中很重要,但在实验中没有对应上的知识点;
练习1:理解通过make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
在此练习中,需要通过静态分析代码来了解:
操作系统镜像tos.img是如何一步步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果);
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
补充材料:
如何调式makefile;
当执行make时,一般只会显示输出,不会显示make到底执行了哪些命令。
如想理解make执行了哪些命令,可以执行 make V=
项目组成:
lab1的整体目录结构如下所示
其中一些比较重要的文件说明如下:
bootloader部分
boot/bootasm.S :定义并实现了boot loader最先执行的函数start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用bootmain.c中的bootmain函数。
boot/bootmain.c :定义并实现了bootmain函数,实现了通过屏幕、串口和并扣显示字符串。bootmain函数加载tos kernel操作系统到内存,然后长跳转到tos的入口处执行。
boot/asm.h :是bootmain.S 汇编文件所需要的头文件,主要是一些与x86保护模式的段访问相关的宏定义。
tos操作系统部分
系统初始化部分:
kern/init/init.c :tos的初始化 启动代码。
内存管理部分:
kern/mm/mmu.h : tos操作系统有关x86 MMU 等硬件相关的定义,包括EFLAGS寄存器中各bit的含义,应用/系统段类型,中断门描述符定义,段描述符定义,任务状态段定义,NULL段声明的宏SEG_NULL,特定段声明的宏SEG,设置中断们描述符的宏SETGATE
kern/mm/pmm.[ch] : 设定了tos 在段机制中要用到的全局变量:
任务状态段ts;
全局描述表gdt[ ];
加载全局描述符寄存器的函数lgdt;
临时的内核栈stack0;
对全局描述符表和任务状态段的初始化函数gdt_init
kern/driver/intr.[ch]: 实现了 通过设置CPU的flags来屏蔽和使能中断的函数;
kern/driver/picirq.[ch] :实现了对中断控制器8259A的初始化和使能操作;
kern/driver/clock.[ch] : 实现了对时钟控制器8253的初始化操作;
kern/driver/console.[ch] :实现了对串口和键盘的中断方式的处理操作;
中断处理部分:
kern/trap/vectors.S : 包括256各中断服务例程的入口地址和第一步初步处理实现。注意,此文件是由tools/verctor.c在编译tos期间动态生成的;
kern/trap/trapentry.S :紧接着第一步处理后,进一步完成第二步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
kern/trap/trap.[ch] :紧接着第二步初步处理后,继续完成具体的各种中断处理操作;
内核调试部分:
kern/debug/kdebug.[ch] :提供源码和二进制对应关系的查询功能,用于显示调用栈关系。其中补全print_stackframe函数是需要完成的练习。
kern/debug/kmonitor.[ch] :实现提供动态分析命令kernel monitor, 便于在tos出现bug或问题后,能够进入kernel monitor中,查看当前调用关系。
kern/debug/panic .c | assert.h : 提供了panic函数和 assert宏,便于在发现错误后,调用kernel monitor。 可以在编程实验中充分利用assert 宏和 panic函数,提高查找错误的效率。
公共库部分:
libs/defs.h :包含一些无符号整型的缩写定义。
libs/x86.h : 一些用GNU C 嵌入式汇编实现的C函数(由于使用了inline 关键字,所以可以理解为宏)。
工具部分:
Makefile 和tools/function.mk :指导make 完成整个软件项目的编译,清除工作;
tools/sign.c :一个C 语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区;
tools/vector.c :生成vector.S,此文件包含了中断向量处理的统一实现。
编译方法
首先下载lab1_source_code, 在lab1目录下执行make,可以生成tos.img(生成于bin目录下)。
tos.img是一个包含了boot loader或os的硬盘镜像,通过执行如下命令可在硬件虚拟环境qemu中运行boot loader或os : make qemu
从机器启动到操作系统运行的过程
BIOS 启动过程
当计算机加电后,一般不直接执行操作系统,而是执行系统初始化软件完成基本IO初始化和引导加载功能。简单地说,系统初始化软件就是在操作系统内核运行之前运行的一段小软件。通过这段小软件,我们可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以方便为最终调用OS kernel准备好正确的环境。最终引导加载程序把OS kernel 映像加载到RAM中,并将系统控制权传递给它。
对于大多数计算机系统而言,os 和 application 软件是存放在磁盘(硬盘)、ROM等可在断电后继续保存数据的存储介质上。计算机启动后,CPU一开始会到一个特定的地址开始执行指令,这个特定的地址存放了系统初始化软件,负责完成计算机基本的IO初始化,这是系统加电后运行的第一段软件代码。对于Inter 80386的体系结构而言,PC机的系统初始化软件由BIOS (Basic Input Output System,即基本输入/输出系统,其本质是一个固化在主板Flash/CMOS上的软件)和位于硬盘引导扇区中的OS Boot Loader (在tos中的bootasm.S 和bootmain.c)一起组成。BIOS实际上是被固化在计算机ROM(只读存储器)芯片上的一个特殊的软件,为上层软件提供最底层的、最直接的硬件控制与支持。更形象的说,BIOS就是PC计算机硬件与上层软件程序之间的一个“桥梁”,负责访问和控制硬件。
以Intel 80836 体系结构为例,计算机加电后,CPU从屋里地址0xffffFFF0(由初始化的CS:EIP 确定,此时CS和EIP的值分别是 0XF000和0xFFF0) )开始执行。在0xffffFFF0这里只是存放了一条跳转指令,通过跳转指令跳转到BIOS例行程序起始点。 BIOS 做完计算机硬件自检和初始化后,会选择一个启动设备(硬盘),并且读取该设备的第一扇区(主引导扇区或 启动扇区) 到内存一个特定的地址 0x7c00 处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了tos的bootloader。
boot loader启动过程:
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。bootloader完成的工作包括:
把CPU从实模式切换到保护模式;并启动分段机制;
读取硬盘中ELF执行文件格式的tos 操作系统到内存;
显示字符串信息;
把控制权交给tos kernel。
对应其工作的实现文件在boot目录下的三个文件asm.h、bootasm.S和bootmain.c。
下面从OS原理上介绍完成上述工作的计算机系统硬件和软件背景知识。
保护模式和分段机制
为什么要了解Intel 80386的保护模式和分段机制?首先,我们知道Intel 80836只有在进入保护模式后,才能充分发挥其强大的功能,提供更好的保护机制和更大的寻址空间,否则仅仅是一个快速的8086而已。没有一定的保护机制,任何一个application 软件都可以任意访问所有的计算机资源,这样也就无从谈起操作系统设计了。
并且 Intel 80836的分段机制一直存在,无法屏蔽或者避免。
其次,在我们的boot loader设计中,涉及到了从实模式切到保护模式的处理,我们的操作系统功能(比如分页机制)是建立在Intel 80836的保护模式上来设计的。
如果不了解保护模式和分段机制,则面向Intel 80386体系结构的操作系统设计实际上是建立在一个空中楼阁之上的。
waring: 虽然学习过x86汇编,对x86硬件架构有一定了解,但对x86保护模式和x86系统编程可能了解不够。可以参考资料(Intel 手册)第4,6,9,10章。
实模式:
在boot loader接手BIOS的工作后,当前的PC系统处于实模式(16bit模式,最大寻址能力16bit)运行状态,在这种状态下软件可访问的物理内存空间的上限为1MB,且无法发挥Intel 80386以上级别的32bit CPU的4GB内存管理能力。
实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。通过修改A20地址线可以完成从实模式到保护模式的转换。
保护模式
只有在保护模式下,80386的全部32根地址线有效,可以寻址高达4GB字节的线性地址空间和物理地址空间,可以访问64TB(有2^14 个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。
这不仅为存储共享和保护提供了硬件支持,而且为实现虚拟存储提供了硬件支持。
通过提供4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码数据的安全以及任务的隔离。
waring:保护模式下,有两个段表: GDT (Global Descriptor Table ) 和LDT (Local Descriptor Table),每一张段表 可以包含8192(2^13)个描述符,因而最多可以同时存在2 * 2^13 = 2 ^14个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。目前所谓的64TB(2^(14+32) = 2 ^46)逻辑地址空间是一个理论值,没有实际意义。在32bit保护模式下,真正的物理空间仍然只有2^32字节那么大。TOS中只用了GDT,并没有用LDT。 (《Intel64 and IA-32 Architecture Software Developer’s Manual》)。
分段存储管理机制
只有在保护模式下才能使用分段存储管理机制。分段机制将内存划分成以起始地址和长度限制这两个二维参数表示的内存块,这些内存块就称之为 段(Segment)。编译器compile 把源程序 编译 成执行程序时用到的 代码段、数据段、堆和栈等概念在这里可以与段俩系起来,二者在含义上是一致的。
分段机制涉及4个关键内容:
逻辑地址
段描述符(描述段的属性)
段描述符表(包含多个段描述符的“数组”)
段选择子(段寄存器,用于定位段描述符表中表项的索引)
转换逻辑地址(Logical Address,application 层程序员看到的地址)到物理地址(Physical Address ,实际的物理内存地址)分下面两步:
分段地址转换:CPU把逻辑地址(由段选择子selector 和段偏移offset组成)中的短选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址+段偏移(base+offset)值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址==物理地址。
分页地址转换,这一步中把线性地址转换为物理地址。(这一步是optional,由OS决定是否需要)。
上述转换过程对于 application层程序员来说是不可见的。线性地址空间由一维的线性地址构成,线性地址空间和物理地址空间对等。线性地址32bit长,线性地址空间容量为4GB字节。分段地址转换的基本过程如下图所示
分段存储管理机制需要在启动保护模式的前提下建立。从上图可以看出,为了使得分段存储管理机制正常运行,需要建立好段描述符和段描述符表(参看 bootasm.S 、mmu.h、pmm.c).
段描述符
在分段存储管理机制的保护模式下,每个段由如下三个擦拿书进行定义: kern/mm/mmu.h 中的 struct segdesc数据结构中有具体的定义。
段基地址(Base Address):规定线性地址空间中段的起始地址。在80836保护模式下,段基地址长32bit.因为基地址长度与寻址地址的长度相同,所以任何一个段都可以从32bit线性地址空间中的任何一个字节开始,而不像 实模式 方式下规定的 边界必须被16bit整除。
段界限:规定段的大小。在80836保护模式下,段界限用20bit表示,而且段界限可以是以字节为单位或以4k字节为单位。
段属性: 确定段的各种性质。
段属性中的粒度位(Granularity),用符号G 标记。G =0表示段界限以字节位位单位,20位的界限可表示的范围是1byte ——1MB,增量为1byte; G =1表示段界限以4kb 为单位,于是20bit的界限可表示的范围是4kb—4GB,增量为4kb;
类型 (Type): 用于区别不同类型的描述符。可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等。
描述符特权级(Descriptor Privilege Level) (GPL) :用来实现保护机制。
段存在位(Segment-Present bit):如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。(os可以任意的使用被标识为可用(AVAILAVLE)的位)。
已访问位(Accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位。OS可以清除该bit。
上述的参数通过段描述符来表示,结构如下图所示
全局描述符表:所示一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48bit,其中高32bit为base address 基地址,低16bit为段界限。由于GDT不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段。注意,全部描述表中第一个段描述段设定为空段描述符。GDTR中的段界限以byte为单位。对于含有N个描述符的描述符表的段界限通常可以设置8*N-1. 在TOS中boot/bootasm.S中的gdt地址处和kern/mm/pmm.c中的全局变量数组gdt[ ]分别有基于汇编语言和C语言的全局描述符表的具体实现。
选择子
线性地址部分的选择子是用来选择哪个描述符表和在该表中索引一个描述符的。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。
选择子的格式如下图所示
段选择子结构:
索引(Index): 在描述表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基地址来索引描述符表,从而选出一个合适的描述符。
表指示位(Table Indicator ,TI):选择应该访问哪一个描述符表。 0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。
请求特权级别(Requested Privilege Level,RPL) :保护机制。
全局描述符表的第一项是不能被CPU使用,所以当一个段选择子的索引(Index)部分和表指示位(Table Indicator)都为0的时候(即段选择子指向全局描述符表的第一项时),可以当做一个空的选择子(见 mmu.h中的SEG_NULL)。当一个段寄存器被加载一个空选择子时,处理器并不会产生一个异常。但是,当用一个空选择子去访问内存时,则会产生异常。
保护模式下的特权级
在保护模式下,特权级总共有4个,编号从0(最高权)到3(最低权)。有3种主要的资源受到保护:内存,I/O端口以及执行特殊机器指令的能力。在任何一时刻,x86 cpu都是在一个特定的特权级下运行的,从而决定了代码可以做什么,不可以做什么。 这些特权级经常被称为 保护环(protection ring),最内的环(ring 0)对应最高特权 0,最外面的环(ring 3)一般给application 程序使用,对应最低特权3.在linux中,cpu只用到了其中的2个特权级:0(内核态) 和3 (用户态)。
有大约15条机器指令被CPU限制只能在内核态执行,这些机器指令如果被用户模式的程序所使用,就会颠覆保护模式的保护机制并引起混乱,所以它们被保留给OS kernel使用。 如果企图在r0 以外运行这些指令,就会导致一个一般保护异常(general-protection exception)。对内存和I/O端口的访问也受类似的特权级限制。
数据段选择子的整个内容可以由程序直接加载到各个段寄存器(如SS或DS等)当中。这些内容里包含了请求特权级别(RPL)字段。然而,代码段寄存器(CS)的内容不能由装载指令(如MOV)直接设置,而只能被哪些会改变程序执行顺序的指令(JMP、INT、CALL)间接地设置。而且CS拥有一个由CPU维护的当前特权级字段(Current Privilege Level, CPL)。二者结构如下图所示:
代码段寄存器中的CPL字段(2bit)的值总是等于CPU的当前特权级,所以只要看一眼CS中的CPL,你就可以知道此刻的特权级了。
CPU会在两个关键点上保护内容:当一个段选择符被加载时,以及,当通过线性地址访问一个内存页时。因此,保护也反映在内存地址转换的过程之中,既包括分段又包括分页。当一个数据段选择符被加载时,就会发生下述的检测过程:
因为越高的数值代表越低的特权,上图中的MAX()用于选择CPL和RPL中特权最低的一个,并与描述符特权级(Descriptor Privilege Level, DPL)比较。 如果DPL的值大于等于它,那么这个访问可正常进行了。RPL背后的设计思想是: 允许内核代码加载特权较低的段。比如,你可以使用RPL=3的段描述符来确保给定的操作所使用的段可以在用户模式中访问。但堆栈段寄存器是个例外,它要求CPL,RPL和DPL这3个值必须完全一致,才可以被加载。下面再总结一下CPL、RPL和DPL
CPL:当前特权级 保存在CS段寄存器(选择子)的最低2bit,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别。
DPL:描述符特权 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身真正的特权级。
RPL :请求特权级 保存在选择子的最低2bit。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置,并不一定RPL>=CPL,但是当RPL < CPL时,实际起作用的就是CPL了,因为访问时的特权检查是判断: max(RPL , CPL ) <= DPL 是否成立,所以RPL可以看成是每次访问时的附加限制,RPL = 0时 附加限制最小,RPL=3时附加限制最大。
地址空间
分段机制涉及4个关键内容:
逻辑地址空间(Logical Address,应用程序员看到的地址,在OS 上称之为 虚拟地址VA),比如下面的程序片段:int val =100; int *point = &val; 其中指针变量point中存储的即是一个逻辑地址。在基于80386的计算机系统中,逻辑地址有一个16bit的段寄存器(也称为段选择子)和一个32bit的偏移量构成。
物理地址空间(Physical Address,实际的物理内存地址),从OS角度看,CPU、内存硬件(内存条)和各种外设是它主要管理的硬件资源而内存硬件和外设分布在物理地址空间中。物理地址空间就是一个“大数组”,CPU通过索引(物理地址)来访问这个“大数组”中的内容。物理地址是指CPU提交到内存中线上用于访问计算机内存和外设的最终地址。
物理地址空间的大小取决于CPU实际的物理地址位数,在基于80386的计算机系统中,CPU的物理地址空间为4GB,如果计算机系统实际上有1GB物理内存,而其他硬件设备的IO寄存器映射到起始物理地址为3GB的256MB大小的地址空间,则该计算机系统的物理地址空间如下所示:
线性地址空间
一台计算机只有一个物理地址空间,但在操作系统的管理下,每个程序都认为自己独占整个计算机的物理地址空间。为了让多个程序能够有效地相互隔离和使用物理地址空间,引入线性地址空间(也称之为 虚拟地址空间)的概念。线性地址空间的大小取决于CPU实现的线性地址位数,在基于80386的计算机系统中,CPU的线性地址空间为4GB。
线性地址空间会被映射到某一部分或整个物理地址空间,并通过索引(线性地址)来访问其中的内容。线性地址有称之为虚拟地址,是进行逻辑地址转换后形成的地址索引,用于寻址线性地址空间。
但CPU未启动分页机制时,线性地址等于物理地址;
当CPU启动分页机制时,线性地址还需要经过分页地址转换形成物理地址后,CPU才能访问内存硬件和外设。
三种地址的关系如下所示:
启动分段机制,未启动分页机制:逻辑地址 - - - >(分段地址转换) - - - >线性地址 ==物理地址
启动分段和分页机制: 逻辑地址- - - >(分段地址转换)- - - - > 线性地址- - - - >(分页地址转换)- - ->物理地址
在操作系统的管理下,采用灵活的内存管理机制,在只有一个物理地址空间的情况下,可以存在多个线性地址空间。
硬盘访问概述
boot loader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。
一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0 - - - 0x1f7 实现的,具体参数见下表。一般第一个IDE通道通过访问IO地址0x1f0 - - - 0x1f7来实现,第二个IDE通道通过访问0x170 - - - 0x17f实现。每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。
表一 硬盘IO地址和对应功能
当前硬盘数据是存储到硬盘扇区中,一个扇区大小512byte。读一个扇区的流程(boot/bootmain.c中的readsect函数实现)大致如下:
等待硬盘准备好
发出读取扇区的命令
等待硬盘准备好
把硬盘扇区数据读到指定内存。
ELF文件格式概述
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
用于执行的可执行(executable file),用于提供程序的进程映像,加载的内存执行。这也是本实现的OS文件类型。
用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
这里只分析与本实验相关的ELF可执行文件类型。ELF header 在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构,其定义在elf.h中:
program header 描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必须的信息。可执行文件的程序头部是一个progrom header结构的数组,每个结构描述了一个段或者系统准备程序执行所必须的信息。目标文件的“段”包含一个或者多个“节区(section)”,也就是“段内容(segment Contents)”。程序头部仅对可执行文件和共享目标文件有意义。可执行目标文件在ELF头部的e_phentsize 和e_phnu成员中给出其自身程序头部的大小。程序头部的数据结构如下表所示:
根据elfhdr和proghdr的结构描述,boot loader就可以完成对ELF格式的tos系统的加载过程(参考boot/bootmain.c中的bootmain函数)。 (参考资料:《程序员的自我修养》)
Link Address 是指编译器指定代码和数据所需要放置的内存地址,由链接器配置。Load Address是指程序被实际加载到内存的位置(由程序加载器ld配置)。一般由可执行文件结构信息和加载器可保证这两个地址相同。Link Address 和 Load Address不同会导致以下问题:
直接跳转位置错误;
直接内存访问(只读数据区或bss等直接地址访问)错误;
堆和栈等的使用不受影响,但是可能会覆盖程序、数据区域
waring:也存在Link Address 和Load Address 不一样的情况(动态库 .so 、.DLL)
操作系统启动过程
当boot loader通过读取硬盘扇区把tos在系统加载到内存后,就跳转到tos操作系统在内存中的入口位置(kern/init.c中的kern_init函数的起始地址),这样tos就接管了整个控制权。当前的tos功能非常简单,只完成基本的内存管理和外设中断管理。
tos主要完成的工作包括:
初始化终端;
显示字符串;
显示堆栈中的多层函数调用关系;
切换到保护模式,启动分段机制;
初始化中断控制器,设置中断描述符表,初始化时钟中断,使能整个系统的中断机制;
执行while(1)死循环。
以后的实验中会大量涉及各个函数直接的调用关系,以及由于中断处理导致异步现象,可能对大家实现操作系统和改正其中的错误有很大影响。而理解好函数调用关系的建立机制和中断处理机制,对后续实验会有很大帮助。下面就练习5涉及的函数栈调用关系和练习6的中断机制的建立进行阐述。
函数堆栈
栈是一个很重要的编程概念(编译原理和程序设计),与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP的作用。一个函数调用动作可分解为: 0到多个PUSH指令(用于参数入栈),一个CALL指令。 CALL指令内部其实还暗含一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈架构(参数入栈顺序与调用方式有关,这里以C语音默认的CDECL为例):
这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。 “mov ebp esp” 这条指令表面上看是用esp 覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底的方向)能获取返回地址、参数值、向下(栈顶方向)能获取函数局部变量值,而该地址处有存储着上一层函数调用时的ebp值。
一般而言, ss:[ebp+4] 处为返回地址, ss:[ebp + 8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4byte内存); ss:[ebp -4]处为第一个局部变量;ss:[ebp]处为上一层ebp值。由于ebp中的地址处重视“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址,参数值, “向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。
中断与异常
操作系统需要对计算机系统中的各种外设进行管理,这就需要CPU和外设能够相互通信才行。一般外设的速度远慢于CPU的速度。如果让OS通过CPU“主动关心”外设的事件,即采用通常的轮询“polling”机制,则太浪费CPU资源了。所以需要OS和CPU能够一起提供某种机制,让外设在需要OS处理外设相关事件的时候,能够“主动通知”OS,即打断OS和应用的正常执行,让OS完成外设的相关处理,然后在恢复OS和应用的正常执行。在OS中,这种机制称为 中断机制。中断机制给OS提供了处理意外情况的能力,同时它也是实现进程/线程抢占式调度的一个重要基石。但中断的引入导致了对OS的理解更加困难。
在操作系统中,有三种特殊的中断事件。由CPU外部设备引起的外部事件如I/O中断、时钟中断、控制台中断等是异步产生的(即产生的时刻不确定),与CPU的执行无关,我们称之为异步中断(asynchronous interrupt)也称外部中断,简称中断,简称中断(interrupt)。而把在CPU执行指令期间检测到不正常的或非法的条件(如除0、地址访问越界)所引起的内部事件称作同步中断(synchronous interrupt),也称内部中断,简称异常(exception)。把在程序中使用请求系统服务的系统调用而引发的事件,称作陷入中断(trap interrupt),也称为软中断(soft interrupt),系统调用(system call)简称trap。
本实验只描述保护模式下的处理过程。当CPU收到中断(通过8259A完成)或者异常的事件时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理例程中,在完成对这个事件的处理后再跳回到刚才被打断的程序或任务中。中断向量和中断服务例程的对应关系主要是由IDT(中断描述符表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符,留待CPU在产生中断后查询对应中断服务例程的起始地址。而IDT本身的起始地址保存在idtr寄存器中。
中断描述符表(Interrupt Descriptor Table)中断描述符表把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8byte 的描述符数组,但IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8 作为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。指令LIDT和SIDT用来操作IDTR。两条指令都有一个显示的操作数:一个6byte 表示的内存地址。指令的含义如下:
LIDT(Load IDT Register)指令: 使用一个包含线性地址基址和界限的内存操作数来加载IDT。操作系统创建IDT时需要执行它来设定IDT的起始地址。这条指令只能在特权级0执行(可参见libs/x86.h中的lidt函数实现,其实就是一条汇编指令)。
SIDT(Store IDT Register)指令:copy IDTR的基址和界限部分到一个内存地址。这条指令可以在任意特权级执行。
IDT 和IDTR寄存器的结构和关系如下图所示:
在保护模式下,最多会存在256个Interrupt /Exception Vectors。 范围[0,31]内的32个向量被异常Exception 和NMI使用,但当前并非所有这32个向量都已经被使用,有几个当前没有被使用的,请不要擅自使用它们,它们被保留,以备将来可能增加新的Exception。范围[32,255]内的向量被保留给用户定义的Interrupt。Intel 没有定义,也没有保留这些Interrupt。用户可以将它们用作外部I/O设备中断(8259A IRQ),或者系统调用(System Call,Software Interrupt)等。
IDT gate descriptors
Interrupts/Exceptions 应该使用Interrupt Gate 和Trap Gate,它们之间的唯一区别就是:当调用Interrupt Gate时, Interrupt 会被CPU自动禁止;而调用Trap Gate时,CPU则不会去禁止或打开中断,而是保留它原来的样子。
所谓“自动静止”,指的是CPU跳转到interrupt gate 里的地址时,在将EFLAGS 保存到栈上之后,清除EFLAGS里的IF位,以避免重复触发中断。在中断处理例程里,操作系统可以将EFLAGS里的IF设上,从而允许嵌套中断。但是必须在此之前做好处理嵌套中断的必要准备,如保存必要的寄存器等。
在tos中访问Trap Gate的目的是为了实现系统调用。用户进程在正常执行中是不能禁止中断的,而当它发出系统调用后,将通过Trap Gate完成了从用户态 r3的用户进程进入内核态r0的OS kernel。
如果在到达OS kernel 后禁止EFLAGS里的IF位,第一没意义(因为不会出现嵌套系统调用的情况),第二还会导致某些中断得不到及时响应,所以调用Tra Gate时,CPU则不会去禁止中断。总之,Interrupt gate 和trap gate 之间没有优先级之分,仅仅是CPU在处理中断时有不同的方法,供操作系统在实现时根据需要进行选择。
在IDT中,可以包含如下3种行嘞的Descriptor:
Task-gate descriptor(这里没有使用)
Interrupt-gate descriptor(中断方法用到)
Trap-gate descriptor (系统调用用到)
下图显示了80836的任务门描述符、中断门描述符、陷阱门描述符的格式:
可参看kern/mm/mmu.h中的struct gatedesc数据结构对中断描述符的具体定义。
中断处理中硬件负责完成的工作
中断服务例程包括具体负责处理中断(异常)的代码是操作系统的重要组成部分。需要注意区别的是,有两个过程由硬件完成:
硬件中断处理过程1(起始):从cpu收到中断事件后,打断当前程序或任务的执行,根据某种机制跳转到中断服务例程去执行的过程。其具体流程如下:
CPU在形象完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有,那么CPU就会在相应的时钟脉冲到达时从总线上读取中断请求对应的中断向量;
CPU根据得到的中断向量(以此为索引)到IDT中找到该向量对应的中断描述符,中断描述符里保存着中断服务例程的段选择子;
CPU使用IDT查到的中断服务例程的段选择子从GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址;
CPU会根据CPL和中断服务例程的段描述符的DPL信息确认是否发生了特权级的转换。比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就讲当前程序使用的用户态的ss和esp压到新的内核栈中保存起来;
CPU需要开始保存当前被打断的程序的现场(即一些寄存器的值),以便于将来恢复被打断的程序继续执行。这需要利用内核栈来保存相关现场信息,即依次压入当前被打断程序使用的EFLAGS,CS,EIP, errorCode(如果有错误码信息)信息;
CPU利用终端服务例程的段描述符将其第一条指令的地址加载到CS和EIP寄存器中,开始执行中断服务例程。这以为着先前的程序被暂停执行,中断服务程序正式开始工作。
硬件中断处理过程2(结束):每个中断服务例程在有中断处理工作完成后需要通过iret(或iretd)指令恢复被打断的程序的执行。CPU执行IRET指令的具体过程如下:
程序执行这条iret指令时,首先会从内核栈里弹出先前保存的被打断的程序的现场信息,即EFLAGS,CS,EIP重新开始执行;
如果存在特权级转换(从内核态转换到用户态),则还需要从内核栈中弹出用户态栈的ss 和esp,这样也就意味着栈也被切换回原先使用的用户态的栈了;
如果此次处理的是带有错误码(errorCode)的异常,CPU在恢复先前程序的现场时,并不会弹出errorcode。这一步需要通过软件完成,即要求相关的中断服务例程在调用iret返回之前添加出栈代码主动弹出errorcode。
下图显示了从总断向量到GDT中相应中断服务程序起始位置的定位方式:
上图是 中断向量与中断服务例程起始地址的关系
中断产生后的堆栈变化
下图显示了给出相同特权级和不同特权级情况下中断产生后的堆栈栈变化示意图:
上图 相同特权级和不同特权级情况下中断产生后的堆栈栈变化示意图
中断处理的特权级转换
中断处理得特权级转换时通过门描述符(gate descriptor)和相关指令来完成的。一个门描述符就是一个系统类型的段描述符,一共4个子类型:调用门描述符(call-gate descriptor),中断门描述符(interrupt-gate descriptor),陷阱门描述符(trap-gate descriptor)和任务门描述表(task-gate descriptor)。与中断处理相关的是中断门描述符合陷阱门描述符。这些门描述符被存储在中断描述符表(Interrupt Descriptor Table,简称IDT)当中。CPU把中断向量作为IDT表项的索引,用来指出当中断发生生时使用哪一个门描述符来处理中断。中断门描述符合陷阱门描述门几乎是一样的。中断发生时实施特权检查的过程如下图所示:
上图为 中断发生时实施特权检查的过程
门中的DPL和段选择符一起控制着访问,同时,段选择符合结合偏移量(Offset)指出了中断处理例程的入口点。
内核一般在门描述符中填入了内核代码段的段选择子。产生中断后,CPU一定不会将运行控制从搞特权环转向低特权环,特权级必须要么保持不变(当操作系统内核自己被中断的时候),或被提升(当用户态程序被中断的时候)。
无论哪一种情况,作为结果的CPL必须等于目的代码段的DPL。
如果CPL发生了改变,一个堆栈切换操作(通过TSS完成)就会发生。
如果中断时被用户态程序中的指令所触发的(比如软件执行 INT n 生产的中断),还会增加一个额外的检查: 门DPL 必须具有与CPL相同或更低的特权。这就防止了用户代码随意触发中断。
如果这些检查失败,会产生一个一般保护异常(general-protection exception)。
lab1中对中断的处理实现
外设基本初始化设置
Lab1实现了中断初始化和对键盘、串口、时钟外设进行中断处理。串口的初始化函数serial_init (位于/kern/driver/console.c)中涉及中断初始化工作的很简单:
键盘的初始化函数kbd_init (位于kern/driver/console.c)完成了对键盘的中断初始化工作,具体操作更加简单:
时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。这样当一个application运行了一定时间后,操作系统会通过时钟中断获得CPU控制权,并可把CPU资源让给更需要CPU的其他应用程序。时钟的初始化函数clock_init (位于kern/driver/clock.c)完成了对时钟控制器8253的初始化:
中断初始化设置
操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责以处理特定的中断事件。系统将所有的中断事件统一进行了编号(0~~255),这个编号称为中断向量。以tos为例子,OS kernel 启动以后,会通过idt_init 函数初始化idt表 (参见trap.c),而其中vectors中存储了中断处理程序的入口地址。vectors定义在vector.S文件中,通过一个工具程序vector.c生成。其中仅有System call中断的权限为用户权限(DPL_USER),即仅能够使用 int 0x30指令。此外还有对tickslock的初始化,该锁用于处理时钟中断。
vector.S文件通过vector.c自动生成,其中定义了每个中断的入口程序和入口地址 (保存在vector数组中)。其中,中断可以分成两类:
一类是压入错误编码的(error code),
另一类不压人错误编码。
对于第二类,vector.S自动压入一个0。此外,还会压入相应中断的中断号。在压入两个必要的参数之后,中断处理函数跳转到统一的入口alltraps处。
中断的处理过程
trap函数(定义在trap.c中)是对中断进行处理的过程,所有的中断在经过中断入口函数__alltraps预处理后(定义在trapasm.S中),都会跳转到这里。在处理过程中,根据不同的中断类型,进行相应的处理。在相应的处理过程结束之后,trap将会返回,被中断的程序会继续运行。整个中断处理流程大致如下:
至此,对整个lab1中的主要部分的背景知识和实现进行了阐述。
实现操作系统TOS lab1
标签:
原文地址:http://blog.csdn.net/liutianshx2012/article/details/51605549
踩
(
0
)
赞
(
0
)
举报
评论
一句话评论(
0
)
登录后才能评论!
分享档案
更多>
2021年07月29日 (22)
2021年07月28日 (40)
2021年07月27日 (32)
2021年07月26日 (79)
2021年07月23日 (29)
2021年07月22日 (30)
2021年07月21日 (42)
2021年07月20日 (16)
2021年07月19日 (90)
2021年07月16日 (35)
周排行
更多
分布式事务
2021-07-29
OpenStack云平台命令行登录账户
2021-07-29
getLastRowNum()与getLastCellNum()/getPhysicalNumberOfRows()与getPhysicalNumberOfCells()
2021-07-29
【K8s概念】CSI 卷克隆
2021-07-29
vue3.0使用ant-design-vue进行按需加载原来这么简单
2021-07-29
stack栈
2021-07-29
抽奖动画 - 大转盘抽奖
2021-07-29
PPT写作技巧
2021-07-29
003-核心技术-IO模型-NIO-基于NIO群聊示例
2021-07-29
Bootstrap组件2
2021-07-29
友情链接
兰亭集智
国之画
百度统计
站长统计
阿里云
chrome插件
新版天听网
关于我们
-
联系我们
-
留言反馈
© 2014
mamicode.com
版权所有 联系我们:gaon5@hotmail.com
迷上了代码!