从程序员角度深入计算机系统的底层 内存地址转换与分段

X86架构(一) —— Intel8086内存分段机制 8086 是Intel 公司第一款16 位处理器,诞生于1978 年,是整个Intel 32 位架构处理器(IA-32)的开山鼻祖。 阅读详情
本文是 Intel 兼容计算机( x86 )的内存与保护系列文章的第一篇,延续了 启动引导 系列文章的主题,进一步分析操作系统内核的工作流程。与以前一样,我将引用 Linux 内核的源代码,但对 Windows 只给出示例(抱歉,我忽略了 BSD , Mac 等系统,但大部分的讨论对它们一样适用)。文中如果有错误,请不吝赐教。

在支持 Intel 的 主板芯片组 上, CPU 对内存的访问是通过连接着 CPU 和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址,编号从 0 开始一直到可用物理内存的最高端。这些数字被北桥映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查。然而,在 CPU 内部,程序所使用的是逻辑内存地址,它必须被转换成物理地址后,才能用于实际内存访问。从概念上讲,地址转换的过程如下图所示:

63d47c286de8a76b33b540bc9d74975b.jpg




x86 CPU开启分页功能后的内存地址转换过程



此图并未指出详实的转换方式,它仅仅描述了在CPU的分页功能开启的情况下内存地址的转换过程。如果CPU关闭了分页功能,或运行于16位实模式,那么从分段单元(segmentation unit)输出的就是最终的物理地址了。当CPU要执行一条引用了内存地址的指令时,转换过程就开始了。第一步是把逻辑地址转换成线性地址。但是,为什么不跳过这一步,而让软件直接使用线性地址(或物理地址呢?)其理由与:“人类为何要长有阑尾?它的主要作用仅仅是被感染发炎而已”大致相同。这是进化过程中产生的奇特构造。要真正理解x86分段功能的设计,我们就必须回溯到1978年。


最初的8086处理器的寄存器是16位的,其指令集大多使用8位或16位的操作数。这使得代码可以控制216个字节(或64KB)的内存。然而Intel的工程师们想要让CPU可以使用更多的内存,而又不用扩展寄存器和指令的位宽。于是他们引入了段寄存器(segment register),用来告诉CPU一条程序指令将操作哪一个64K的内存区块。一个合理的解决方案是:你先加载段寄存器,相当于说“这儿!我打算操作开始于X处的内存区块”;之后,再用16位的内存地址来表示相对于那个内存区块(或段)的偏移量。总共有4个段寄存器:一个用于栈(ss),一个用于程序代码(cs),两个用于数据(ds,es)。在那个年代,大部分程序的栈、代码、数据都可以塞进对应的段中,每段64KB长,所以分段功能经常是透明的。


放一些老式的8086级别的寄存器描述架构图


   dd09efff6d698abd71fb48f32f4a7ff3.jpg



c099d07b15ad9fed89c6f1bcf947ac85.jpg



可以很明显的看出来,在8088设备上的段寄存器属于一种存储器影射控制设备。这是否让我们联想到了现在的[size=-1]物理地址扩展( PAE)技术?



现今,分段功能依然存在,一直被 x86 处理器所使用着。每一条会访问内存的指令都隐式的使用了段寄存器。比如,一条跳转指令会用到代码段寄存器( cs ),一条压栈指令( stack push instruction )会使用到堆栈段寄存器( ss )。在大部分情况下你可以使用指令明确的改写段寄存器的值。段寄存器存储了一个 16 位的段选择符( segment selector );它们可以经由机器指令(比如 MOV )被直接加载。唯一的例外是代码段寄存器( cs ),它只能被影响程序执行顺序的指令所改变,比如 CALL 或 JMP 指令。虽然分段功能一直是开启的,但其在实模式与保护模式下的运作方式并不相同的。

在实模式下,比如在 引导启动的初期 ,段选择符是一个 16 位的数值,指示出一个段的开始处的物理内存地址。这个数值必须被以某种方式放大,否则它也会受限于 64K 当中,分段就没有意义了。比如, CPU 可能会把这个段选择符当作物理内存地址的高 16 位(只需将之左移 16 位,也就是乘以 2^16 )。这个简单的规则使得:可以按 64K 的段为单位,一块块的将 4GB 的内存都寻址到。遗憾的是, Intel 做了一个很诡异的设计,让段选择符仅仅乘以 24 (或 16 ),即左移4bit(总计20 bit Addr =1MB Space),从而一举将寻址范围限制在了 1MB ,还引入了过度复杂的转换过程。下述图例显示了一条跳转指令, cs 的值是 0x1000 :

