80386处理器基础
以80386处理器为例,学习其硬件方面的基础知识,为后面操作系统实验研究而准备。
Intel 80386运行模式
80386处理器有四种运行模式:实模式、保护模式、SMM模式和虚拟8086模式。
实模式
个人计算机早期的8086处理器采用的一种简单运行模式,当时微软的MS-DOS操作系统主要就是运行在8086的实模式下。80386加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。
保护模式
保护模式:保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。实际上,80386就是通过在实模式下初始化控制寄存器(如GDTR,LDTR,IDTR与TR等管理寄存器)以及页表,然后再通过设置CR0寄存器使其中的保护模式使能位置位,从而进入到80386的保护模式。当80386工作在保护模式下的时候,其所有的32根地址线都可供寻址,物理寻址空间高达4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。
Intel 80386内存架构
逻辑地址(虚拟地址)
应用程序能够使用的地址
逻辑地址空间 从应用程序的角度看,逻辑地址空间就是应用程序员编程所用到的地址空间,比如下面的程序片段: int val=100; int * point=&val;其中指针变量point中存储的即是一个逻辑地址。在基于80386的计算机系统中,逻辑地址有一个16位的段寄存器(也称段选择子,段选择子)和一个32位的偏移量构成。
线性地址
线性地址空间是80386处理器通过段(Segment)机制控制下的形成的地址空间。在操作系统的管理下,每个运行的应用程序有相对独立的一个或多个内存空间段,每个段有各自的起始地址和长度属性,大小不固定,这样可让多个运行的应用程序之间相互隔离,实现对地址空间的保护。一台计算机只有一个物理地址空间,但在操作系统的管理下,每个程序都认为自己独占整个计算机的物理地址空间。为了让多个程序能够有效地相互隔离和使用物理地址空间,引入线性地址空间(也称虚拟地址空间)的概念。线性地址空间的大小取决于CPU实现的线性地址位数,在基于80386的计算机系统中,CPU的线性地址空间为4GB。线性地址空间会被映射到某一部分或整个物理地址空间,并通过索引(线性地址)来访问其中的内容。线性地址又称虚拟地址,是进行逻辑地址转换后形成的地址索引,用于寻址线性地址空间。
物理地址
CPU通过总线访问物理内存用到的物理地址,32位地址总线则可以寻址最大4GB。物理地址空间 从操作系统的角度看,CPU、内存硬件(通常说的“内存条”)和各种外设是它主要管理的硬件资源而内存硬件和外设分布在物理地址空间中。
三种地址转换关系如下所示,程序给出逻辑地址,通过段处理机制,页处理机制来转换为最终的物理地址。
Intel 80386寄存器
80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,宽度都是32位。程序员可以看到寄存器包括通用寄存器,段寄存器,指令指针寄存器,标志寄存器。
General Register(通用寄存器)
EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP,这些寄存器的低16位就是8086的 AX/BX/CX/DX/SI/DI/SP/BP,对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位 (AH,AL,BH,BL,CH,CL,DH,DL)。
其中EAX累加器,EBX基址寄存器,ECX计数器,EDX数据寄存器,ESI源地址指针寄存器,EDI目的地址指针寄存器,EBP基址指针寄存器,ESP堆栈指针寄存器
Segment Register(段寄存器)
也称 Segment Selector,段选择符,段选择子
除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,用于不同属性内存段的寻址。
其中CS代码段,DS数据段,ES附加数据段,SS堆栈段,FS,GS附加段
Instruction Pointer(指令指针寄存器)
EIP的低16位就是8086的IP,它存储的是下一条要执行指令的内存地址,在分段地址转换中,表示指令的段内偏移地址。
Flag Register(标志寄存器)
CF进位标志位 PF奇偶标志位 AF辅助进位标志位 ZF零标志位 SF符号标志位
IF中断允许标志位,由CLI,STI两条指令来控制;设置IF位使CPU可识别外部(可屏蔽)中断请求,复位IF位则禁止中断,IF位对不可屏蔽外部中断和故障中断的识别没有任何作用;DF向量标志位,由CLD,STD两条指令来控制;OF溢出标志位; IOPL I/O特权级字段,它的宽度为2位,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性故障中断;NT控制中断返回指令IRET,它宽度为1位。若NT=0,则用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;若NT=1,则通过任务切换实现中断返回。在ucore中,设置NT为0。
段机制
处理器进入保护模式,则启动分段机制,此时逻辑地址经过分段机制转换为线性地址=物理地址。
分段地址转换
逻辑地址由段寄存器和指令寄存器表示,段寄存器中内存保存段选择子 某一个段在段描述符表中的索引位置,指令寄存器保存在段内偏移;
因此通过段寄存器中段选择子,从段描述符表中找个段描述符,进而找到该段的段基地址,与段内偏移相加,可以得到最终的线性地址,如果启用分页机制,则使用页处理,将线性地址转换为物理地址,如果没有启用分页机制,则此时线性地址=物理地址。
分段分页机制下逻辑地址到物理地址的转换
段描述符
每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。
段描述符主要是由编译器,连接器,装载器或者操作系统构造。
段基地址:规定线性地址空间中段的起始地址。在80386保护模式下,段基地址长32位。因为基地址长度与寻址地址的长度相同,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始。
段界限:规定段的大小。在80386保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。
段属性:确定段的各种性质。
- 段属性中的粒度位(Granularity),用符号G标记。G=0表示段界限以字节位位单位,20位的界限可表示的范围是1字节至1M字节,增量为1字节;G=1表示段界限以4K字节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节。
- 类型(TYPE):用于区别不同类型的描述符。可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等。
- 描述符特权级(Descriptor Privilege Level)(DPL):用来实现保护机制。
- 段存在位(Segment-Present bit):如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。
- 已访问位(Accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位。操作系统可清除该位。
全局描述符表
全局描述符表的是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。由于GDT 不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段。注意,全局描述符表中第一个段描述符设定为空段描述符。GDTR中的段界限以字节为单位。
段描述符表的长度不固定,可以最多包含8192个8字节的描述符,有两种描述符表全局描述符表GDT和局部描述符表LDT。
段选择子
线性地址部分的选择子是用来选择哪个描述符表和在该表中索引一个描述符的。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。
索引(Index):在描述符表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。
表指示位(Table Indicator,TI):选择应该访问哪一个描述符表。0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。
请求特权级(Requested Privilege Level,RPL):保护机制
保护模式下的特权级
在保护模式下,特权级总共有4个,编号从0(最高特权)到3(最低特权)。有3种主要的资源受到保护:内存,I/O端口以及执行特殊机器指令的能力。当一个段选择符被加载时,以及当通过线性地址访问一个内存页时。因此,保护也反映在内存地址转换的过程之中,既包括分段又包括分页。当一个数据段选择符被加载时,就会发生下述的检测过程:
CPL:当前特权级(Current Privilege Level) 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别)
DPL:描述符特权(Descriptor Privilege Level) 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身能被访问的真正特权级。
RPL:请求特权级RPL(Request Privilege Level) RPL保存在选择子的最低两位。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。
ELF文件格式
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
用于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
ELF header
ELF header在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构,其定义在elf.h中:
struct elfhdr {
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry; // 程序入口的虚拟地址
uint phoff; // program header 表的位置偏移
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum; //program header表中的入口数目
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
program header描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。可执行文件的程序头部是一个program header结构的数组, 每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的 “段” 包含一个或者多个 “节区”(section) ,也就是“段内容(Segment Contents)” 。程序头部仅对于可执行文件和共享目标文件有意义。可执行目标文件在ELF头部的e_phentsize和e_phnum成员中给出其自身程序头部的大小。
Program header
程序头部的数据结构如下表所示:
struct proghdr {
uint type; // 段类型
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;
uint filesz;
uint memsz; // 段在内存映像中占用的字节数
uint flags;
uint align;
};
参考资料:
https://chyyuu.gitbooks.io/ucore_os_docs/
https://github.com/chyyuu/os_course_info
http://www.xuetangx.com/courses/course-v1:TsinghuaX+30240243X+sp/info
参考:清华大学 操作系统 陈渝 http://os.cs.tsinghua.edu.cn/oscourse/OS2015/