29

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
wolfofuniverse
  • 441
  • 4
  • 8
  • Pointers can't point to registers on any CPU architecture I know of, only to memory locations. Granted, I don't know all CPU architectures, like big-iron systems or ancient architectures. But it's not possible on any common PC-type CPU. – Some programmer dude Nov 02 '20 at 09:03
  • @Someprogrammerdude As far as I remember registers were memory mapped on C64. Cannot cite/quote anything though... (Please do not count it among ancient, or you make me feel among that, too.) – Yunnosch Nov 02 '20 at 09:06
  • @Yunnosch Perhaps you're thinking of the zero-page "registers"? Because the standard accumulator (`A`) and index registers (`Y` and `X`) and the others weren't. Not that I recall anyway, and not that I can find any information about now. – Some programmer dude Nov 02 '20 at 09:11
  • 1
    I could be wrong. Do not bother. But I remember thinking "Cool, I can look into the CPU." (about exactly A,X,Y) as a teenager. I changed my mind about being called "ancient" (or "vintage"). It actually sounds kind of respectful and "awed". It is fine. – Yunnosch Nov 02 '20 at 09:14
  • @Someprogrammerdude Why not? I mean, aren't pointers just a logical representation of the memory model. If a compiler decides to allocate a variable to a register, why can't it just give the pointer that is pointing to it a "reserved' value? – wolfofuniverse Nov 02 '20 at 09:15
  • The standards only specify the observable behaviour of a program relative to an abstract model of memory. The implementation is required to ensure the observable effects of the program remain consistent with what C++ standard requires. From a perspective of the standard, any means by which that effect is achieved is "legal" since the standard only specifies the required observable behaviour, it doesn't specify how that observable behaviour is achieved. – Peter Nov 02 '20 at 09:16
  • @wolfofuniverse But that would make the registers memory-mapped, as the "reserved value" would be the memory-mapped address of the register. – Some programmer dude Nov 02 '20 at 09:16
  • 1
    @Someprogrammerdude As I understand the term memory-mapped refers to an architecture model where registers are part of the "physical memory address space"(not sure if it makes sense for CPU registers to be part of it as I think they can be reached anyways). The question is about pointers which are logical representation of the C++ memory model.(again, could be wrong) – wolfofuniverse Nov 02 '20 at 09:42
  • Look up the "as-if rule" – user253751 Nov 02 '20 at 11:48
  • Generally pointers can't point to registers, anyway, because registers are constantly overwritten, e.g. by the print function. You might call a function with a pointer to EAX, but EAX will have a different value inside the function, so it won't work.. – user253751 Nov 02 '20 at 11:50
  • 4
    @Someprogrammerdude, AVR microcontrollers have memory-mapped main registers. They're used in e.g. Arduino boards, which are programmed in C++. Not big iron, not ancient, though not PC either, since that practically means x86(-64), but not such an obscure architecture either. – ilkkachu Nov 02 '20 at 22:00
  • When a compiler encounters a variable, it is allowed to use a register for that variable (provided that the register is wide enough to hold the contents of the data type). However, when the address is taken of a variable, the compiler will generally convert the variable from register to memory; because most platforms don't support addresses of registers. – Thomas Matthews Nov 02 '20 at 22:56
  • The closest thing I can remember to the C64 thing is: the BASIC interpreter would load values from certain locations in memory into A, X, Y registers before calling the address given in a `SYS` instruction. I don't remember, but it's possible it might have saved the final values back into those locations upon being returned to, also. That's quite far from having memory mapped registers. – Daniel Schepler Nov 02 '20 at 23:18

4 Answers4

