Assembly Language, Machine Code, and the CPU

Assembly language is a readable notation for instructions a particular processor can execute. Machine code is the encoded bytes that processor fetches and decodes. They are closely related, but neither is “the language of every computer.” An x86-64 assembler, an AArch64 assembler, and their object-file conventions make different promises. This lesson builds a useful mental model before choosing a target.

From a program to changing state

A CPU repeatedly obtains an instruction from an address, decodes its bits according to its architecture, performs the specified operation, and chooses the next address. Its visible state includes registers, memory, a program counter, and status information. The exact implementation may pipeline, predict branches, or execute work out of order internally; the architecture specifies the results software may rely on. A mnemonic such as add is therefore not an action in the abstract. It denotes one or more precisely documented encodings and state changes for one ISA, or instruction set architecture.

Assembly is usually closer to an ISA than C or Python, yet it remains source text. Assemblers accept dialects, directives, labels, comments, and expressions, then emit bytes and metadata. Intel and AT&T x86 syntaxes even reverse the usual displayed operand order. A compiler can produce assembly as an intermediate artifact, but hand-written assembly is not automatically faster or more direct. The ABI, compiler, assembler, linker, operating system, and processor all still matter.

LayerUseful questionExample
ISAWhich operations and registers exist?x86-64 or AArch64
Assembly syntaxHow are those operations written?Intel x86 versus GNU AT&T
ABIHow do separately built parts cooperate?argument registers and stack alignment
Machine codeWhich bytes are decoded?an opcode and operands

Bits, bases, and signed values

A bit is a zero or one. Eight bits form a byte in the systems discussed here. Binary makes bit positions visible: 10110110₂ is 182 in decimal. Hexadecimal compactly groups four bits per digit, so 0xB6 describes the same byte. Prefixes are notation conventions: many assemblers use 0x, while some debuggers or documents use a suffix or a different style. Never assume a bare number’s base without checking the tool’s syntax.

An unsigned n-bit value ranges from zero through 2n − 1. Most current CPUs represent signed integers with two’s complement: the top bit has negative weight. An 8-bit pattern 11111101 is 253 unsigned and −3 signed. Negating a two’s-complement value means invert the bits and add one. This representation has one zero and makes ordinary binary addition useful for signed and unsigned arithmetic alike, but overflow interpretation differs. The same bits do not carry a type by themselves; an instruction, register width, memory access, and surrounding program give them meaning.

BitsBinaryUnsignedTwo’s-complement signed
801111111127127
810000000128−128
811111101253−3

“Word” is a terminology trap. It historically meant a processor’s natural unit, but manuals and ABIs use it differently. Say “16-bit,” “32-bit,” or “64-bit” when width matters. Likewise, a 64-bit CPU can load an 8-bit byte, and a 64-bit pointer does not mean every integer calculation is 64 bits.

Bytes in memory: order and alignment

Memory has byte addresses. A multibyte number occupies consecutive addresses, and endianness specifies which byte comes first. For the 32-bit value 0x12345678, little-endian memory at increasing addresses holds 78 56 34 12; big-endian holds 12 34 56 78. x86 and mainstream ARM64 systems are ordinarily little-endian, but endianness is a property of a data representation and execution mode, not a synonym for “Intel” or “ARM.” Network protocols often specify their own byte order. Examine bytes, type width, and format specification together.

Alignment means placing an object at an address divisible by a chosen boundary: a 4-byte object at an address ending in binary 00 is 4-byte aligned. Alignment can make accesses simpler or faster and is required by some instructions, ABIs, or atomic operations. x86 commonly permits many unaligned ordinary accesses; ARM64 has defined behavior for many ordinary unaligned accesses too, while particular instructions and memory attributes can impose constraints. “Works on my x86 machine” is not a portable alignment rule. Compilers insert padding in structures and arrange stack alignment to meet their target’s ABI.

Two deliberately different designs

