I understand that you cannot have a pointer to reference a register, but I don't understand the reason for that.
Registers like RAX have register numbers (0 to 15 since x86-64 has 16 general-purpose registers), which are only usable when embedded into machine code. They're a separate address-space from memory, and no indirect addressing of register numbers is supported, only register-direct.
So even in assembly language or machine code, there's no way you can do indirect access to a register (with a reg num in another register); if you have some stuff in regs that you need to index, you have to store them to memory and index that array.
Or write self-modifying code which stuffs the 3 bits of the register number into the ModR/M byte and the high bit into a REX prefix, but that will perform extremely badly (full pipeline nuke on out-of-order-exec CPUs), so only something you'd do while JIT-compiling something once to run many times.
C pointers point into the memory address space, not I/O space (port numbers for x86 in
/ out
instructions) and not register numbers. This is true across all mainstream architectures.
When someone says "pointer", without additional context they always mean memory address. With context, they might possibly mean I/O address, but even that would be unusual. "Pointer" never mean register number, although register numbers are sometimes called "addresses" in computer science when talking about a "3-address architecture". (e.g. add dst, src1, src2
vs. a 2-address architecture doing add dst, src
as dst += src
that can't non-destructively copy the result to a 3rd operand.
In a register machine (like x86-64 and all modern CPU ISAs), these "addresses" will usually be register numbers, although x86-64 allows one of the explicit addresses to be a memory operand, including with addressing-modes like [rip+rel32]
not involving any normal registers, so you can directly address static data.
In an accumulator machine with only an accumulator, instructions would be things like add [mem]
with the accumulator as an implicit destination, so the "address" would be a memory address, taking up probably 16 bits of space in the machine code for every instruction for a 1-operand accumulator machine. vs. a 3-address machine with 32 registers (like MIPS) taking up 15 bits in every instruction for three register numbers. (Most real 8-bit micros like 6502 and 8080 also had a couple other regs for indirect memory addressing and/temporaries, even though an accumulator was the only one that could be the destination of most math instructions).
x86-64 is a 2-address architecture for most things like classic integer instructions, having some compact 1-address instructions like inc
. And 3-operand for FP/SIMD math with AVX extensions, otherwise 2-address with SSE2. Legacy x87 is a 1-operand register-stack design, modern x86-64 includes a whole tasting menu of ISA design choices, for better or worse, often worse :P)
So register numbers are "addresses" in this sense, but not in the sense where you can "take their address" and get a pointer.
Fun fact: historical C/C++ have a register
keyword which can only be applied to local vars whose address is never taken. Compilers now (and humans writing asm) don't need the hint (except in unoptimized debug builds), so your textbook is correctly treating all local vars which haven't had their address taken as implicitly register
. They're pointing out that isn't possible for variables that do have their address taken, especially when the address is passed to another function that isn't getting inlined.
If it was inlining, it could optimize away the address-taking and dereferencing of a simple swap
-by-reference function and just keep track of the fact that the C variable names are now associated with opposite registers, using zero asm instructions. (Or an xchg reg,reg
if it was in a loop and unrolling didn't make the swap go away, or whatever other reason.)
But of course an optimizing compiler would also do constant propagation aka constant folding and just compile the function to mov eax, imm32
/ ret
, since the function used two locals initialized with constants, instead of two function args with runtime-variable values. If you want to actually look at compiler-generated asm, write int foo(int a, int b)
, not local constants, so you can enable optimization and still have code to look at. (See How to remove "noise" from GCC/clang assembly output?)
Or in this case, a non-inline function call would be sufficient. So just prototype swap_add
but don't define it. Or if you also want to look at its asm, either give it a different name so the compiler doesn't know it's the function you wanted to call, or declare it with __attribute__((noinline,noipa))
for GCC or clang. (no IPA = no Inter-Procedural Analysis. Even when not inlining, GCC can still notice stuff about another function, like which registers it leaves unmodified, or if it doesn't do anything, and optimize callers accordingly, not treating it as a black box.
A few architectures (e.g. AVR, an 8-bit RISC microcontroller) have the storage space for their registers also accessible in memory address-space, i.e. their CPU registers are "memory mapped". Presumably that means they really do just use part of their onboard SRAM storage as a register file. (With extra read + write ports in that part of it I guess.) This would be a disaster for out-of-order execution or even aggressive pipelining where the CPU needs to detect "hazards", like dependencies between instructions. Comparing 4-bit numbers that are only ever hard-coded into machine instructions is way easier than having loads and stores also potentially reading and writing register values.
This is as far as the cross-over between registers and memory gets: even though AVR registers are accessible through memory addresses, the pointer is a memory address. You deref it with normal load/store instructions.
And even on architectures where registers have memory addresses, see Joshua's answer - the code in some function you're calling might save/restore those registers around some other use for them. In that sense, registers are like global variables that each function uses at different times. If you follow a calling convention correctly, they work as private local variables. But if you started taking their address and passing it to other functions, their reuse across functions would be a problem. So pointers to the memory space occupied by registers is only useful in asm where you're taking this into account; C compilers for AVR and other such ISAs can't use those addresses when taking the address of a local variable and passing to other functions.