三种内存地址
本文将要介绍内存分段,这里是基于x86体系架构,分别介绍硬件和linux的分段。
首先我们介绍一下在x86上的三种不同的内存地址:
Logical address, Linear address, Physical address,下图说明了它们三者的关系,而我们要介绍的分段就是介于Logic address与Linear address之间的一层转换。
首先我们要解决的问题就是为什么我们需要这三种不同类型的内存地址? why? a hard question. 我觉得它们是一种对内存进行管理的不同抽象层次。逻辑地址是对于程序员来说的,对于一个去编写应用程序的人,内存地址就是一个符号代表他需要存储的内容。这等于屏蔽掉了一些关于底层的细节,程序员只需要知道他自己的逻辑就够了。那线性地址就是对内存本身的一种抽象,线性地址把内存看做一个连续的存储单元,而事实上,在线性地址上连续的空间在物理地址上并不一定要连续。这只是我对这个问题的一些想法。
硬件的分段
现在就让我们看一下分段是如何把逻辑地址转换成线性地址的(在硬件中)
逻辑地址是由段选择符(segment selector) + 偏移量(Offset)组成的。那问题就变得简单了,既然有offset,那就需要找到一个base。找base的过程基本上就是segmentation的过程。base存在哪里呢?问题进一步明确,我们拥有segment selector要去寻找base。base是存放在segment Descriptor中的,segment descriptor是用来描述段的各个方面的信息的,其中就包括我们要找的段的base地址,也就是这个段第一个字节的地址。从下图我们看到有不同类型的段,它们代表各自不同的用途,这个我们先放在一边(在描述linux分段的时候会介绍)。
既然知道base在segment descriptor中可以找到,那如何找到段描述符呢?段描述符是存放在GDT(Global Descriptor Table)中的,这是一个统一存放段描述符的一段内存。在系统初始化的时候就会被初始化。当然还有一种可能就是存放在LDT(Local Descriptor Table)中,这是针对每个进程而言的,GDT中存放中一些共用的段描述符,如果进程要自己创建附加的段就需要放在LDT中。我们可以通过gdtr和ldtr寄存器找到GDT和当前LDT的地址。那么是如何在GDT或者LDT表中找到我们需要的段描述符的呢,这里就用到了segment selector这把钥匙,在段选择符中有一个13位的偏移量就是GDT或者LDT的index,另外有一位TI(Table Indicator)是用来说明在GDT还是在LDT中找。那段选择符(16bits)的另外两位是RPL(Requestor Privilege Level),表明是Kernel或者User的特权级。我们还剩一个问题就是,在程序中我们使用的内存地址应该是32bit的偏移量。那段选择符存放在哪里呢?你肯定听说过一个叫段寄存器(Segmentation Register)的东西,没错这就是指的我们通常所说的cs,ds,ss…段选择符就是放在这个寄存器中。
好,现在我们应该把可以把线索理清了:
这张图简单明了,为什么不早点拿出来呢?可能不经过上面这么啰嗦的话,这张图也不会这么清晰吧。那好,现在主要的逻辑我们已经走通了,不过还剩下一些细节:
1. 你可能发现对于一个内存地址的转换上面的步骤是不是太繁琐了一些,毕竟这种转换在计算机中太频繁了,那么我们介绍下硬件提供给我们的一种优化:快速访问段描述符的寄存器。对于每一个段寄存器,都配有一个快速访问段描述符的寄存器来存放段描述符,除非段选择器被修改了,那么每次的转换就可以直接从寄存器中取base地址。
2. GDT的第一项总是0,原因是避免空的段选择符,从而产生一个异常。
Linux的分段
下面我们介绍linux的分段,虽然x86架构给我们提供了进程级的分段,我们可以把一个程序根据需要分成相应的段,不过linux在使用时做了简化,它让所有进程使用相同的段寄存器值,共享同样的一组线性地址。这样做可以简化存储器管理,并且得到更高的可移植性。GDT存放在gdt_table中,它的表项如下所示。
我们看到有这么几种类型的段描述符:
1. Kernel code/ Kernel data 和 User code/ User data
Kernel code/data分别表示内核的代码段和数据段。它们的base都是0x00000000,limit都是0xfffff(1M)。你可能会奇怪为什么段的大小是1M呢,事实上是因为这里是以页为单位的(由G位控制),4KB一页,那1M个4KB就是4GB了,不过要注意的是这里最小的粒度就是4KB。同样用户段也是从0x00000000-0xffffffff,唯一的不同是DPL和TYPE。
2. TSS
有关TSS的详细描述会在进程的部分来说明。这里简单的介绍下,描述tss的类型为tss_struct,存放在init_tss数组中(里面存放了所有CPU对应的tss),GDT中的tss描述符的base就是指向init_tss中相应的tss_struct。tss的大小为0xeb(236B),tss_struct类型如下图所示:
3. APM(用于电源管理,这里就不做介绍了)