- Learn
- Computer Science & IT
- Computer Science
- Computer Organization and Architecture
- Instruction Sets and Assembly Language
Instruction Sets and Assembly Language
IntermediateLesson 2 of 6 in Computer Organization and Architecture Course
What you will learn
- Explain what an instruction set is, read RISC-V assembly, and use registers, memory and constants as the operands of instructions.
- Explain what an instruction set architecture isDescribe the instruction set as the contract between software and hardware, the stored-program idea, and how assembly language, machine code and the RISC-V base set relate.
- Use registers to evaluate expressionsName the 32 RISC-V integer registers and their conventional roles, and translate arithmetic expressions into sequences of register instructions.
- Load and store data in memoryUse byte addresses, data sizes, alignment and byte order to move words, halfwords and bytes between memory and registers, including array elements.
- Work with constants and immediatesUse immediate operands for small constants, and build full 32-bit constants and addresses with lui, auipc, addi and the li and la pseudo-instructions.
- Explain what an instruction set architecture is
- Use arithmetic, logic, shift and comparison instructions, and translate C decisions and loops into branches and jumps.
- Use arithmetic, logic and shift instructionsCompute with add, subtract, bitwise logic, shifts and set-less-than, including overflow, signed and unsigned interpretation, masks and multiplying or dividing by powers of 2.
- Branch and jump with conditional and unconditional instructionsUse compare-and-branch instructions, signed and unsigned conditions, jumps and their pseudo-instructions to change which instruction runs next.
- Translate C decisions and loops into assemblyTurn if-else statements, compound conditions, while and for loops and switch statements into branches, and trace the resulting code with real values.
- Use arithmetic, logic and shift instructions
- Convert between assembly instructions and 32-bit machine code using the six RISC-V instruction formats, and explain why the formats are designed as they are.
- Encode register, immediate and store instructionsLay out the fields of R-type, I-type and S-type instructions and convert instructions such as add, sub, addi, shifts, lw and sw into binary and hexadecimal.
- Encode branches, jumps and upper immediates, and decode machine codeEncode B-type, J-type and U-type instructions with pc-relative offsets and upper immediates, and decode any 32-bit RV32I word back into assembly.
- Encode register, immediate and store instructions
- Follow the RISC-V calling convention to pass arguments, return results and preserve registers, use stack frames for nested and recursive procedures, and translate C pointers, arrays and strings.
- Call and return from procedures using the calling conventionPass arguments and results in registers, call with jal and return with ret, and decide which registers the caller or the callee must preserve.
- Build stack frames for nested and recursive proceduresAllocate and free stack frames, save the return address and registers in nested and recursive calls, and place the stack in the memory layout of a program.
- Translate C pointers, arrays and strings into assemblyTurn pointer dereferences, pointer arithmetic, structure fields, array parameters and null-terminated strings into loads, stores and address calculations.
- Call and return from procedures using the calling convention
- Explain how a compiler, an assembler, a linker and a loader turn C source code into a program running in memory, including object files, relocation and dynamic linking.
- Explain how compilers and assemblers produce object filesDescribe the translation hierarchy, what compilers and assemblers do, assembler directives and symbols, and the parts of an object file.
- Explain how linkers and loaders create and start an executableDescribe how a linker places modules and patches addresses, compare static and dynamic linking, and explain how a loader and system calls start and end a program.
- Explain how compilers and assemblers produce object files
- Contrast RISC-V with complex instruction sets such as x86, and judge instruction set trade-offs for performance, code size and compatibility.
- Contrast RISC and CISC design featuresDescribe the defining features of RISC and CISC instruction sets and compare RISC-V with x86 on operands, encoding, addressing modes, registers and condition codes.
- Judge instruction set trade-offs for performance, code size and compatibilityUse the performance equation to compare designs, explain how modern processors blur the RISC–CISC line, and evaluate common fallacies about instruction sets.
- Contrast RISC and CISC design features
Instruction Sets and Assembly Language study guide
1Instructions, registers and memory
An instruction set connects a program to a processor. Start by reading the destination and sources of a RISC-V instruction, then follow values through registers and memory. Byte addresses explain how arrays and structures are accessed; immediate fields explain why some constants need more than one instruction. Keeping values, addresses and memory contents distinct makes each assembly line easier to trace.
1.1 The instruction set as a software contract
A processor executes instructions represented by bits. An instruction set architecture (ISA) gives those bits a shared meaning, so software can request the same operations on different implementations.
What the ISA defines
The ISA specifies instructions, visible registers, data sizes, memory behavior and instruction encodings. An instruction such as add a0, a1, a2 must produce the defined result on every processor that supports it. The internal circuits and scheduling belong to the microarchitecture. Two chips can differ greatly in speed and power use while implementing the same ISA.
Binary compatibility also needs the software environment to match. A program that calls an operating-system service or library depends on more than instruction meanings. Register conventions and data layouts are part of the application binary interface, or ABI.
From readable code to bits
For C, a compiler selects assembly instructions; an assembler encodes them as machine code. Assembly is text with instruction names, registers, comments and labels. Machine code is the binary representation the processor fetches. A label such as Start: names an instruction address; it is not an instruction executed at run time. A comment begins with #.
Start:
add a0, a1, a2 # a0 receives a1 + a2
The first register is the destination. If a1 holds 14 and a2 holds 5, the instruction writes 19 into a0; it leaves the sources unchanged. More complex expressions need several operations. Adding four register values takes three additions, with intermediate results held in registers.
Most instruction names correspond to one machine instruction. A pseudo-instruction is an assembler shorthand: mv requests a register copy and li requests a constant. A pseudo-instruction can expand into one or several real instructions, so counting assembly lines does not always give code size.
Instructions stored in memory
Under the stored-program concept, instructions and data are both represented as numbers in memory. The program counter, pc, identifies the instruction to fetch. For a sequence of 32-bit instructions, ordinary execution advances it by 4 bytes. Branches and jumps choose a different next address. The processor executes the encoding, not the assembly spelling that originally produced it.
RISC-V bases and extensions
RISC-V is an open, royalty-free ISA standard maintained by RISC-V International. RV32I supplies 40 base integer instructions and 32-bit integer registers; RV64I uses 64-bit integer registers. Both provide 32 integer registers. Optional extensions add capabilities: M supplies integer multiplication and division, A atomic operations, F and D floating point, C compressed instructions, and V vectors. The examples use RV32I and use M where mul appears.
RISC-V assembly is specific to RISC-V. A C program can be recompiled for x86 or ARM, but its RISC-V machine code cannot execute directly as those ISAs. A simulator can execute RISC-V instructions on a host with a different ISA and expose register and memory changes one step at a time.
Key Takeaways
- The ISA defines visible behavior; microarchitecture implements it.
- Assembly text is encoded by an assembler; pseudo-instructions may expand.
- With 32-bit instructions, sequential execution advances the pc by 4 bytes.
- RISC-V combines a base ISA with optional extensions.
1.2 Registers and expression results
Registers hold the values an instruction can use immediately. In RV32I each integer register holds 32 bits, and a register name identifies storage, not a C variable or a data type.
Register names and roles
There are 32 integer registers, x0 through x31. The program counter is separate. Five bits identify one integer register because there are 32 possible 5-bit patterns. A small register file also limits access cost and the space register fields consume in instructions.
| Registers | ABI names | Conventional purpose |
|---|---|---|
| x0 | zero | Always reads as 0 |
| x1, x2 | ra, sp | Return address and stack pointer |
| x3, x4 | gp, tp | Global and thread pointers |
| x5–x7, x28–x31 | t0–t6 | Temporary values |
| x8–x9, x18–x27 | s0–s11 | Values preserved across calls |
| x10–x17 | a0–a7 | Arguments; a0–a1 also return results |
Register s0 can also serve as fp, the frame pointer. Writes to zero are discarded, so it can supply a zero operand without first loading a constant. For example, add a0, a1, zero copies a1.
These names express a software agreement. In particular, the hardware does not preserve an s register automatically. A procedure that changes it must arrange to restore its incoming value. The standard ABI also reserves gp and tp for their environment roles; they are not ordinary scratch registers.
Break an expression into operations
Translate f = (g + h) - (i + j); with g, h, i, j in s1–s4 and the result in s0:
add t0, s1, s2
add t1, s3, s4
sub s0, t0, t1
With values 9, 4, 6 and 1, the first instruction leaves 13 in t0. The second leaves 7 in t1. The subtraction writes 6 into s0. Each temporary holds a result until the next instruction needs it.
The order of the two source operands matters for subtraction: sub s0, t1, t0 would give −6. The destination is always first, so sub t0, s0, t1 would instead overwrite a temporary and read the old s0. When tracing, write down the destination after each line; do not update both source registers.
Bits acquire meaning through use
A register has no signed or unsigned tag. The pattern 0xFFFFFFFF can mean signed −1 or unsigned 4,294,967,295. A signed comparison and an unsigned comparison can therefore disagree about the same stored bits. Addresses are also register bit patterns; a load interprets its base as an address.
Keep values alive for as long as needed
A compiler assigns frequently used values to registers. If it needs more live values than registers can hold, it stores some in memory, a process called spilling, and reloads them later. A spill is an actual store/load cost.
Calls create another boundary. A called procedure can overwrite t0–t6 and a0–a7. Keep a value needed after a call in a properly preserved s register, or save it to the stack before the call. Choosing t0 merely because it was free does not make its value survive.
Key Takeaways
- RV32I has 32 integer registers of 32 bits; pc is separate.
- Writes to zero disappear, and reads always give 0.
- Register arithmetic writes the first operand and reads the following operands.
- Temporary results must remain available until their last use.
- Caller-saved registers may change across a procedure call.
1.3 Move values between memory and registers
Memory addresses identify bytes. Loads bring data into registers, and stores copy register bits back to memory. In a load-store architecture, arithmetic does not read its operands directly from memory.
Sizes and byte offsets
A byte occupies one address, a halfword two, and a 32-bit word four. RV32 has a 32-bit address space of up to 4 GiB. In the ILP32 C ABI, char uses 1 byte, short 2, and int, long and pointers 4.
The instructions lw and sw transfer words. Halfword loads are lh and lhu, and byte loads are lb and lbu. Stores use sh and sb for the smaller sizes. A store copies the low bits of its source; sb writes only the low 8 bits.
An address is a base register plus a signed 12-bit byte offset:
lw t0, 32(s3)
sw t0, 48(s3)
The load reads the word at the address in s3 plus 32. It does not add 32 to the loaded value, and it does not change s3. The store writes that value at the base plus 48. The offset range is −2048 through 2047.
Translate an array access
For an int array whose base is in s3, element A[i] begins at the base plus 4 times the index. To implement A[12] = h + A[8];, with h in s1:
lw t0, 32(s3) # A[8]
add t0, s1, t0
sw t0, 48(s3) # A[12]
If A[8] is 80 and h is 3, A[12] becomes 83. An offset of 8 would select A[2], since the address counts bytes. For a variable index in s4, form the address first:
slli t1, s4, 2
add t1, s3, t1
lw t0, 0(t1)
Scaling follows element size, not the processor name. An RV64 LP64 pointer or long occupies 8 bytes and uses ld/sd; an RV64 int still occupies 4.
Extend a small load
A register must hold 32 bits even when a load reads only 8. lb copies the byte's sign bit into every higher bit; lbu fills the higher bits with zeros. Thus byte 0xF0 becomes 0xFFFFFFF0 (−16) with lb, but 240 with lbu. The same distinction applies to lh and lhu. Unsigned bytes, including string bytes, use lbu.
Read the byte order
In little-endian memory, the least significant byte occupies the lowest address. Storing 0x12345678 at 0x1000 places 0x78, 0x56, 0x34, 0x12 at addresses 0x1000 through 0x1003. A byte load from the first address reads 0x78. Big-endian order places 0x12 first; RISC-V systems can define either order, while the usual teaching systems are little-endian.
A naturally aligned address is a multiple of the access size: 4 for a word and 2 for a halfword. Naturally aligned accesses have architectural support. A misaligned access may work, take extra time or raise an exception, depending on the execution environment. Place words on 4-byte boundaries and advance by 4 to reach the next word.
Key Takeaways
- Load and store offsets count bytes, not array elements.
- Compute a variable-index address before issuing the load or store.
- Signed loads sign-extend; unsigned loads zero-extend.
- Little-endian memory puts the least significant byte first.
- Natural alignment follows the access size.
- RV64 LP64 widens pointers and long values, while int remains 4 bytes.
1.4 Fit constants into instruction fields
An immediate is a constant encoded inside an instruction. It lets a common operation such as incrementing a counter avoid a separate load.
Small constants
addi t0, t0, 1 adds 1 to t0. Its immediate is signed 12-bit two's complement, from −2048 to 2047. The processor sign-extends it to 32 bits before addition. Subtract a small constant with a negative immediate, as in addi t0, t0, -5; RV32I has no subtract-immediate instruction.
Loads, jalr, and ordinary arithmetic/logical immediate instructions use signed 12-bit immediates too. In particular, andi, ori and xori do not zero-extend their constants. Shift-immediate instructions instead encode an RV32 shift amount of 0 through 31.
Useful assembler shorthands include:
| Text | Base instruction or expansion |
|---|---|
mv rd, rs | addi rd, rs, 0 |
nop | addi x0, x0, 0 |
not rd, rs | xori rd, rs, -1 |
neg rd, rs | sub rd, x0, rs |
li rd, c, for a small c | addi rd, x0, c |
The constant −1 becomes all ones when sign-extended, which is why xori with −1 flips every bit.
Build all 32 bits
lui places a 20-bit field in bits 31–12 and clears the low 12 bits. For 0x12345678:
lui t0, 0x12345
addi t0, t0, 0x678
The intermediate value is 0x12345000; adding 1,656 supplies the low part. A low part with bit 11 set needs more care. Its 12-bit pattern represents a negative addend, so increase the upper part before adding it.
For 0x00001800, the low pattern 0x800 means −2048. Use:
lui t0, 0x2
addi t0, t0, -2048
The first result is 0x2000. Subtracting 0x800 produces 0x1800. Using upper field 0x1 would produce 0x800, one block of 4096 too low.
For any 32-bit constant, read its low 12 bits as a signed value. Subtract that signed value from the desired constant, divide by 4096, and retain the low 20 bits for lui. For 0xDEADBEEF, the signed low part is −273, so use upper field 0xDEADC, then addi with −273. Assemblers perform this split for li.
Constants, addresses and contents
li t0, 100 puts the number 100 in a register. lw t0, 100(zero) reads the contents of memory at address 100. An address can itself be loaded as a value: la t0, Item asks the assembler for the address named by Item.
auipc adds an upper immediate shifted left 12 bits to the address of that instruction. Combined with an immediate operation or load, it provides pc-relative access across about ±2 GiB. A local la commonly uses auipc plus addi; other expansions support shared-library access.
A constant such as 5,000 cannot be an addi operand. Load it into a second register with li, then use add. A larger value does not gain a wider immediate just because it is written in hexadecimal.
Key Takeaways
- A signed 12-bit immediate ranges from −2048 to 2047.
- lui supplies the upper 20 bits with 12 low zeros.
- If the low 12-bit part is negative, compensate in the upper field.
- li loads a value, la forms an address, and lw reads memory.
- auipc builds an address relative to its own pc.
2Arithmetic and control flow
Once values are in registers, instructions calculate results and decide what runs next. Bitwise logic and shifts work on representations; signed and unsigned comparisons give those patterns different meanings. Branches and jumps turn C decisions and loops into explicit paths. Tracing a path means tracking both the current values and the next instruction address, including the final failed loop test.
2.1 Compute with bits and signed values
An arithmetic instruction reads register bits and writes a new bit pattern. Understanding that pattern requires both the operation and the intended signed or unsigned interpretation.
Addition, subtraction and overflow
In RV32I, add, sub and addi retain the low 32 result bits. Adding 1 to 0x7FFFFFFF produces 0x80000000, which represents −2,147,483,648 as a signed integer. The instruction raises no overflow exception.
For unsigned addition, software can detect wraparound:
add t0, a0, a1
bltu t0, a0, Overflow
A wrapped unsigned sum is smaller than an input. This unsigned test is not the rule for signed overflow, where input and result signs matter.
The M extension supplies mul, mulh, mulhu, div, divu, rem and remu. mul keeps the low product bits; the high-product instructions distinguish signed and unsigned inputs. Division by zero returns an all-ones quotient and the original dividend as remainder, without a division exception.
Select, set and flip bits
and, or and xor operate independently on each bit. AND with 1 keeps a bit; AND with 0 clears it. OR with 1 sets a bit, and XOR with 1 toggles it.
srli t0, a0, 4
andi t0, t0, 0xF
With a0 = 0xABCD, shifting right gives 0xABC. Masking with 0xF leaves 0xC, the original bits 7–4. The shift puts the field at the bottom; the mask removes everything above it. To set bit 4 without changing the others, use ori a0, a0, 0x10. xori with −1 flips all 32 bits.
Choose a shift by its fill rule
A left shift, sll or slli, fills low positions with zeros. A logical right shift, srl or srli, fills high positions with zeros. An arithmetic right shift, sra or srai, copies the sign bit into high positions. RV32 register shifts use only the low 5 bits of the shift-count register.
Shifting 13 left by 3 produces 104. This multiplies by 8 because no needed bits are lost. Left shifts that discard significant bits instead retain only the result that fits in the register.
For unsigned values, right shift divides by a power of 2 and rounds down. For signed values, arithmetic right shift rounds toward negative infinity: shifting −7 right by 1 gives −4. C signed division truncates toward zero, giving −3 for -7 / 2. A compiler needs a correction before replacing that signed division with a shift. A logical shift of the same negative pattern fills with zeros and gives a large positive result.
Compare to produce a value
slt writes 1 when its signed first source is smaller, and 0 otherwise. sltu uses unsigned interpretation. With 0xFFFFFFFF and 1 as inputs, slt writes 1, while sltu writes 0. Immediate versions are slti and sltiu; the latter still sign-extends its immediate before the unsigned comparison.
Bitwise AND is different from C's &&. A bitwise result can contain many set bits; logical AND produces 0 or 1 and may skip evaluating the second expression.
Key Takeaways
- RV32 arithmetic keeps low 32 bits and does not trap on overflow.
- AND selects bits, OR sets them, and XOR toggles them.
- Arithmetic right shift preserves the sign; logical right shift fills zeros.
- srai and C signed division round negative nonmultiples differently.
- slt and sltu interpret the same register patterns differently.
- M-extension multiplication and division are separate from RV32I.
2.2 Choose the next instruction
A conditional branch compares two registers and changes the program counter when its test succeeds. When it fails, execution continues with the next instruction.
Pick the comparison
| Instruction | Branch condition |
|---|---|
beq, bne | Equal, unequal |
blt, bge | Signed less than, signed greater than or equal |
bltu, bgeu | Unsigned less than, unsigned greater than or equal |
RISC-V combines comparison and branch in one instruction. It has no integer condition flags to set first. To test against zero, use zero as one source. beqz a0, L expands to beq a0, zero, L.
Greater-than and less-than-or-equal forms are assembler shorthands with swapped sources. bgt a0, a1, L expands to blt a1, a0, L. Swapping the source order matters just as much as selecting the correct mnemonic.
Signed and unsigned control flow
If a0 holds 0xFFFFFFFF and a1 holds 1, blt a0, a1, L takes the branch because signed −1 is less than 1. bltu does not, because the first pattern is a large unsigned value. Use comparisons consistent with the data's meaning, including unsigned comparison for addresses.
An unsigned bounds check can reject negative signed indexes as well as indexes at or above a small nonnegative length:
bgeu a0, a1, OutOfBounds
For a length of 20, an index of −1 appears as unsigned 4,294,967,295 and branches. An index of 19 does not; an index of 20 does. The length must represent the array's actual nonnegative length.
Jumps and return addresses
jal ra, f writes the next instruction address, pc + 4, into ra, then jumps to f. A plain j L expands to jal zero, L; its link value is discarded. This is appropriate for a loop back edge, but does not establish a return address for a call.
jalr rd, offset(rs1) adds a signed 12-bit offset to a register value, clears bit 0 of the target, and writes its own pc + 4 into rd. ret expands to jalr zero, 0(ra), returning to the saved address without keeping a new link.
Conditional branches and jal store offsets relative to their own instruction addresses. Their reach is about ±4 KiB and ±1 MiB respectively. jalr uses a register-based target and can reach across the address space. A far conditional transfer can invert the test to skip a longer jump:
bne a0, a1, Skip
jal zero, Far
Skip:
Here Far is within jal reach. If equality holds, the short branch falls through into the jump; otherwise it skips it.
Recognize a basic block
A basic block has one entry at its beginning and no transfer of control before its end. Inside it, instructions run in sequence. Branch targets start new blocks; branches and jumps end them. This makes it possible to reason about which assignments always run before a particular test or exit.
Key Takeaways
- Choose signed or unsigned branches to match the meaning of the operands.
- RISC-V branches compare registers directly without condition flags.
- jal saves a link; j discards it.
- jalr uses a register plus an immediate and clears target bit 0.
- A conditional offset is relative to the branch instruction itself.
2.3 Turn C control flow into branches
Assembly makes every path through a decision or loop explicit. Labels mark destinations, conditional branches choose paths, and plain jumps prevent unwanted fall-through.
An if-else needs an exit path
Translate if (i == j) f = g + h; else f = g - h;, with f, g, h, i, j in s0–s4:
bne s3, s4, Else
add s0, s1, s2
j Exit
Else:
sub s0, s1, s2
Exit:
The first branch tests the opposite of the C condition so that equality falls through into addition. The jump after addition prevents execution of subtraction. With g = 10 and h = 3, equal indexes give 13; unequal indexes give 7. Removing j Exit makes the subtraction run after the addition too.
For a < b && b < c, branch to the else-part as soon as either comparison fails. For a < b || b < c, branch to the then-part as soon as either succeeds. This implements short-circuit evaluation, including skipping a second expression whose value is unnecessary.
A while loop tests before its body
For while (save[i] == k) i++;, keep i in s3, k in s5 and the array base in s6:
Loop:
slli t1, s3, 2
add t1, t1, s6
lw t0, 0(t1)
bne t0, s5, Exit
addi s3, s3, 1
j Loop
Exit:
Each visit forms the current element address, loads it and checks it. With the array {7, 7, 7, 2, 7}, initial index 0 and k = 7, the loop exits with index 3. The failed test does not increment the index. The array must contain a stopping element within its valid range.
Sum a counted array
For a nonnegative element count in a1 and the array base in a0:
li t1, 0
li t0, 0
Loop:
bge t0, a1, Done
slli t2, t0, 2
add t2, a0, t2
lw t3, 0(t2)
add t1, t1, t3
addi t0, t0, 1
j Loop
Done:
mv a0, t1
ret
The top test handles a zero length without a load. With four elements {5, -2, 10, 7}, the running sum is 5, 3, 13, then 20. The sequence executes 33 instructions: 2 setup instructions, 7 for each of four successful iterations, one failed test and 2 finish instructions.
A pointer version forms an end address once and advances the current address by 4. For a valid array range whose end address does not wrap, it can use unsigned address comparisons:
li t1, 0
slli t0, a1, 2
add t0, a0, t0
bgeu a0, t0, Done
Loop:
lw t3, 0(a0)
add t1, t1, t3
addi a0, a0, 4
bltu a0, t0, Loop
Done:
mv a0, t1
ret
There are 4 setup instructions, 4 per iteration and 2 finish instructions, totaling 22 for four elements. The initial branch skips the body for an empty range; the pointer advances by one element after each load.
Multiway decisions
A switch can use repeated comparisons or a jump table when case values are dense. A table dispatch checks the case range, scales an index by the 4-byte entry size, loads the case address and jumps through it with jalr zero, 0(t0). The range check must precede the table load so that an invalid case cannot select an unrelated address.
Key Takeaways
- Branching around the then-part commonly uses the opposite condition.
- Jump past the else-part after completing the then-part.
- Compound conditions must preserve short-circuit evaluation.
- Check a counted loop before reading its first element.
- Pointer loops can avoid repeated index scaling.
- A jump table requires a range check before dispatch.
3Instruction fields and machine code
Assembly names become fixed fields inside machine instructions. Work from the operation to its format, then assign registers and immediates to their bit positions. Loads, stores, branches and jumps use different layouts because they carry different information. Encoding and decoding in both directions exposes offset, sign and field-order mistakes that readable assembly can hide.
3.1 Encode register operations, loads and stores
A machine instruction is a 32-bit word divided into fields. Encoding means choosing the right layout, assigning each operand to a field, and writing every field at its full width.
Fixed positions simplify decoding
The low 7 bits, 6–0, hold the opcode. Where present, rd occupies bits 11–7, funct3 bits 14–12, rs1 bits 19–15 and rs2 bits 24–20. Five-bit register fields name x0 through x31. Function fields distinguish operations sharing an opcode.
RV32I uses R, I, S, B, U and J formats. They fit different operands into the same 32 bits while keeping register positions stable. For signed immediates, bit 31 carries the sign.
R-type: two sources and a destination
From bit 31 down to bit 0, the R layout is:
funct7 | rs2 | rs1 | funct3 | rd | opcode
7 5 5 3 5 7 bits
For add s0, a0, a1, rd = 8, rs1 = 10, rs2 = 11. ADD uses opcode 0110011, funct3 = 000, funct7 = 0000000:
0000000 | 01011 | 01010 | 000 | 01000 | 0110011
0000 0000 1011 0101 0000 0100 0011 0011
The hexadecimal word is 0x00B50433. SUB changes only funct7 to 0100000, giving 0x40B50433 for sub s0, a0, a1. Source fields appear in a different order from the assembly operands, so do not simply concatenate the printed register names.
I-type: one source and an immediate
The I layout is imm[11:0] | rs1 | funct3 | rd | opcode. addi uses opcode 0010011 and funct3 = 000. In addi sp, sp, -16, both register fields contain 00010. The 12-bit immediate is 111111110000 (0xFF0), producing 0xFF010113.
A load uses I-type too, with the address base as rs1 and the loaded register as rd. lw t0, 32(s3) has immediate 32, base x19, destination x5, opcode 0000011 and funct3 = 010, giving 0x0209A283.
Shift-immediate operations use the low 5 bits of the immediate region as the shift amount. The upper control bits distinguish shifts: srli t0, t1, 3 is 0x00335293, while srai t0, t1, 3 is 0x40335293. Bit 30 selects the arithmetic fill rule.
S-type: store a source value
A store has no register destination. The S layout uses that space for part of its immediate:
imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode
For sw t0, 48(s3), rs2 = 5 supplies the stored value and rs1 = 19 supplies the base. The 12-bit immediate 48 splits into 0000001 and 10000. With funct3 = 010 and opcode 0100011, the result is 0x0259A823. There is no rd to fill with t0.
After joining fields, group the complete 32-bit string into eight 4-bit nibbles and convert each to one hex digit. Then decode the result: confirm the opcode, register numbers and signed immediate. Padding a register field to 5 bits is essential; dropping a leading zero changes the positions of later fields.
Key Takeaways
- Find the format from the operation before assigning operand fields.
- Register fields are 5 bits and occupy stable positions.
- R-type uses funct7 to distinguish add from sub.
- I-type keeps a 12-bit immediate together.
- S-type splits the offset and has rs2 but no rd.
- Check an encoded word by decoding it back into operands.
3.2 Reassemble offsets and decode machine words
Branch and jump encodings save space by omitting an offset bit that is always zero. Their fields still represent byte distances, and the sign must be reconstructed before calculating the target.
B-type branch offsets
Compute the offset as the target address minus the branch address itself. A bne at 0x1010 targeting 0x1000 uses −16 bytes, not −20 and not −4 instructions.
B-type uses opcode 1100011. Its signed offset has 13 bits, including an unstored zero bit 0. The range is −4096 through +4094 bytes, in steps of 2. Placement is:
| Offset bits | Instruction bits |
|---|---|
| imm[12] | 31 |
| imm[10:5] | 30–25 |
| imm[4:1] | 11–8 |
| imm[11] | 7 |
The source fields stay in their familiar positions. The funct3 values are 000 for beq, 001 for bne, 100 for blt, 101 for bge, 110 for bltu and 111 for bgeu.
For bne t0, t1, -16, the 13-bit offset is 1 1111 1111 0000. Its scattered parts are 1, 111111, 1000 and 1 in the table's order. With rs1 = 5 and rs2 = 6, the instruction is 0xFE6298E3. Replacing the offset with +12 gives 0x00629663.
Offsets support 2-byte steps because compressed instructions can be 2 bytes long. A taken target must still satisfy the active instruction alignment: RV32I without compressed instructions requires 4-byte alignment.
J-type and U-type
jal uses opcode 1101111. Its 21-bit signed byte offset omits bit 0 and places bits in the order imm[20|10:1|11|19:12] across instruction bits 31–12. The range is −1,048,576 through +1,048,574 bytes. jal ra, 192 becomes 0x0C0000EF.
U-type places its upper 20-bit value directly in instruction bits 31–12, followed by rd and the opcode. lui uses 0110111; auipc uses 0010111. Thus lui t0, 0x12345 is 0x123452B7.
Decode by the opcode first
For 0x00A12423, the low 7 bits are 0100011: a store. funct3 = 010 selects sw. The source fields give base x2 (sp) and value x10 (a0). The two immediate fragments join to 8, so the assembly is sw a0, 8(sp).
For a branch, join the scattered pieces in offset-bit order, append the implied zero, and sign-extend the full 13-bit result. Treating the 12 stored bits as an ordinary I-type immediate loses both the correct arrangement and the scale. The sign bit's fixed position and reuse of other field positions reduce decoder work, although the printed layout is less convenient to read by hand.
Relocation preserves relationships
If a branch and its target both move by the same distance, subtracting their new addresses gives the old offset. A reference into another section may change when sections are laid out separately. A linker can therefore preserve an internal relative branch while patching a reference to an external symbol.
Key Takeaways
- Branch and jal offsets count bytes from the instruction address.
- B-type and J-type omit bit 0, which is zero.
- Reassemble an offset before interpreting its sign.
- The opcode determines how the remaining bits are divided.
- Moving an instruction and its target together preserves their relative distance.
4Procedure calls and stack storage
A call has to deliver arguments, preserve useful state and return to the right place. Register conventions let separate procedures cooperate; stack frames keep the state of nested and recursive calls apart. The same address calculations used for memory operands also implement pointers, structure fields and strings. Follow a value across a call as carefully as you follow it across an arithmetic instruction.
4.1 Pass arguments and preserve register values
A procedure call transfers control and data between independently written pieces of code. The calling convention tells both sides where arguments and results go and which register values must survive.
The call and return sequence
The caller places the first eight integer arguments in a0–a7; additional arguments go on the stack. The procedure returns an integer result in a0, using a1 as well for a 64-bit result in RV32.
jal ra, f stores its own address plus 4 in ra, then jumps to f. jal f is its short spelling. The pseudo-instruction call f normally expands to auipc plus jalr for longer reach; the linker may shorten it to one jal. In either case, the return address follows the actual call sequence.
ret is jalr zero, 0(ra). It jumps through the saved address, discarding a new link. A plain j f also discards its link, so it does not set up an ordinary call and return.
Decide who saves a value
Caller-saved registers include t0–t6, a0–a7 and ra. The callee can change them. If the caller needs one of their current values later, it saves that value before the call.
Callee-saved registers include s0–s11 and sp. A callee that changes an s register must save the incoming value and restore it before returning. Restoring sp returns the caller to its own frame. The processor does not enforce these promises; compiled modules cooperate because their software follows the ABI.
Suppose a caller has a loop counter in t0 and invokes a helper. The helper can legally use t0 for its own calculation. Moving the counter into s1 can make it survive helpers, but the surrounding procedure must then preserve the s1 value it received from its own caller.
A leaf procedure can need no frame
A leaf procedure calls no other procedure. This function computes (g + h) - (i + j) from arguments in a0–a3:
leaf:
add t0, a0, a1
add t1, a2, a3
sub a0, t0, t1
ret
It uses only caller-saved working registers and leaves ra unchanged. For inputs 9, 4, 6 and 1, it returns 6. It needs no stack frame.
If the calculation uses s0 instead, it must preserve that register:
addi sp, sp, -16
sw s0, 12(sp)
add t0, a0, a1
add t1, a2, a3
sub s0, t0, t1
mv a0, s0
lw s0, 12(sp)
addi sp, sp, 16
ret
The result is moved to a0 before the old s0 is restored. If the incoming s0 was 1234, it is still 1234 afterward. The allocated frame is 16 bytes to preserve standard stack alignment even though the saved word uses only 4.
A call can be read as six actions: place arguments, transfer control, acquire storage, perform the task, place the result, and return. At each boundary, distinguish the value being returned from the older values that the convention requires the procedure to preserve.
Key Takeaways
- Integer arguments start in a0–a7, and the first result is in a0.
- ra points after the actual call instruction or expanded call sequence.
- Callers protect live caller-saved values; callees restore saved registers they change.
- A leaf using only caller-saved registers may need no frame.
- Move the result before restoring a saved register that held it.
4.2 Give each call its own stack frame
Nested calls need somewhere to keep the outer call's data. The stack provides last-in, first-out storage: the most recent call finishes first and releases its own frame.
Allocate below the old stack pointer
The stack grows toward lower addresses. addi sp, sp, -16 reserves 16 bytes; addi sp, sp, 16 releases them. The standard ABI keeps sp a multiple of 16 throughout execution. A frame can hold saved registers, the return address, spilled values, local objects and space for outgoing stack arguments.
A common nonleaf pattern is:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
# body, including calls
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
The offsets refer to the new sp. If it is 0x2000, the saved ra occupies 0x200C through 0x200F. The matching epilogue restores values while the frame still exists, then releases it. Every return path must balance the allocation.
A frame pointer, conventionally s0, can give local objects fixed offsets when sp changes within a procedure. Its incoming value must be preserved like any other use of s0.
Recursion keeps separate copies
Factorial needs the original n after computing fact(n - 1). It also needs its own return address after the recursive jal overwrites ra:
fact:
addi sp, sp, -16
sw ra, 12(sp)
sw a0, 8(sp)
li t0, 2
blt a0, t0, Base
addi a0, a0, -1
jal ra, fact
lw t1, 8(sp)
mul a0, t1, a0
j Done
Base:
li a0, 1
Done:
lw ra, 12(sp)
addi sp, sp, 16
ret
The M-extension mul multiplies the returned inner result by the original n. For fact(3), frames for 3, 2 and 1 coexist. The base call returns 1; the next returns 2; the outer call returns 6. With initial sp = 0x7FFFFF00, three frames lower it by 48 bytes to 0x7FFFFED0. Returns release them in reverse order.
The base test handles nonnegative 0 and 1. For this 32-bit result, factorial is exact through 12!: 479,001,600. The low product bits no longer represent the full result at 13!, which is 6,227,020,800.
Place the stack in program memory
A typical layout has code in text, globals in static data, dynamically allocated objects in the heap and call frames in the stack. The heap often grows upward and the stack downward. Actual placement belongs to the execution environment.
A pointer to a local stack object becomes invalid when that object's lifetime ends on return. Restoring sp does not erase the old bytes, but later calls can reuse them. Similarly, changing sp and continuing to use old save offsets can read the wrong frame's bytes.
Key Takeaways
- Subtract from sp to allocate; add the same amount to release.
- The standard ABI requires 16-byte stack alignment.
- A nested call overwrites ra, so preserve the outer return address.
- Each recursive activation owns separate saved arguments and return state.
- All exit paths must restore the frame and required registers.
- A local object does not outlive its stack frame.
4.3 Use addresses for arrays, structures and strings
A pointer is an address value. Assembly implements a dereference by using that address as a load or store base, and implements pointer arithmetic by adding a byte distance.
Follow the pointer, then access the value
With int *p in a0, lw t0, 0(a0) reads *p, while sw t0, 0(a0) writes it. The register holds the address; the memory at that address holds the integer. If a pointer variable itself is stored in memory, first load that pointer value, then use it as the base for a second load.
Taking the address of a local C object requires addressable storage, normally a stack slot. A hardware register has no memory address that can be passed as &x.
C pointer arithmetic scales automatically. For RV32 ILP32, incrementing int *p adds 4 bytes, while incrementing char *p adds 1. Assembly has no pointer type to provide the scale:
addi a0, a0, 4 # next int
addi a1, a1, 1 # next char
An array argument is its base address, not a copy of its elements. Pass the element count separately when a procedure needs it; the pointer alone does not encode the array length.
Structure fields have byte offsets
For struct item { int id; int score; };, id occupies bytes 0–3 and score bytes 4–7. With the object's address in a0, lw t0, 4(a0) reads the score. In general, field offsets follow the ABI's layout and padding rules. Do not infer every field offset just by counting source declarations.
For an array of these 8-byte structures, the next element starts 8 bytes later. Within each element the score remains 4 bytes from that element's beginning. Element stride and field offset are separate parts of the address calculation.
Copy a null-terminated string
A C byte string ends with a zero byte. To copy it, read and store one byte before deciding whether the copy is finished:
Loop:
lbu t0, 0(a1)
sb t0, 0(a0)
addi a0, a0, 1
addi a1, a1, 1
bnez t0, Loop
ret
Here a1 is the source and a0 the destination. The destination must have enough space. lbu reads an unsigned byte; sb stores only its low 8 bits. Both pointers advance by 1, including on the final iteration. The branch tests the copied byte, so the terminating zero is stored too.
Copying "RISC" takes five iterations for four letters and the zero byte. At five instructions per iteration plus ret, it executes 26 instructions. An empty string still needs one iteration to copy its terminator. A word load would read four bytes at once and would not implement this byte-at-a-time algorithm.
UTF-8 can encode one character in several bytes. This loop copies those bytes unchanged; it does not count displayed characters. Pointer-based array loops can similarly advance by the element stride without recomputing a scaled index each time. Optimizing compilers can make that transformation even when the C program uses ordinary array indexes.
Key Takeaways
- Dereferencing uses the address held by the pointer.
- Assembly pointer arithmetic must include the element size.
- Array parameters need a separate length when the procedure uses a count.
- Structure stride and field offset serve different parts of an address.
- A string-copy loop must store the terminating zero byte.
5From source code to execution
A program passes through tools that know different things about its final form. A compiler chooses instructions, an assembler encodes them, a linker joins modules, and a loader prepares execution. Symbols and relocations carry unfinished references between stages. Following one call from source to its final address makes the roles of object files, shared libraries and startup code concrete.
5.1 Translate source into a relocatable object
A source file becomes executable instructions through several translation stages. Each stage has a different responsibility, and intermediate files retain information needed by later stages.
The compiler chooses instructions
For C, the usual path is source, assembly, object file, executable, then a running process. A compiler analyzes source code, optimizes it, allocates registers and selects instructions for a target ISA. A compiler driver can run several stages in one command.
gcc -S program.c stops after producing assembly. gcc -c program.c produces an object file without linking. The target compiler determines which ISA that assembly uses; a RISC-V program needs a RISC-V-targeting toolchain.
At -O0, values often pass through stack slots and the assembly closely follows individual source statements. At -O2, register allocation, eliminated work and restructured loops can obscure that correspondence. A C statement has no single fixed assembly translation.
The assembler encodes and records
The assembler turns mnemonics and register names into bits, expands pseudo-instructions and resolves labels it can locate. A forward branch refers to a label encountered later. An assembler can first collect positions and then encode references, or record places to fill in once the target is known.
Directives tell the assembler how to organize output; the processor does not execute them:
.data
.align 2
count:
.word 5
message:
.string "hi"
.text
.globl main
main:
# instructions follow
.word 5 emits a 4-byte data value. .string "hi" emits the character bytes and a terminating zero. On the GNU RISC-V assembler, .align 2 aligns the next item to a 4-byte boundary. .globl makes a symbol visible to other object files. Labels name positions; they do not reserve bytes by themselves.
What an object file keeps
A relocatable object typically contains a header, code in text sections, static data, a symbol table and relocation information. The symbol table distinguishes names defined here from references defined elsewhere. Optional debugging information connects instructions to source lines; -g requests it.
Common object formats include ELF, Mach-O and PE/COFF. The format organizes machine code and metadata; it is distinct from the ISA that defines each instruction's bits.
For call printf, the assembler can emit an auipc/jalr sequence without knowing the final function address. It records an undefined symbol and relocation entries so the linker can finish the reference. A local branch's distance can already be known when its target moves with the same section, though later layout changes such as relaxation can require adjustment.
Other execution paths
An interpreter performs operations through another running program. A just-in-time compiler generates machine code during execution, often after identifying frequently used code. Java commonly uses bytecode followed by interpretation and JIT compilation; Python commonly uses an interpreter. A JIT trades compilation work during execution for faster repeated execution.
An object file is therefore more than a raw instruction list, but it is not yet a complete running process. Unresolved symbols and final layout still need attention from the linker and loader.
Key Takeaways
- The compiler selects and optimizes instructions for a target ISA.
- The assembler encodes instructions and expands pseudo-instructions.
- Directives arrange output; labels name positions.
- Symbols and relocations preserve references that later stages must resolve.
- -S stops at assembly, while -c stops at an object file.
- Interpreters and JIT compilers provide different routes to execution.
5.2 Link modules and start a process
A linker constructs an executable file from object modules and libraries. A loader uses that file to create a running process. Knowing which stage owns an address helps explain both relocation and startup errors.
Lay out modules and patch references
The linker places input sections in the executable's address space, resolves symbol definitions and uses relocation entries to patch references. A name that no linked module defines produces an "undefined reference" error.
Suppose text starts at 0x00010000 and data at 0x10000000. Object A contributes 0x100 text bytes and 0x20 data bytes; B follows immediately. B's text starts at 0x00010100 and its data at 0x10000020.
A jal in A at text offset 0x40 has address 0x00010040. If procB begins at B's text start, the call's offset is 0xC0, or 192 bytes, encoded with ra as 0x0C0000EF. A variable at offset 0x10 in B's data lands at 0x10000030. With gp = 0x10000800, its displacement is −2000, which fits a signed 12-bit load offset. These are separate text and data calculations.
Choose how library code is supplied
Static linking copies required library object code into the executable. The program carries that code, increasing file size; applying a library fix requires relinking it. Static libraries do not eliminate the program's dependency on its operating-system interface.
Dynamic linking keeps library code in shared files, such as .so, .dll or .dylib. Compatible library code pages can be shared by processes. A compatible updated library can serve programs without relinking each executable, but the required version must be available when it is needed.
With ELF lazy binding, the first call goes through a procedure linkage table (PLT) stub. The dynamic linker resolves the target and records it in the global offset table (GOT); later calls use that recorded address. Other configurations resolve symbols at load time.
Position-independent code reaches local targets using relative addressing, including auipc sequences, and uses indirection for external targets. A library's own relative distances remain valid when it moves as a unit. Binding external references may still require relocation work.
The loader creates the running state
The loader reads executable metadata, creates an address space, maps or copies code and data, sets up arguments and the initial stack, initializes required register state and transfers control to startup code. Startup code calls main; after it returns, runtime code requests process exit. Modern systems can map file pages and bring them into memory as needed.
ecall requests an environment service. On RISC-V Linux, a7 holds the system-call number and a0–a5 hold arguments. li a7, 93, li a0, 0, ecall requests exit with status 0; service 64 is write. Simulator service numbers come from the simulator's own interface.
A library missing during linking causes a link failure. A required shared library removed after linking can instead prevent loading or cause failure during runtime binding. The executable may have been built successfully even though it cannot now start.
Key Takeaways
- The linker lays out an executable and resolves and patches symbol references.
- The loader maps that executable and creates initial process state.
- Static linking copies library code; dynamic linking needs compatible shared libraries.
- Lazy binding records a resolved target for later calls.
- Position independence preserves local relative references across movement.
- ecall service numbers belong to the execution environment.
6Instruction set design trade-offs
Instruction sets make different choices about how much work one instruction expresses. Compare RISC-V and x86 through their operands, encodings and address calculations, then connect those choices to real constraints. Performance needs instruction count, CPI and clock timing together; code density and software compatibility answer different questions. A useful design judgment names the workload and the implementation being compared.
6.1 Compare RISC-V and x86 instructions
RISC and CISC describe broad instruction-set design approaches. The useful comparison is how each represents work: operand locations, instruction length, address calculation and control flow.
Regular operations or richer instructions
RISC designs favor simple operations, many registers and load-store access. The approach developed through IBM's 801 work in the mid-1970s and Berkeley RISC and Stanford MIPS in the early 1980s. RISC-V, ARM AArch64, MIPS and Power follow this broad tradition.
CISC designs offer variable-length instructions, richer addressing and operations that can combine several steps. x86 grew from the 8086 introduced in 1978; earlier CISC families include VAX and Motorola 68000.
In Intel x86 syntax, add eax, [ebx+8] reads a 32-bit value from memory and adds it to eax. An equivalent RISC-V operation needs a load followed by arithmetic:
lw t0, 8(s1)
add a0, a0, t0
The x86 instruction combines a memory access with addition. RISC-V exposes them as separate instructions. x86 calls a 32-bit value a doubleword; its word is 16 bits. A word in the RV32 examples is 32 bits.
Encoding and addressing
An x86 instruction is 1 to 15 bytes long. Base RISC-V instructions occupy 4 bytes, with 2-byte compressed forms available through C. Regular field positions simplify decoding. Variable lengths can save space by using short encodings for common operations.
RISC-V uses register operands, immediates, base-plus-displacement addresses, and pc-relative addresses. jalr belongs to register-base addressing, while branches, jal and auipc use the pc.
x86 also combines a base, scaled index and displacement. mov eax, [ebx + esi*4 + 8] reads an indexed 4-byte element with an extra offset in one instruction. RISC-V forms the scaled index and effective address with separate instructions before loading. The work still has to occur internally in either processor.
Registers and comparisons
RISC-V has 32 integer registers, including its fixed zero register. Conventional x86-64 has 16 general-purpose registers; 32-bit x86 has 8. Many x86 arithmetic instructions use a destination that is also a source: add eax, ebx changes eax. RISC-V's three-operand add can keep both source values while writing a third register.
x86 arithmetic and cmp set flags; a later branch such as jl tests those flags. RISC-V uses blt to compare and branch directly. ARM AArch64 also uses flags, so the presence of flags alone cannot classify an ISA as CISC.
What reduced means
RISC does not mean a modern ISA must have a tiny total instruction count. RV32I has 40 base instructions, but optional extensions add many more. The central idea is regular, relatively simple operations and encodings. Compressed encodings also show that fixed instruction length is a design tendency, not a definition that every RISC instruction must satisfy.
Key Takeaways
- RISC-V separates memory access from arithmetic.
- x86 can combine memory operands and arithmetic in one instruction.
- Instruction length and addressing modes affect decoding and code density.
- RISC-V compares within a branch; x86 commonly uses condition flags.
- Instruction count alone does not define a RISC design.
6.2 Judge speed, code size and compatibility
An instruction set shapes the work a program expresses, but execution speed also depends on the processor that implements it. Compare measured or modeled program behavior, not the number of operations promised by one instruction.
Combine all three performance factors
CPU time equals instruction count times average cycles per instruction (CPI) times clock cycle time. Equivalently, divide instruction count times CPI by clock frequency. A lower instruction count can be outweighed by a higher CPI or a slower clock.
At 2 GHz, each cycle takes 0.5 ns. A version with 1.0 billion instructions and CPI 2.0 uses 2.0 billion cycles and takes 1.0 s. Another with 1.3 billion instructions and CPI 1.2 uses 1.56 billion cycles and takes 0.78 s. The second executes 30% more instructions but is about 1.28 times as fast. CPI is an average for that program on that implementation, not a universal property of an ISA.
Complex instructions still need internal work
Many modern x86 processors translate most instructions into simpler internal micro-operations. These can then use execution techniques similar to those in modern RISC cores. The external ISA still matters for encoding, decoding and compatibility even when internal operations look similar.
A complex instruction is not a promise of a fixed cycle count. A block-copy instruction and a simple copy loop can trade places in performance across processors and workloads. The processor version, data size and memory behavior matter; a one-instruction source listing cannot decide the result.
Code density is a separate measure
Shorter machine-code sequences can reduce storage and instruction-cache pressure. CISC variable lengths often help density. RISC-V's C extension provides 16-bit encodings for common operations alongside 32-bit instructions. The specification reports typical use for 50–60% of instructions, giving roughly 25–30% smaller code.
For 100 instructions with half compressed, 50 use 2 bytes and 50 use 4: 300 bytes instead of 400. The executed instruction count remains 100. A size reduction does not by itself specify a speedup, because runtime depends on more than bytes fetched.
Compatibility and tools shape the choice
Existing x86 binaries and libraries give compatibility considerable value. RISC-V freezes base sets and adds extensions. RV32I code can run on an implementation supporting RV32I when the ABI and operating environment match; using M or V adds those requirements. RV32E is a different reduced-register base and does not promise RV32I compatibility.
ARM is widely used in mobile devices as well as laptops and servers; x86 is widely used in PCs and servers; RISC-V serves embedded systems, accelerators, research and teaching. Software availability, licensing and the quality of specific chips all matter when choosing a platform.
Hand-written assembly can help a small critical routine or expose a special instruction, but it ties code to an ISA and increases maintenance work. Optimizing compilers handle register allocation and code restructuring well. Use measurement to justify replacing clear source code with assembly.
Key Takeaways
- Compare instruction count, CPI and cycle time together.
- Micro-operations do not change the ISA visible to software.
- Compressed encodings reduce bytes without necessarily reducing executed instructions.
- Binary compatibility also requires matching ABI and environment.
- Measure a specific program and implementation before claiming an ISA is faster.
Instruction Sets and Assembly Language examples
1Trace operands from an expression to memory
15 steps
- 1IntroductionFollow one value at a time
An assembly instruction names both its operation and its operands. Start with a readable register calculation, then bring a value in from memory and store an updated result. Finally, construct a constant that is too large for one immediate. At each step, track exactly what changes: a destination register, a group of memory bytes, or the next instruction address.
- 2ExplanationRead the instruction contract
The ISA defines what
add a0, a1, a2must do: read the two sources and write their sum to the destination. Microarchitecture determines how the chip carries out that request. Assembly gives the request readable names; the assembler produces its encoded bits. Different chips implementing the same instruction contract must agree on the visible result. - 3ChallengeCount instructions after assembly
An assembler expands one
liinto two machine instructions, followed by oneadd. With only 32-bit encodings, how many instruction bytes do these two assembly lines produce?---reveal---
There are three actual instructions, each 4 bytes, for 12 bytes. The text line count is two, but the expansion determines the machine-code size. Labels and comments add no executed instructions.
- 4ExampleKeep the intermediate sum
Compute
r = (u + v) - wwithu = 18,v = 7,w = 6ina1,a2,a3:add t0, a1, a2 sub a0, t0, a3After the first instruction,
t0holds 25. After the second,a0holds 19. The three inputs remain available because neither instruction writes their registers.t0has one purpose: preserve the first result until the subtraction reads it. - 5ChallengeChange the destination, keep the sources
The same inputs now run
sub a3, t0, a3after the addition. What changes compared with writing the result toa0?---reveal---
The numerical result is still 19, but it replaces the old value 6 in
a3. The instruction reads the source values before writing its destination.a0is unchanged, so a caller expecting the result there would not receive this new result. - 6ExplanationTurn an index into an address
An integer array starts at the address in
s1. Each RV32 ILP32intoccupies 4 bytes, so element 2 begins 8 bytes after the base and element 5 begins 20 bytes after it. The loadlw t0, 8(s1)reads the value at element 2; it does not put the address itself intot0. The base register stays unchanged. - 7ExampleLoad, calculate, store
Implement
A[5] = A[2] + rwithr = 19ina0:lw t0, 8(s1) add t0, t0, a0 sw t0, 20(s1)If
A[2]is 11, the load gives 11, the addition gives 30, and the store writes 30 intoA[5]. Reading and writing memory require separate instructions around the arithmetic. The value ofA[2]remains 11. - 8ChallengeSpot the wrong unit in an offset
Someone changes the first line to
lw t0, 2(s1)because the expression namesA[2]. What has gone wrong?---reveal---
The offset is 2 bytes, not two elements. Element 2 needs 8 bytes from the base. The altered address is also not naturally aligned for a word when the base is aligned, so the access may fail or require special handling before it could even supply useful data.
- 9ExampleRead a byte from a stored word
Store
0x7193D2A6at an aligned address on a little-endian machine. Its increasing-address bytes are0xA6,0xD2,0x93,0x71. A byte load at offset 1 selects0xD2.lbuyields 210, whilelbyields −46 by extending bit 7 into the high register positions. The memory byte is identical; the load instruction supplies the interpretation. - 10ExplanationFind the signed low part
Now construct
0x00003D20in a register. Its low 12 bits are0xD20, which represent −736 as a signed 12-bit immediate. The upper step must therefore start one 4096-byte block higher than the visible0x3suggests. Choosing0x4gives0x4000, from which subtracting0x2E0reaches the desired value. - 11ExampleBuild and check the constant
Use:
lui t1, 0x4 addi t1, t1, -736The first instruction writes 16,384; the second subtracts 736, leaving 15,648, or
0x3D20. The low part is an addend with a sign, not just digits to paste into place.li t1, 0x3D20lets the assembler choose the construction, but its expansion still occupies real instructions. - 12ChallengeRepair a constant split
A split uses
lui t1, 0x3andaddi t1, t1, -736. How far is its result from0x3D20, and which field needs repair?---reveal---
It produces
0x2D20, exactly 4096 too small. Increase the upper field from0x3to0x4and keep the signed low addend. Changing the addend to positive0xD20would exceedaddi’s signed positive range. - 13TipTrack a register through its last use
When tracing a sequence, write a value next to the register that currently holds it. Cross it out only when that register is overwritten. A later procedure call is an overwrite risk for temporary and argument registers even if no explicit assignment appears beside the call. Save a still-needed value before crossing that boundary.
- 14ChallengeCombine the address and value steps
With array base
s1, elementA[6]is 17.t1holds the correctly built constant0x3D20. What doeslw t0, 24(s1); add t0, t0, t1; sw t0, 4(s1)change?---reveal---
It reads
A[6], adds 17 to 15,648 and stores 15,665 intoA[1]. The two offsets count bytes, while the constant is an arithmetic value. The original element at index 6 and the base address ins1remain unchanged. - 15RecapTrace the next sequence yourself
For a new assembly sequence, mark the registers read and written, expand any pseudo-instructions whose size matters, and translate each memory offset into bytes. Check small-load extension separately from byte order. When constructing a constant, verify the intermediate upper value plus its signed low addend before using the result as data or an address.
2Trace arithmetic into decisions and loops
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionMake the execution path visible
- 2ExampleMove the field into position
- 3ExampleKeep only the field width
- 4ChallengePredict a signed shift
- 5ExplanationCompare using the right interpretation
- 6ExampleSeparate a jump from a call
- 7ChallengeTest one pattern two ways
- 8ExampleDraw the two paths of an if
- 9ExplanationPlace the bound before the load
- 10ExampleTrace three body executions
- 11ChallengeCount the whole executed path
- 12ConnectionReplace repeated scaling with a moving address
- 13ChallengeProtect a load with short-circuit control
- 14RecapCheck values and paths together
3Encode an instruction and reconstruct a branch
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionTranslate in both directions
- 2ExplanationIdentify the format and operands
- 3ExampleWrite the fields at full width
- 4ExampleConvert and decode the word
- 5ChallengeLocate a store’s value source
- 6ExplanationMeasure the backward distance
- 7ExampleSplit the branch immediate
- 8ExampleAssemble the complete branch
- 9ChallengeRecover the missing zero bit
- 10ConnectionKeep the relative distance when code moves
- 11ChallengeUse the opcode to reject a false decode
- 12RecapCheck a machine word in reverse
4Keep values alive across nested calls
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionProtect what the caller still needs
- 2ExampleSpecify the wrapper’s result
- 3ExplanationApply the preservation promises
- 4ChallengeChoose the state that crosses the call
- 5ExampleAllocate and fill the frame
- 6ExampleCombine results before releasing storage
- 7ExplanationRepeat the pattern recursively
- 8ChallengeUnwind the saved arguments
- 9ExplanationSeparate an address from its object
- 10ExampleChoose byte operations for a string
- 11ChallengeFinish at the right addresses
- 12TipRestore the saved value, not the new result
- 13ConnectionObject lifetime follows the frame
- 14ChallengeKeep a destination pointer across a helper
- 15RecapAudit one full call boundary
5Follow a call from source to startup
11 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionAsk what each tool knows
- 2ExplanationCompile for a target ISA
- 3ExampleLeave the external call unfinished
- 4ChallengeDistinguish data emission from execution
- 5ExamplePlace the target module
- 6ExamplePatch the relative reference
- 7ExplanationChoose when to supply library code
- 8ChallengeLocate the stage of failure
- 9ConnectionCreate a process before calling main
- 10ChallengeExplain why an object is not the finished program
- 11RecapTrace a new error to its stage
6Compare designs using the right measure
10 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionKeep three measurements separate
- 2ExampleCompare the visible memory operation
- 3ExplanationRelate encoding to the design choice
- 4ChallengeClassify a feature carefully
- 5ExampleCalculate the first execution time
- 6ExampleCompare a larger instruction count
- 7ChallengeSeparate byte savings from speedup
- 8ConnectionCompatibility crosses the ISA boundary
- 9ChallengeEvaluate an assembly optimization claim
- 10RecapState the evidence behind the comparison
Instruction Sets and Assembly Language practice questions
Set 1: Registers, memory and constants10 Practice Questions
Question 1 of 10
In RV32, the sequence below runs with t1 = 0x7000; the word at 0x7030 is 6. What value is stored at 0x7034?
lui t0, 0x4
addi t0, t0, -1984
lw t2, 48(t1)
add t2, t2, t0
sw t2, 52(t1)
Show answerHide answer
0x4000 minus 0x7C0, or 0x3840. The load supplies 6, so the addition produces 0x3846, which the final store writes. 0x3840 is only the constructed constant and omits the loaded operand.Question 2 of 10
Show answerHide answer
add, so both implementations must produce that result. Their internal pipelines can differ. Clock frequency affects timing, but it does not redefine what addition means to a program.Question 3 of 10
sub a2, a0, a1, a0 contains 28 and a1 contains 9. What value is written to a2?Show answerHide answer
a2. Reversing the sources would give −19, but that is not their order here.Question 4 of 10
A[7] begin?Show answerHide answer
Question 5 of 10
sw stores 0x86C4217D at 0x3000. What value does lbu t0, 2(s0) produce when s0 = 0x3000?Show answerHide answer
0x7D, 0x21, 0xC4 and 0x86 at increasing addresses. Offset 2 selects 0xC4, and lbu zero-extends it to 0x000000C4. The sign-extended result would require lb, not the unsigned byte load used here.Question 6 of 10
lui upper field and signed addi immediate construct 0x00002ABC in RV32?Show answerHide answer
0xABC represents −1348. Starting from 0x3000 and subtracting 1348 reaches 0x2ABC, so the upper field is 0x3. Treating 2748 as a positive addi operand fails because it exceeds the signed 12-bit positive limit.Question 7 of 10
t3, calls a conforming helper, then needs the original value. What must the caller arrange?Show answerHide answer
t3 is caller-saved, so a conforming helper may overwrite it. The caller must save the live value or arrange an equivalent preserved location before calling. The helper has no obligation to restore temporary registers merely because its caller will need them.Question 8 of 10
auipc t0, 0x2 executes at 0x4000, followed by addi t0, t0, -24. What final value is in t0?Show answerHide answer
auipc adds 0x2000 to its own address, producing 0x6000. Subtracting 24 leaves 0x5FE8. Using the address of the following instruction as the pc base would incorrectly increase the result by 4 bytes.Question 9 of 10
0xB2 is loaded once with lb and once with lbu. What decimal results do the two loads produce, in that order?Show answerHide answer
0xB2 is 178 minus 256, or −78. lb sign-extends that value, while lbu produces unsigned 178. Making both results positive ignores the sign-extension rule of the signed load.Question 10 of 10
struct rec { int key; int value; }; under RV32 ILP32, with no extra padding. Its base is 0x5000. What address is read for A[3].value, and what signed byte does lb return from that address if the field holds 0x000000E9 in little-endian memory?Show answerHide answer
0x501C. The first byte there is 0xE9, which lb sign-extends to −23. Returning 233 would apply the unsigned extension rule.Set 2: Arithmetic and paths through a program10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
add adds 1 to 0xFFFFFFFF?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
bgt a2, a3, Done?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
srli t0, a0, 8 and andi t0, t0, 0x1F, what value is in t0 if a0 = 0x0000D6B9?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
a0 = -11, what results do srai t0, a0, 2 and C integer -11 / 4 produce, respectively?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
a0 = 0xFFFFFFFE and a1 = 0x00000003. Which of blt a0, a1, L and bltu a0, a1, L takes its branch?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
if (x != y) r = 8; else r = 3; branches to Else when equality holds. What goes after the assignment of 8 so that r stays 8 on the unequal path?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
while (A[i] == 6) i++; starts at i = 1 with A = {6, 6, 6, 4, 6}. What is i when it exits?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
A leaf runs the code below on a 4-byte integer array {2, -5, 9}, with its base in a0 and count 3 in a1. Each displayed instruction is one machine instruction. What result and dynamic instruction count does it produce?
addi t0, zero, 0
addi t1, zero, 0
L: bge t0, a1, D
slli t2, t0, 2
add t2, a0, t2
lw t3, 0(t2)
add t1, t1, t3
addi t0, t0, 1
jal zero, L
D: addi a0, t1, 0
jalr zero, 0(ra)
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
0x8000, jalr ra, 7(t0) runs with t0 = 0x903A. What target is used, what goes into ra, and what instruction can return through that link?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
p != 0 && *p == 9, with p in a0. For the then-path, what control-flow order preserves the condition and avoids dereferencing a null pointer?Show answerHide answer
Save this lesson to see this answer.
Set 3: Instruction fields and relative targets10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
bne a2, zero, Loop is at 0x4018, with Loop at 0x400C. Using opcode 0x63 and funct3 = 001, what encoded word reaches Loop?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
0x6000, the word 0x000022B7 is followed by 0xFE028293 at 0x6004. What value is left in t0? These decode as U-type LUI and I-type ADDI, respectively.Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
0xFEC12623. The opcode 0x23 selects a store and funct3 = 010 selects a word. What assembly instruction does it represent?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
sw a2, 20(sp), which register supplies the rs1 field?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
addi t2, s1, -36?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
0x308C targets 0x3068. What signed byte offset is encoded?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
jal at 0x1200 has signed offset +84 and destination ra. What target and return address result?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
addi t2, s1, -36? Here t2 = x7, s1 = x9, ADDI opcode is 0x13, and funct3 = 000.Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
0x30 and 0x54. The section moves from 0x9000 to 0xB000 without changing its internal layout. What branch offset and target address result?Show answerHide answer
Save this lesson to see this answer.
Set 4: Calls, recursion and pointer access10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
s4. What value must s4 hold when it returns?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
a0 to the next element when it holds an int *?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
sp = 0x8000, allocates 32 bytes, then executes sw ra, 28(sp). At what address does the saved word begin?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
a0, computes into s1, then restores its incoming s1 before returning. What must it do before that restoration to return the computed integer?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
ra and s2, each one word. What is the smallest positive frame size that preserves the required stack alignment?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
n in a0 and must return helper(n) + n. helper follows the ABI and may overwrite all caller-saved registers. The function’s incoming ra must also survive. Which plan works with a 16-byte frame?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
n and ra, returns 1 for n < 2, and otherwise returns n * f(n - 1). With initial n = 4 and sp = 0x9000, what result and lowest sp occur?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
0xA000 holds pointer value 0xB000. At 0xB000 begins an array of 4-byte integers, with element 4 equal to 27. With a0 = 0xA000, what sequence leaves that element in a0?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
ra at 28(sp) and s0 at 24(sp), then calls a helper. Its exit first adds 16 to sp, then loads from 24(sp) and 28(sp), then adds another 16 and returns. Why can this fail even though sp ends with its original value?Show answerHide answer
Save this lesson to see this answer.
Set 5: Build, link and start a program10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
gcc -c module.c produce without linking?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
main but calls mix, which is defined in a second object. What lets the linker connect that call to the definition?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
filter. The library archive is then fixed, but the executable is left untouched. What code does the executable contain for filter?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
a7, 7 in a0, then executes ecall. What service is requested?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
0x20000 and data at 0x30000000. Module P contributes 0x180 text bytes and 0x30 data bytes; Q follows immediately. A call in P is at text offset 0x70; its Q target is at Q text offset 0x20. Q’s variable is at Q data offset 0x14. What are the call’s byte displacement and the variable’s address?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
0x1000 contains the following expansion choices: li t0, 5000 uses two instructions, mv a0, t0 one, and call Helper an unrelaxed two-instruction pair. Helper returns normally. At what address does execution resume after the call, and how many code bytes precede that resumption?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
0x2C0 in a shared library. A local caller is at offset 0x90; a function pointer outside the library is stored in a GOT slot. The library base changes from 0x40000000 to 0x50000000. Which description correctly handles both references?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
main. What explains the failure, and what would change for code statically included from that library?Show answerHide answer
Save this lesson to see this answer.
Set 6: Evaluate designs and diagnose assembly10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
add eax, [ebx+12], with eax corresponding to a0 and ebx to s1?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
mul, expects the ILP32 calling convention, and invokes RISC-V Linux system calls. A board advertises RV32I, uses ILP32, and runs a simulator with its own service interface. What additional requirements must be met before that binary can run as intended?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
struct sample { int id; int reading; }; without extra padding. A pointer to element 2 is in a0. The next element’s reading is 31. What address calculation and load retrieve that reading without changing a0?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
This leaf must return the sum of n integers, with n nonnegative. It is called with n = 0 and an address that must not be read. What minimal control-flow repair avoids the invalid access while preserving positive-count behavior?
addi t1, zero, 0
L: lw t0, 0(a0)
add t1, t1, t0
addi a0, a0, 4
addi a1, a1, -1
bne a1, zero, L
D: addi a0, t1, 0
ret
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Application binary interface
Arithmetic right shift
Assembler
call and can leave relocation entries for a function defined in another module.Assembler directive
.word 5 emits a 4-byte value, while .text selects where subsequent code belongs.Assembly language
add a0, a1, a2 names an operation that has a defined RISC-V machine-code encoding.B
Basic block
Binary compatibility
Bit mask
0xFF retains the low byte while clearing all higher register positions.Branch displacement
Byte addressing
C
Callee-saved register
s1 internally if it restores the caller’s original s1 before returning.Caller-saved register
t2 needs saving before a helper that is free to use that register.Calling convention
a0 through a7.CISC
Code density
Compiler
Compressed instruction
Condition flags
Conditional branch
bne t0, t1, Loop returns to Loop only while the two register patterns differ.Cycles per instruction
D
Dereference
lw t0, 0(a0) reads a 4-byte integer through the address currently held in a0.Disassembly
Displacement addressing
lw t0, 20(s1), the loaded word begins 20 bytes beyond the address stored in s1.Dynamic instruction count
Dynamic linking
E
Element stride
Epilogue
sp keeps their offsets relative to the allocated frame.F
Fall-through
Function field
funct7 bits.I
Immediate
addi t0, t0, 1 is encoded in the instruction as an immediate.Implied bit
Indirect jump
jalr can implement a function-pointer call, a return or a jump-table dispatch.Instruction format
Instruction set architecture
Integer register
J
Jump and link
jal ra, Helper records where the caller should resume when Helper returns.Jump table
Just-in-time compilation
L
Lazy binding
Leaf procedure
Linker
Little-endian byte order
0x12345678 places 0x78 in the first byte and 0x12 in the fourth.Load address
la t0, Item, a separate load is needed to read the value held at Item.Load-store architecture
Loader
Logical right shift
Loop bound
M
Micro-operation
Microarchitecture
N
Natural alignment
Nibble
Null terminator
O
Object lifetime
Opcode
Overflow
P
PC-relative address
auipc sequence can reach local data even when the whole code-and-data group moves.Pointer
Position-independent code
Program counter
Prologue
ra before a nested call protects the address needed to return to the outer caller.Pseudo-instruction
li depends on the value of its constant operand.R
Recursion
Register allocation
Register field
a0 as register number 10, written 01010 in binary.Relative relocation
Relocatable object file
-c stops before the library and module combination needed for an executable.RISC
S
Scaled-index addressing
Set less than
slt and sltu can disagree when a source’s top bit gives it different signed and unsigned values.Short-circuit evaluation
&& prevents evaluation of the second operand in C.Sign extension
0xF0 with lb produces 0xFFFFFFF0, the 32-bit representation of −16.Spilling
Stack frame
Stack pointer
sp reserves an aligned frame at lower addresses in the RISC-V convention.Static linking
Stored-program concept
Structure field offset
Symbol table
System call
ecall with 93 in a7 requests exit, using a0 for the status.U
Upper immediate
lui places above 12 low zeros when constructing an RV32 register value.addi can supply a signed low part to complete a full 32-bit constant.Z
Zero extension
0xF0 with lbu produces the positive value 240 in a 32-bit register.Zero register
zero when the program does not need to save a return address.- 1Computer Performance
- 2Instruction Sets and Assembly Language
- 3Computer Arithmetic
- 4Processor Datapath and Pipelining
- 5Memory Hierarchy and Caches
- 6Parallel Processors and Multicore
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.