2fceff6a5cbd35fa7a763ff33c061579.jpg



实模式分段功能



实模式的段地址以16个字节为步长,从0开始编号一直到0xFFFF0(即1MB)。你可以将一个从0到0xFFFF的16位偏移量(逻辑地址)加在段地址上。在这个规则下,对于同一个内存地址,会有多个段地址/偏移量的组合与之对应,而且物理地址可以超过1MB的边界,只要你的段地址足够高(参见臭名昭著的A20线)。同样的,在实模式的C语言代码中,一个远指针(far pointer)既包含了段选择符又包含了逻辑地址,用于寻址1MB的内存范围。真够“远”的啊。随着程序变得越来越大,超出了64K的段,分段功能以及它古怪的处理方式,使得x86平台的软件开发变得非常复杂。这种设定可能听起来有些诡异,但它却把当时的程序员推进了令人崩溃的深渊。

在32位保护模式下,段选择符不再是一个单纯的数值,取而代之的是一个索引编号,用于引用段描述符表中的表项。这个表为一个简单的数组,元素长度为8字节,每个元素描述一个段。看起来如下:


510c697b8c95052bdb06e8a12bffd988.jpg



段描述符



有三种类型的段:代码,数据,系统。为了简洁明了,只有描述符的共有特征被绘制出来。基地址(base address)是一个32位的线性地址,指向段的开始;段界限(limit)指出这个段有多大。将基地址加到逻辑地址上就形成了线性地址。DPL是描述符的特权级(privilege level),其值从0(最高特权,内核模式)到3(最低特权,用户模式),用于控制对段的访问。

这些段描述符被保存在两个表中:全局描述符表(GDT)和局部描述符表(LDT)。电脑中的每一个CPU(或一个处理核心)都含有一个叫做gdtr的寄存器,用于保存GDT的首个字节所在的线性内存地址。为了选出一个段,你必须向段寄存器加载符合以下格式的段选择符:


e3f97741e54688660ae674f17d97a2f1.jpg



段选择符



对GDT,TI位为0;对LDT,TI位为1;index指出想要表中哪一个段描述符(译注:原文是段选择符,应该是笔误)。对于RPL,请求特权级(Requested Privilege Level),以后我们还会详细讨论。现在,需要好好想想了。当CPU运行于32位模式时,不管怎样,寄存器和指令都可以寻址整个线性地址空间,所以根本就不需要再去使用基地址或其他什么鬼东西。那为什么不干脆将基地址设成0,好让逻辑地址与线性地址一致呢?Intel的文档将之称为“扁平模型”(flat model),而且在现代的x86系统内核中就是这么做的(特别指出,它们使用的是基本扁平模型)。基本扁平模型(basic flat model)等价于在转换地址时关闭了分段功能。如此一来多么美好啊。就让我们来看看32位保护模式下执行一个跳转指令的例子,其中的数值来自一个实际的Linux用户模式应用程序:

c0c19c1ca537feab8abb229ed89c0227.jpg



保护模式的分段



段描述符的内容一旦被访问,就会被cache(缓存),所以在随后的访问中,就不再需要去实际读取GDT了,否则会有损性能。每个段寄存器都有一个隐藏部分用于缓存段选择符所对应的那个段描述符。如果你想了解更多细节,包括关于LDT的更多信息,请参阅《Intel System Programming Guide》3A卷的第三章。2A和2B卷讲述了每一个x86指令,同时也指明了x86寻址时所使用的各种类型的操作数:16位,16位加段描述符(可被用于实现远指针),32位,等等。

在Linux上,只有3个段描述符在引导启动过程被使用。他们使用GDT_ENTRY宏来定义并存储在boot_gdt数组中。其中两个段是扁平的,可对整个32位空间寻址:一个是代码段,加载到cs中,一个是数据段,加载到其他段寄存器中。第三个段是系统段,称为任务状态段(Task State Segment)。在完成引导启动以后,每一个CPU都拥有一份属于自己的GDT。其中大部分内容是相同的,只有少数表项依赖于正在运行的进程。你可以从segment.h看到Linux GDT的布局以及其实际的样子。这里有4个主要的GDT表项:2个是扁平的,用于内核模式的代码和数据,另两个用于用户模式。在看这个Linux GDT时,请留意那些用于确保数据与CPU缓存线对齐的填充字节——目的是克服冯·诺依曼瓶颈。最后要说说,那个经典的Unix错误信息“Segmentation fault”(分段错误)并不是由x86风格的段所引起的,而是由于分页单元检测到了非法的内存地址。唉呀,下次再讨论这个话题吧。


