首页 > 代码库 > 内存寻址一(分段)

内存寻址一(分段)

内存地址

逻辑地址(logical address)

包含在机器语言指令中用来指定一个操作数或一条指令的地址。这种寻址方式在80x86著名的分段结构表现的尤为具体。每个逻辑地址都有一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。

线性地址(linear address)(也称虚拟地址virtual address)

是一个32位无符号整数,可以用来表示高达4G的地址。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff。

物理地址(physical address)

用于内存芯片级内存单元寻址。它们与微处理器的地址引脚发送到内存总线上的电信号相对应。物理地址由32位或36位无符号整数表示。


内存管理单元(MMU)通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址;接着,第二个称为分页单元(paging unit)的硬件电路把线性地址转换为一个物理地址。


当Intel处理器工作在保护模式下,分段机制是必须使用的,而分页是可选的。当分页没有使用时,线性地址空间就直接映射到物理地址空间。物理地址空间是指处理器在地址总线产生的地址空间范围,如处理器为32位但地址总线是36位,这物理地址空间就是0~2^36


硬件中的分段

一个逻辑地址由两部分组成:一个段标识符和一个指定段内相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符

段选择符(segment selector)


Index 指定了放在GDT或者LDT中的相应段描述符的入口。从存放在GDT或者LDT中的8192个描述符中选择段描述符,由于一个段描述符是8个字节长(下面 会讲道),因此它在GDT或者LDT内的相对地址是由段选择符的最高13位的值乘以8得到的。例如:如果GDT在0x00020000(这个值保存在gdtr寄存器中),且由段选择符所指定的索引号为2,那么相应的段描述符地址是0x00020000 + 2*8

TI    TI(Table Indicator)标志;指明段描述符是在GDT中(TI=0)或在LDT中(TI=1)

RPL   请求特权等级(Requested Privilege Level):当相应的段选择符装入到CS寄存器中时指示出CPU当前的特权等级。优先级范围从0到3,0代表最高优先级,而3代表最低优先级。Linux只用到0级和3级,分别称之为内核态和用户态


为了快速方面地找到段选择符,处理器提供段寄存器,段寄存器的唯一目的是存放段选择符。这些寄存器称为cs,ss,ds,es,fs和gs。

6个寄存器中有3个有专门用途:

cs  代码段寄存器,指向包含程序指令的段。

ss  栈段寄存器,指向包含当前程序的段。

ds  数据段寄存器,指向包含静态数据或者全局数据段

对intel处理器,有两种类型的指令用于加载段寄存器

一类是显式引用,如MOV, POP, LDS, LES, LSS, LGS, and LFS

另一类是隐式加载,如CALL,JMP,RET,SYSENTER,SYSEXIT,IRET,INTn等这些指令都会附带修改CS或者其他段寄存器内容


段描述符表(Segment Descriptor Tables)

段描述符表是一个段描述符的数组,一个描述符表的大小最多为8192(2^13)8byte的描述符

有两种类型的描述符表

全局描述符表(Global Descriptor Table,GDT)

局部描述符表(Local Descriptor Table,LDT)



通常只定义一个GDT,而每个进程除了存放在GDT中的段之外还需要创建附加的段,就可以有自己的LDT。GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正被使用的LDT地址和大小存放在ldtr控制寄存器中。


段描述符(Segment Descriptor)


Base  包含段的首字节的线性地址,处理器把3个base address域形成一个32bit值,段基地址应该是要16字节边界对齐的但不是必须的,因为16字节对齐可以使程序性能最大化通过代码与数据16字节边界对齐

G     粒度(granularity)标志:如果该位清0,则段大小以字节为单位,否则以4096字节的倍数计

D/B   称为D或B的标志,取决于是代码段还是数据段。D或者B的含义情况在两种情况下稍有区别,但是如果段偏移量的地址是32位长,就是基本上把它置为       1,如果这个偏移量是16位长,它被清0

Limit 存放段中最后一个内存单元的偏移量,从而决定段的长度。如果G被设置为0,则一个段的大小在1个字节到1MB之间变化;否则,将在4KB到4GB之间       变化    

P     Segment-Present标志:等于0表示段当前不在主存中。Linux总是把这个标志设置为1,因为它从来不把整个段交换到磁盘上去。

