指向C ++寄存器的指针合法吗?

问题描述

让我们说一个C ++编译器为cpu寄存器没有内存映射的体系结构编译的代码。而且也可以说,同一编译器为cpu寄存器保留了一些指针值。

例如,如果编译器出于某种原因(例如优化原因)使用变量的寄存器分配(而不是谈论register关键字),并且我们打印对该变量的引用值,则编译器将返回保留的“地址值”之一。

是否认为编译器符合标准?

从我可以收集的信息(我没有读完整的书- Working Draft,Standard for Programming Language C++ )中,我怀疑该标准没有提及RAM内存或有效内存之类的东西并且它定义了自己的内存模型,而指针则作为地址的表示形式(可能是错误的)。

现在,由于寄存器也是内存的一种形式,我可以想象将寄存器视为内存模型一部分的实现是合法的。

解决方法

指向C ++寄存器的指针合法吗?

是的

是否认为编译器符合标准?

好的。

C ++并不知道“寄存器”,无论如何。指针指向对象(和函数),而不是“内存位置”。该标准描述了程序的行为 not 的实现方式。描述行为使其抽象化-与使用何种方式和方式无关紧要,只有 result 是重要的。如果程序的行为与标准所说的相符,则与对象的存储位置无关。

我可以提到intro.memory

  1. 存储位置是不是位域的标量类型对象,或者是宽度均非零的相邻位域的最大序列。

compund

可以通过以下方式构造化合物类型:

  • 指向cv void或给定类型的对象或函数(包括类的静态成员)的指针,

[...]指针类型的每个值都是以下之一:

  • 指向对象或函数的指针(据说该指针指向该对象或函数),或者
  • 超出对象末尾的指针([expr.add]),或
  • 该类型的空指针值,或
  • 无效的指针值。

[...]指针类型的值表示形式是实现定义的。 [...]

要对指针执行任何有用的操作,例如应用*运算符unary.op或比较指针expr.eq,它们必须指向某个对象(边缘情况除外,例如NULL比较时)。精确存储对象的“位置”的概念相当模糊-内存存储“对象”,内存本身可以在任何地方。


例如,如果编译器由于某种原因(例如优化原因)使用变量的寄存器分配(而不是谈论register关键字),我们将打印对该变量的引用值,则编译器将返回以下值之一保留的“地址值”

std::ostream::operator<<调用std::num_putvoid*的转换为%p facet.num.put.virtuals。 来自C99 fprintf

[转换%] p

该参数应为指向void的指针。指针的值以实现定义的方式转换为一系列打印字符。

但是请注意,来自C99 fscanf

[转换指定为%] p

匹配实现定义的序列集,该序列集应与%p可能生成的序列集相同 fprintf函数的转换。相应的论点应 指向指向void的指针。输入项将转换为 以实现定义的方式指定指针值。如果输入项目 是在相同程序执行期间较早转换的值, 结果应等于该值的指针;否则 %p转换的行为是不确定的。

该对象的打印内容必须唯一,仅此而已。因此,编译器必须为寄存器中的地址选择一些唯一值,并在请求转换时将其打印出来。从uintptr_tpack的转换也将以实现定义的方式实现。但这全在实现中-C ++程序员看不到如何实现代码行为的实现细节。

,

指向C ++寄存器的指针合法吗?

是,不是。在C ++中,i_N关键字(如果未弃用)是对编译器的建议,而不是要求。

编译器是否实现要注册的指针取决于平台是否支持指向寄存器的指针或寄存器是否映射到内存。在某些平台上,某些寄存器是通过内存映射的。

当编译器遇到POD变量声明时,允许编译器使用该变量的寄存器。但是,如果平台不支持指向寄存器的指针,则编译器可以在内存中分配变量;否则,编译器可能会在内存中分配变量。尤其是当使用变量的地址时。

举个例子:

`N[int(i)]`

在许多常见平台(例如ARM处理器)中,寄存器位于处理器的内存区域(特殊区域)内。这些寄存器没有来自处理器的地址线或数据线。因此,它们不占用处理器地址空间中的任何空间。也没有ARM指令返回寄存器的地址。因此,对于ARM处理器,如果代码使用变量的地址,则编译器会将变量的分配从寄存器分配到内存(在处理器外部)。

,

在大多数情况下,CPU具有内存映射的寄存器,使用其中一些寄存器的编译器将指定使用哪些寄存器。像其他任何类型的I / O寄存器一样,可以使用volatile限定的指针来访问编译器文档中未使用的寄存器,前提是它们不会以编译器不会影响CPU状态的方式进行操作。没想到。读取可能由编译器使用的寄存器通常会产生编译器生成的代码碰巧保留在那里的任何值,这不太可能是有意义的。编译器使用的寄存器写操作可能会以无法有效预测的方式破坏程序行为。

,

从理论上讲,是的,但仅对于永久固定到该寄存器的全局变量来说确实可行
(当然,假设一个ISA首先具有内存映射的CPU寄存器 1 ;通常只有微控制器ISA才这样;这会使高性能实现变得更加困难。)

将指针传递给qsortprintf之类的函数或您自己的函数时,指针必须保持有效(保持指向同一对象)。但是复杂的函数通常会将一些寄存器保存到内存(通常是堆栈)to be restored at the end of the function中,并且在该函数内部会将它们自己的值放在这些寄存器中。

