指针指向 C++ 寄存器是否合法?

Is it legal for a pointer to point to a C++ register?

假设一个 C++ 编译器为 CPU 寄存器未进行内存映射的体系结构编译了代码。并且假设同一个编译器为 CPU 寄存器保留了一些指针值。

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

该编译器是否符合标准?

从我能收集到的信息(我还没有读完整篇文章 - Working Draft, Standard for Programming Language C++),我怀疑标准没有提到这样的事情RAM 内存或操作内存,它定义了自己的内存模型,并将指针作为地址的表示(可能是错误的)。

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

Is it legal for a pointer to point to C++ register?

是的。

Would that compiler be considered standard-compliant?

当然可以。

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

我可以提一下 intro.memory:

  1. A memory location is either an object of scalar type that is not a bit-field or a maximal sequence of adjacent bit-fields all having nonzero width.

compund

Compound types can be constructed in the following ways:

  • pointers to cv void or objects or functions (including static members of classes) of a given type,

[...] Every value of pointer type is one of the following:

  • a pointer to an object or function (the pointer is said to point to the object or function), or
  • a pointer past the end of an object ([expr.add]), or
  • the null pointer value for that type, or
  • an invalid pointer value.

[...] The value representation of pointer types is implementation-defined. [...]

要用指针做任何有用的事情,比如应用 * 运算符 unary.op or compare pointers expr.eq 它们必须指向某个对象(边缘情况除外,例如 NULL 在比较的情况下)。对象确切存储在“何处”的表示法相当模糊 - 内存存储“对象”,内存本身可以在任何地方。


For example, if compiler, for whatever reason(optimization reasons for example), uses register allocation for a variable(not talking about register keyword), we print the value of the reference to that variable, the compiler would return one of the reserved "address values"

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

[The conversion %]p

The argument shall be a pointer to void. The value of the pointer is converted to a sequence of printing characters, in an implementation-defined manner.

但请注意来自 C99 fscanf:

[The conversion specified %]p

Matches an implementation-defined set of sequences, which should be the same as the set of sequences that may be produced by the %p conversion of the fprintf function. The corresponding argument shall be a pointer to a pointer to void. The input item is converted to a pointer value in an implementation-defined manner. If the input item is a value converted earlier during the same program execution, the pointer that results shall compare equal to that value; otherwise the behavior of the %p conversion is undefined.

打印的内容对于该对象必须是唯一的,仅此而已。因此,编译器必须为寄存器中的地址选择一些唯一值,并在请求转换时打印它们。转换 from/to uintptr_t 也将以 implementation-defined 方式实现。但这一切都在实现中——代码行为如何实现的实现细节对于 C++ 程序员来说是不可见的。

在大多数情况下,CPU 有 memory-mapped 个寄存器,使用其中一些寄存器的编译器将指定它们使用哪些寄存器。编译器文档说它不使用的寄存器可以使用 volatile 限定的指针访问,就像任何其他类型的 I/O 寄存器一样,前提是它们不影响 CPU 状态以编译器不期望的方式。读取编译器可能使用的寄存器通常会产生编译器生成的代码碰巧留在那里的任何值,这不太可能有意义。写入编译器使用的寄存器可能会以无法有效预测的方式破坏程序行为。

Is it legal for a pointer to point to C++ register?

是也不是。在 C++ 中,register 关键字如果不被弃用,是对编译器的建议,而不是要求。

编译器是否实现指向寄存器的指针取决于平台是否支持指向寄存器的指针或寄存器是内存映射的。有些平台的某些寄存器是内存映射的。

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

举个例子:

int a; // Can be represented using a register.  

int b;
int *p_b = &b;  // The "b" variable may no longer reside in a register
               // if the platform doesn't support pointers to registers.  

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

理论上是的,但只有永久固定到该寄存器的全局才真正合理
(当然,假设带有 memory-mapped CPU 的 ISA 寄存器首先 1;通常只有微控制器 ISA 是这样的;它使 high-performance 实施起来要困难得多。)

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

所以指向 CPU 寄存器的指针将指向其他东西,可能是函数的局部变量之一,当该函数取消引用您传递给它的指针时,如果您只是选择一个普通的 call-preserved 注册.

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

