前言
大家好,我是jiantaoyab,内存中的每一个字节都有一个唯一的地址,通过这个地址我们能去取出一个个比特,我们可以称这个过程为寻址。在现实生活中,我们去菜鸟拿快递的时候,是通过取件码上的编号到架子上找的。取件码上面的信息就包含物品在哪个区域偏移多少。
同样的程序编译好之后放到磁盘上,运行的时候加载到内存中,同样的存在寻址问题,这里有2种寻址方式。
一是绝对地址,必须把某个指令加载到内存中的某个地址上面,否则不能运行。这种方式在运行多个程序的时候是行不通的,除非我们事前确定每个程序放在哪个内存上。
二是相对地址,把程序加载到内存的起始地址,再加上偏移地址得出真实的物理地址,解决了重定位的问题。
逻辑地址和物理地址
逻辑地址是指与当前数据在内存中的物理分配地址无关的访问地址,在执行内存访问之前必须转为物理地址。相对地址是逻辑地址的一个特例,通常是相对程序的开始处的存储单元。
物理地址或绝对地址是数据在内存中的实际位置。
分段式内存管理
程序的内存从内容上可以分为存放机器指令的代码区域、存放全局变量的数据区域、保存函数运行时信息的栈区等,显然我们可以将程序按照这种划分进行分段管理,段内使用相对地址,这样无论这些段被加载到内存的哪个区域我们都能方便的计算出正确的物理内存地址。
我们将各个段在内存中的起始地址放到专用的寄存器中,X86 CPU中有这样几个段寄存器,CS、DS、SS以及ES。
- 保存机器指令的区域,这个区域就是代码段(Code Segment),因此我们可以使用一个寄存器来专门指向代码段,这就是CS寄存器的作用,CS也是Code Segment的缩写。
- 程序运行起来后还有专门的区域用来保存数据,因此必须要专门的寄存器指向数据段(Data Segment),这就是DS寄存器的作用,DS是Data Segment的缩写。
- 程序运行起来后还有运行时栈(Stack Segment),因此可以使用SS寄存器来指向程序员运行时栈,SS是Stack Segment的缩写。
- 还有ES寄存器,Extra Segment,其用作临时段寄存器。
实模式
实模式(real mode),实模式的特点是20 bit分段内存(segmented memory)地址空间(精确到1 MB的可寻址内存)以及 对所有可寻址内存,I/O地址和外设硬件的无限制直接软件访问。
1MB的内存空间划分
最早期的8086 CPU只有一种工作方式,那就是实模式,而且数据总线为16位,地址总线为20位,实模式下所有寄存器都是16位。寄存器只有16位,16位寄存器是没有办法访问整个1MB内存的,16位寄存器最多能访问64K大小的内存,要想访问1MB内存那么内存地址就需要20位,而寄存器本身就16位,因此根本装不下,怎么办呢?
使用2个寄存器,第一个寄存器被叫做selector,也叫做段寄存器, segment register。第二个寄存器被叫做offset,又叫做区域内的偏移,此时内存地址的计算方式是这样的:16 ∗ selector + offset,这里计算出来的内存地址就是物理内存地址。
此外,在实模式下CPU不提供内存保护机制,程序可以随意读写任何内存区域,哪怕是操作系统所在的区域其它程序也可以读写,实模式也不支持多任务处理和代码权限级别。
随着发展在80286开始就有了保护模式,从80386开始CPU数据总线和地址总线均为32位,而且寄存器都是32位。设计师向前兼容之前使用了实模式的x86系列处理器在重置时会首先进入实模式,对于不使用实模式的现代操作系统来说简单的初始化工作后会跳转到保护模式。
保护
每个进程都受到保护,该进程以外的其他进程的程序不能未经授权进行读操作或写操作该进程的内存单元。
通常,用户进程不能访问操作系统的任何部分,无论是程序还是数据,并且处理器能够终止一个进程中的程序想要访问另一个进程中数据区。内存保护由处理器来完成,因为操作系统不能预测程序可能产生的所以内存访问,只能在指令访问内存的时来判断内存访问是不是合法。
保护模式
保护模式是相对于实模式而言的,它首次出现是在Intel 80286 CPU中首次出现的,紧跟着8086,为了克服在实模式下的种种问题,处理器厂商开发出保护模式。
32 位 CPU 具有保护模式和实模式两种运行模式,可以兼容实模式下的程序。假设我们使用的是32位的CPU,当开机时,32位的CPU是处于先处于实模式之后再进入保护模式的,在保护模式下每运行一个实模式下的程序,就要为其虚拟一个实模式环境,故称虚拟模式。
保护模式和实模式的寻址有很大的不同,保护模式下利用段选择子从段描述符表(GDT,LDT)中找到对应的段描述符,段描述符中含有需要寻址的段基地址,段长度,段属性等,利用段选择子找到的段描述符和偏移量就能寻址到对应的内存地址。可以先简单的理解,段描述符表是一个数组,数组中的每一项都是一个段描述符,段选择子是数组的下标。
段选择子
原先实模式下的各个段寄存器作为保护模式下的段选择子(CS,SS,DS,ES,FS,GS)16位寄存器。段选择子也不全是寄存器,可以是常数。
段选择子的大小为16bit,包含符索引,TI,请求特权级(RPL)
- 0 - 1 位用来存RPL,请求特权级可以表示 0、 1、 2、 3 四种特权级。
- 第2位是TL位, Table Indicator,用来指示选择子是在 GDT 中,还是 LDT 中索引描述符。 TI为 0 表示在 GDT 中索引描述符, TI 为 1 表示在 LDT 中索引描述符。
- 选择子的高 13 位,即第 3~15 位是描述符的索引值,由于选择子的索引值部分是 13 位,即 2 的 13 次方是 8192,故最多可以索引 8192 个段。
通过段选择子找到对应的GDT或LDT表中的表项
关于特权级的说明
任务中的每一个段都有一个特定的级别。每当程序试图访问某一个段,就将该进程拥有的特权级和要访问的特权级相比较来决定是否能访问该段。CPU只能访问同一特权或者低特权的段。
Linux0.11中只用了0级和3级,操作系统的内核区为0级,用户区为3级,特权级别有CPL、DPL、RPL。
- CPL表示当前特权级目标是当前正在执行的程序特权级,存放在cs和ss寄存器中,int指令可以将CPL改为0
- DPL:为描述符特权级,表示要访问的段(目标特权级)。DPL存在段描述符号或者门描述符中。
- RPL为请求特权级,存放在段选择子(ds、cs等寄存器中)
当一个程序访问一个段时候,处理器会检测CPL、DPL、RPL,只有当DPL >= CPL 且 DPL >= RPL 处理器才能允许进程访问该段。
值小权限越高。
段描述符
段描述符共64位,8个字节大小,用于描述内存段拥有的属性。
段描述符第31~24位:段基址高8位
段描述符第23位G:1代表段界限单位为4kB;0代表段界限单位为1B。
段描述符22位D/B:如果是代码段则这位称为D位:1代表有效地址操作数是32位,使用32位寄存器eip;0代表16位使用eip低16位ip。如果是栈段则这位称为B位:1代表使用32位寄存器esp,操作数大小32位;0代表使用esp低16位sp,操作数大小16位。
段描述符第21位L:用来设置是否是64位代码段,1代表代码段是64位;0代表是32位。
段描述符第20位AVL:保留暂无意义。
段描述符第19~16位:段界限高四位。
段描述符第15位P:段是否存在,1代表段存在内存中;0代表不在,cpu进行检查,如果为0则抛出异常。操作系统再将这个段从硬盘加载到内存
把p再设置为1
段描述符14~13位DPL:代表权级别,数字越小级别越高,os级别为0,应用程序为3。
段描述符第12位S:1代表该内存段是非系统段;0代表该内存段是系统段,配合type使用。
段描述符第11~8位:用于指定内存段的具体类型。
段描述符第7~0位:段基址中间8位。
- 段基址低16位
- 段界限低16位
GDT
当系统在保护模式下运行时,程序的所有内存访问操作都需要经过全局描述符表和局部描述符表。
GTD(Global Describe Table)全局描述符表是系统用的,存放在内存中,只有一张全局可见,GDT中有用户的CS和DS段描述符还有内核的CS和DS段描述符等等。GDT是一个段描述符类型的数组。
全局描述符表位于内存中,需要用专门的寄存器指向它后, CPU 才知道它在哪里。这个专门的寄存器便是 GDTR,即 GDT Register,专门用来存储 GDT 的内存地址及大小。
GDT界限表示它能存储的段描述符的多少,由于 GDT 的大小是 16 位,其表示的范围是2的 16次方等于 65536字节。每个描述符大小是 8字节,故,GDT中最多可容纳的描述符数量是 65536/8=8192个,即 GDT 中可容纳 8192 个段或门。
CPU处理一个逻辑地址“cs:offset”时:
- 将GDTR中的基址加上cs中的下标值,得到段描述符
- 从段描述符取出段基址
- 最后将段基址与偏移值相加,就得到线性地址(虚拟地址)
- 得到线性地址后,由CPU的MMU,将线性地址映射为物理地址,然后就可交给地址总线进行读写
访问GDT
-
当TI=0时表示段描述符在GDT中,如上图所示:
-
先从GDTR寄存器中获得GDT基址。
-
然后再根据段选择子高13位位置索引值得到段描述符。
-
段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址才得到地址。
LDT
一个处理器只有一个GDT表,是内核权限访问的。在多个任务的系统中,所有任务的段描述符(一个任务的数据段描述符、代码段描述符、堆栈描述符等等)都放在GDT表中这对操作系统来说,是个负担。
而且每次访问GDT表,还要陷入内核级才能访问,这比较费时间。这就有了LDT,操作系统在加载每一个任务的时候,都在给这个任务创建一个LDT表,存储着该任务的信息。
LDT的局部段描述表,和GDT不同的是,LDT可以存在多个,只对引用他们的任务可见,每个任务最多可以拥有一个LDT,另外每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
访问LDT
-
当TI=1时表示段描述符在LDT中,如上图所示:
-
还是先从GDTR寄存器中获得GDT基址。
-
从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位),此时的LDTR可以看成一个段选择子;
-
以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
-
用段选择器高13位位置索引值从LDT段中得到需要访问内存段描述符。
-
段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址才得到最后的线性地址。
80286访问内存的全过程
mov ax, 0x08
mov ds, as //1.给段寄存器一个16位的值(包含段选择子,类型,优先级)
xor si, si
mov [si], 0x07 //2.通过段地址+偏移地址访问某个内存
3.CPU通过TI位置的值决定去GDT还是LDT查看具体的段描述符,
4.CPU通过段寄存器的高13位来获取段选择子的值确定要访问的段
5.根据段选择子,找到段描述符,并比较PRL与DPL的值,当RPL的值小于等于DPL的值,说明本次请求被允许
6.如果能访问段描述符,就取出24位段地址
7.把段地址与偏移地址相加,得到真实的物理地址
总结
GDT只是一个段描述类型的数组,用于内存的寻找,为了对内存区域进行保护,将段描述符号设计成三部分。LDT和GDT类似,可以理解成GDT为1级描述符表,LDT为2级描述符表。
LDT不在GDT中,GDT包含LDT描述符一个指向LDT起始地址的指针。