The Contract Between Hardware and Software

The code a programmer writes ultimately gets converted into instructions the CPU understands. But what happens if every CPU understands different instructions? The compiler would have no way of knowing which instructions to generate, and the operating system could not control the hardware.

The Instruction Set Architecture (ISA) solves this problem. An ISA is a specification that defines the interface between hardware and software. It prescribes which instructions exist, what format each instruction takes, how many registers there are and what roles they play, and how memory addressing works. If the ISA is the same, the same program executes identically on two processors with completely different internal implementations.

Why does this matter? Thanks to the ISA, hardware designers can freely improve the internal microarchitecture, and software developers can write programs that are not tied to a specific hardware implementation. Any processor that supports the x86 ISA runs the same binary, whether Intel or AMD manufactured it. This abstraction is the foundation that has allowed hardware and software to evolve independently for decades.

Instruction Formats

In an ISA, each instruction is encoded as a fixed bit pattern. This bit pattern includes what operation to perform (the opcode) and what operands to use (register numbers, immediate values, addresses, and so on).

In RISC architectures like MIPS and RISC-V, instruction formats are organized into a small number of clean types. Taking RISC-V as an example:

RISC-V Basic Instruction Formats (32-bit fixed)

R-type (register-register operations: add, sub, and, or, etc.)
+---------+-----+-----+------+-----+---------+
| funct7  | rs2 | rs1 |funct3| rd  | opcode  |
| 7 bits  |5 bit|5 bit|3 bits|5 bit| 7 bits  |
+---------+-----+-----+------+-----+---------+

I-type (immediate operations, loads: addi, lw, etc.)
+--------------+-----+------+-----+---------+
|  imm[11:0]   | rs1 |funct3| rd  | opcode  |
|   12 bits    |5 bit|3 bits|5 bit| 7 bits  |
+--------------+-----+------+-----+---------+

S-type (stores: sw, sb, etc.)
+---------+-----+-----+------+--------+---------+
|imm[11:5]| rs2 | rs1 |funct3|imm[4:0]| opcode  |
| 7 bits  |5 bit|5 bit|3 bits| 5 bits | 7 bits  |
+---------+-----+-----+------+--------+---------+

B-type (branches: beq, bne, etc.)
+----+--------+-----+-----+------+--------+---+---------+
|[12]|[10:5]  | rs2 | rs1 |funct3| [4:1]  |[11]| opcode |
+----+--------+-----+-----+------+--------+---+---------+

The commonality across these formats is worth noting. The opcode always occupies the lower 7 bits, and the positions of the source registers (rs1, rs2) and destination register (rd) remain consistent regardless of the format. This regularity simplifies the hardware decoder. Instead of needing complex branching logic to interpret an instruction, the decoder can extract the required fields directly from predetermined bit positions.

Addressing Modes

The method of specifying where an instruction's operands are located is called the addressing mode. Different ISAs support different addressing modes, and these differences significantly affect the programming model and compiler design.

Addressing ModeDescriptionExample
ImmediateOperand is embedded in the instructionaddi r1, r2, 5
RegisterOperand is in a registeradd r1, r2, r3
DirectMemory address is specified in the instructionload r1, [0x1000]
IndirectA register holds the memory addressload r1, [r2]
DisplacementAddress computed as register + offsetload r1, [r2 + 16]
IndexedAddress computed as base + index registerload r1, [r2 + r3]

RISC architectures tend to restrict addressing modes to a minimum. In RISC-V, memory access is permitted only through load and store instructions, and addressing uses only the displacement mode. These constraints actually simplify the hardware and facilitate pipelining.

In contrast, x86 supports a wide variety of addressing modes. A single arithmetic instruction can read directly from memory, perform the operation, and write the result back to memory. This is convenient for the programmer, but it makes hardware implementation correspondingly more complex.

CISC vs RISC

The most fundamental fork in ISA design philosophy is the opposition between CISC (Complex Instruction Set Computer) and RISC (Reduced Instruction Set Computer).

The CISC philosophy aims for a single instruction to perform complex work. One instruction can read a value from memory, perform a computation, and store the result back to memory. Instructions have variable lengths, come in very large numbers, and a single instruction may execute over multiple clock cycles. x86 is the quintessential CISC architecture.

