以80x86为例介绍硬件和软件层面的寻址操作。
地址可以分为三类:逻辑地址 线性地址 物理地址。逻辑地址是代码编译完成后目标文件里使用的地址。线性地址是一个32位的无符号整数,是一个中间过度地址。物理地址就是内存芯片引脚上的电信号,最根本最直接用于寻址的信息。逻辑地址通过分段单元转换成线性地址,线性地址通过分页单元转换成物理地址,这些转换的实现需要软件和硬件的结合。
80x86的分段单元
逻辑地址由16位段选择符和32位的偏移量组成。段选择符负责找到段描述符,然后由段描述符中的Base域和偏移量相加得到线性地址。
段选择符如何找到段描述符
段选择符放在段寄存器里,段寄存器包括有固定用途的cs ss ds和通用的 es fs gs。段选择符由三部分组成:1、13位的索引 2、描述表知识符(TI) 3、请求特权级(只适用于cs)。
每当一个新的段选择符被装入段寄存器时,cpu首先根据描述表指示符的值决定段描述符是在GDT里(Ti=0)还是在LDT里(TI=1)。然后去相应的寄存器(DGTR LDTR)里找到GDT或LDT在内存里的地址,GDTR LDTR里存储的是32位的物理地址,最后用段选择符前13位存储的索引在GDTR里找到段描述符。
找到段描述符怎么得到线性地址
短描述符表根据作用不同分为GDT和LDT。GDT 全局描述符表,系统只定义一个GDT。LDT 局部描述符表,每个进程可以有自己的LDT,注意是一个进程有一个LDT表。段描述符表里的段描述符是8字节64bit,包含以下信息
- 32位BASE,该段的起始地址,用于和逻辑地址中32位的偏移量组合成32位的线性地址
- 20位LIMIT 和1位的G,二者组合起来规定该段的长度
- 1位S,指定该段是系统段还是普通的用户段。从这里可以看到分段管理。
- 4位Type,该段的作用和存储权限。不同通途的段有不同的权限,如数据段可读写,代码段可执行,除此之外还包括 任务状态段描述符(TSSD),用于存放寄存器的状态,只能存在GDT中,推测是在时分复用cpu时保存现场用的 局部描述符表描述符(LDTD)
- 2位DPL,指定特权级,和cs里特权级对应。
- 1位segment-present,表示该段是否在内存里或者被置换到磁盘上。Linux总是置为1,因为Linux从不把一个段全部放到磁盘上。
- 略
当拿到段描述符后,根据段里的额外的控制信息进行检查,如权限是否满足,该段是否在内存里,然后就把BASE域和逻辑地址的偏移地址组合成线性地址。
Linux中的段
Linux作为一种兼容多平台的OS,内存分段管理和80x86硬件的内存分段管理不是完全契合的。Linux使用非常有限的分段管理方案而更倾向于使用分页管理内存,一个好处是如果所有的进程使用相同的Base那么所有的进程在同一个段里,那么进程的内存管理也会变得简单。仔细想想80x86的内存管理设计是为了把同样的逻辑地址映射成不同的物理地址,Linux即使不采用分段管理靠分页管理同样可以实现同样的功能。
Linux下所有段描述符都在GDT里,具体而言GDT表由数组gdt_table实现,即GDTR里存着gdt_table的地址。具体的GDT里存放着6中类型的段描述符。
1、内核代码段
该段线性地址的起始地址是0且具有可读可执行权限。DPL为0意味着内核态才能访问。
1、内核数据段
除了Type不一样外其余都一样,而且发现内核代码段和内核数据段线性地址重叠。
1、用户代码段
和内代码段不同之处在于权限不一样,BASE也是0,所有的线性地址都是重叠的。所有用户态下的进程共享用户代码段
1、用户代数据
同上,所有进程共享且线性地址重叠
6、任务状态段
TSS,每个进程有自己独立的TSS段。每个进程的TSS段的BASE域存放该进程描述符的tss域地址
7、LDT
略
每一个进程被创建的时候都要在GDT里加入每个进程都不同的TSS和LDT,因此GDT的大小也是限制进程数目大小的一个因素。
在段管理中一个很重要的地方是权限管理即用户态和内核态。首先用户态内核态指的是进程的权限,具体体现在CS寄存器中。每次进程发生权限切换都要为当前CPU中的ss ds段更换段选择符使当前进程能正确的寻找到该进程所能够访问到的GDT中的段描述符,即是内核数据/代码还是用户数据/代码