x86的内存寻址方式

news2024/9/29 7:17:22

文章目录

    • 一、实模式寻址
    • 二、保护模式寻址
    • 三、段页式内存管理
    • 四、Linux的内存寻址
    • 五、进程与内存
      • 1、内核空间和用户空间
      • 2、内存映射
      • 3、进程内存分配与回收

一、实模式寻址

在16位的8086时代,CPU为了能寻址超过16位地址能表示的最大空间(因为 8086 的地址线 20 位而数据线 16 位),引入了段寄存器。通过将内存空间划分为若干个段(段寄存器像 ds、cs、ss 这些寄存器用于存放段基址),然后采用段基地址+段内偏移的方式访问内存,这样能访问1MB的内存空间了。

使用这样的寻址方式的好处是所见即所得,程序员指定的地址就是物理地址,物理地址对程序员是可见的。但是,由此也带来两个问题:

  1. 无法支持多任务
  2. 程序的安全性无法得到保证(用户程序可以改写系统空间或者其他用户的程序内容)。

实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,系统程序和用户程序没有区别对待,而且每一个指针都是指向"实在"的物理地址。

这样一来,用户程序的一个指针如果指向了系统程序区域或其他用户程序区域,并改变了值,那么对于这个被修改的系统程序或用户程序,其后果就很可能是灾难性的。

为了克服这种低劣的内存管理方式,处理器厂商开发出保护模式:物理内存地址不能直接被程序访问,程序内部的地址(虚拟地址)要由操作系统转化为物理地址去访问,从而保护进程地址空间,程序对此一无所知。

二、保护模式寻址

从IA-32开始,cpu有三种工作方式:实模式,保护模式和虚拟8086模式。只有在刚刚启动的时候是实模式,等操作系统运行起来以后就运行在保护模式。虚拟8086模式是运行在保护模式中的实模式,为了在32位保护模式下执行纯16位程序,它不是一个真正的CPU模式,还属于保护模式。在保护模式下,CPU 有更强的寻址能力。

两者的区别主要体现在段寄存器如CS,DS,ES,SS的解释方式不同——实模式解释为段寄存器,保护模式解释为段选择子。

在实模式(也就是16位模式)情况下,一个地址由段和偏移两部分组成,计算公式为:Segment << 4 + Offset(所以实模式下只能访问1M的内存空间)。CS (代码段寄存器) 和 DS (数据段寄存器) 主要用于存储代码段和数据段的起始地址。

在保护模式下寻址方式还是段基址+偏移地址。但是此时寄存器不再直接存储段的基地址,而是存储段描述符表中的索引即段选择子,如下:

|-----------------------------------|-----|--------|
|              索引号(13)            |TI(1)| RPL(2) |
|-----------------------------------|-----|--------|
TI:表指示器,表示使用的是哪个段描述符表(GDT或LDT)
RPL:请求者特权级

段基址不直接放在段寄存器了,而是在 GDT即全局描述符表(或LDT即局部描述符表)中,如下:

此外CPU中单独添置了两个寄存器,用来指向这两个表,分别是gdtr和ldtr。在寻址的时候,CPU首先根据段寄存器的TI位和gdtr或ldtr找到描述符表,之后根据段寄存器的索引号到GDT/LDT中找出对应的段描述符,然后再取出这个段的基地址,最后再结合段内的偏移完成内存寻址。

三、段页式内存管理

x86架构的CPU(保护模式下)采用的是分段+分页的内存管理方式,上述根据段基址和偏移地址其实只是段寻址的过程,用于将逻辑地址转换为线性地址,所以还需要进行页寻址,将线性地址转换为物理地址。

要将线性地址转换为物理地址,那就得有地方记录它们之间的映射关系,这是通过页表的实现的。页表是用来记录虚拟内存页面和物理内存页面之间的映射关系的,每一个页表项记录一个页面的映射关系。但进程的地址空间很大,这样算下来需要的页表项的数量也会非常多。而实际上进程地址空间中很多页面都没有真正使用,也就没有映射关系,这样是一种浪费。为了解决这个问题,CPU引入了多级页表的机制,在32位下一般是2级页表,像下面这样:

在这里插入图片描述