Intel巧妙的绕过了他们原先设计的那个拼拼凑凑的分段方法,而是提供了一种富于弹性的方式来让我们选择是使用段还是使用扁平模型。由于很容易将逻辑地址与线性地址合二为一,于是这成为了标准,比如现在在64位模式中就强制使用扁平的线性地址空间了。但是即使是在扁平模型中,段对于x86的保护机制也十分重要。保护机制用于抵御用户模式进程对系统内核的非法内存访问,或各个进程之间的非法内存访问,否则系统将会进入一个狗咬狗的世界!在下一篇文章中,我们将窥视保护级别以及如何用段来实现这些保护功能。

计算机保研面试核心考点:数据结构、操作系统、计组、数据库、离散数学深度解析 数据结构与算法是计算机科学的基石,其核心在于通过高效的数据组织方式(如数组、链表、树、图、哈希表)和算法逻辑,解决实际的计算与存储问题。理解其底层原理,例如缓存友好性、时间复杂度与空间复杂度的权衡,对于构建高性能系统至关重要。操作系统则管理着这些程序与算法的运行环境,其进程与线程调度、内存管理、文件系统和I/O多路复用等机制,直接决定了软件的并发能力和资源利用效率。在工程实践中,深入理解**红黑树**的近似平衡特性及其在Java HashMap等集合库中的应用,掌握**epoll**等I/O多路复用模型以实 阅读详情

相关推荐

手把手教你用Simulink搭建DSOGI-PLL模型(附完整代码下载)

本文详细介绍了在Simulink中搭建高鲁棒性DSOGI-PLL模型的完整工程实践指南。针对传统锁相环在非理想电网下的不足,文章深入剖析了双二阶广义积分器锁相环(DSOGI-PLL)的核心原理与优势,并提供了从模块构建、系统组装到参数整定与测试验证的全流程手把手教程,附有完整代码下载,助力工程师应对谐波、电压跌落等复杂电网工况。

weixin_29223603的博客 389

Linux操作系统实验1——地址转换

在分段时先用段选择符在相应的段描述符表中找到段描述符,也就是某一个段的基地址,再加上段内偏移量就得到了对应的线性地址,线性地址也称之为虚拟地址。1.在内核中先申请一个页面,使用内核提供的函数,按照寻页的步骤一步步的找到物理地址。(fileview.c)为了增加可移植性并没有完整的使用分段机制,它让所有的段都指向相同的段地址范围,段的基地址都为。命令卸载模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。位中的情况一样,现在还未被使用,它的页目录项只有一个,线性地址中也没有它的索引位。

木子一个Lee的博客 3343

闲人闲谈PS之一项目库存跨公司业务STO解决方案--SAP闲人的开篇

写这篇文章之前,简单说一下发这篇文章的动机。也算是在SAP这一行干了快10多年,在对一个模块了解越来越深入,越发觉自己的SAP知识浅薄。总的来说,和多数顾问一样,对于**标准功能**的应用业务场景理解还是存在或多或少的知识盲区。 因为一个偶然,看到SAP团子君的文章,分享了很多干货文章。这也是自己一直想做的时候,不管水平高低与否,分享本身就意味着提升。好了废话不多说,先在优快云上发一篇吧,困惑自己将近10年的问题的解决方案。 业务场景:在项目型企业中,通常会有这样的业务,母公司和客...

SAP闲人的博客 9269

内存地址转换与分段

原文标题:Memory Translation and Segmentation原文地址:http://duartes.org/gustavo/blog/ [注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下。一来自己复习,二来与大家分享。] 本文是Intel兼容计算机(x86)的内存与保护系列文章的第一篇,延续了启动引导系列文章的主题,进一步分析操作系统内核的工作流程。与

沈雷的专栏 1万+

手把手教你用Boot-Repair快速修复Ubuntu启动分区缺失(附详细命令行步骤)

本文详细介绍了如何使用Boot-Repair工具快速修复Ubuntu启动分区缺失问题,包括诊断、应急准备、自动修复和手动命令操作。通过grub rescue模式下的关键命令和Boot-Repair的图形界面操作,帮助用户高效解决启动分区故障,确保系统恢复正常运行。

weixin_29229989的博客 564

3分钟接入满血版DeepSeek-R1:字节火山引擎手把手教程!免费50万tokens!

3分钟接入满血版DeepSeek-R1:字节火山引擎手把手教程!免费50万tokens!

hunteritself的博客 2万+

x86的内存分段机制

