Linux内存寻址(一):分段机制

参考链接https://www.cnblogs.com/zgq0/p/8612910.html

https://blog.csdn.net/farmwang/article/details/52333583

https://www.jianshu.com/p/22ea1135ee16

操作系统的地址

以下基于80x86微处理器描述

逻辑地址:逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。每个逻辑地址都是由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。

注:指的就是段选择符或者叫段标识符16bit 偏移量是32bit

 Intel——段式管理内存。Intel设计了4个新的寄存器:CS、DS、SS、ES分别用于存储指令、数据、堆栈、其他。

物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相相应。物理地址32位或36位无符号整数表示。

线性地址(也称虚拟地址)

一个32位无符号整数,可以用来表示高达4G的地址。内存(不要与机器上插那条对上号)的抽像描写叙述。它是相对于物理内存来讲的,能够直接理解成“不真实的”,“假的”内存。比如,一个0x08000000内存地址。它并不正确就物理地址上那个大数组中0x08000000 - 1那个地址元素。之所以是这样。是由于现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。

内存管理单元(MMU)通过一种分段单元的硬件电路把一个逻辑地址转换成虚拟地址;接着第二个分页单元的已安检电路把线性地址转换为物理地址

CPU段式内存管理,逻辑地址怎样转换为线性地址

每个段在能够使用之前,都要为这个段建立一个描述符。每个描述符占8个字节,这些描述符集中存放在内存的某个区域,一个挨着一个,就构成了一张“表”。

80x86中有两种描述符表:

  • 全局描述符表(Global Descriptor Table, 简称GDT
  • 局部描述符表(Local Descriptor Table,简称LDT

    在进入保护模式之前,必须要定义GDT,也就是说,我们要在内存中构建出一张表。

    需要说明的是:在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT);GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口。

    你也许会问:CPU如何知道GDT的入口呢?别担心,在处理器内部,有一个48位的寄存器,名叫GDTR,也就是全局描述符表寄存器。其结构如下图:

  • Linux内存寻址(一):分段机制_第1张图片Linux内存寻址(一):分段机制_第2张图片

该寄存器分为2部分:

  • 32位的线性基地址:GDT在内存中的起始线性地址(我们还没有涉及到分页,所以这里的线性地址等同于物理地址,下同,以后同);
  • 16位的表界限:在数值上等于表的大小(总字节数)减去1;

注意:在处理器刚上电的时候,基地址默认为0,表界限默认为0xFFFF; 在保护模式初始化过程中,必须给GDTR加载一个新值。

因为表界限是16位的,最大值是0xFFFF,也就是十进制的65535,那么表的大小就是65535+1=65536.又因为一个描述符占用8个字节,所以65536字节相当于8192个描述符(65536/8=8192).故理论上最多可以定义8192个描述符。实际上,不一定这么多,具体多少根据需要而定。

理论上,GDT可以放在内存中的任何地方。但是,我们必须在进入保护模式之前就定义GDT(不然就来不及了),所以GDT一般都定义在1MB以下的内存范围中。当然,允许在进入保护模式后换个位置重新定义GDT。

 

逻辑地址转换为线性地址的过程

Intel 微处理器两种不同的方式执行地址转换 实模式和保护模式 接下来描述保护模式下的地址转换

一个逻辑地址由两部份组成,段标识符: 段内偏移量。

先检查段选择子的T1字段,已决定段描述符号,存储在全局段描述符表GDL还是局部段描述符表中。如果在全局段描述符表GDL中,则分段单元则从GDTR中得到GDT的线性基地址,相反如果局部段描述符表中则从LDTR中获取到LDT的线性基地址。
从段选择子的index索引字段,计算段描述符的地址,index字段的值乘以8(一个段描述符的大小是8个字节),然后这个值(偏移)与GDTR或者LDTR的寄存器的值(全局/局部段描述符表的基地址)相加,全局/局部段描述符表中偏移为index*8的这个地址就存储着我们当前段的段描述符。
把得到的段描述符的base字段与逻辑地址额偏移offset值相加就得到了线性地址

段标识符是由一个16位长的字段组成,称为段选择符

Linux内存寻址(一):分段机制_第3张图片

当我们要访问某个段中的一个地址时候: 
1.从GDTR中拿到GDT在内存中的基地址,得到段描述符表 
2.从段选择子中的前13位得到我们要访问的段的描述符在段描述符表中的索引(需要考虑TI和RPL) 
3.从段描述符表中得到要访问的段的描述符,得到其基地址 
4.基地址加上偏移地址就是我们要访问的内存地址(当然这里是虚拟地址,接下来是分页机制的功能将虚地址转换为物理地址,不做讨论。)
Linux内存寻址(一):分段机制_第4张图片

 

当所有段从0x0000 0000开始,得出一个重要结论,那就是在Linux下逻辑地址和线性地址是一致的。即逻辑地址的偏移量字段的值与相应的线性地址值总是一致的

在实模式下,逻辑地址空间中存储单元的地址由段值和段内偏移两部分组成。在保护方式下,虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分组成。与实模式相比,段选择子代替了段值。

注:为了快速找到段选择符,处理器提供段寄存器,这些寄存器唯一的目的就是存放段选择符。设定了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data Segment):数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器