线性地址被分为三段,页目录索引、页表索引和页内偏移:

  1. 页目录索引(Page Directory Index):用于在页目录中查找对应的页表入口。
  2. 页表索引(Page Table Index):用于在找到的页表中查找对应的页框入口。
  3. 页内偏移(Offset):在找到的页框内的具体地址。

用页目录索引去页目录(PGD)中拿到页表入口,接着用页表索引去页表(Page Table)中拿到页表项(Page Table Entry)。

页表项(Page Table Entry,简称PTE)是页表的组成部分,其主要目标是存储虚拟地址到物理地址的映射信息。每一个页表项对应一个虚拟页面到物理页面的映射。找到页表项后,就可以找到里面存储的物理内存块的起始地址(其实就是是物理内存编号),把它加上页内偏移就得到了最终的物理地址。我们把这个过程称作页式内存管理。

上面这些地址转换的实现,就是由 MMU 来完成的。

虚拟地址和线性地址:

其实在 Intel IA-32 手册里并没有提到虚拟地址这个术语,但是在内核的确是用到了这个概念,比如__va和__pa这两个宏定义。经过我的考证,virtual address就是linear address的别名,俩词汇是一个意思,内核代码和我们编程中喜欢用virtual address这个术语,而Intel手册里只用linear address这个术语。

引用自Linux 线性地址,逻辑地址和虚拟地址的关系?

四、Linux的内存寻址

按照 Intel 的设计,段式内存管理中的段类型分为代码段、数据段、栈段、扩展段四个段(即对应CPU中的cs、ds、ss、es四个段寄存器),实在是太麻烦了。我们只靠页式内存管理就已经可以完成Linux内核需要的所有功能,根本不需要段映射。

因此Linux为了简化处理,采用了平坦内存模型。在这个模型中,所有的段的基址都被设置为0,限长被设置为最大的地址,也就是4GB(对于32位系统)或更大(对于64位系统)。这样,每一个段都覆盖了整个线性地址空间,即从0到最大的地址。也就是说在Linux系统中虽然保留了段机制,但是进程的代码段、数据段、栈段、扩展段这四个段全部重合了,而且是整个进程地址空间共计4GB成为了一个段。虽然仍然有代码段(CS)和数据段(DS)这样的名词,但它们实际上都指向同一个,覆盖了整个地址空间的段。

所以说起来是分段,实际上等于没分了,再加上段的基地址全部是0,那进行地址翻译的时候,对于任何一个给定的逻辑地址,只需要看它的偏移量就可以知道它在内存中的位置,不需要去查找段描述符表和进行基址加偏移的运算。这大大简化了内存管理,尤其是在进行上下文切换时,因为不需要去加载不同的段表。即虽然逻辑地址和线性地址是两种不同的地址空间,但在Linux中逻辑地址就等于线性地址。

GDT、LDT是供分段式内存使用的设施,Intel/AMD的64位模式下内存并不分段(硬件保证所有段的基址都是0),所以它们就用不上了。32位模式下还是需要的,虽然当今主流32位操作系统也是平坦(flat)内存模型,但是操作系统是以软件将每个段都设置为0基址,也就是通过正确初始化GDT、LDT来实现的。

Linux的段式管理事实上只是“哄骗”了一下硬件而已,按照Intel的本意是需要去通过段描述符来拿到段基址,之后再与偏移地址相加来拿到线性地址的,但是Linux对所有的进程都使用了相同的段来对指令和数据寻址。即所有的段的基地址都是0,段长4G。所以也就是说进程使用的地址可以直接理解为是线性地址,因为段基址都是0,只需要进行页式转换即可。但这并不意味着段机制彻底没用到,CPU的任务管理TSS还是需要用到的。

五、进程与内存

1、内核空间和用户空间

Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G(对32位而言)的线性虚拟空间,其中内核空间占1GB,用户空间占3GB。

  1. 用户空间与内核空间是人为划分的,用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
  2. 用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。
  3. 每个进程的用户空间都是完全独立、互不相干的。

在这里插入图片描述

内核空间包括内核镜像、物理页面表、驱动程序等

用户空间分为五个不同的区域:

  • 代码段:只读,存放可执行文件的操作指令;镜像;
  • 数据段:存放可执行文件中已初始化全局变量;存放静态变量和全局变量;
  • BSS段:未初始化全局变量;
  • 堆:存放被动态分配的内存段;
  • 栈:存放临时创建的局部变量;

在这里插入图片描述