或者当然允许 C++ 实现自行决定为某些 C++ 对象(可能是全局对象)执行所有这些操作,包括生成所有库代码以遵守 whole-program 寄存器分配。

如果我们只讨论在有限的范围内执行此操作(不是为了调用未知函数),确保将 int *p = &x; 编译为如果 escape analysis 证明 p 的所有使用都是有限的,则取 CPU 寄存器的地址 x 当前所在的地址。我想说这将是无用的,因为任何此类证明都会为您提供足够的信息来优化间接寻址并编译 *p 以作为寄存器而不是内存访问,但是有一个 use-case:

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


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


ISO C++ 对此有何评论:不多

ISO C++抽象机只有有内存,每个对象都有地址。 (如果从未使用过地址,则遵守 as-if 规则。)将数据加载到寄存器是一个实现细节。

所以是的,在像 AVR(8 位 RISC 微控制器)或 8051 这样的机器中,其中一些 CPU 寄存器是 memory-mapped,C++ 指针可以指向它们1 。拥有 memory-mapped CPU 寄存器是某些微控制器(如 AVR2 上的事情。 (例如 有一个图表。(并且问了一个奇怪的问题,即为什么我们有寄存器,而不是仅仅使用内存地址,如果它们要被内存映射的话。)

这个 AVR Godbolt link 并没有真正显示太多,主要只是玩弄 GNU C register-asm 全局。


脚注 1:在普通 ISA 的普通 C++ 实现中,C++ 指针非常直接地映射到可以以某种方式从 asm 中取消引用的机器地址。 (Perhaps very inconveniently 在像 6502 这样的机器上,但仍然如此)。

在没有虚拟内存的机器上,这样的指针一般是物理地址。 (假设是普通的平面内存模型,没有分段。)我不知道有任何带有虚拟内存 memory-mapped CPU 寄存器的 ISA,但是有很多我不知道的晦涩的 ISA。如果存在,将寄存器映射到 虚拟 地址 space 的固定部分可能是有意义的,因此可以在 TLB 查找的同时检查地址的寄存器访问.无论哪种方式,它都会使 ISA 的流水线实现变得非常痛苦,因为检测 RAW hazards that require bypass forwarding (or stalling) now involves checking memory accesses. Normal ISAs only need to match register numbers against each other while decoding a machine instruction. With memory allowing indirect addressing via registers, memory disambiguation / 存储转发之类的危险需要与检测指令何时读取先前寄存器写入的结果进行交互,因为读取或写入可能通过记忆。

旧的 non-pipelined CPU 具有虚拟内存,但流水线是您 永远不会 想要 memory-map 的主要原因之一在现代 ISA 上注册并有任何被使用的野心作为性能相关的台式机/笔记本电脑/移动设备的主要 CPU。如今,将虚拟内存的复杂性包括在内已经毫无意义,但 而不是 流水线设计。有一些流水线微控制器/ low-end CPU没有虚拟内存。

脚注 2:Memory-mapped CPU 寄存器在现代主流 32 位和 64 位 ISA 上基本上是 non-existent。

具有 memory-mapped CPU 寄存器的微控制器通常将寄存器文件实现为内部 SRAM 的一部分,无论如何它们都可以充当常规内存。

在 ARM、x86-64、MIPS 和 RISC-V 以及所有类似的 ISA 中,寻址寄存器的唯一方法是将寄存器编号编码到指令的机器代码中。寄存器间接寻址仅适用于 self-modifying 代码,C++ 不需要,正常实现也不使用。此外,寄存器编号与内存分开address-space。例如ARM 有 16 个基本整数寄存器,因此像 add r0, r1, r2 这样的指令在该机器指令的编码中将具有三个 4 位字段,每个操作数一个。 (在 ARM 模式下,不是 Thumb。)这些寄存器编号与内存地址 012.

无关

请注意,memory-mapped I/O 寄存器在所有现代 ISA 上都很常见,通常与 RAM 共享物理地址 space。 I/O地址通常所说的寄存器,但是寄存器在外设里面,像网卡,不在CPU里面。读取或写入它会有一些 side-effect,因此在 C++ 中,您通常会使用 volatile int *constexpr ioport = 0x1234; 或类似的东西来表示 MMIO。 MMIO 寄存器绝对不是可以在 AArch64 add w0, w1, w2.

等指令中使用的 general-purpose 整数寄存器之一