DPL   描述符特权级(Descriptor Privilege Level)字段:用于限制对这个段的存取。它表示为访问这个段而要求的CPU最小的优先级。因此,DPL设       为0的段只能当CPL为0时(即在内核态)才可以访问的,而DPL设为3的段对任何CPL值都是可以访问的

S     系统标志:如果它被清零,则这是一个系统段,存储诸如LDT这种关键的数据结构,否则它是一个普通的代码段或数据段

Type  描述了段的类型特征和它的存取权限

有几种不同类型的段以及和它们对应的段描述符。下面列出了Linux中被广泛采用的类型:

代码段描述符

表示这个段描述符代表一个代码段,它可以放在GDT或者LDT中。该描述符置S标志位1

数据段描述符

表示这个段描述符代表一个数据段,它可以放在GDT或者LDT中。该描述符置S标志位1。栈段是通过一般数据段实现的

任务状态段描述符(TSSD)

表示这个段描述符代表一任务状态段(Task State Segment,TSS),也就是说这个段用于保存处理器寄存器的内容。它只能出现在GDT中。根据相应的进程是否正在CPU上运行,其Type字段的值分别为11或9.这个描述符的S标志置为0

局部描述符表描述符(LDTD)

表示这个段描述代表一个包含LDT的段,它只能出现在GDT中。相应的Type字段的值为12,S标志位0。


总结一下分段单元是如何把逻辑地址转换成线性地址的

a、先检查段选择符的TI字段,以决定段描述符保存在哪个描述符表。

b、从段选择符的Index字段计算段描述符的地址,Index字段的值乘以8,这个结果与gdtr或者ldtr寄存器的内容相加

c、把逻辑地址的偏移量与描述符Base字段的值相加就得到了线性地址


Linux中的分段

Linux以非常有限的方式使用分段。与分段相比,Linux更喜欢使用分页方式,因为:

a、当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享同样的一组线性地址

b、Linux设计目标之一是可以把它移植到绝大多数流行的处理器平台上。然而,RISC体系结构对分段的支持有限

2.6版本的Linux只有在80x86结构下才需要使用分段。


运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址:它们分别叫做内核态代码段和内核数据段。

四个主要的Linux段的段描述符字段的值

段         Base       G  Limit    S  Type  DPL  D/B  P

用户代码段  0x00000000  1  0xfffff  1  10    3    1    1

用户数据段  0x00000000  1  0xfffff  1  2     3    1    1

内核代码段  0x00000000  1  0xfffff  1  10    0    1    1

内核数据段  0x00000000  1  0xfffff  1  2     0    1    1


相应的段选择符由宏__USER_CS、__USER_DS、__KERNEL_DS、__KERNEL_CS。例如,为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的值装进cs段寄存器即可

注意,与段相关的线性地址从0开始,达到2^32-1的寻址限长。这意味着在用户态或者内核态下的所有进程可以使用相同的逻辑地址。

所有段都从0x00000000开始,这可以得出另一个重要结论,那就是在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的

摘自linux-2.6.32/arch/x86/include/asm/segment.h

#define GDT_ENTRY_DEFAULT_USER_CS	14
#define GDT_ENTRY_DEFAULT_USER_DS	15
#define GDT_ENTRY_KERNEL_BASE	12
#define GDT_ENTRY_KERNEL_CS		(GDT_ENTRY_KERNEL_BASE + 0)
#define GDT_ENTRY_KERNEL_DS		(GDT_ENTRY_KERNEL_BASE + 1)
#define __KERNEL_CS	(GDT_ENTRY_KERNEL_CS * 8)
#define __KERNEL_DS	(GDT_ENTRY_KERNEL_DS * 8)
#define __USER_DS     (GDT_ENTRY_DEFAULT_USER_DS* 8 + 3)
#define __USER_CS     (GDT_ENTRY_DEFAULT_USER_CS* 8 + 3)

Linux GDT

在单处理器系统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT。所有的GDT都存放在cpu_gdt_table数组中。

每一个GDT中包含的18个段描述符指向下列的段:

a、用户态和内核态下的代码段和数据段共4个

b、任务状态段(TSS),每个处理器有1个。每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个子集。

c、一个包括缺省局部描述符表的段,这个段通常是被所有进程共享的段

d、3个局部线程存储段(Thread-Local Storage,TLS):这种机制允许多线程应用程序使用最多3个局部于线程的数据段。系统调用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段


Linux LDT

大多数用户态下的Linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT共大多数进程共享。


内存寻址一(分段)