这里的段和前面提到的是不同层次上的概念,可以理解为前面是操作系统的段式管理为每个进程都划分出了一个大的0-4G的段。段式管理中划分为了数据段、代码段、栈段和扩展段,但是在Linux里都不管了,全部都划到这个4G的段里。然后在这个段中再进行一个划分,按照不同属性抽象出了这五个不同的区域,将相同属性的数据集中放在一起。

2、内存映射

物理地址空间是有限的(取决于实际物理设备),虚拟地址空间可以是任意大小(受限于CPU位数),对于32位的CPU,虚拟地址空间可以为4G,其中内核空间占1GB,用户空间占3GB。如果物理内存也是4GB的大小,那么他们之间的映射关系如下图:
在这里插入图片描述
因为内核的虚拟地址空间只有1GB,但它需要访问整个4GB的物理空间,因此从物理地址0~896MB的部分(ZONE_DMA+ZONE_NORMAL),直接加上3GB的偏移(在Linux中用PAGE_OFFSET表示),就得到了对应的虚拟地址,这种映射方式被称为线性/直接映射(Direct Map)。

而896M-4GB的物理地址部分(ZONE_HIGHMEM)需要映射到(3G+896M)-4GB这128MB的虚拟地址空间,显然也按线性映射是不行的。采用的是做法是,ZONE_HIGHMEM中的某段物理内存和这128M中的某段虚拟空间建立映射,完成所需操作后需要断开与这部分虚拟空间的映射关系,以便ZONE_HIGHMEM中其他的物理内存可以继续往这个区域映射,即动态映射的方式。

在64位系统中,内核空间的映射变的简单了,因为这时内核的虚拟地址空间已经足够大了,即便它要访问所有的物理内存,直接映射就是,不再需要ZONE_HIGHMEM那种动态映射机制了。

3、进程内存分配与回收

进程所能直接操作的地址都为虚拟地址。当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存(物理页面),获得的仅仅是对一个新的线性地址区间的使用权。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请求页机制”产生“缺页”异常,从而进入分配实际页面的例程。

该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去真正为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了系统的物理内存上(当然,如果页被换出到磁盘,也会产生缺页异常,不过这时不用再建立页表了)。这种请求页机制把页面的分配推迟到不能再推迟为止,节约了空闲内存。

当程序试图访问的内存页面不在物理内存中时(也就是说,这个页面被换出到磁盘(页表项有效位为0),或者还未被分配(没有对应页表项)),处理器会触发一个缺页异常。这个异常会导致当前的程序暂停,并且切换到操作系统的内核模式。

操作系统的缺页异常处理程序会首先检查这个访问是否有效(也就是说,程序是否有权访问这个地址)。如果这个访问是无效的(例如,程序试图访问它没有权限访问的内存),操作系统会终止这个程序。

如果访问是有效的,缺页异常处理程序会试图修复这个异常。它可能会从磁盘的交换空间中读取所需的页面,或者分配一个新的页面。然后,它会更新页表,把虚拟地址映射到新加载或新分配的物理页面上。

页表项(Page Table Entry,PTE)与物理内存基址的对应关系由内存管理单元(MMU)和操作系统的内存管理子系统共同决定。以下是这个过程的大致步骤:

  1. 内存分配:当一个进程需要更多的内存时(例如,因为程序的执行或者动态内存分配请求),操作系统会分配一个或多个物理内存页给这个进程。操作系统通常会选择一些空闲的、未被其他进程使用的内存页进行分配。
  2. 虚拟地址选择:操作系统为这些新分配的内存页选择一些虚拟地址。这些虚拟地址通常会在进程的虚拟地址空间中找到。
  3. 页表更新:操作系统在页表中创建或更新一些页表项,将新选择的虚拟地址映射到新分配的物理内存页。具体来说,操作系统会将每个页表项的物理地址部分设置为对应的物理内存页的基址。
  4. 内存访问:之后,当CPU执行该进程的代码时,如果遇到对这些虚拟地址的访问,MMU会通过查找页表,将虚拟地址转换为对应的物理地址,然后访问对应的物理内存。

这样,页表项和物理内存基址的对应关系就是由操作系统在分配内存和更新页表时确定的。这个对应关系是动态的,可以随着内存的分配和释放,进程的创建和销毁,以及内存管理的其他活动而改变。

