有哪些内存地址空间?

21

有哪些形式的内存地址空间被使用过?

如今,大型扁平虚拟地址空间很常见。历史上,曾使用过更复杂的地址空间,比如基地址和偏移量的对、段号和偏移量的对、字地址加上一些索引以表示字节或其它子对象等。

经常有人在回答和评论中断言C(或C++)指针本质上是整数,这是C(或C++)的一个错误模型,因为地址空间的多样性无疑是C(或C++)关于指针操作规则的一部分原因。例如,不定义数组外的指针算术简化了基地址和偏移量模型中的指针支持。指针转换的限制简化了地址加额外数据模型的支持。

这种经常出现的断言激发了这个问题。我正在寻找有关各种地址空间的信息,以说明C指针并不一定是一个简单的整数,并且鉴于要支持的各种机器的广泛性,C对指针操作的限制是明智的。

有用的信息可能包括:

  • 具有各种地址空间的计算机体系结构的示例以及对这些空间的描述。
  • 目前正在生产的机器中仍在使用各种地址空间的示例。
  • 文档或说明的参考,特别是URL。
  • 有关地址空间如何激发C指针规则的详细说明。

这是一个广泛的问题,因此我愿意听取有关如何管理它的建议。我很乐意看到单一的总体包容性答案上的协作编辑。然而,这可能无法按照应得的方式奖励声望。我建议对多个有用的贡献进行点赞。


5
基本上是替你做功课吗? - Marc B
8
这不是一个讨论版块。 - Marc B
21
这个问题是一个实际可回答的问题,它是编程专业所特有的。很明显,存在着特定的多样化内存地址空间,这不仅仅是个人观点。为了理解为什么C/C++规则是这样的,就需要了解这些地址空间的特点。毫无疑问,语言委员会必须掌握这些信息才能制定标准。 - Eric Postpischil
4
http://c-faq.com/null/machexamp.html - melpomene
3
维基百科可能是一个列出它们的好地方,但不是一个收集这些清单的好地方。我试图收集信息,因此提了一个问题。 - Eric Postpischil
显示剩余6条评论
4个回答

18
几乎你能想象到的任何东西都可能被使用。第一个主要区别在于字节寻址(所有现代架构)和字寻址(IBM 360/PDP-11之前,但我认为现代Unisys大型机仍然是字寻址)。在字寻址中,char* 和 void* 往往比 int* 大;即使它们不比 int* 大,“字节选择器”也将位于高位,这些高位必须为0,否则将忽略除字节外的任何内容。(例如,在PDP-10上,如果p是char*,(int)p < (int)(p+1)常常会为false,即使int和char*具有相同的大小。)
在字节寻址机器中,主要变种是分段和非分段体系结构。虽然今天两者仍然广泛存在,但对于Intel 32位(一种48位地址的分段体系结构),一些更广泛使用的操作系统(Windows和Linux)人为地限制用户进程到一个单一段,模拟一个平面寻址。
尽管我没有最近的经验,但我希望在嵌入式处理器中会看到更多的变化。特别是,在过去,嵌入式处理器经常使用哈佛架构,其中代码和数据在独立的地址空间中(因此,函数指针和数据指针,强制转换为足够大的整数类型,可以相等比较)。

6

我认为你的问题有些不对,除非只是出于历史好奇。即使你的系统恰好使用平面地址空间,事实上,即使从现在到永远每个系统都使用平面地址空间,你仍然不能将指针视为整数。

C和C++标准留下了各种指针算术“未定义”的情况。这可能会影响你当前的任何系统,因为编译器会假设你避免未定义的行为并相应地进行优化。

举个具体的例子,在三个月前,Valgrind中出现了一个非常有趣的错误:

https://sourceforge.net/p/valgrind/mailman/message/29730736/

(点击“查看整个线程”,然后搜索“未定义的行为”。)

基本上,Valgrind在指针上使用小于号和大于号来确定自动变量是否在某个范围内。由于不同聚合体之间的指针比较是“未定义的”,因此Clang只是优化掉了所有比较,返回一个常量true(或false;我忘了)。

