深入理解计算机系统-之-内存寻址--linux中分段机制的实现方式
Posted CHENG Jian
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解计算机系统-之-内存寻址--linux中分段机制的实现方式相关的知识,希望对你有一定的参考价值。
linux中的分段机制
前面说了那么多关于分段机制的实现,其实,Linux以非常有限的方式使用分段。因为,Linux基本不使用分段的机制(注:并不是不使用,使用分段方式还是必须的,会简化程序的编写和运行方式),或者说,Linux中的分段机制只是为了兼容IA32的硬件而设计的。实际上,分段和分页在某种程度上显得有些多余,因为它们都可以划分进程的物理地址空间,分段可以给每一个进程分配不同的线性地址,而分页可以把同一线性地址,映射到不同的物理地址空间。与分段相比,linux更喜欢分页方式,因为:
当所有进程使用相同的段寄存器值时,内存管理变得简单,因为他们可以共享同样的一组线性地址,或者更通俗的说,虚拟地址与线性地址一致。
linux设计目标之一是可以把它移植到绝大多数流行的处理器平台。然而,RISC体系结构的对分段支持很有限。
Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。
从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。2.6版的linux也只有在80x86结构下才使用分段。
在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作 系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。 万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说 “偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说 虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。
linux中的GDT
在单处理器的系统中只有一个GDT,但是在多处理器系统中每个CPU对应一个GDT。
所有的GDT均存储在cpu_gdt_table数组中,而所有GDT的地址和它们的大小被存放在cpu_gdt_descr数组中。
每个GDT包含18个段和14个空的、未使用的或者保留的项。插入未使用的的目的是为了使经常一起访问的描述符能够在处于同一个32字的硬件告诉缓冲行中。
而那些被使用的18个段必定是如下几种段类型
用户态和内核态的数据段以及代码段4个段
由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB 的段描述符。
不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32段保护机制规定,特权级3的程序是无法访问特权级为 0的段的,所以Linux必须为内核用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。
相应的段描述符由宏__USER_CS, __USER_DS, __KERNEL_CS和__KERNEL_DS分别定义。因此为了对内核代码段寻址,内核只需要将__KERNEL_CS的值装载进CS段寄存器即可。
注意
与段相关的线性地址从0开始,达到 232−1 的寻址长度。这就意味着在用户态和内核态下所有进行均可使用相同的逻辑地址。而所有的段都是从地址0x00000000开始的,我们可以知道在linux下逻辑地址与线性地址一致(linux并没有过多的使用分段技术),即逻辑地址的偏移量字段与相应的线性地址字段的值是一致的。
任务状态段TSS
TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。
TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。
TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域、内层堆栈指针区域、地址映射寄存器区域、寄存器保存区域和其它字段等五个区域。
寄存器保存区域
寄存器保存区域位于TSS内偏移20H至5FH处,用于保存通用寄存器、段寄存器、指令指针和标志寄存器。当TSS对应的任务正在执行时,保存区域是未定义的;在当前任务被切换出时,这些寄存器的当前值就保存在该区域。当下次切换回原任务时,再从保存区域恢复出这些寄存器的值,从而,使处理器恢复成该任务换出前的状态,最终使任务能够恢复执行。
各通用寄存器对应一个32位的双字,指令指针和标志寄存器各对应一个32位的双字;各段寄存器也对应一个32位的双字,段寄存器中的选择子只有16位,安排再双字的低16位,高16位未用,一般应填为0。
内层堆栈指针区域
为了有效地实现保护,同一个任务在不同的特权级下使用不同的堆栈。例如,当从外层特权级3变换到内层特权级0时,任务使用的堆栈也同时从3级变换到0级堆栈;当从内层特权级0变换到外层特权级3时,任务使用的堆栈也同时从0级堆栈变换到3级堆栈。所以,一个任务可能具有四个堆栈,对应四个特权级。四个堆栈需要四个堆栈指针。
但是,当特权级由内层向外层变换时,并不把内层堆栈的指针保存到TSS的内层堆栈指针区域。实际上,处理器从不向该区域进行写入,除非程序设计者认为改变该区域的值。这表明向内层转移时,总是把内层堆栈认为是一个空栈。因此,不允许发生同级内层转移的递归,一旦发生向某级内层的转移,那么返回到外层的正常途径是相匹配的向外层返回。
地址映射寄存器区域
从虚拟地址空间到线性地址空间的映射由GDT和LDT确定,与特定任务相关的部分由LDT确定,而LDT又由LDTR确定。如果采用分页机制,那么由线性地址空间到物理地址空间的映射由包含页目录表起始物理地址的控制寄存器CR3确定。所以,与特定任务相关的虚拟地址空间到物理地址空间的映射由LDTR和CR3确定。显然,随着任务的切换,地址映射关系也要切换。
但是,在任务切换时,处理器并不把换出任务但是的寄存器CR3和LDTR的内容保存到TSS中的地址映射寄存器区域。事实上,处理器也从来不向该区域自动写入。因此,如果程序改变了LDTR或CR3,那么必须把新值人为地保存到TSS中的地址映射寄存器区域相应字段中。可以通过别名技术实现此功能。
链接字段
链接字段安排在TSS内偏移0开始的双字中,其高16位未用。在起链接作用时,地16位保存前一任务的TSS描述符的选择子。
如果当前的任务由段间调用指令CALL或中断/异常而激活,那么链接字段保存被挂起任务的 TSS的选择子,并且标志寄存器EFLAGS中的NT位被置1,使链接字段有效。在返回时,由于NT标志位为1,返回指令RET或中断返回指令IRET将使得控制沿链接字段所指恢复到链上的前一个任务。
其它字段
为了实现输入/输出保护,要使用I/O许可位图。任务使用的I/O许可位图也存放在TSS中,作为TSS的扩展部分。在TSS内偏移66H处的字用于存放I/O许可位图在TSS内的偏移(从TSS开头开始计算)。关于I/O许可位图的作用,以后的文章中将会详细介绍。
在80386中,只定义了一种属性,即调试陷阱。该属性是字的最低位,用T表示。该字的其它位置被保留,必须被置为0。在发生任务切换时,如果进入任务的T位为1,那么在任务切换完成之后,新任务的第一条指令执行之前产生调试陷阱。
3个局部线程存储(Thread-Local Storage,TLS)段
线程局部存储区(Thread Local Storage, TLS):将数据与一个正在执行的特定函数关联起来。这种机制允许多线程应用程序使用最多3个局部于线程的数据段。
linux系统可以使用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段。
线程局部存储是将现有函数变为线程安全的有用技巧。
当一个函数中访问并修改全局或静态变量,那么这个函数就是不可重入的。若使之变为可重入的函数,可以使用线程同步,也可以使用线程局部存储。线程局部存储为每一个访问此变量的线程提供一个此变量独立的副本,线程可以修改此变量,而不会影响到其他线程。
注:通过以上描述可以看出,线程局部存储不是用来共享变量的。
具体可参照 每天进步一点点——Linux中的线程局部存储
与高级电源管理(AMP)相关的3个段
由于Bios代码使用了分段机制,所以当linux APM驱动程序调用BIOS函数来获取或者设置APM设备的状态时,就可以使用自定义的代码段和数据段。
与支持即插即用(PnP)功能的BIOS服务程序相关的5个段
前面一种情况下,就像前述与APM相关的3个段的情况一样,由于BIOS例程使用段,所以当linux的PnP设备驱动程序调用BIOS函数来检测PnP设备使用的资源时,就可以使用自定义的代码段和数据段。
处理”双重错误”异常的特殊TSS段
处理一个异常的时候可能会引发另外一个异常,在这种情况下产生双重错误。
linux中的LDT
大多数用户态下的linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。
如果在某些情况下,进程仍然需要创建自己的局部描述符表,(例如wine这样的程序,他执行面向段的微软windows应用程序),可以使用modify_ldt()系统调用允许进程创建自己的局部描述符表。
modify_ldt() 读取或一个进程写入本地描述符表(ldt)。 ldt 是使用i386处理器每个进程的内存管理表。对于该表的详细信息,请参阅英特尔386处理器手册。
任何被modify_ldt()创建的自定义局部描述符表仍然需要他自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该CPU的GDT副本中的LDT表项相应的就被修改了。
用户态的程序同样也利用modify_ldt()来分配新的段,但内核却从不使用这些段,它也不需要了解相应的段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中。
linux中GDT,LDT和IDT结构定义
GDT描述符表gdt_page定义
struct gdt_page
struct desc_struct gdt[GDT_ENTRIES];
__attribute__((aligned(PAGE_SIZE)));
DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);
段描述符结构定义
ia32机器上的定义
定义在arch/x86/include/asm/desc_defs.h文件中
struct desc_struct
union
struct
unsigned int a;
unsigned int b;
;
struct
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
;
;
__attribute__((packed));
联合体——对成员域访问和设置成为一种很优美的方法。上面第一个匿名结构体用来作为成员访问取值的出口,下面第二个结构体对真实的成员设置值的入口。
字段 | 描述 |
---|---|
limit | 段长度 |
base | 段的首字节的线性地址,有base0,base1,base2三部分构成 |
type | 段的类型和存取权限 |
s | 系统标志。1-系统段;0-普通段 |
dpl | 描述符特权级 |
p | segment-Present。linux下总是1 |
avl | linux不用 |
d | 区分代码段还是数据段 |
g | 段大小粒度。以4K倍数计算 |
在32位机器上,这就是所有描述符的数据结构喽,没有细分门和非门!
typedef struct desc_struct gate_desc;
typedef struct desc_struct ldt_desc;
typedef struct desc_struct tss_desc;
由于三类描述符都是一个结构类型,从而一律使用下面宏初始化在GDT中表项
#define GDT_ENTRY_INIT(flags, base, limit) \\
.a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \\
.b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \\
((limit) & 0xf0000) | ((base) & 0xff000000), \\
x64机器上的定义
但是在64位机器上,Linux则进行了细致划分:
/* 16byte gate */
struct gate_struct64
u16 offset_low;
u16 segment;
unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
u16 offset_middle;
u32 offset_high;
u32 zero1;
__attribute__((packed));
16字节LDT或TSS描述符结构
/* LDT or TSS descriptor in the GDT. 16 bytes. */
struct ldttss_desc64
u16 limit0;
u16 base0;
unsigned base1 : 8, type : 5, dpl : 2, p : 1;
unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8;
u32 base3;
u32 zero1;
__attribute__((packed));
typedef struct gate_struct64 gate_desc;
typedef struct ldttss_desc64 ldt_desc;
typedef struct ldttss_desc64 tss_desc;
从上面代码看出无论是32位还是64位机器上,都使用typedef重新定义,以提供给系统其他使用此描述符的部分一致的类型名
区分描述符的枚举量
enum
GATE_INTERRUPT = 0xE,
GATE_TRAP = 0xF,
GATE_CALL = 0xC,
GATE_TASK = 0x5,
;
enum
DESC_TSS = 0x9,
DESC_LDT = 0x2,
DESCTYPE_S = 0x10, /* !system */
;
系统GDT,IDT指针描述结构
struct desc_ptr
unsigned short size;
unsigned long address;
__attribute__((packed)) ;
这个结构记录了系统的GDT或者IDT的大小以及在系统中的线性基地
以上是关于深入理解计算机系统-之-内存寻址--linux中分段机制的实现方式的主要内容,如果未能解决你的问题,请参考以下文章