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

Viewed 3906

Let's say a C++ compiler compiled code for an architecture where CPU registers are not memory-mapped. And also let's say that same compiler reserved some pointer values for CPU registers.

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

Would that compiler be considered standard-compliant?

From what I could gather (I haven't read the whole thing - Working Draft, Standard for Programming Language C++), I suspect that the standard doesn't mention such a thing as RAM memory or operative memory and it defines its own memory model instead and the pointers as representation of addresses (could be wrong).

Now since registers are also a form of memory, I can imagine that an implementation which considers registers to be a part of the memory model could be legal.

4 Answers

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

Yes.

Would that compiler be considered standard-compliant?

Sure.

C++ is not aware of "registers", whatever that is. Pointers point to objects (and functions), not to "memory locations". The standard describes the behavior of the program and not how to implement it. Describing behavior makes it abstract - it's irrelevant what is used in what way and how, only the result is what matters. If the behavior of the program matches what the standard says, it's irrelevant where the object is stored.

I can mention 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.

and 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. [...]

To do anything useful with a pointer, like apply * operator unary.op or compare pointers expr.eq they have to point to some object (except edge cases, like NULL in case of comparisons). The notation of "where" exactly objects are stored is rather vague - memory stores "objects", memory itself can be anywhere.


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<< calls std::num_put and conversion for void* is %p facet.num.put.virtuals. From 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.

But note that from 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.

What is printed has to be unique for that object, that's all. So a compiler has to pick some unique value for addresses in registers and print them whenever the conversion is requested. The conversions from/to uintptr_t will have also be implemented in an implementation-defined manner. But it would be all in implementation - the implementation details of how the behavior of the code is achieved is invisible to a C++ programmer.

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

Yes and no. In C++ the register keyword, if not deprecated, is a suggestion to the compiler, not a demand.

Whether the compiler implements a pointer to register depends on whether the platform supports pointers to registers or the registers are memory mapped. There are platforms where some registers are memory mapped.

When the compiler encounters a POD variable declaration, the compiler is allowed to use a register for the variable. However, if the platform does not support pointers to registers, the compiler may allocate the variable in memory; especially when the address of the the variable is taken.

Given an example:

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.  

In many common platforms, such as the ARM processors, the registers are located within the processor's memory area (a special area). There are no address lines or data lines for these registers that come out of the processor. Thus, they don't occupy any space in the processor's address space. There are also no ARM instructions to return the address of a register. So for ARM processors, the compilers would change the allocation of a variable from register to memory (external to the processor) if the code uses the address of the variable.

In most cases where a CPU has memory-mapped registers, compilers that use some of them will specify which ones they use. Registers that the compiler's documentation says it doesn't use may be accessed using volatile-qualified pointers, just like any other kind of I/O registers, provided they don't affect the CPU state in ways the compiler doesn't expect. Reads of registers that may used by the compiler will generally yield whatever value the compiler's generated code happened to leave there, which is unlikely to be meaningful. Writes of registers that are used by the compiler will be likely to disrupt program behavior in ways that cannot be usefully predicted.

In theory yes, but only really plausible for a global pinned to that register permanently.
(Assuming an ISA with memory-mapped CPU registers in the first place1, of course; typically only microcontroller ISAs are like this; it makes a high-performance implementation much harder.)

Pointers have to stay valid (keep pointing to the same object) when you pass them to functions like qsort or printf, or your own functions. But complicated functions will often save some registers to memory (typically the stack) to be restored at the end of the function, and inside that function will put their own values in those registers.

So that pointer to a CPU register will be pointing to something else, potentially one of the function's local variables, when that function dereferences a pointer you passed it, if you just pick a normal call-preserved register.

The only way I see around this problem would be to reserve a register for a specific C++ object program-wide. Like something similar to GNU C/C++ register char foo asm("r16"); at global scope, but with a hypothetical compiler where that doesn't prevent you from taking its address. Such a hypothetical compiler would have to be stricter than GCC about making sure the value of the global was always in that register for every memory access through a pointer, unlike what GCC documents for register-asm globals. You'd have to recompile libraries to not use that register for anything (like gcc -ffixed-r16 or let them see the definition.)

Or of course a C++ implementation is allowed to decide to do all that on its own for some C++ object (likely a global), including generating all library code to respect that whole-program register allocation.

If we're only talking about doing this over a limited scope (not for calls into unknown functions), sure it would be safe to compile int *p = &x; to take the address of the CPU register x was currently in, if escape analysis proved that all uses of p were limited. I was going to say this would be useless because any such proof would give you enough info to just optimize away the indirection and compile *p to access as a register instead of memory, but there is a use-case:

If you have two or more variables and do if (condition) p = &y; before dereferencing p, the compiler might know that x would definitely still be in the same register when *p is evaluated, but not know whether p is pointing to x or y. So it would be potentially useful to keep x or y in registers, especially if they're also being read/written directly by other code mixed with derefs of p.


Of course I've been assuming a "normal" ISA and a "normal" calling convention. It's possible to imagine weird and wonderful machines, and/or C++ implementations on them or normal machines, that might work very significantly differently.


What ISO C++ has to say about this: not much

The ISO C++ abstract machine only has memory, and every object has an address. (Subject to the as-if rule if the address is never used.) Loading data into registers is an implementation detail.

So yes, in a machine like AVR (8-bit RISC microcontroller) or 8051 where some CPU registers are memory-mapped, a C++ pointer could point at them1. Having memory-mapped CPU registers is a thing on some microcontrollers like AVR2. (e.g. What is the benefit of having the registers as a part of memory in AVR microcontrollers? has a diagram. (And asks the odd question of why we have registers at all, instead of just using memory addresses, if they're going to be memory mapped.)

This AVR Godbolt link doesn't really show much, mostly just playing around with a GNU C register-asm global.


Footnote 1: In normal C++ implementations for normal ISAs, a C++ pointer maps pretty directly to a machine address that can be dereferenced somehow from asm. (Perhaps very inconveniently on machines like 6502, but still).

In a machine without virtual memory, such a pointer is normally a physical address. (Assuming a normal flat memory model, not segmented.) I'm not aware of any ISAs with virtual memory and memory-mapped CPU registers, but there are lots of obscure ISAs I don't know about. If one exists, it might make sense for the register mapping to be into a fixed part of virtual address space so the address could be checked for register access in parallel with TLB lookup. Either way it would make a pipelined implementation of the ISA a huge pain because detecting hazards like 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 / store forwarding would need to interact with detecting when an instruction reads the result of the previous register write, because that read or write could be via memory.

There are old non-pipelined CPUs with virtual memory, but pipelining is one major reason you'd never want memory-map the registers on a modern ISA with any ambitions of being used as the main CPU for a desktop / laptop / mobile device where performance is relevant. These days, it would make little sense to include the complexity of virtual memory but not pipeline the design. There are some pipelined microcontrollers / low-end CPUs without virtual memory.

Footnote 2: Memory-mapped CPU registers are basically non-existent on modern mainstream 32 and 64-bit ISAs. Do general purpose registers are generally memory mapped?

Microcontrollers with memory-mapped CPU registers often implement the register file as part of internal SRAM that they have anyway to act as regular memory.

In ARM, x86-64, MIPS, and RISC-V, and all similar ISAs, the only way to address registers is by encoding the register number into the machine code of an instruction. Register indirection would only be possible with self-modifying code, which C++ does not otherwise require and which normal implementations don't use. And besides, register numbers are a separate address-space from memory. e.g. ARM has 16 basic integer regs, so an instruction like add r0, r1, r2 will have three 4-bit fields in the encoding of that machine instruction, one for each operand. (In ARM mode, not Thumb.) Those register numbers have nothing to do with memory address 0, 1, or 2.

Note that memory-mapped I/O registers are common on all modern ISAs, normally sharing physical address space with RAM. The I/O addresses are normally called registers, but the register is in the peripheral, like a network card, not in the CPU. Reading or writing it will have some side-effect, so in C++ you'd normally use a volatile int *constexpr ioport = 0x1234; or something for MMIO. MMIO registers are definitely not one of the general-purpose integer registers you can use in an instruction like AArch64 add w0, w1, w2.

Related