【引自ShareHub的博客】1.前言 最近在学习Linux内核,看了《深入理解Linux内核》的内存寻址章节。我以为我已经了解了分段和分页机制,结果发现我了解的不多。于是,查找了很多资料,终于理清了内存寻址的知识点。现记录下自己的理解,希望对内核学习者有所帮助,错误之处还望大家指出。 二、什么是分段?知道为什么不知道为什么。我们先来看看分段机制的历史。 实模式的诞生(16位处理器与寻址) 在8086处理器诞生之前,内存寻址方式是直接访问物理地址。为了寻址1M的内存空间,8086处理器将地址总线扩展到20位。然而,一个尴尬的问题出现了,ALU的宽度只有16位,也就是说ALU无法计算出20位的地址。为了解决这个问题,切分机制被引入并走上了历史舞台。 为了支持段,8086处理器设置了四个段寄存器:CS、DS、SS、ES。每个段寄存器都是16位,访问内存的指令中的地址也是16位。然而,在将它发送到地址总线之前,CPU首先将它添加到段寄存器中的值。这里注意:段寄存器的值对应的是20位地址总线中的高16位,所以加法实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留。这样就形成了一个20位的实际地址,实现了从16位的内存地址到20位的实际地址的转换,或者说“映射”。 保护模式的诞生(32位处理器与寻址) ◆80286处理器的地址总线为24位,寻址空间达到16M,同时引入保护模式(访问内存segmentisrestricted) ◆80386处理器是32位处理器,ALU和地址总线都是32位的,寻址空间最大可达4G。也就是说可以直接访问4G的内存空间,不需要经过分段机制。虽然它是新时代的小王子,超越了无数前辈,但它也需要承载家族的使命——兼容上一代处理器。即必须支持实模式和保护模式。所以80386在段寄存器的基础上建立了保护模式,预留了16位的段寄存器。 ◆从80386开始的处理器架构基本相似,统称为IA32(Intel32BitArchitecture)。 3.IA32的内存寻址机制 寻址硬件 在8086的实模式下,将某个寄存器左移4位,然后与地址ADDR相加,直接发送到内存总线,相加后的地址为内存单元的物理地址,这个地址在程序中称为逻辑地址(或虚拟地址)。在IA32的保护模式下,这个逻辑地址不是直接发送到内存总线,而是发送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其作用是将逻辑地址映射到物理地址,即进行地址转换,如图所示。 MMU IA32三种地址 ◆逻辑地址:机器语言指令仍然使用这种地址来指定一个操作数的地址或一条指令的地址。这种寻址方式在Intel的分段结构中尤为特殊,它允许MS-DOS或Windows程序员将程序划分为段。每个逻辑地址由段和偏移量组成。 ◆线性地址:线性地址是一个32位的无符号整数,最多可以表示232(4GB)个地址。线性地址通常用十六进制表示,取值范围为0x00000000~0xffffffff。 ◆物理地址:即内存单元的实际地址,用于芯片级内存单元寻址。物理地址也由32位无符号整数表示。 MMU地址转换过程 MMU是一个硬件电路,它包括两个元件,一个是分段元件,一个是分页元件。这里,我们分别称它们为分段机制和分页机制。从逻辑的角度理解硬件的实现机制是有好处的。分段机制将逻辑地址转换为线性地址;然后,分页机制将线性地址转换为物理地址。 MMU_translate IA32的段寄存器 IA32中有6个16位的段寄存器:CS、DS、SS、ES、FS、GS。与8086的段寄存器不同,这些寄存器不再存放段的基地址,而是存放段的选择符。#p# 4.分段机制的实现 段是虚拟地址空间的基本单位。分段机制必须将虚拟地址空间中的地址转换为线性地址空间中的线性地址。 要实现这个映射,仅仅使用段寄存器来确定一个基地址是不够的,至少要描述段的长度,还有一些关于段的其他信息,比如访问权限等必需的。因此,这里需要的是一个数据结构,它包括三个方面: 1。段基地址(BaseAddress):段在线性地址空间中的起始地址。 2.段限制(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。 3.报文段的保护属性(Attribute):表示报文段的特性。例如,段是否可读或可写,或者段是否作为程序执行,段的特权级等。 以上数据结构称为段描述符,由多个段描述符组成的表称为段描述符表一个8字节的内存位置。在实模式下,一个段的属性无非就是代码段、堆栈段、数据段、段起始地址、段长度等,而在保护模式下就复杂多了。IA32将它们与一个称为描述符的8字节数字结合起来。 IA32通用段描述符的结构 从图中可以看出,一个段描述符表示段的32位基地址和20位的段限制(即段长度)。这里我们只关注基地址和段边界,其他属性略过。 1。段描述符表 各种用户描述符和系统描述符放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表(即段表)定义了IA32系统所有段的情况。所有的描述符表本身都占用一个字节为8的倍数的内存空间,该空间的大小在8字节(至少包含一个描述符)到64K字节(最多包含8K)的描述符之间。 2.GlobalDescriptorTable(GDT) GlobalDescriptorTableGDT(GlobalDescriptorTable),除了任务门、中断门和陷阱门的描述符外,还包含了系统中所有任务共享的那些段的描述符。它的第一个八位字节位置没有被使用。 3.InterruptDescriptorTableIDT(中断描述符表) InterruptDescriptorTableIDT(中断描述符表),包括256个门描述符。IDT只能包含任务门、中断门和陷阱门描述符。IDT表虽然最大可达64K字节,但只能访问2K字节以内的描述符,即256个描述符。这个数字是为了保持与8086的兼容性。 LocalDescriptorTable(LDT) LocalDescriptorTableLDT(本地描述符表),包含与给定任务相关的描述符,每个任务都有一个LDT。使用LDT,给定任务的代码和数据可以与其他任务隔离。每个任务的局部描述符表LDT也用一个描述符来表示,称为LDT描述符,它包含了局部描述符表的信息,放在全局描述符表GDT中。 总结 IA32的内存寻址机制完成了逻辑地址-线性地址-物理地址的转换。其中,逻辑地址的段寄存器中的值提供了段描述符,然后从段描述符中得到段基地址和段边界,再加上逻辑地址的偏移量得到一个线性地址。线性地址通过分页机制得到物理地址。 首先要明确一点,分段机制是IA32提供的寻址方式,是硬件层面的。也就是说不管你是windows还是linux,只要是使用IA32的CPU访问内存,都必须经过MMU的转换过程才能得到物理地址,也就是说必须经过逻辑地址-线性地址-物理地址的转换。 5.Linux中Segmentation的实现 Segmentation机制的实现说了这么多。其实对于Linux来说,是没有用的。因为,Linux基本上没有使用分段机制,或者说,Linux中的分段机制只是为了兼容IA32硬件而设计的。 Intel微处理器的段机制是从8086提出来的,当时引入的段机制解决了CPU内部16位地址到20位真实地址的转换。为了保持这种兼容性,386仍然使用了段机制,只是比以前复杂了很多。因此,Linux内核的设计并没有全部采用Intel提供的段方案,只是在有限的范围内使用了段机制。这不仅简化了Linux内核的设计,也为Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,了解段机制的相关知识是进入Linux内核的必由之路。 从2.2版本开始,Linux允许所有进程(或任务)使用相同的逻辑地址空间,因此不需要使用局部描述符表LDT。但是内核中也用到了LDT,而且只有在VM86模式下运行Wine时才会用到,因为在Linux上模拟Winodws软件或者DOS软件程序时都会用到。 任意给定IA32上的地址是虚拟地址,即任意地址通过“selector:offset”的方式给定,这是段机制内存访问方式的基本特征。因此,在IA32上设计操作系统时,段机制是绕不开的。一个虚拟地址最终会通过“段基地址+偏移量”的方式转换为线性地址。但是,由于大多数硬件平台都不支持段机制,只支持分页机制,所以为了让Linux更加便携,我们需要去掉段机制,只使用分页机制。但遗憾的是,IA32规定不能禁止段机制,所以绕过它直接给出线性地址空间的地址是不可能的。无奈之下,Linux的设计者干脆让段的基地址为0,段的上限为4GB。此时,如果任意给定一个偏移量,等式为“0+偏移量=线性地址”,即“偏移量=线性地址”。另外,由于段机制规定了“offset<4GB”,offset的范围是0H~FFFFFFFFH,恰好是线性地址空间的范围,也就是说,虚拟地址直接映射到线性地址。我们后面提到的虚拟地址Address和线性地址指的是同一个地址。看来Linux在没有避开段机制的情况下巧妙地绕过了段机制。 另外,由于IA32的段机制也规定必须为代码段和数据段创建不同的段,因此Linux必须为代码段创建一个基地址为0,段限制为4GB的段描述符和数据段。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级3,根据IA32段保护机制,特权级3的程序不能访问特权级0的段,所以Linux必须是内核用户程序分别创建它们的代码和数据段。这意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。 6。总结 分段机制是IA32架构CPU的一个特点,并不是操作系统寻址方式的必然选择。Linux为了跨平台,巧妙地绕过了段机制,主要使用分页机制进行寻址。 参考 《深入分析Linux内核源码》