我们知道不同的进程之间看到的虚拟地址范围是一样的,所以多个进程下,不同进程的相同的虚拟地址可以映射不同的物理地址。这就会造成歧义问题。例如,进程A将地址0x2000映射物理地址0x4000。进程B将地址0x2000映射物理地址0x5000。当进程A执行的时候将0x2000对应0x4000的映射关系缓存到TLB中。当切换B进程的时候,B进程访问0x2000的数据,会由于命中TLB从物理地址0x4000取数据。这就造成了歧义。

如何消除这种歧义,我们可以借鉴VIVT数据cache的处理方式,在进程切换时将整个TLB无效。切换后的进程都不会命中TLB,但是会导致性能损失。

参考文献:

现代操作系统内存管理到底是分段还是分页,段寄存器还有用吗? - 知乎 (zhihu.com)

逻辑地址、物理地址、虚拟地址_虚拟地址是逻辑地址吗_闫晟的博客-CSDN博客

linux内核中 逻辑地址、虚拟地址、线性地址和物理地址大扫盲

Linux的进程地址空间[一] - 知乎 (zhihu.com)

【转】Linux内存管理(最透彻的一篇) - ralap7 - 博客园 (cnblogs.com)

操作系统中的多级页表到底是为了解决什么问题?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/933547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小研究 - J2EE 应用服务器的软件老化测试研究

软件老化现象是影响软件可靠性的重要因素&#xff0c;长期运行的软件系统存在软件老化现象&#xff0c;这将影响整个业务系统的正常运行&#xff0c;给企事业单位带来无可估量的经济损失。软件老化出现的主要原因是操作系统资源消耗殆尽&#xff0c;导致应用系统的性能下降甚至…

virtuoso61x中集成calibre

以virtuoso618为例&#xff0c;在搭建完电路、完成前仿工作之后绘制版图&#xff0c;版图绘制完成之后需要进行drc和lvs【仅对于学校内部通常的模拟后端流程而言】&#xff0c;一般采用mentor的calibre来完成drc和lvs。 服务器上安装有virtuoso和calibre&#xff0c;但是打开la…

servlet,Filter,责任的设计模式,静态代理

servlet servlet是前端和数据库交互的一个桥梁 静态网页资源的技术&#xff1a;在前端整个运行的过程中 我们的网页代码不发生改变的这种情况就称为静态的网页资源技术动态网页资源的技术&#xff1a;在前端运行的过程中 我们的前端页面代码会发生改变的这种情况就称为 动态的网…

电商版面设计之优惠券设计

1、画一个矩形---最快的方法&#xff0c;提前写好 2、ALT复制矩形图层 3、提前把优惠券的文案准备好 4、改一下字体---72 5、字体改成12号字体 6、上面对齐选择第二个去做&#xff0c;最上方 7、后面那个就是门槛 8、用Alt复制4个 9、改字就行 10、看见不错的优惠劵设计可以参…

word如何调整页码

文章目录 如何调整页码 如何调整页码 用 word 写报告的时候&#xff0c;经常遇到要求说是要从正文开始才显示页码&#xff0c;那如何实现呢 把鼠标放在我们正文的那一页的顶部&#xff0c;点击 布局 ,再点击分隔符&#xff0c;再点击连续 再点击编译页脚 选择你想要的页脚格式…

十四、pikachu之XSS

文章目录 1、XSS概述2、实战2.1 反射型XSS&#xff08;get&#xff09;2.2 反射型XSS&#xff08;POST型&#xff09;2.3 存储型XSS2.4 DOM型XSS2.5 DOM型XSS-X2.6 XSS之盲打2.7 XSS之过滤2.8 XSS之htmlspecialchars2.9 XSS之href输出2.10 XSS之JS输出 1、XSS概述 Cross-Site S…

探讨uniapp的组件使用的问题

1 view Flex是Flexible Box的缩写&#xff0c;意为“弹性布局”&#xff0c;用来为盒状模型提供最大的灵活性。 当设置display: flex后&#xff0c;继续给view等容器组件设置flex-direction:row或column&#xff0c;就可以在该容器内按行或列排布子组件。uni-app推荐使用flex布…

[Linux]进程

