Registers, Memory, Bits, and Addressing
Programs manipulate values held in a small set of fast architectural registers and a much larger addressable memory. The distinction is central, but it is not a speed guarantee: caches, virtual memory, and out-of-order execution affect timing. Start with the architectural contract—what instruction reads, writes, and addresses—then use a platform’s manuals for details.
Registers are named state, not variables
A general-purpose register holds a fixed-width bit pattern. x86-64 names registers such as RAX, RBX, RSP, and R8; legacy names also select lower portions, for example EAX and AX. AArch64 names 64-bit general registers X0 through X30 and usually refers to their low 32-bit views as W0 through W30. The program counter and stack pointer have special architectural or ABI roles. Floating-point and vector registers are distinct resources. Register names, widths, and effects are ISA-specific: translating a name by resemblance is unsafe.
Registers are reused constantly. A compiler may place a parameter in one register, overwrite that register after consuming it, and keep a temporary in another. A debugger’s “local variable” view is reconstructed from debug metadata and may be unavailable under optimization. Treat a register’s current contents as a moment in a data-flow story, not as a permanently assigned source variable.
| Concept | x86-64 example | AArch64 example | Caution |
|---|---|---|---|
| 64-bit general register | RAX | X0 | Names do not imply identical roles |
| 32-bit view | EAX | W0 | Writes have target-specific extension rules |
| stack pointer | RSP | SP | ABI alignment applies |
| instruction location | RIP | PC | Usually not an ordinary operand |
Memory holds bytes at addresses
An address identifies a byte location in a process’s virtual address space. A pointer value is an address interpreted under language and operating-system rules; it is not automatically safe merely because it is numerically nonzero. Translation from virtual to physical memory, permissions, page faults, and caching are largely below ordinary assembly source, but they determine whether a load or store succeeds. Reading an unmapped or protected address can fault. Programs should not “probe” arbitrary addresses.
Loads copy bytes from memory into a register; stores copy bytes from a register to memory. A register does not become a reference simply because it contains an address. Brackets in an assembly syntax conventionally mean dereference, but exact spelling differs. Width is crucial: loading one byte versus eight bytes changes which bytes are read and how a narrow result is extended. Signed extension preserves a signed value’s meaning; zero extension treats it as unsigned. An unqualified mov in a discussion is incomplete unless syntax and operand sizes are known.
; Architecture-neutral pseudo-assembly
LOAD8U r1, [r0] ; read one byte, zero-extend into r1
LOAD8S r2, [r0 + 1] ; read one byte, sign-extend into r2
STORE32 [r0 + 4], r3 ; write four bytes beginning at r0+4
If r0 holds 0x1000, these accesses concern addresses 0x1000, 0x1001, and 0x1004 through 0x1007. They do not describe a C object by themselves. C’s bounds, effective-type, lifetime, aliasing, and data-race rules remain relevant when source code generates assembly.
Bits inside values
Bitwise instructions combine bit positions independently. AND clears bits where its mask has zeroes, OR sets selected bits, XOR toggles selected bits or compares for difference, and shifts move positions. Masks are often displayed in hexadecimal because every digit maps to four bits. For an 8-bit status field, flags & 0x04 tests bit 2, while flags | 0x04 sets it. A test is not necessarily a Boolean value in a register; many ISAs set condition state instead.
Left shifts and right shifts require terminology care. A logical right shift introduces zeroes; an arithmetic right shift commonly replicates a sign bit. Shift counts at or beyond a value width have language-level and ISA-level rules that must not be conflated. Rotates, carry bits, bit-field instructions, and atomics bring further target-specific behavior. “One instruction” does not make a read-modify-write operation safe between threads; use the documented synchronization primitives and memory ordering for the target.
Computing effective addresses
An effective address is the address an instruction uses after combining its addressing components. x86-64 commonly offers base + index × scale + displacement in a memory operand. The scale is normally 1, 2, 4, or 8, useful for arrays. It also supports RIP-relative references, where an encoded displacement is interpreted relative to the next instruction. AArch64 load/store instructions have their own base-register plus immediate or register-offset forms; larger addresses are often formed by separate instructions. That difference expresses the load/store design, not a capability gap.
| Intent | x86-64 illustration | AArch64 illustration |
|---|---|---|
| array element address | [base + index*4 + 8] | compute or use base plus scaled register offset when supported |
| global nearby code | RIP-relative displacement | often page-relative address formation then load |
| pointer field | memory operand can include displacement | load from a base plus offset |
These are illustrations, not interchangeable source. On x86, a bracketed expression may be consumed directly by arithmetic, comparison, or move instructions. On ARM64, arithmetic generally consumes registers, so code first loads memory into a register. An assembler can reject an expression that looks reasonable but exceeds an instruction’s encodable displacement. Compilers may choose a different sequence for position independence, code size, alignment, or scheduling.
Endianness and representation revisited
Suppose bytes at addresses 0x2000–0x2003 are 78 56 34 12. A little-endian 32-bit load yields 0x12345678; a big-endian load yields 0x78563412. A byte load from 0x2000 yields 0x78 under either ordering because a byte has no byte order. Bit numbering is a separate convention: specifications often call the least significant bit bit 0 even when bytes are printed in memory order. Label the width, endianness, and address direction in notes.
Alignment affects layout and access. Arrays have a stride established by element size; structures may contain padding before members or at their end. An ABI may require the stack pointer to meet an alignment boundary at calls. x86’s tolerance for many misaligned ordinary accesses must not be read as a universal promise; ARM64’s ordinary access rules and penalties also depend on context. Packed layouts used for wire formats need deliberate byte conversion and careful access techniques rather than casual casts.
Address versus value mistakes
Three errors recur: confusing an address with the value at it, confusing a value’s signed interpretation with its bits, and assuming an address calculation dereferences memory. On x86-64, lea calculates an address-like arithmetic expression without loading from it despite its “load effective address” name. On AArch64, add similarly computes a number that may later be used as an address. An address is just a number until an instruction uses it for memory access; whether that access is valid depends on context.
Safe inspection exercises
- Compile a function that returns
a[i]to assembly for a target you own. Identify where the index scaling or address formation occurs, but do not assume another optimization level will retain it. - Place
0x12345678in a static object, build an object file, and inspect its contents withreadelf -x,objdump -s, orllvm-objdump -s. Compare the four bytes without executing the file. - Use compiler-generated assembly to compare signed and unsigned
charpromotion. Look for extension operations, then verify the language type and target defaults.
Next, Instructions, Flags, Branches, Calls, and Returns uses these values and addresses to explain control flow.
Memory is shared state, not a row of variables
At the machine level, two names can designate overlapping bytes. A 32-bit store at one address changes four byte locations; a later byte load can observe one of them. Compilers use source-language aliasing promises to optimize, but the assembly reader should track actual access widths and addresses. This is also why inspecting only a register dump can mislead: a value may have been spilled to memory, reloaded under a different width, or replaced by a later store.
Virtual addresses are per-process abstractions. The same numeric address in two processes need not identify the same physical memory, and a debugger may display an address whose mapping changes between runs because of address-space layout randomization. Page boundaries are typically much larger than ordinary alignment boundaries. An access can be naturally aligned yet cross into an unmapped page, or be misaligned yet valid for a target’s ordinary load instruction. Validity, alignment, and performance are separate questions.
Concurrency adds another distinction. A plain load and store may be individually well formed but still fail to implement a required atomic update or ordering relationship. Some ISAs provide acquire, release, exclusive, or locked forms; language atomics choose mappings based on the target. Do not turn a two-step load/add/store sequence into shared-counter code. Learn its address behavior by inspection, and use the language’s documented atomics for real programs.
Finally, an address computation can overflow at the machine width even where a high-level language would define or reject the operation differently. Array indexing is safe only when its base, bounds, element size, and lifetime are valid. Assembly’s ability to form a number does not confer permission to dereference it. These cautions make disassembly reading more precise rather than less useful.
dispelled