这个bug本身引发了一个有趣的StackOverflow问题

因此,尽管原始的指针算术定义可能适用于实际机器,并且可能出于其自身的目的而有趣,但它实际上与今天的编程无关。今天相关的是,您不能假定指针像整数一样运作,无论您使用的系统如何。 “未定义的行为”并不意味着“发生了奇怪的事情”;它意味着编译器可以假定您不会参与其中。当你这样做时,你引入了一个矛盾到编译器的推理中;而从矛盾中,任何事情都会发生...这只取决于你的编译器有多聪明。

他们一直变得更聪明。


@EricPostpischil:已修复,暂时解决。 - Nemo
实现者可以通过指定指针行为的有用方面来扩展语言,超出标准规定的范围,以便允许程序完成无法以其他方式完成的任务。标准并不试图定义完成任何特定任务所需的所有行为,而是依赖于实现来支持可能需要的任何“流行扩展”来完成所需的工作。 - supercat
@supercat:实现可以定义任何行为。如果您正在为特定的实现编码,那很好。但是,如果您在使用C++编码,则必须避免未定义的行为,无论如何。我甚至举了一个现实生活中的例子,Clang将未定义的指针比较优化为无操作,暴露了Valgrind中的错误,尽管系统具有平坦的地址空间。因此,您的评论与此问题或我的答案无关。 - Nemo
如果想进行低级编程,就需要使用专门设计或配置适合该目的的实现。当启用优化时,clang和gcc的作者们更注重符合标准所要求的代码性能,而不是能否执行标准未提供的操作,但这也意味着启用优化会使它们不适用于某些特定目的。 - supercat

3
有各种形式的分段存储器。
我曾经在一个嵌入式系统上工作,它有总共128KB的内存:64KB的RAM和64KB的EPROM。指针只有16位,因此指向RAM的指针可能与指向EPROM的指针具有相同的值,尽管它们引用不同的内存位置。
编译器跟踪指针的类型,以便在解引用指针之前生成选择正确存储器区块的指令。
你可以认为这就像段+偏移量,而在硬件层面上,它实际上是这样的。但是段(或更正确地说,存储器区块)是从指针的类型中隐含的,并且不作为指针的值存储。如果你在调试器中检查指针,你只会看到一个16位的值。要知道它是指向RAM还是ROM的偏移量,你必须知道它的类型。
例如,Foo*只能在RAM中,const Bar*只能在ROM中。如果你必须将Bar复制到RAM中,那么复制实际上将是一个不同的类型。(它并不像const/non-const那么简单:所有在ROM中的内容都是const,但并非所有的const都在ROM中。)
这全部都是在C语言中完成的,我知道我们使用了非标准扩展来实现这一点。我怀疑一个100%兼容的C编译器可能无法处理这个问题。

-3
从 C 程序员的角度来看,有三种主要的实现需要关注:
  1. 那些针对具有线性内存模型的机器,并且被设计和/或配置为可用作“高级汇编程序”--标准的作者明确表示他们不希望排除这些内容。大多数实现在禁用优化时会表现出这种方式。
  2. 那些可用作具有不同内存架构的机器的“高级汇编程序”的实现。
  3. 那些其设计和/或配置使它们仅适用于不涉及低级编程的任务,包括启用优化时的 clang 和 gcc。
针对第一种实现的内存管理代码通常与该类型所有实现兼容,只要目标使用相同的指针和整数表示形式即可。针对第二种实现的内存管理代码通常需要针对特定的硬件体系结构进行定制。不使用线性寻址的平台非常罕见且差异很大,除非需要编写或维护某个特定的不寻常硬件的代码(例如因为它驱动着一件昂贵的工业设备,而更现代的控制器不可用),否则对任何特定架构的了解都不太可能有用。

第三种类型的实现应仅用于不需要执行任何内存管理或系统编程任务的程序。因为标准并不要求所有实现都能够支持此类任务,所以一些编译器编写者即使针对线性地址机器也不会尝试支持其中任何有用的语义。甚至一些原则,如“两个有效指针之间的相等比较最多只会产生0或1,以可能未指定的方式选择”,也不适用于这种实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接