文章目录 1. 进程控制1.1 进程概述1.1.1 并行和并发1.1.2 PCB1.1.4 进程状态1.1.5 进程命令 1.2 进程创建1.2.1 函数1.2.2 fork() 剖析 1.3 父子进程1.3.1 进程执行位置1.3.2 循环创建子进程1.3.3 终端显示问题1.3.4 进程数数 1.4 execl和execlp函数1.4.1 execl()1.4.2 execlp(…

Android 13.0 首次开机默认授予app运行时权限(去掉运行时授权弹窗)

1.概述 在13.0的系统产品开发中,在android6.0以后对于权限的申请,都需要动态申请,所以会在系统首次启动后,在app的首次运行时,会弹出授权窗口,会让用户手动授予app运行时权限,在由于系统产品开发需要要求默认授予app运行时权限,不需要用户默认授予运行时弹窗,所以需要…

基于OpenCV的迷宫路径查找

附上代码&#xff1a; import cv2 import numpy as np# 读取图像 img cv2.imread("img_3.png") thres_min 150 # 二值化最小阈值if not img is None:# 二值化处理ret, img cv2.threshold(img, thres_min, 255, cv2.THRESH_BINARY)cv2.imshow("img_thres&qu…

【C++】list类的模拟实现

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录 前言一、list类的模拟实现1.1 list的…

使用Python写入数据到Excel:实战指南

在数据科学领域&#xff0c;Excel是一种广泛使用的电子表格工具&#xff0c;可以方便地进行数据管理和分析。然而&#xff0c;当数据规模较大或需要自动化处理时&#xff0c;手动操作Excel可能会变得繁琐。此时&#xff0c;使用Python编写程序将数据写入Excel文件是一个高效且便…

如何推广你的开源项目?

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

mongodb聚合排序的一个巨坑

现象&#xff1a; mongodb cpu动不动要100%&#xff0c;如下图 分析原因&#xff1a; 查看慢日志发现&#xff0c;很多条这样的查询&#xff0c;一直未执行行完成&#xff0c;占用大量的CPU [{$match: {"tags.taskId": "64dae0a9deb52d2f9a1bd71e",grnty: …

电商版面设计之首页设计

首页设计资料 1、首页----多看大美工 2、手表首页 3、水密码官方旗舰店 4、AK男装 5、百雀羚首页设计 6、活动专区 7、店铺有一些活动&#xff0c;会在里面进行体现 8、提前构思&#xff0c;多看别人的店铺设计&#xff0c;是提升自己店铺设计最好的方法 9、产品专区 10、买一送…

date_range()函数--Pandas

1. 函数功能 生成连续的日期时间序列 2. 函数语法 pandas.date_range(startNone, endNone, periodsNone, freqNone, tzNone, normalizeFalse, nameNone, inclusiveboth, *, unitNone, **kwargs)3. 函数参数 参数含义start可选参数&#xff0c;起始日期end可选参数&#xff…

物理机ping不通windows server 2012

刚才尝试各种方法&#xff0c;在物理机上就是ping不能wmware中的windows server 2012 . 折腾了几个小时&#xff0c;原来是icmp 被windows server 2012 禁用了 现在使用使用以下协议就能启用Icmp协议。 netsh firewall set icmpsetting 8然后&#xff0c;就能正常ping 通虚…

银河麒麟服务器系统服务安装流程,会根据服务是否正常判断是否重装服务

流程图 【金山文档】 linux系统服务安装相关https://kdocs.cn/l/csiyUvMWjmwc 总结 要站在面上考虑问题&#xff0c;解压和拷贝这个过程仅仅是系统服务安装的其中一个步骤&#xff0c;不能只是判断文件是否解压去判断&#xff0c;去执行相关逻辑

Linux c++ - 01-开发环境配置

一、环境配置 1.安装gcc,gdb sudo apt update sudo apt install build-essential gdb 安装成功确认 gcc --version g --version gdb --version 2.安装cmake sudo apt install cmake 安装成功确认 cmake --version 3.总结 gcc 用于编译C代码 g 用于编译C代码 VSCode是通…

vs2019 ,c++的STD库全局函数 _Pocma 的思考

&#xff08;1&#xff09;在阅读vs2019上的 STL库的 map 源码时&#xff0c;遇到了这个函数&#xff0c;之前&#xff0c;在别的源码中也经常出现这个函数。那么这个函数起什么作用呢&#xff1f; 在1880行&#xff0c;有对该函数的调用。其定义如下图&#xff1a;&#xff0…