LYOS —— Memory Segments & GDT

来源:互联网 发布:程序员加薪 压力大 编辑:程序博客网 时间:2024/04/30 01:28

LYOS —– 一个最基本的操作系统


(二)Memory Segments & GDT

第二篇介绍保护模式下内存的访问

实模式和保护模式下内存的访问。

先明确几个概念:

段描述符
8个字节64位,每一个段都有一个对应的描述符。根据描述符所描述的对象不同,描述符可分为三类:储存段描述符,系统段描述符,门描述符(控制描述符)。在描述符中定义了段的基址,限长和访问类型等属性。其中基址给出该段的基础地址,用于形成线性地址;限长说明该段的长度,用于存储空间保护;段属性说明该段的访问权限、该段当前在内存中的存在性,以及该段所在的特权级。

段选泽符
32位汇编中16位段寄存器(CS、DS、ES、SS、FS、GS)中不再存放段基址,而 是段描述符在段描述符表中的索引值,D3-D15位是索引值,D0-D1位是优先级(RPL)用于特权检查,D2位是描述符表引用指示位TI,TI=0指 示从全局描述表GDT中读取描述符,TI=1指示从局部描述符中LDT中读取描述符。这些信息总称段选择符(段选择子) 。

段描述表
IA-32处理器把所有段描述符按顺序组织成线性表 放在内存中,称为段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务 都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。GDT包含系统使用的代码段、数 据段、堆栈段和特殊数据段描述符,以及所有任务局部描述符表LDT的描述符。

段寄存器

  • GDTR全局描述符寄存器
    48位,高32位存放GDT基址,低16为存放GDT限长。
  • LDTR局部描述符寄存器
    16位,高13为存放LDT在GET中的索引值。

在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了Protected Mode,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,Protected Mode的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何值),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:【Base Address, Limit, Access】,它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。
怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所讨论的通过段寄存器引用段的模式是一样的。
 ——百度百科

LYOS是32位操作系统,运行在保护模式下,所以不能直接使用段加偏移的方式访问RAM,需要使用GDT。
使用LGDT汇编指令加载GDT,加载GDT所使用的结构如下:
GDTR
其中Size是全局描述表的大小减1,因为这个值的最大值是65535bytes,而GDT的大小可以为65536bytes(最大有8192个条目)。而且Size不可能是0,所以要减去1。Offset是全局描述表的线性地址。

GDT包含8个字节,结构如下:
GDT
“Limit 0:15” 这种格式的意思是这一字段包含Limit值的0到15bits。Base字段32bits,包含该segment开始的线性地址。Limit字段20bit, 指明最大可寻址单元 (不论是1byte的单元还是一个页).所以,如果你选择一个页面粒度(4 KiB) 并设置是limit值为 0xFFFFF 这个段将包含整个4G空间.下边是Access和Flags的具体解释:
Access&Flags

  • Pr:当前位,这个值必须设置为1对于任意有效的选择器。(存在标志)
  • Privl:特权级别,2bits,包含ring level,0为kernel级,3为用户应用程序级。表示访问该段时CPU所需处于的最低特权级
  • Ex:可执行标志位,如果是1则是code selector,可执行,如果是0则是deta selector,不可执行。
  • DC: Direction 位/Conforming 位
    • Direction位为扩展方向位,为data selectors设计:设置为0,segment向上扩展,设置为1,segment向下扩展。
    • Conforming 执行权限位,为code selectors设计:如果是1,那么能够被同级或更低特权级执行。如果是0,那么只能被Pr位设置的ring执行。
  • RW: 可读/可写标志位
    可读位为code selectors设计,无论读权限是否允许,写权限始终不允许给code segments。
    可写位为data selectors设计,无论是否可写,读权限始终允许给data segments。
  • Ac:被访问位,设置为0,当segment被访问,CPU设置这个值为1。
  • Gr: 粒度位。
    G=0时,段限长的20位为实际段限长,最大限长为2^20=1MB
    G=1时,则实际段限长为20位段限长乘以2^12=4KB,最大限长达到4GB
  • Sz: 默认操作大小位. Sz=0,16位段,Sz=1,32位段。

了解了这些基本概念后,梳理一下逻辑地址转换成线性地址的原理和过程:
logicToLiner

开始动手

工程目录结构

➜  /home/andy/ProgrammingPlayground/LYOS git:(master) ✗ >tree.├── include│   ├── common│   │   └── types.h│   └── gdt.h├── linker.ld├── Makefile└── src    ├── gdt.cpp    ├── kernel.cpp    └── loader.s3 directories, 7 files