The RISC philosophy takes the opposite approach. Each instruction performs only one simple operation, instruction length is fixed, and most instructions are designed to execute in a single clock cycle. Memory access is only possible through load/store instructions, and arithmetic operations occur exclusively between registers. ARM, MIPS, and RISC-V belong to this category.

So which approach is superior? When RISC emerged in the 1980s, CISC proponents argued that programs would require more instructions, increasing code size and wasting memory bandwidth. RISC proponents countered that simple instructions favored pipelining, and predictable execution times for each instruction made hardware optimization easier.

History has validated both sides to some extent. Modern x86 processors externally maintain the CISC instruction set, but internally decompose complex instructions into RISC-like micro-operations (micro-ops) for execution. In other words, they preserve the CISC interface for software compatibility while borrowing RISC execution principles for performance.

x86 vs ARM

The CISC versus RISC opposition materializes in the competition between two dominant ISAs: x86 and ARM.

x86 began with the Intel 8086 in 1978 and has maintained backward compatibility for nearly 50 years. It features variable-length instructions (from 1 to 15 bytes), complex addressing modes, and a rich instruction set. It has long dominated the desktop and server markets, and its greatest strength is the vast software ecosystem.

ARM is a RISC architecture that originated at Acorn in 1985. It features 32-bit fixed-length instructions (in ARM mode), conditional execution, and a load/store architecture. Thanks to its simple instruction structure, it achieves high power efficiency and dominates the mobile and embedded markets. More recently, through Apple Silicon and AWS Graviton, it has been expanding into desktop and server territory.

Characteristicx86-64ARMv8 (AArch64)
Design PhilosophyCISCRISC
Instruction LengthVariable (1-15 bytes)Fixed (4 bytes)
General-Purpose Registers1631
Memory AccessAllowed in most instructionsLoad/store only
Instruction EncodingComplex, many prefixesRegular, easy to decode
Power EfficiencyRelatively lowerRelatively higher
Software EcosystemDominant in desktop/serverDominant in mobile

In the past, x86 held the advantage in performance while ARM led in power efficiency, but this landscape is shifting. Apple's M-series processors demonstrated desktop-class performance on an ARM foundation, and Intel and AMD have been investing heavily in power efficiency improvements. It has become an era where microarchitecture and manufacturing process matter more than the ISA itself for determining performance and efficiency.

Why RISC-V Is Gaining Interest

Both x86 and ARM are proprietary ISAs. Designing a processor using these ISAs requires paying license fees to Intel or ARM Holdings. RISC-V challenges this arrangement as an open-source ISA.

Started at UC Berkeley in 2010, RISC-V was designed with the goal of an ISA that anyone can freely implement. The base instruction set is extremely compact (47 base instructions), and it adopts a modular structure where extensions are added as needed.

RISC-V is attracting attention not simply because it is free. It reflects lessons from decades of ISA design, featuring a clean architecture that avoids the design inefficiencies x86 and ARM carry for historical reasons. Furthermore, its modular structure enables everything from minimal configurations for IoT devices to full configurations for high-performance servers to be implemented on the same ISA foundation.

Currently, RISC-V is being rapidly adopted in the embedded and IoT sectors, and in the long term it is expected to partially displace ARM's territory. The massive barrier of software ecosystem certainly remains, but the power of open source has already been proven by Linux.

Instruction Encoding in Practice

Let's look at a concrete example of how instruction encoding works. Encoding add x1, x2, x3 in RISC-V produces the following:

add x1, x2, x3  ->  R-type format

funct7   rs2    rs1   funct3  rd     opcode
0000000  00011  00010  000   00001  0110011
|        |      |      |     |      |
|        x3     x2     ADD   x1     OP (integer op)
|
indicates ADD

32-bit machine code: 0000000_00011_00010_000_00001_0110011
Hexadecimal:         0x003100B3

The crucial point in this encoding is that the position of each field is fixed. The hardware decoder can begin extracting the rs1, rs2, and rd fields and accessing the register file before it has even determined the instruction type. This contributes to improving pipelining efficiency.

In contrast, the x86 encoding of the equivalent instruction can vary in length depending on context, and interpretation can change based on prefix bytes. This is why decoder design inevitably becomes complex.

In the next post, we'll look at pipelining and parallel processing.