内存寻址(一) —— 基本概念与机制

1. 内存地址#

在编程中我们(编译器)为各种数据分配的内存均为逻辑地址,逻辑地址通过操作系统转换为物理地址。在使用Intel 80x86处理器时,应当分清以下三种“地址”:

  • 逻辑地址(logical address)
  • 线性地址(linear address)
  • 物理地址(physical address)

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

image

2. 分段机制#

最早的程序直接读写物理内存。在这种情况下,通常同时运行两个程序是不可能的,因为它们很可能同时访问同一个物理地址导致程序崩溃。

当Intel 8086处理器出现时,内存寻址迎来第一次飞跃,它引入了一个重要的概念——段,段式内存使得程序地址不再需要静态重定位,支持了更大的地址。

8086处理器的数据总线是16位的,但其目标是寻址1MB的内存,这意味着需要20位的地址总线。为了解决这个问题,当时引入了分段的方法。

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

image

2.1 段寄存器#

为了方便找到段选择符,处理器提供6个段寄存器专门用来存放段选择符。它们是cs、ss、ds、es、fs、gs

  • cs为代码段寄存器;
  • ss为栈段寄存器;
  • ds为数据段寄存器;

其他三个段寄存器作一般用途,6个段寄存器均为16位

2.2 段描述符#

每个段由一个8字节的段描述符(segment descriptor)来具体描述。段描述符放在全局描述符表(Global Descriptor Table ,GDT)或局部描述符表(Local Descriptor Table, LDT)中。段描述符被存在一个非编程CPU寄存器中,非编程寄存器对程序员不可见,仅供6个可编程的段寄存器使用。

image

B31 ~ B24 和 B23 ~ B16分别为基地址的 bit16 ~ bit23 和 bit24 ~ bit31
L19 ~ L16和L15 ~ L0为段LIMIT的 bit10 ~ bit15 和 bit16 ~ bit19

4位type域描述了段的类型特征和存取权限,广泛使用如下几种:
1.代码段描述符(描述符代表一个代码段,可以放在GDT或LDT中,S标志位为1)
2.数据段描述符(描述符代表一个数据段,可以放在GDT或LDT中,S标志位为1)
3.任务状态段描述符(代表一个任务状态段(TSS),只能存放在GDT中,S标志位为9或11)
4.局部描述符表描述符(代表一个LDT段,只能存放在GDT中,S标志位为0)

2.3 段选择符#

之前提到16位的段选择符存放在段寄存器中,它直接指向段描述符。每当一个段选择符被装入段寄存器,相应的段描述符就被装入非编程寄存器。此时CPU只需直接引用段描述符所在寄存器即可,不需访问主存中的GDT或LDT。仅当段寄存器的内容改变时,才有必要访问GDT或LDT。

image

段选择符包含以下域:

  • 13位的索引,指定了段描述符在GDT或LDT中的入口。
  • TI标志指明了段描述符是在GDT中(TI = 0)还是在LDT中( TI = 1)
  • 两位RPL(请求特权级)

image

2.4 逻辑地址到线性地址的转换#

分段单元(segmentationunit)执行以下操作:

先检查段选择符的TI字段,以决定段描述符保存在哪一个描述符表中。TI字段指明描述符是在GDT中(在这种情况下,分段单元从GDTR寄存器中得到GDT的线性基地址)还是在LDT中(在这种情况下,分段单元从LDTR寄存器中得到LDT的线性基地址)

再来看一下index部分。我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引。

现在我们假设有一个段的段选择符,它的TI=0,index=8。我们可以知道这个段的描述符是在GDT数组中,并且他的在数组中的索引是8。

假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index(只所以乘8是因为地址是连续的,假设内存地址最小单位为字节,index=8,一个段描述符大小为8字节,那么乘8是为了将前面7个段描述符跳过直接定位到第8个地址),从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。

image

3. 线性地址到物理地址转换#

分页单元在分段之后运行,把线性地址转换成物理地址

分页单元的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)(把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。)。注意,为了实现每个任务平摊虚拟内存,每个任务都有自己的页目录表和页表。

为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。

32 位的线性地址被分成 3 个域:

  • Directory(目录):最高10 位
  • Table(页表):中间10 位
  • Offset(偏移量):最低12 位

最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。

页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。

页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。

还是以 mov 0x80495b0, %eax 中的地址为例分析一下线性地址转物理地址的过程。

内核先将当前任务的页目录表的物理地址填入cr3寄存器。

线性地址 0x80495b0 转换成二进制后是 0000 1000 0000 0100 1001 0101 1011 0000,最高10位0000 1000 00的十进制是32,CPU查看页目录表第32项,里面存放的是页表的物理地址。

线性地址中间10位00 0100 1001 的十进制是73,页表的第73项存储的是最终物理页的物理起始地址。

物理页基地址加上线性地址中最低12位的偏移量,CPU就找到了线性地址最终对应的物理内存单元。

image

怎样防止进程访问不属于自己的线性地址(如内核空间)或无效的地址呢?内核里记录着每个进程能访问的线性地址范围(进程的vm_area_struct 线性区链表和红黑树里存放着),在引发缺页异常的时候,如果内核检查到引发缺页的线性地址不在进程的线性地址范围内,就发出SIGSEGV信号,进程结束,我们将看到程序员最讨厌看到的Segmentation fault。

posted @   ZT丶  阅读(576)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示
主题色彩