分类:
2006-06-07 12:22:26
研究80x86微处理器的寻址,我们需要区分三种地址:
o. 逻辑地址(logical address):机器语言指令中用于指定指令操作数的地址。每个逻辑地址包括一个段地质和一个偏移量。
o. 线性地址(linear address):也称为虚地址(virtual address),一个32bit无符号整数,可以寻址至多4G空间。
o. 物理地址:用于对内存片进行寻址,对应于微处理器的地址引脚和内存总线之间的电信号。
MMU(内存管理单元)把一个逻辑地址变换为对应的线性地址,做这个转换的硬件电路称为Segmentation Unit;之后Paging Unit将线性地址转换为对应的物理地址。
多处理器系统中所有的cpu共享系统的内存,因此ram可能被不同cpu同时访问。但是对ram的读写操作必须顺序进行,因此一个称为memory arbiter的硬件电路被加入ram chip和总线之间,如果ram chip是空闲的,arbiter就会允许任何cpu访问它;但如果该ram chip正在被某个cpu访问,aribiter就会拒绝其他cpu对这个ram chip的访问请求。即使是单处理器系统也有arbiter,因为arbiter包括了一个特别的协处理器,称为“DAM控制器”,与cpu协同工作。
多处理器系统中的arbiter更复杂些,因为输入端口更多。例如奔腾的双核处理器,在每个ram chip的入口都有一个两端口的arbiter,两个cpu试图使用公共总线前必须先交换同步信息。从编程角度看arbiter是透明的,它是由硬件电路直接管理的。
从80286开始,intel cpu按照两种不同的方式作地址转换:实模式(real mode)和保护模式(protected mode)。实模式主要是为了向后兼容以及系统启动时用;80x86多数时间工作在保护模式下。
一个逻辑地址由两个部分组成:段地址和偏移量(段内相对地址)。段地址是16bit,称为segment selector,偏移量占32bit。Cpu提供了专门用于保存segment selector的段寄存器。段寄存器的名字是cs, ss, ds, es, fs, gs。其中三个有特别的用处:
cs: code segment register, 指向程序指令所在的段。
ss: stack segment register, 指向当前程序堆栈段。
ds: data segment register, 指向当前程序的数据段。
剩下的三个段寄存器是通用的段寄存器,可以指向任何数据段。Cs寄存器还有另外一个重要功能:它包含了一个2bit的域用于指定cpu的当前特权级别(current privilege level, CPL),0最高,3最低。Linux只用0和3两个级别,分别称为核心态和用户态。
每一个内存段由一个8字节的段描述符来代表,段描述符保存在Global/Local Descriptor Table中。通常只有一个全局描述符表(Global Descriptior Table, GDT),每个进程依据自己的需要可以创建私有的内存段,并且将这些私有的段的描述符保存在局部描述符表(Local Descriptor Table, LDT)中。GDT在主内存中的地址与大小保存在控制寄存器ldtr中,LDT的相关信息保存在ldtr中。
介绍段描述符中比较重要的几个域的含义:
Base- 该段的第一个字节的线性地址
S- System flag. 如果该标记被清除,表示该段是保存关键数据结构(如LDT)的系统段,如果该位被设置,则该段为普通的代码段或者数据段。
Type- 段类型和访问权限。
有多种不同类型的段,对应的有多种不同的段描述符。最常见的有:
code segment descriptor: 指向一个code segment,在LDT,GDT中均可出现,S标志被设置(非系统段的描述符)
data segment descriptor: 指向一个数据段,在LDT,GDT中均可出现,S标志被设置(同上),堆栈段被实现为一种通用的数据段。
Task state segment descriptor: TSSD, 指向一个task state segment,这种段用于保存处理器寄存器的内容,这种descriptor只能出现于GDT中,Type为11或者9(具体取值依赖于当前进程是否正在cpu上运行),S标志被清除(系统段的描述符)
Local descriptor segment descriptor: LDSD, 指向一个保存LDT的段,这种描述符只能出现在GDT中,Type为2,S标志被清除
为了加快逻辑地址到线性地址的转换,80x86处理器针对六个可编程的段寄存器提供了对应的六个不可编程的寄存器。这些不可编程的寄存器都包含了一个8字节的segmentation descriptor,这个segmentation descriptor是对应的段寄存器中的segment selector指定的。每当一个segment selector被装载到一个段寄存器中的时候,对应的segmentation descriptor就同时被从内存中装载到对应的不可编程的寄存器中。这之后的任何逻辑地址转换,如果需要引用segmentation descriptor的内容,都是从这个不可编程寄存器中直接得到,而不用访问内存中的LDT/GDT。
一个segment selector包含了三个域:
index- GDT/LDT中的Segment descriptor的索引。
TI- Table Indicator, 指定了对应的Segment desciptor是在LDT(TI=1)中还是在GDT(TI=0)中。
RPL- Requestor Privilege Level, 指定了Segment Selector被装载到cs寄存器的时候CPU的CPL(当前特权级别)。它还可以被用于在访问数据段的时候,选择性地降低处理器的特权级别(详情参见Intel的手册)。
Segment descriptor的相对地址计算过程是:
1- 从gdtr寄存器中取到GDT的起始地址(比如是0x00020000)
2- 从segment selector中得到某个segment descriptor的索引(比如是2)
3- 因为一个segment descriptor的长度是8字节,将索引值乘以2加到GDT起始地址上0x00020000 + (2 * 8) = 0x00020010
GDT的第一个条目总是被设置为0,这是为了保证任何Segment selector为Null的逻辑地址都会被认为是非法地址,从而引发一个处理器异常。GDT中可以保存的最大Segment descriptor编号是8191(2^13-1)。
逻辑地址到线性地址的转换过程
segmentation unit对逻辑地址进行下列操作:
1- 检查逻辑地址中segment selector的TI域,来确定是GDT还是LDT保存着这个segment descriptor。依据这个结果从gdtr寄存器或者当前活动的ldtr寄存器中取得GDT/LDT的base linear address
2- 依据segment selector的index域的值,计算segment descriptor的地址:GDT/LDT的base linear address加上(index * 8),取得segment descriptor。
3- 把这个逻辑地址的偏移量(segment selector之外的16bits)和segment descriptor的Base域(保存着该段第一个字节的线性地址)的值相加,得到逻辑地址对应的线性地址。
注意,上面的操作中,因为cpu中为段寄存器设置了对应的不可编程寄存器,所以步骤1和2只在段寄存器的内容发生变化的时候才是必需的,否则直接读取不可编程寄存器的内容即可。
Linux的内存分段
80x86微处理器对内存段的支持,使程序员可以把应用程序划分为逻辑上有关联的实体,比如子程序,全局数据区和局部数据区。Linux对内存段功能的使用是非常有限的。事实上内存的既分段又分页的做法是冗余的,因为分段和分页都可以被用于进程物理地址空间的分区,分段可以为每一个进程分配一个不同的线性地址空间,分页可以把相同的线性地址空间映射到不同的物理地址空间。Linux更多地使用了内存的分页功能,原因是:
1- 所有进程都使用相同的段寄存器,即它们共享相同的线性地址空间集合的情况下,内存管理相对更简单些。
2- Linux的设计目标之一是对很多硬件体系结构都能达到很好的可移植性,RISC体系的硬件通常对内存分段的支持很有限。
Linux 2.6内核仅仅在80x86系列的硬件要求必须使用内存段的时候才会用。全部运行在用户态的Linux进程都使用相同的几个段,做指令和数据寻址。这些段分别被称为用户代码段和用户数据段。类似地,全部运行在核心态的Linux进程也都用同样的这些段做指令和数据寻址,这些段此时被称为核心代码段和核心数据段。对应的segment selector被定义为以下的宏:__USER_CS, __USER_DS, __KERNEL_CS, __KERNEL_DS。例如在核心代码段中寻址,kernel就会把__KERNEL_CS指向的内容装载到段寄存器cs中。
注意和这些段相关的线性地址总是从0开始,上限是2^32-1。这意味着全部的进程,不管是处于用户态还是核心态,都可以使用相同的逻辑地址。全部段都从0x00000000开始的另外一个重要结果是,在Linux中逻辑地址和线性地址是重合的,即逻辑地址中的偏移量部分和对应的线性地址中的对应部分总是相同的。
CPU的CPL(当前特权级别)显示了进程是处在用户态还是核心态。CPL保存在Cs寄存器中的segment selector的RPL域。当CPL发生改变的时候,一些和segmentation相关的寄存器都要相应地更新。例如CPL是3(用户态)时,ds寄存器保存的必须是用户数据段的segment selector;但如果CPL是0(核心态),ds寄存器保存的就应该是核心数据段的segment selector。堆栈段寄存器ss也有类似的要求,不同情况下分别保存用户堆栈和核心堆栈的segment selector。
保存一个指向指令或者数据的指针时,kernel并不需要保存这个逻辑地址的segment selector,因为ss寄存器已经保存了当前的segment selector。例如,当kernel调用一个函数,它执行的只是一个汇编语言call指令,操作数仅仅是被调函数逻辑地址的偏移量部分。Segment selector隐含使用cs寄存器指向的那个,因为只有一个“核心态可执行代码”类型的内存段,即__KERNEL_CS宏所指定的代码段,CPU切换到核心态的时候,只要把__KERNEL_CS指向的代码段segment selector装载到cs里就够了。同样的分析可被用于指向核心数据结构(隐含使用ds寄存器的)的指针,以及指向用户数据结构的指针(这种情况下kernel显式地使用es寄存器)。
除了上边描述过的四个寄存器,linux还使用了另外一些特殊寄存器。后续章节中会提到。
Linux GDT
单处理器系统中,只有一个GDT;多处理器系统中每个CPU都有一个GDT。所有的GDT都保存在cpu_gdt_table数组里,它们的地址和大小保存在cpu_gdt_descr数组里,用于初始化gdtr寄存器。这些符号定义在arch/i386/kernel/head.s中。
每一个GDT包含了18个segment descriptor和14个空白/未用/保留条目。插入未用条目的目的是为了使那些经常一起访问的segment descriptor被放在同一个32字节的硬件cache line里面。
每个GDT包含的18个segment descriptor指向下列这些段:
1- 其中四个指向用户/核心的代码/数据段
2- 一个TSS(Task State Segment),对系统中每个处理器都是不同的。和一个TSS对应的线性地址空间,只是核心数据段对应的线性地址空间的一个小子集。TSS被顺序地存放在init_tss数组里。特别地,第n个CPU的TSS descriptor的Base域指向的就是init_tss数组的第n个元素。TSS descriptor中的G标志被清除,Limit域被设置为0xeb(十进制236),这是因为TSS段长度为236字节。Type字段取值9或者11,DPL被设置为0(DPL是Descriptor Privilege Level,用于限制对该段的访问。CPU的privilege level高于DPL的时候才能访问该段),用户态的进程是不能访问TSS的。
3- 其中一个指向一个包含了缺省的LDT的段,该段被全部进程共享。
4- 三个Thread-Local Storage(TLS)段。TLS机制使多线程的应用程序能够使用至多3个包含了线程局部数据的段。Set_thread_area()和get_thread_area()系统调用分别用于为当前正在执行的继承创建和释放一个TLS段。
5- 三个和高级电源管理(Advanced Power Management, APM)相关的段。BIOS代码也使用内存段,因此当Linux的APM驱动程序调用BIOS的函数以获取或者设置APM设备状态的时候,会用到这些代码/数据段。
6- 五个和BIOS中的即插即用(Plug and Play, PnP)服务相关的段。Linux PnP驱动程序调用BIOS函数检测即插即用设备的时候,BIOS中的相关部分会用到这些内存段。
7- 一个特别的TSS段。Kernel 用这个段处理”Double fault” exception。
系统中每一个处理器都有GDT的一份拷贝。GDT的所有拷贝保存了同样的条目,除了少数例外情况。首先,每个处理器有自己的TSS段,GDT的各个拷贝中的这个条目是不同的。第二,GDT中的某些条目可能依赖于该处理其中正在运行的进程的状况(比如LDT 和TLS段描述符)。第三,某些情况下处理器也会临时修改它自己的那份GDT拷贝中的某些条目,比如在调用APM相关的BIOS过程的时候。