因此,如果您只是选择一个普通的调用保留寄存器,那么当该函数取消引用您传递给它的指针时,指向CPU寄存器的指针将指向其他内容,可能是该函数的局部变量之一。

我解决此问题的唯一方法是为程序范围内的特定C ++对象保留寄存器。类似于在全局范围内类似于GNU C / C ++ register char foo asm("r16");的东西,但是使用了一个假定的编译器,其中不会阻止您获取其地址。对于GCC documents for register-asm globals而言,对于通过指针进行的每次内存访问,这样的假设编译器必须比GCC更严格,以确保全局值始终在该寄存器中始终。您必须重新编译库才能不将该寄存器用于任何内容(例如gcc -ffixed-r16或让他们查看定义。)

当然,或者允许C ++实现决定自己对某个C ++对象(可能是全局对象)执行所有操作,包括生成所有库代码以尊重整个程序寄存器的分配。

如果我们仅是在有限范围内讨论此操作(不适用于对未知函数的调用),请确保编译int *p = &x;以获取目标地址是安全的。如果escape analysis证明x的所有使用受到限制,则CPU寄存器p当前在。我要说这将是无用的,因为任何此类证明都将为您提供足够的信息,以优化间接性并编译*p以作为寄存器而不是内存进行访问,但是有一个用例:>

如果您有两个或多个变量,并且在取消引用if (condition) p = &y;之前执行p,则编译器可能会知道x肯定仍在同一寄存器中当评估*p时,但不是知道p是指向x还是y。因此,将xy保留在寄存器中可能很有用,尤其是当它们也被其他与p的deref混合的代码直接读取/写入时。


当然,我一直在假设一个“正常”的ISA和一个“正常”的调用约定。可以想象奇怪而奇妙的机器,和/或它们或普通机器上的C ++实现,它们的工作方式可能会大不相同。


ISO C ++必须说些什么:

仅ISO C ++抽象机 具有内存,并且每个对象都有一个地址。 (如果从未使用过该地址,则视情况而定。)将数据加载到寄存器中是实现细节。

是的,在诸如AVR(8位RISC微控制器)或8051之类的机器中,某些CPU寄存器被映射到内存,C ++指针可能指向它们 1 。在某些微控制器(例如AVR 2 )上具有内存映射的CPU寄存器是一件很重要的事情。 (例如What is the benefit of having the registers as a part of memory in AVR microcontrollers?有一个图表。(并问一个奇怪的问题,为什么我们要全部使用寄存器,而不是仅仅使用内存地址,如果它们将要进行内存映射。)

这个AVR Godbolt link并没有显示太多,大部分只是在玩GNU C register-asm global。


脚注1 :在用于普通ISA的普通C ++实现中,C ++指针相当直接地映射到可以从asm取消引用的机器地址。 (Perhaps very inconveniently在类似6502的机器上,但仍然如此)。

在没有虚拟内存的机器中,此类指针通常是物理地址。 (假设是正常的平面内存模型,没有分段。)我不知道任何带有虚拟内存内存映射CPU寄存器的ISA,但是有很多我不知道的模糊ISA 。如果存在,将寄存器映射放入虚拟地址空间的固定部分可能是有意义的,因此可以与TLB查找并行地检查该地址以进行寄存器访问。不管采用哪种方式,这都会使ISA的流水线实现非常麻烦,因为检测诸如RAW hazards之类的需要旁路转发(或停顿)的危害现在涉及检查内存访问。普通ISA在解码机器指令时仅需要使寄存器号相互匹配。在内存允许通过寄存器间接寻址的情况下,memory disambiguation /存储转发将需要与检测指令何时读取前一个寄存器写入的结果进行交互,因为该读取或写入可以通过存储器进行。

过去有带有虚拟内存的非流水线CPU,但是流水线是您从不想要在现代ISA上以任何用作主要对象的内存映射寄存器的主要原因。与性能相关的台式机/笔记本电脑/移动设备的CPU。如今,包含虚拟内存的复杂性已经变得毫无意义,而不是流水线化了设计。有一些没有虚拟内存的流水线微控制器/低端CPU。

脚注2 :内存映射的CPU寄存器在现代主流的32位和64位ISA上基本上不存在。 Do general purpose registers are generally memory mapped?

具有内存映射CPU寄存器的微控制器通常将寄存器文件实现为内部SRAM的一部分,而它们仍然可以用作常规内存。

在ARM,x86-64,MIPS和RISC-V以及所有类似的ISA中,寻址寄存器的唯一方法是将寄存器编号编码为指令的机器代码。只能通过自修改代码实现寄存器间接访问,否则C ++不需要,而常规实现则不使用。此外,寄存器号是与内存分开的地址空间。例如ARM具有16个基本整数寄存器,因此,类似add r0,r1,r2的指令在该机器指令的编码中将具有三个4位字段,每个操作数一个。 (在ARM模式下,不是Thumb。)这些寄存器号与内存地址012无关。

请注意,memory-mapped I/O寄存器在所有现代ISA上都是通用的,通常与RAM共享物理地址空间。 I / O地址通常称为寄存器,但该寄存器位于外围设备(如网卡)中,而不位于CPU中。读取或写入它会产生一些副作用,因此在C ++中,您通常会使用volatile int *constexpr ioport = 0x1234;或MMIO来使用它。 MMIO寄存器绝对不是可以在诸如AArch64 add w0,w1,w2之类的指令中使用的通用整数寄存器之一。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...