cs:代码段寄存器,指向包含程序指令的段。 
ss:栈段寄存器,指向包含当前程序栈的段。 
ds:数据段寄存器,指向包含静态数据或者全局数据段。
其它三个段寄存器作一般用途,可以指向任意的数据段。
cs寄存器还有一个很重要的功能:它包含一个两位的字段,用以指明CPU的当前特权级(CPL)。值为0代表最高优先级,值为3代表最低优先级。Linux只用0级和3级,分别称为内核态和用户态。

段描述符

段描述符是由8个字节组成,存放在全局描述符表中(Global Descriptor Table,GDT)或者局部描述符(local descriptor table,LDT)GDT在贮存中的地址和大小存放在上图中的gdtr控制寄存器里

在Linux内核找那个结构体为(kernel 4.4

/* 8 byte segment descriptor */
struct desc_struct {
    union {
        struct {
            unsigned int a;
            unsigned int b;
        };
        struct {
            u16 limit0;
            u16 base0;
            unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
            unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
        };
    };
} __attribute__((packed));

注:段是实现逻辑地址到线性地址转换机制的基础。在保护方式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。

在80x86保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。段属性中有一位对此进行定义,把该位 成为粒度位,用符号g标记。g=0表示段界限以字节位位单位,于是20位的界限可表示的范围是1字节至1M字节,增量为1字节;g=1表示段界限以4K字 节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节。当段界限以4K字节为单位时,实际的段界限LIMIT可通过下面的公式从 20 位段界限Limit计算出来:
LIMIT=limit*4K+0FFFH=(Limit SHL 12)+0FFFH
所以当粒度为1时,段的界限实际上就扩展成32位。由此可见,在80x86保护模式下,段的长度可大大超过64K字节。
基地址和界限定义了段所映射的线性地址的范围。基地址Base是线性地址对应于段内偏移为 0的虚拟地址,段内偏移为X的虚拟地址对应Base+X的线性地址。段内从偏移0到Limit范围内的虚拟地址对应于从Base到Base+Limit范围内的线性地址。

下图表示一个段如何从逻辑地址空间定位到线性地址空间。图中BaseA等代表段基地址, LimitA等代表段界限。另外,段C接在段A之后,也即BaseC=BaseA+LimitA。

例如:设段A的基地址等于00012345H,段界限等于5678H,并且段界限以字节为单位(G=0),那么段A对应线性地址空间中从00012345H-000179BDH的区域。如果段界限以4K字节为单位(G=1),那么段A对应线性地址空间中从00012345H-0568B344H(=00012345H+5678000H+0FFFH)的区域。

 

段描述符有3种:

代码段描述符
表示这个段描述符代表一个代码段,它可以放在GDT或LDT中。该描述符置S标志为1。
数据段描述符
表示这个段描述符代表一个数据段,它可以放在GDT或LDT中,该描述符置S标志为1。
任务状态段描述符
表示这个段描述符代表一个任务状态段,也就是说这个段用于保存处理器寄存器的内容。它只能出现在GDT中,根据相应的进程是否正CPU上运行,其Type字段的值分别为11或

段属性

(1)G为就是段界限粒度(Granularity)

G=0表示界限粒度为字节;G=1表示界限粒度为4K字节。注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。

(2)D/B位是一个很特殊的位在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同

在描述可执行段的描述符中,D位决定了指令使用的地址及操作数所默认的大小。

D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段;

D=0表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。

在向下扩展数据段的描述符中,D位决定段的上部边界。

D=1表示段的上部界限为4G;

D=0表示段的上部界限为64K,这是为了与80286兼容。

在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。

D=1表示使用32位堆栈指针寄存器ESP;

D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。
(3)AVL位是软件可利用位
80386对该位的使用未做规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。此为被linux和windows操作系统忽略。
(4)P位称为存在(Present)
P=1表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;P=0表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
(5)DPL表示描述符特权级(DescriptorPrivilegelevel)
共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。
(6)DT位说明描述符的类型
对于存储段描述符而言,
DT=1,以区别与系统段描述符和门描述符(DT=0)。
(7)TYPE说明存储段描述符所描述的存储段的具体属性
存储段描述符中的TYPE字段所说明的属性可归纳为下表:

各个位的含义总结为下表

其中的0指示描述符是否被访问过(Accessed),用符号A标记。
A=0表示尚未被访问,
A=1 表示段已被访问,
当把描述符的相应选择子装入到段寄存器时,80386把该位置为1,表明描述符已被访问,操作系统可测试访问位,已确定描述符是否被访问过。
此为位1,对应的type为奇数,因此type对应的数值为奇数时标识该描述符被访问过,否则为偶数标识未被访问过(可从前面的表中看出)
其中的3指示所描述的段是代码段还是数据段,用符号E标记
E=0表示段为数据段,相应的描述符也就是数据段(包括堆栈段)描述符。数据段是不可执行的,但总是可读的。
E=1表示段是可执行段,即代码段,相应的描述符就是代码段描述符。代码段总是不可写的,若需要对代码段进行写入操作,则必须使用别名技术,即用一个可写的数据段描述符来描述该代码段,然后对此数据段进行写入。
此位为1时,type所对应的数值>=8(1000),因此type<8标识为数据段,type>=8标识为代码段(可从前面的表中看出)
在数据段描述符中(E=0的情况)TYPE中的位1指示所描述的数据段是否可写,用W标记
W=0表示对应的数据段不可写。反之,W=1表示数据段是可写的。注意,数据段总是可读的。
TYPE中的位2ED位,指示所描述的数据段的扩展方向。ED=0表示数据段向高端扩展,也即段内偏移必须小于等于段界限。ED=1表示数据段向低扩展,段内偏移必须大于段界限。
在代码段描述符中(E=1的情况)TYPE中的位1指示所描述的代码段是否可读,用符号R标记
R=0表示对应的代码段不可读,只能执行。
R=1表示对应的代码段可读可执行。
注意代码段总是不可写的,若需要对代码段进行写入操作,则必须使用别名技术。在代码段中,TYPE中的位2指示所描述的代码段是否是一致代码段,用C标记。C=0表示对应的代码段不是一致代码段(普通代码段),C=1表示对应的代码段是一致代码段。关于一致代码段的说明,后面的文章将会详细介绍。
此外,描述符内第6字节中的位5必须置为0,可以理解成是为以后的处理器保留的。

局描述符表GDT和局部描述符表GDT

一个任务会涉及多个段,每个任务需要一个描述符来描述,为了便于组织管理,80386把描述符组织成线性表。由描述符组成的线性表称为描述符表。在80386中有三种类型的描述符表:全局描述符表GDT(GlobalDescriptor Table)、局部描述符表LDT(LocalDescriptor Table)和中断描述符表IDT(InterruptDescriptorTable)。
在整个系统中,全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张,每个任务可以有一张。
每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可包含有8K(8192)个描述符.
每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。随着任务的切换,系统当前的局部描述符表LDT也随之切换。
全局描述符表GDT含有每一个任务都可能或可以访问的段的描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符,也包含多种特殊数据段描述符,如各个用于描述任务LDT的特殊数据段等。
在任务切换时,切换LDT,并不切换GDT
通过LDT可以使各个任务私有的各个段与其它任务相隔离,从而达到受保护的目的。通过GDT可以使各任务都需要使用的段能够被共享。
一个任务可使用的整个虚拟地址空间分为相等的两半,一半空间的描述符在全局描述符表中,另一半空间的描述符在局部描述符表中。由于全局和局部描述符表都可以包含多达8192个描述符,而每个描述符所描述的段的最大值可达4G字节,因此最大的虚拟地址空间可为:
4GB*8192*2=64MMB=64TB

 

 

快速访问段描述符

由于一个段描述符是8字节长,因此在GDT或者LDT内相对地址的选择符最高13位(index)乘以8得到

比如:gdt在0x00020000(该值保存在gdtr寄存器中),端选择符指定的索引号为2,那么响应的段描述符地址是0x00020000+(2*8) 即0x00020010

注:gdt的第一项总是设置为0,这就确保空段选择符的逻辑地址会被认为是无聊的,会引发处理器异常。能保存gdt中的段描述符最大数目为2^13 -1.

猜想:这是用户态进程访问0x0000 0000地址访问出错的原因(如有其它请指正)

 

 

控制寄存器

从上表可见,80386有四个32位的控制寄存器,分别命名位CR0、CR1、CR2和CR3。但CR1被保留,供今后开发的处理器使用,在80386中不能使用CR1,否则会引起无效指令操作异常。
CR0包括指示处理器工作方式的控制位,包含启用和禁止分页管理机制的控制位,包含控制浮点协处理器操作的控制位。
CR2及CR3由分页管理机制使用。CR0中的位5—位30及CR3中的位0至位11是保留位,这些位不能是随意值,必须为0。
控制寄存器CR0的低16位等同于80286的机器状态字MSW。

保护控制位

控制寄存器CR0中的位0用PE标记,位31用PG标记,这两个位控制分段和分页管理机制的操作,所以把它们称为保护控制位。
PE控制分段管理机制。
PE=0,处理器运行于实模式;
PE=1,处理器运行于保护方式。
PG控制分页管理机制。
PG=0,禁用分页管理机制,此时分段管理机制产生的线性地址直接作为物理地址使用;
PG=1,启用分页管理机制,此时线性地址经分页管理机制转换位物理地址。
关于分页管理机制的具体介绍在后面的文章中进行。

由于只有在保护方式下才可启用分页机制,所以尽管两个位分别为0和1共可以有四种组合,但只有三种组合方式有效。PE=0且PG=1是无效组合,因此,用PG为1且PE为0的值装入CR0寄存器将引起通用保护异常。

需要注意的是,PG位的改变将使系统启用或禁用分页机制,因而只有当所执行的程序的代码和至少有一部分数据在线性地址空间和物理地址空间具有相同的地址的情况下,才能改变PG位。

协处理器控制位

控制寄存器CR0中的位1—位4分别标记为MP(算术存在位)、EM(模拟位)、TS(任务切换位)和ET(扩展类型位),它们控制浮点协处理器的操作。
当处理器复位时,ET位被初始化,以指示系统中数字协处理器的类型。
如果系统中存在80387协处理器,那么ET位置1;如果系统中存在80287协处理器或者不存在协处理器,那么ET位清0。
EM位控制浮点指令的执行是用软件模拟,还是由硬件执行。
EM=0时,硬件控制浮点指令传送到协处理器;
EM=1时,浮点指令由软件模拟。
TS位用于加快任务的切换,通过在必要时才进行协处理器切换的方法实现这一目的。每当进行任务切换时,处理器把TS置1。
TS=1时,浮点指令将产生设备不可用(DNA)异常。
MP位控制WAIT指令在TS=1时,是否产生DNA异常。
MP=1和TS=1时,WAIT产生异常;
MP=0时,WAIT指令忽略TS条件,不产生异常。

CR2和CR3

控制寄存器CR2和CR3由分页管理机制使用。
CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在CR2中。
操作系统中的页异常处理程序可以检查CR2的内容,从而查出线性地址空间中的哪一页引起本次异常。
CR3用于保存页目录表的其始物理地址。由于目录是页对齐的,所以仅高20位有效,低12位保留未用。
向CR3中装入一个新值时,低12位必须为0;但从CR3中取值时,低12位被忽略。
每当用MOV指令重置CR3的值时,会导致分页机制高速缓冲区的内容无效,用此方法,可以在启用分页机制之前,即把PG位置1之前,预先刷新分页机制的高速缓存。
CR3寄存器即使在CR0寄存器的PG位或PE位为0时也可装入,如在实模式下也可设置CR3,以便进行分页机制的初始化。
在任务切换时,CR3要被改变,但是如果新任务中CR3的值与原任务中CR3的值相同,那么处理器不刷新分页高速缓存,以便当任务共享也表时有较快的执行速度。

系统地址寄存器

全局描述符表GDT、局部描述符表LDT和中断描述符表IDT等都是保护方式下非常重要的特殊段,它们包含有为段机制所用的重要表格。

为了方便快速地定位这些段,处理器采用一些特殊的寄存器保存这些段的基地址和段界限。我们把这些特殊的寄存器称为系统地址寄存器。

全局描述符表寄存器GDTR

在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。

GDTR长48位,其中高32位为基地址,低16位为界限。由于GDT不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段提供一个伪描述符。GDTR给定了GDT。

GDTR中的段界限以字节为单位。由于段选择子中只有13位作为描述符索引,而每个描述符长8个字节,所以用16位的界限足够。通常,对于含有N个描述符的描述符表的段界限设为8*N-1。

局部描述符表寄存器LDTR

局部描述符表寄存器LDTR规定当前任务使用的局部描述符表LDT。

LDTR类似于段寄存器,由程序员可见的16位的寄存器和程序员不可见的高速缓冲寄存器组成。实际上,每个任务的局部描述符表LDT作为系统的一个特殊段,由一个描述符描述。而用于描述符LDT的描述符存放在GDT中。

在初始化或任务切换过程中,把描述符对应任务LDT的描述符的选择子装入LDTR,处理器根据装入LDTR可见部分的选择子,从GDT中取出对应的描述符,并把LDT的基地址、界限和属性等信息保存到LDTR的不可见的高速缓冲寄存器中。

随后对LDT的访问,就可根据保存在高速缓冲寄存器中的有关信息进行合法性检查。

LDTR寄存器包含当前任务的LDT的选择子。所以,装入到LDTR的选择子必须确定一个位于GDT中的类型为LDT的系统段描述符,也即选择子中的TI位必须是0,而且描述符中的类型字段所表示的类型必须为LDT。

可以用一个空选择子装入LDTR,这表示当前任务没有LDT。在这种情况下,所有装入到段寄存器的选择子都必须指示GDT中的描述符,也即当前任务涉及的段均由GDT中的描述符来描述。

如果再把一个TI位为1的选择子装入到段寄存器,将引起异常。

中断描述符表寄存器IDTR

中断描述符表寄存器IDTR指向中断描述符表IDT。

IDTR长48位,其中32位的基地址规定IDT的基地址,16位的界限规定IDT的段界限。

由于80386只支持256个中断/异常,所以IDT表最大长度是2K,以字节位单位的段界限为7FFH。IDTR指示IDT的方式与GDTR指示GDT的方式相同。

任务状态段寄存器TR

任务状态段寄存器TR包含指示描述当前任务的任务状态段的描述符选择子,从而规定了当前任务的状态段。

TR也有程序员可见和不可见两部分。当把任务状态段的选择子装入到TR可见部分时,处理器自动把选择子所索引的描述符中的段基地址等信息保存到不可见的高速缓冲寄存器中。

在此之后,对当前任务状态段的访问可快速方便地进行。装入到TR的选择子不能为空,必须索引位于GDT中的描述符,且描述符的类型必须是TSS。

 

 

你可能感兴趣的:(Linux内存)