新建了三个文件

文件 描述 include/common/types.h 定义了一些基本系统数据类型 include/gdt.h 全局描述表头文件 src/gdt.cpp 全局描述表实现

关键代码

gdt.h

#ifndef __LYOS__GDT_H#define __LYOS__GDT_H#include <common/types.h>namespace lyos{    class GlobalDescriptorTable             //全局描述表    {        public:            class SegmentDescriptor //段描述符            {                private:                    common::uint16_t limit_lo;              //limit的低16位                    common::uint16_t base_lo;               //base的低16位                    common::uint8_t base_hi;                //base的高16位                    common::uint8_t access;                 //8位对自身的访问权限                    common::uint8_t flags_limit_hi;         //limit的高4位和flags所占4位                    common::uint8_t base_vhi;               //base的最高8位                public:                    SegmentDescriptor(common::uint32_t base, common::uint32_t limit, common::uint8_t access);                    common::uint32_t Base();    //返回基址地址                    common::uint32_t Limit();   //返回limit大小            } __attribute__((packed));        private:            SegmentDescriptor nullSegmentSelector;            SegmentDescriptor unusedSegmentSelector;            SegmentDescriptor codeSegmentSelector;            SegmentDescriptor dataSegmentSelector;        public:            GlobalDescriptorTable();            ~GlobalDescriptorTable();            common::uint16_t CodeSegmentSelector(); //代码段选择子偏移量            common::uint16_t DataSegmentSelector(); //数据段选择子偏移量    };}#endif

gdt.cpp

#include <gdt.h>using namespace lyos;using namespace lyos::common;GlobalDescriptorTable::GlobalDescriptorTable()    : nullSegmentSelector(0, 0, 0),      unusedSegmentSelector(0, 0, 0),                 //Selector 0x00 不能被使用      codeSegmentSelector(0, 64 * 1024 * 1024, 0x9A), //Selector 0x08 作为代码段      dataSegmentSelector(0, 64 * 1024 * 1024, 0x92)  //Selector 0x10 作为数据段{    uint32_t i[2];                                    //LGDT参数    i[1] = (uint32_t)this;                            //入口地址    i[0] = (sizeof(GlobalDescriptorTable) - 1) << 16; //大小    asm volatile("lgdt (%0)" //在构造函数中加载gdt                 :                 : "p"(((uint8_t *)i) + 2));}GlobalDescriptorTable::~GlobalDescriptorTable(){}uint16_t GlobalDescriptorTable::DataSegmentSelector() //数据段选择子地址偏移量{    return (uint8_t *)&dataSegmentSelector - (uint8_t *)this;}uint16_t GlobalDescriptorTable::CodeSegmentSelector() //代码段选择子地址偏移量{    return (uint8_t *)&codeSegmentSelector - (uint8_t *)this;}//段描述符GlobalDescriptorTable::SegmentDescriptor::SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t access){    uint8_t *target = (uint8_t *)this;    if (limit <= 65536)    {        target[6] = 0x40; //设置Gr为0    }    else    {        if ((limit & 0xFFF) != 0xFFF) //大于4G            limit = (limit >> 12) - 1;        else //小于4G            limit = limit >> 12;        target[6] = 0xC0; //设置Gr为1    }    //limit    target[0] = limit & 0xFF;    target[1] = (limit >> 8) & 0xFF;    target[6] |= (limit >> 16) & 0xF;    //base    target[2] = base & 0xFF;    target[3] = (base >> 8) & 0xFF;    target[4] = (base >> 16) & 0xFF;    target[7] = (base >> 24) & 0xFF;    //type    target[5] = access;}uint32_t GlobalDescriptorTable::SegmentDescriptor::Base() //返回Base值{    uint8_t *target = (uint8_t *)this;    uint32_t result = target[7];    result = (result << 8) + target[4];    result = (result << 8) + target[3];    result = (result << 8) + target[2];    return result;}uint32_t GlobalDescriptorTable::SegmentDescriptor::Limit() //返回Limit值{    uint8_t *target = (uint8_t *)this;    uint32_t result = target[6] & 0xF;    result = (result << 8) + target[1];    result = (result << 8) + target[0];    if ((target[6] & 0xC0) == 0xC0)        result = (result << 12) | 0xFFF;    return result;}

运行

同第一篇,由于只定义了GDT,所以运行无任何效果。

源码

https://github.com/qvjp/LYOS/

原创粉丝点击