x86-64 is commonly described as a variable-length CISC ISA. Instructions can be from one to fifteen bytes and can combine register operands, immediate constants, and sophisticated memory addressing. A decoder must find instruction boundaries from the bytes and mode. Its many historical features make compact code and rich encodings possible, but a byte sequence decoded at the wrong start address can produce a different instruction stream.

AArch64, commonly called ARM64, uses fixed 32-bit instruction words in its base ISA and follows a load/store model: arithmetic generally names registers, while separate load and store instructions transfer values between registers and memory. Fixed width makes instruction boundaries regular, though data, literal pools, and different execution states must still be distinguished. AArch64 has its own immediate encodings and addressing forms; fixed-width does not mean every constant fits in one instruction.

Propertyx86/x86-64AArch64 (ARM64)
Instruction sizeVariable lengthFixed 32-bit base instructions
Arithmetic memory operandOften permitted by an instructionUsually load first, operate on registers
RegistersNamed general registers with legacy subregisters31 general registers plus special roles
Assembly portabilityNot source-compatible with ARM64Not source-compatible with x86-64

The comparison is a teaching shorthand, not a performance verdict. Modern implementations of both translate and schedule work in sophisticated ways. “CISC” and “RISC” do not tell you how many cycles an instruction takes. Consult the architecture manual for semantics and measure a defined workload only when optimization is actually needed.

A neutral reading example

; Architecture-neutral pseudo-assembly, not accepted by an assembler
LOAD  r0, [address_of_count] ; read a value from memory
ADD   r0, r0, 1              ; compute a new register value
STORE [address_of_count], r0 ; write it back

This makes data movement explicit, which resembles ARM64 style. An x86 instruction might combine the addition and memory reference, while ARM64 code normally separates load, add, and store. Neither form says whether concurrent code needs an atomic operation: that is a separate semantic requirement. A label such as address_of_count is a symbolic name resolved by tools; it is not a universal numeric address.

Safe observation exercises

  1. Write a tiny C function that returns x + 1. Ask a compiler you trust for assembly only, such as cc -S, for two targets available in your environment. Do not run downloaded binaries.
  2. Use objdump -s, llvm-objdump -s, or an IDE’s object viewer on an object you built. Identify bytes for a known initialized integer and predict their little-endian order.
  3. Change an intentionally local structure from char, int to int, char, then inspect compiler layout reports or debug information. Record observed padding; do not infer a universal layout.

The next lesson, Registers, Memory, Bits, and Addressing, turns this state model into careful address computations.

Reading claims with the right scope

CPU documentation distinguishes an instruction’s architectural semantics from its encoding and from a particular microarchitecture’s timing. “This instruction adds two numbers” describes semantics only after operand width, destination, flags, and exceptional cases are supplied. “It is one byte” is an encoding claim that can be false for another operand form. “It is fast” needs a processor model, surrounding instructions, cache state, and a measurement method. Keeping these levels separate prevents many misleading assembly explanations.

Data is not automatically code. In an executable image, bytes may be instructions, constants, tables, unwind information, or padding. A disassembler uses an address and architecture mode to make one plausible instruction interpretation; it cannot recover original source intent from arbitrary bytes with certainty. Variable-length x86 decoding makes a correct entry boundary particularly important, but fixed-width ARM64 code still requires correct section, alignment, and execution state. Symbol and relocation information makes inspection more reliable than decoding a raw byte stream in isolation.

Instruction-set manuals also distinguish architectural registers from physical implementation resources. A modern processor may rename registers internally or speculate along a predicted path, but programs observe behavior constrained by the ISA’s ordering and exception rules. This is why a simple fetch–decode–execute picture is useful for learning without being a literal performance simulator. Do not derive security, concurrency, or cycle-count conclusions from that simplified diagram.

Assembly source is correspondingly a poor interchange format across targets. Even when both systems are 64-bit and little-endian, register names, instruction encodings, calling conventions, system-call interfaces, object formats, and assembler directives can differ. Port an algorithm through a language or carefully rewritten target-specific source; do not rename registers mechanically. For binaries supplied by another party, use isolated, authorized analysis procedures rather than executing them to answer questions that file inspection can answer.

References