x86的内存寻址大家都懂,为了兼容以前的产品,intel保留了段机制,然而linux中弱化了这一机制。下面先说下段机制的历史:
早在8086的时候cpu的地址总线是20根,这样本可以对2^20=1M的地址空间进行寻址,但是由于其数据总线的位宽以及提供段内偏移地址的寄存器位宽只有16位,造成了8086只能最大寻址到2^16=64k的尴尬局面。为了解决这个问题,intel在8086中加了4个段寄存器,分别是我们熟知的CS, DS, SS, ES,并用于代码段,数据段,堆栈段和其他段。同时添加了地址加法器,这使得寻址范围得以扩大,这也是分段机制的由来。不过,这只是时模式下的分段机制,在80286时代,intel引入了保护模式,内存访问收到了级别的限制。然而,80286的地址总线虽然扩大到了24位,但是其数据总线和段内偏移地址寄存器为了兼容前面的芯片依旧保持16位,因此286的段内偏移依旧限制在64k。但是到了386时代,cpu的数据总线和段内偏移地址寄存器达到了32位,这使得cpu的寻址能力扩大到了4G空间。cpu的寻址能力达到4G,但是intel为了兼容前面的产品依旧保留了段机制。这就是段机制的由来和延续至今的原因。
下面主要介绍linux中保护模式下的分段机制。
在32位的cpu中有6个段寄存器,分别是cs,ss,ds,es,fs和gs。其中cs用作代码段寄存器;ss用于栈段寄存器;ds用于数据段寄存器。
剩下的3个段寄存器用于其它用途,可以指向任意数据段。这些段寄存器都是16位寄存器,在其中存放的是段选择符,段选择符的格式如下图所示:
其中RPL记录该段的特权级,TI是表指示器,用于指示是全局描述符表还是局部描述符表。
下面我写了一个很简单的hello world程序用以查看linux中这几个段描述符中所存的段选择符到底是什么。
代码如下:
#include
void main (void)
{
printf ("hello world!\n");
}
编译之后在gdb中运行,用info registers查看其寄存器的值,情况如下:
可以看到CS的值为0x73,而其他三个主要的段寄存器都被置为0x7b对应的十进制数分别是115和123。大家可能会奇怪为什么是这样的,其实在linux中是这样定义的__USER_CS和__USER_DS的:
189 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8+3) 190 #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8+3)
而GDT_ENTRY_DEFAULT_USER_CS以及
GDT_ENTRY_DEFAULT_USER_DS的定义是这样的:
74 #define GDT_ENTRY_DEFAULT_USER_CS 14 75 76 #define GDT_ENTRY_DEFAULT_USER_DS 15
这样可以算得__USER_DS的值为8*15+3=123, 而__USER_CS的值为8*14+3=115和上面我们在程序中看到的值是一样的。那么__USER_CS对应的二进制数为:0000000001110011可以看出__USER_CS的RPL=3,TI=0,而索引号为14。同理__USER_DS的二进制数为:0000000001111011所以其RPL=3,TI=0,索引号为15。
在内核中也定义了__KERNEL_CS和__KERNEL_DS
187 #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8) 188 #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS*8)
其中GDT_ENTRY_KERNEL_CS和
GDT_ENTRY_KERNEL_DS的定义如下
78 #define GDT_ENTRY_KERNEL_BASE (12) 79 80 #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE+0) 81 82 #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE+1)
由此可以算得__KERNEL_CS的值为96而__KERNEL_DS为104,其对应的二进制数为0000000001100000和0000000001101000这样可以看到其RPL=0,TI=0在GDT中的索引号分别为12和13.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
但是这里有个非常奇怪的问题,当我使用内核模块和添加系统调用的方法分别打印出__KERNEL_CS和__KERNEL_DS时,发现ds寄存器的值并不是__KERNEL_DS所定义的值,而是用户态__USER_DS的值。尝试去解决,但是目前还没有搞定,先mark在这,留待以后再更新。
这里之前和redhat的一个程序员讨论过,安他的意思是2.6以后的内核就开始用__USER_DS了,原因是这样的访问首先在权限上是么有问题的,然后在安全性方面也是没有问题....然后就用了这个...
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
在这里有必要提出关于这几个寄存器的情况,大家看到的资料上都会讲这几个寄存器是16位寄存器,如果大家去看ULK上面会讲到在x86中有一种非编程寄存器用作缓存段描述符然后他给出的图如下:
大家看到的非编程寄存器是适合段寄存器分开的,这样容易造成误解,其实在intel文档中是这样的描述的:
大家可以看到所有的段寄存器都有可见部分和隐藏部分,当可见部分被载入时隐藏部分也会相应的载入段描述符表中相应的信息,这样在下一次取出信息时就不用再访问内存了,提高了效率。下面是intel文档的原文描述:
Every segment register has a “visible” part and a “hidden” part. (The hidden part is sometimes referred to as a “descriptor cache” or a “shadow register.”) When a segment selector is loaded into the visible part of a segment register, the processor also loads the hidden part of the segment register with the base address, segment limit, and access control information from the segment descriptor pointed to by the
segment selector. The information cached in the segment register (visible and hidden) allows the processor to translate addresses without taking extra bus cycles to read the base address and limit from the segment descriptor. In systems in which multiple processors have access to the same descriptor tables, it is the responsibility of software to reload the segment registers when the descriptor tables are modified.
If this is not done, an old segment descriptor cached in a segment register might be used after its memory-resident version has been modified.
下面介绍段描述符,段描述符存放在段描述符表中,由8个字节组成,其中包含了段基质base,最大偏移量limit以及一些控制信息,这些将在下面的文章中详细介绍。
intel给出的段描述符的结构如下:
可以看出,再8个字节的段描述符表中用0~3字节中的0~15位以及4~8字节中的16~19位一共20位用来表述段的最大偏移量Segment Limit(共20位),用0~3字节中的16~31位以及以及4~8字节中的16~19, 24~31位表示段基址Base(共32位)。
剩下12位的其他信息含义如下:
G:粒度,当G置为0时,此段的大小范围将在1字节到1M字节的范围内;如果G置为1的话此段的范围将在4字节到4G字节的范围内。
(为什么会这样?我的想法是:G就相当于最大段偏移量的单位,如果G为0,则其单位为1字节,如果为1则段偏移量的单位为4字节。这样段最大偏移量再0001H到FFFFH的范围变化其段的大小将在1byte~1Mbyte或4kbyte~4Mbyte之间变化。或者最大段偏移量的计算公式:当G为0时LIMT+1H;当G为1时LIMT*4K+FFFH。究竟时limit的范围在0001H~FFFFH之间还是在0000H~FFFFH之间有待研究)。
D/B:这个位再不同的段中含义不一样,但有一个统一的含义,如果该位是1则表示该段为32位段,如果是0则是16为段,为了与以前的cpu进行兼容。比如:
1)当此段为代码段描述符时,D=1表示该段使用32位地址级32位或8位操作数;当D=0时表示该段使用16位地址,以及16位或8位操作数。 2)当此段为向下拓展数据段时,D=1表示该段上限为FFFFFFFFH(4G),如果是0表示该段上线为FFFFH(64K)。
3)当此段为栈段时,如果D=1表示该栈段使用32位指针,并且其指针存放在ESP寄存器中;如果为0,表示该栈段使用16位指针,其指针存放在SP寄存器中。如果该栈段在向下拓展数据段中该标志位同时也表示栈的上限,同数据段一样。
L(仅限于代码段):这个标志位如果被置1则表示该代码段在64位模式下执行,如果是0表示再32位兼容模式下执行。如果L位被置1那么D位就要被清除,否则再意义上就会有冲突。如果再32位模式下或者该段不是代码段那么该位无用,需被置0。
AVL:Available and reserved bits。时预留给操作系统使用的,但被linux忽略了。
P:当该段在内存中的时候此标志位被置为1否则置为0.如果该段被装载如段寄存器中并发现该段的P标志位为0则cpu抛出一个异常,操作系统将从硬盘中把这个段交换到内存中。因为linux中使用了纯分页机制进行虚拟内存管理,所以在linux系统中该位永远被置为1。
(PS:以前想过一个问题,如果一个操作系统使用段机制,如何进行多任务管理。当看到这个标志位的时候瞬间明白了。类似于分页机制系统可以把不同程序的段装载的内存中,但是cpu每次只能运行一个进程,所以可以将当前正在运行的程序的相关段装载入内存。如果进行进程的切换便将现在的段对换入硬盘的交换分区,用这样的方式实现虚拟内存管理是可行的。但是这样的方式再进程切换的效率上是极低的,因为如果进行进程切换就将进行段的换入换处操作;而且这种虚拟内存的管理运用在多处理机的cpu上就会出现不同进程在物理地址的映射上发生冲突的事情。这可能也是大多造作系统抛弃段机制,而使用纯的分页机制进行虚拟内存管理的一个原因吧。这只是个人观点,求拍砖.....)
当P置0时intel给出了段描述符的格式如下:
DPL:段的特权级,范围从0~3,但是linux中只用了0和3这两个特权级。
S:如果该位被置0则表示该段为系统段,里面存储了LDT等这种关键的数据结果。如果为1则表示该段为一普通的代码段或数据段等。
TYPE:描述了段的类型特征和它的存取权限。
如下,intel给出了TYPE成员的不同值所对应的权限表:
那么如何通过段机制将逻辑地址转换成线性地址,下面将进行说明:
intel给出了三种使用分段的模式,分别是基本平模式,保护平模式,和多段模式,并且给出了三种分段模式的示意图如下:
如上便是基本平模式,定义段机制为0,最大偏移量为4G(FFFFFFFFH),将所有的段机制寄存器指向同一个段描述符。
此图为保护平模式,他是将代码段和其他段分开存放。
首先声明,在linux中使用了第一种最简单的分段模式,至于为什么使用这中分段模式,ULK中给出了解释,大致是因为linux需要移植到大多数平台上,但是RISC的体系对分段支持有限;同时,当所有的进程都使用相同的段寄存器的值的时候内存管理将变得简单。
以上便是linux段机制的简单总结.....