采用分段策略之后,一个内存单元的地址实际上就可以用“段:偏移”或者“段地址:偏移地址”来表示,这就是通常所说的逻辑地址。为了在硬件一级提供对“段地址:偏移地址”内存访问模式的支持,处理器至少要提供两个段寄存器,分别是代码段寄存器(CS)和数据段寄存器(DS)。对 CS 内容的改变将导致处理器从新的代码段开始执行。同样,在开始访问内存中的数据之前,也必须首先设置好 DS 寄存器,使之指向数据段。除此之外,最重要的是, 当处理器访问内存时,它把指令中指定的内存地址看成是段内的偏移地址,而不是物理地址。

shijiyingjie的博客 961

SpringBoot 指标监控——Actuator

1、 访问:localhost:8000/actuator 设置健康监控显示具体情况 metrics http://localhost:8000/actuator/metrics/jvm.gc.live.data.size 定制化health信息 //利用构造器,在注册中心中添加cityService.saveCity.count指标 public CityServiceImpl(MeterRegistry meter...

weixin_44302046的博客 4631

从程序员角度深入计算机系统的底层 CPU的运行环、特权级与保护

可能你凭借直觉就知道应用程序的功能受到了Intel x86计算机的某种限制,有些特定的任务只有操作系统的代码才可以完成,但是你知道这到底是怎么一回事吗?在这篇文章里,我们会接触到x86的特权级(privilege level),看看操作系统和CPU是怎么一起合谋来限制用户模式的应用程序的。特权级总共有4个,编号从0(最高特权)到3(最低特权)。有3种主要的资源受到保护:内存,I/O端口以及

651

RDM协议控制端实战:从数据包构造到高效设备发现算法

本文深入探讨了RDM协议控制端的核心开发流程,涵盖从数据包构造、发送到响应解析的全过程。重点解析了如何利用二分搜寻算法高效实现网络设备发现,并分享了组包、解包中的关键代码实现与常见调试技巧,为舞台灯光与智能硬件领域的嵌入式开发提供实战指导。

week9的博客 458

【深度学习驱动流体力学】Python流体力学Ansys Fluent

PyFluent 是一个将 Python 编程语言与 Ansys Fluent 流体动力学 (CFD) 仿真软件集成的工具。它允许用户通过 Python 脚本来控制和自动化 Fluent 中的仿真任务,实现从预处理、求解到后处理的全流程控制。PyFluent 结合了 Python 的灵活性和 Fluent 的强大功能,提供了一种高效且灵活的 CFD 仿真方法。PyFluent 是一个强大而灵活的工具,使用户能够通过 Python 脚本全面利用 Ansys Fluent 的功能。

源代码杀手的博客 3125

Linux内存寻址之分段机制

前言最近在学习Linux内核,读到《深入理解Linux内核》的内存寻址一章。原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解。于是,查找了很多资料,最终理顺了内存寻址的知识。现在把我的理解记录下来,希望对内核学习者有一定帮助,也希望大家指出错误之处。分段到底是怎么回事相信学过操作系统课程的人都知道分段分页,但是奇怪的是书上基本没提分段分页是怎么产生的,这就导致我们知其然不知其所以然。下面

renwotao2009的专栏 914

VTK:vtkAssembly用法实战

VTK:vtkAssembly用法实战程序输出程序完整源代码 程序输出 程序完整源代码 #include <vtkActor.h> #include <vtkAssembly.h> #include <vtkAxesActor.h> #include <vtkCubeSource.h> #include <vtkNamedColors.h> #include <vtkNew.h> #include <vtkOrientation

希望我的博客,能帮上你解决学习中工作中所遇到的问题 936

函数的执行、调用、堆栈与递归

前提 先需要简要叙述一下一个C程序的运行流程。 我们知道基本的冯诺依曼计算机体系:CPU–内存–外存。 CPU中具有一些寄存器可以用来暂存要操作的对象,并存储处理结束后的数据。 程序编译完成之后,当内核通过exec函数来执行C程序是,在调用main前会先调用一个特殊的启动例程,该例程将会将 段页式内存管理数据总线、控制总线 参考 【1】 UNIX环境高级编程 【2】 汇编语言,第三版 ...

SinjoyWong的博客 361

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

C语言底层视角:内存地址与读取大小决定数据类型本质

在计算机系统中,数据类型本质上是编译器提供的高级抽象,用于简化编程。其底层原理基于内存地址和读取大小:程序运行时,数据以二进制字节形式存储在特定内存地址中,编译器根据类型声明生成相应的读取指令,决定如何解释这些字节。这一机制是理解指针、内存对齐和结构体内存布局等技术概念的基础,对于系统编程、性能优化和调试至关重要。通过掌握内存地址与读取大小的关系,开发者能够深入理解类型双关、缓冲区溢出等底层现象,从而在嵌入式开发、逆向工程和高性能计算等应用场景中编写更高效、更可靠的代码。