40

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.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • This is already convincing. But could you top it off by sprinkling a few quotes from standard? – Yunnosch Nov 02 '20 at 09:17
  • I had a similar intuition but some replies on this post seem suggest it is depends on the architecture https://stackoverflow.com/questions/3937171/may-a-pointer-ever-point-to-a-cpu-register . – wolfofuniverse Nov 02 '20 at 09:23
  • 9
    Well, it depends on architecture if it's _possible_, not if C++ standard allows it. On some architectures it's just not possible, physically, to take an address of an registers. So... on these architectures compilers just don't do it. Still it would be possible for a compiler to translate a code like `*pointer` to pseudocode like `if (pointer == 0x1) { use_EAX_register; } elseif (pointer == 0x2) { use_another_register; } elseif ( etc. etc. etc. for each register } else { dereference_the_actual_pointer; }`, however the resulting code will be super slow. – KamilCuk Nov 02 '20 at 09:45
  • 1
    The specification for `fscanf` would seem to preclude the possibility of any kind of garbage collector in a conforming C++ implementation, since a program could output a pointer's address to a printer and then abandon it without the implementation having any possible way of knowing whether someone might later read that address and place it where `fscanf` would receive it. If that happened, the object which was identified by that pointer should still be live, even if no copy of the address had existed anywhere in the machine prior to that. – supercat Nov 02 '20 at 23:28
  • 2
    @supercat: The GC implementation can rely on [`std::declare_reachable`](https://en.cppreference.com/w/cpp/memory/gc/declare_reachable). Your assumption that the object should be kept alive is incorrect. – MSalters Nov 03 '20 at 09:48
  • @supercat With or without a GC, you can simply delete the object before a call to `fscanf`. The resulting pointer doesn't have to point to a still valid object, it simply has to be a value equal to the one that was printed before. – IS4 Nov 03 '20 at 15:48
  • @MSalters: When was that added to the Standard? I don't think I'd heard of it before, and don't quite see how it could be implemented efficiently and robustly for use in multi-threaded contexts. – supercat Nov 03 '20 at 15:51
  • @IllidanS4supportsMonica: I can't think of what purpose would be served by saying that that pointers are equal if it didn't imply that anything that was accessible via the first would be likewise accessible via the second. Is there anything in the Standard that specifies that the lifetime of an object can spontaneously end as a consequence of an "ordinary" pointer to it being overwritten? – supercat Nov 03 '20 at 15:55
  • @supercat it's says "Since C++11" on that page. – Dan M. Nov 03 '20 at 16:07
  • @DanM.: Thanks. I'd looked at the top and bottom of the page, but hadn't seen that. Is there anything that specifies what invariants must be upheld, and the semantics of calling destructors for objects that become unreachable? – supercat Nov 03 '20 at 16:14
  • @supercat Be aware that the C++ standard doesn't permit very "broad" implications, in this regard. If it mandates the values be equal, they must be equal; nothing more, nothing less. I might be incorrect but pointers to deleted objects can still be compared for equality. As for `std::declare_reachable`, I assume it is completely implementation-defined, so until some GC specification is added to the standard, no invariants can be derived. – IS4 Nov 03 '20 at 16:43
  • @IllidanS4supportsMonica: I don't know if C++ allows pointers to deleted objects to be checked for equality, but in C any attempted comparison would yield Undefined Behavior. Further, the ability to read and write pointers would be rather useless if the only thing one could do with the resulting pointer was compare it for equality with other pointers. Conceptually, it might make sense to say that implementations may specify as much or as little as they see fit about what uses of round-tripped pointers would have defined behavior, but from a requirements standpoint... – supercat Nov 03 '20 at 17:14
  • ...that would be equivalent to saying that doing anything with round-tripped pointers would have undefined behavior, even though the intention would be nothing like the way clang and gcc interpret that phrase. I can't think of any situation where the C or C++ Standard would regard some action as having defined behavior under an Implementation-Defined set of circumstances, and invoking Undefined Behavior otherwise, since implementations are always allowed to specify how they will process actions characterized as UB (except in some circumstances involving SFINAE, where "UB" isn't). – supercat Nov 03 '20 at 17:17
  • @IllidanS4supportsMonica: In any case, the C++ spec seems simultaneously over- and under-specified; it could would be more useful to specify predefined macros or other such means of indicating what guarantees an implementation would offer with respect to round-tripping pointers via various means, and then only require that such operations be meaningful to the extent guaranteed via such macros (which could include having `scanf` not support a `%p` format specifier at all in cases where it would serve no useful purpose). – supercat Nov 03 '20 at 17:27
  • This gives the wrong impression that the question even makes sense & promotes misconceptions. – philipxy Nov 04 '20 at 02:20
8

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.

Thomas Matthews
  • 56,849
  • 17
  • 98
  • 154
  • 1
    *processor's memory area* - I think it would be clearer to call it the "register file", although that does mix up physical vs. logical descriptions. But yes, in most CPUs, registers are a separate address space, separate from memory. (So I don't like the word "memory" in "memory area"). And yes, no indirect addressing is possible for regs, only via small integer fields in the machine-code encoding of instructions themselves, like ARM `add r0, r1, r2` has three 4-bit fields, each one selecting one of the 16 general-purpose integer registers for that operand. – Peter Cordes Nov 03 '20 at 01:13
  • Also, `int *p_b` has to be a pointer, not an int, for that to compile. – Peter Cordes Nov 03 '20 at 01:16
  • "When the compiler encounters a POD variable declaration,..." - that's just now how modern compilers work. Modern compilers can and will shuffle variables around, and the relevant triggers are assignments to the variable. They may even delay the assignment of a value to `p_b` until it's unavoidable; for instance in the code fragment above there's no need to assign a value to `p_b` at all. Even if you'd add `std::cout << *p_b;` it wouldn't be necessary as the compiler can detect that `b` is still uninitialized. – MSalters Nov 05 '20 at 13:58
  • It doesn't matter that ARM has no instruction to return the address of a register. It doesn't have an instruction to return the address of an object in memory either. Consider this: why would you want to know the address of `R2`? You already know it's `2`. And the address of memory address `0x00000008` is equally just `0x00000008`. C needs `&foo` because `&` is a **compiler** operation. Where did the compiler put `foo`? – MSalters Nov 05 '20 at 14:01
4

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.

supercat
  • 77,689
  • 9
  • 166
  • 211
4

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.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847