weixin_30843605的博客 374

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

计算机系统基础栈,计算机系统基础(八)虚拟存储器

这篇文章我们的角度下放,重点来看一下程序在运行过程中,内存是如何管理的。内存问题计算机出现的早期,其工作内容比较简单,同一时间只做一个计算任务,然后输出结果。但随着人类需求的提高,人们希望计算机能同时计算多个任务,提高计算机的使用率。逐渐地,多任务计算机出现,但要想同时运行多个任务,那各任务之间如何分配和使用计算机资源呢?为了解决这个问题,人类发明出操作系统,所有的计算机资源交由操作系统统一管理,...

weixin_42299427的博客 835

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

操作系统 内存地址(逻辑地址、线性地址、物理地址)概念(转)

逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长

悠然 932

计算机考研408核心考点:分页地址转换机制与TLB原理详解

地址转换是现代操作系统的核心机制,它实现了逻辑地址到物理地址的映射,是内存管理的基础。其核心原理在于通过页表这一数据结构,将程序使用的连续虚拟地址空间,离散地映射到物理内存的不同区域。这项技术的核心价值在于实现了内存保护、共享,并为虚拟内存提供了支撑,使得多个进程可以安全、高效地共享有限的物理内存。在工程实践中,为了提升转换速度,普遍引入了快表(TLB)这一高速缓存来加速地址转换过程。特别是在处理大地址空间时,多级页表与TLB的协同工作成为关键。本文聚焦于计算机考研408中的高频难点,通过图解和分步计算,系

weixin_30319153的博客 363

关于MMU那些事儿

MMU诞生之前: 在传统的批处理系统如DOS系统,应用程序与操作系统在内存中的布局大致如下图: 应用程序直接访问物理内存,操作系统占用一部分内存区。 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。 如果我们一直是单任务处理,则不会有任何问题,也或者应用程序所需的内存总是非常小,则这种架构是不会有任何问题的。然而随着计算机科学技术的发展,所需解决的问题越来越复杂,单任务批处理已不...

嵌入式客栈 748

C 语言指针与地址

近段复习C语言,感觉C语言相对于C++、Java等高级语言最大的特点就是:简洁紧凑、灵活方便。C语言特征不多,带有面向硬件思维,小巧而精炼。C语言非常贴近底层,要真正理解指针的本质,还需要些微机原理、计算机组成与原理、操作系统和编译原理等背景知识。物理地址是外部连接使用的、唯一的,它是“与地址总线相对应”;逻辑地址是内部和编程使用的、并不唯一。逻辑地址就是逻辑段管理内存而形成的。C语言中使用的地址...

Life runs on code 3875

Linux内核学习(1)_C程序执行过程

理论预备知识 存储程序计算机工作模型 冯诺依曼结构,透明计算颠覆了冯诺依曼结构? 存储程序 计算机(指硬件)应由运算器、存储器、控制器、输入设备和输出设备五大基本部件组成; 计算机内部采用二进制来表示指令和数据; 将编好的程序和原始数据事先存入存储器中,然后启动计算机工作,这就是存储程序的基本含义. 指令和数据都是二进制信息是没有什么区别的,指令和数据只是应用上的含义

hengfanz的专栏 536

计算机寻址方法深度解析:从原理到实战的九大核心方式

在计算机组成与体系结构中,寻址方法是CPU访问内存中指令和数据的核心机制,它定义了如何从指令中的地址信息计算出操作数的实际位置。其原理基于内存的线性地址空间,通过组合指令地址字段、程序计数器、基址/变址寄存器等要素,实现高效灵活的数据定位。这项技术的核心价值在于缩短指令长度、提升编程灵活性、支持代码重定位与动态数据结构访问,是程序得以正确高效运行的基础。在应用场景上,寻址方式直接影响着数组遍历、函数调用、指针操作以及操作系统的内存管理等关键环节。本文聚焦于**立即寻址**、**直接寻址**、**间接寻址**

weixin_34014555的博客 396

goalng开发_开发篇

Go 面试,基础知识

qq_46640341的博客 1161
上一篇: 从程序员角度深入计算机系统的底层 内核引导过程
下一篇: 从程序员角度深入计算机系统的底层 CPU的运行环、特权级与保护
zxycode007
博客等级 码龄18年 53粉丝 21原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值