Topik ini berisi tur berpemandu ke dua ISA, singkatan dari instruction set architecture yang berarti kontrak instruksi antara perangkat keras dan perangkat lunak, yaitu x86-64 dan RISC-V, tanpa maksud memenangkan salah satunya. Tur berhenti di tiga tempat, yaitu silsilah kedua arsitektur, gaya encoding yang menjadi wajah khas masing-masing, dan satu fungsi C kecil yang diperlihatkan dalam dua pencahayaan berbeda. Seluruh buktinya keluaran nyata, mulai dari disassembly, yaitu pembacaan balik kode mesin menjadi daftar instruksi, hasil kerja gcc, hingga jalannya program RISC-V pada simulator.
Dua silsilah yang berbeda#
x86-64 adalah keturunan langsung dari 8086, prosesor 16 bit yang diperkenalkan Intel pada 1978. Setiap generasi berikutnya mewarisi tugas yang sama, yaitu tetap menjalankan perangkat lunak lama, sehingga pola instruksi 16 bit dan 32 bit masih hidup di dalam prosesor 64 bit hari ini. Perluasan 64 bit diperkenalkan AMD pada 2003 dengan nama AMD64, kemudian diadopsi Intel, dan kini dipakai hampir seluruh laptop, desktop, serta server. Nama x86-64, x64, dan AMD64 menunjuk keluarga instruksi yang sama.
RISC-V lahir dari arah sebaliknya. Proyek ini dimulai di University of California, Berkeley pada 2010 sebagai arsitektur yang dirancang ulang dari nol tanpa beban kompatibilitas, dan kini dikelola RISC-V International, badan standar terbuka yang berkedudukan di Swiss. Dasarnya bernama RV32I, yaitu set instruksi 32 bit yang kecil, sedangkan kemampuan tambahan datang sebagai ekstensi yang dirinci pada tabel di bawah.
Cara mengelola standarnya pun berbeda. x86-64 hanya dapat diimplementasikan dengan lisensi dari pemiliknya, sedangkan RISC-V dirilis sebagai open standard, yaitu spesifikasi yang bebas diimplementasikan siapa pun tanpa royalti. ARM, penyedia prosesor ponsel, berdiri di antara keduanya dengan model lisensi yang menyerupai jalur x86. Tabel berikut merangkum perbedaan yang dibedah pada bagian-bagian berikutnya.
| Aspek | x86-64 | RISC-V |
|---|---|---|
| Encoding | panjang instruksi bervariasi 1 hingga 15 byte | tetap 4 byte, ekstensi C menambah bentuk 2 byte |
| Dekoder | rumit dan mahal, membongkar instruksi lama menjadi operasi internal sederhana | sederhana karena batas tiap instruksi selalu jelas |
| Ukuran kode | padat pada operasi yang menyentuh memori | lebih panjang pada RV32I, memampat kembali dengan ekstensi C |
| Ekstensibilitas | ditambahkan oleh Intel dan AMD sesuai kebutuhan keduanya | ekstensi resmi M, A, F, D, C, V, dan G, ditambah ekstensi kustom |
| Model bisnis | lisensi tertutup dari pemilik patennya | open standard bebas royalti |
| Ekosistem | matang, hampir semua perangkat lunak desktop dan server tersedia | lebih muda, dukungan Linux dan toolchain terus bertambah |
Dua baris pertama tabel adalah keputusan teknis yang saling berkaitan, yaitu kerumitan dekoder dan panjang encoding, sedangkan baris sisanya menyangkut organisasi di balik arsitektur. Tiga bagian berikutnya membedah perbedaan itu dengan keluaran nyata, dimulai dari fungsi C kecil yang sama pada kedua sisi.
Satu fungsi C, dua pencahayaan#
Perbandingan yang paling adil dimulai dari fungsi yang sama pada kedua sisi, lalu hasil keduanya dibaca langsung tanpa jembatan penengah. Berkas berikut memuat dua fungsi kecil, yaitu tambah_skor yang menambahkan bonus ke sel memori yang ditunjuk pointer lalu mengembalikan nilainya, dan konstanta yang mengembalikan nilai besar 100000 untuk menyorot cara tiap arsitektur menanamkan konstanta besar ke dalam instruksi. Kompilasi menargetkan x86-64 dengan gcc, dan disassembly-nya dibaca dengan objdump.
// tidak-dijalankan
// tambah-skor.c - fungsi kecil yang sama dalam dua pencahayaan ISA
int tambah_skor(int *skor, int bonus) {
*skor = *skor + bonus;
return *skor;
}
int konstanta(void) {
return 100000;
}# kompilasi ke berkas objek dengan opsi yang menstabilkan keluaran
gcc -O1 -fno-pie -no-pie -fno-asynchronous-unwind-tables -c tambah-skor.c -o tambah-skor.o
# tampilkan disassembly berkas objek
objdump -d tambah-skor.o
tambah-skor.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <tambah_skor>:
0: 89 f0 mov %esi,%eax
2: 03 07 add (%rdi),%eax
4: 89 07 mov %eax,(%rdi)
6: c3 ret
0000000000000007 <konstanta>:
7: b8 a0 86 01 00 mov $0x186a0,%eax
c: c3 retKolom tengah keluaran memuat byte instruksi sebagaimana dilihat prosesor, dan panjangnya langsung terlihat tidak seragam. Instruksi ret hanya satu byte, instruksi add dan pasangan mov-nya dua byte, sedangkan mov $0x186a0,%eax menempati lima byte karena konstanta 100000 membutuhkan empat byte tambahan di dalam instruksinya. Instruksi add (%rdi),%eax memperlihatkan ciri khas x86-64, yaitu operand memori, yakni kemampuan aritmetika mengambil satu operand langsung dari memori tanpa instruksi muat terpisah. Badan tambah_skor karena itu hanya menempati tujuh byte untuk empat instruksi.
Notasi keluaran di atas mengikuti gaya AT&T yang dipakai objdump, yaitu operand tujuan ditulis paling kanan, sehingga add (%rdi),%eax berarti menambahkan isi alamat pada rdi ke dalam eax. Alamat pada kolom kiri bergerak 0, 2, 4, 6, lalu 7 dan c, karena setiap instruksi membawa panjangnya sendiri. Tidak ada tanda batas antarinstruksi pada deretan byte-nya, dan pembacaannya menuntut penafsiran dari awal berkas.
Sisi RISC-V melalui simulator#
Sandbox tempat topik ini ditulis tidak memiliki toolchain RISC-V native, yaitu rangkaian compiler dan assembler yang menerjemahkan C menjadi instruksi RISC-V untuk dijalankan langsung. Padanan RV32I dari tambah_skor karenanya ditulis tangan dan dijalankan pada simulator kecil yang disertakan dalam bundle sebagai sim.py, yang menafsirkan subset RV32I beserta ecall gaya RARS, yaitu layanan pencetakan dan keluar program yang nomornya ditaruh pada register a7. Daftar instruksinya bukan keluaran compiler, melainkan daftar yang disusun agar bentuknya terlihat jernih.
#!/usr/bin/env python3
"""sim.py — simulator RV32I subset untuk pembelajaran arsitektur komputer.
Pemakaian:
python3 sim.py [--trace] program.s
python3 sim.py (tanpa argumen: cetak bantuan)
Cakupan:
- Instruksi dasar RV32I (aritmetika, logika, shift, branch, jump,
load/store, lui, auipc) beserta pseudo-instruksi umum.
- Direktif: .text, .data, .globl, .align, .word, .byte, .asciz, .space.
- ecall gaya RARS: a7=1 cetak bilangan bulat (a0), a7=4 cetak string
di alamat a0, a7=11 cetak karakter (a0), a7=10 keluar.
- sp (x2) diinisialisasi ke 0x10000; memori di-nol-kan; pc mulai 0.
"""
from __future__ import annotations
import re
import sys
MASK32 = 0xFFFFFFFF
STACK_TOP = 0x10000
STEP_LIMIT = 1_000_000
REGS = {"zero": 0, "ra": 1, "sp": 2, "gp": 3, "tp": 4,
"t0": 5, "t1": 6, "t2": 7, "s0": 8, "fp": 8, "s1": 9,
"a0": 10, "a1": 11, "a2": 12, "a3": 13, "a4": 14,
"a5": 15, "a6": 16, "a7": 17,
"s2": 18, "s3": 19, "s4": 20, "s5": 21, "s6": 22,
"s7": 23, "s8": 24, "s9": 25, "s10": 26, "s11": 27,
"t3": 28, "t4": 29, "t5": 30, "t6": 31}
for _i in range(32):
REGS[f"x{_i}"] = _i
INT_RE = re.compile(r"^[+-]?(0[xX][0-9a-fA-F]+|0[bB][01]+|\d+)$")
MEMOP_RE = re.compile(r"^([+-]?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\d+))\s*\(\s*([a-zA-Z0-9]+)\s*\)$")
class SimError(Exception):
pass
def reg(name: str) -> int:
key = name.strip().lower()
if key not in REGS:
raise SimError(f"register tidak dikenal `{name}`")
return REGS[key]
def to_int(text: str) -> int:
s = text.strip().replace("_", "")
if not INT_RE.match(s):
raise SimError(f"angka tidak valid `{text}`")
neg = s.startswith("-")
s = s.lstrip("+-")
value = int(s, 0)
return -value if neg else value
def signed(value: int) -> int:
value &= MASK32
return value - (1 << 32) if value & 0x80000000 else value
def u32(value: int) -> int:
return value & MASK32
class Program:
def __init__(self) -> None:
self.labels: dict[str, int] = {}
self.instructions: list[tuple[int, str, str]] = [] # (pc, teks, mnemonic)
self.data: dict[int, int] = {} # alamat byte -> nilai byte
def parse_source(text: str) -> Program:
prog = Program()
pc = 0
dataptr = None # None = sedang tidak menandai alamat data berikutnya
pending: list[str] = []
def flush_labels() -> None:
for name in pending:
if name in prog.labels:
raise SimError(f"label duplikat `{name}`")
prog.labels[name] = dataptr if dataptr is not None else pc
for lineno, raw in enumerate(text.split("\n"), start=1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
pending = []
while True:
m = re.match(r"^([A-Za-z_.][\w.]*)\s*:\s*(.*)$", line)
if not m:
break
pending.append(m.group(1))
line = m.group(2).strip()
if not line:
flush_labels()
continue
parts = line.split(None, 1)
head = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
if head.startswith("."):
flush_labels()
if head in (".text", ".data", ".globl", ".global", ".section"):
if head == ".text":
dataptr = None
elif head == ".data":
dataptr = pc
continue
if head == ".align":
align = 1 << to_int(rest)
if dataptr is not None:
while dataptr % align:
prog.data[dataptr] = 0
dataptr += 1
else:
pad = (align - pc % align) % align
pc += pad
continue
if head in (".word", ".byte", ".asciz", ".string", ".space", ".zero"):
addr = pc if dataptr is None else dataptr
addr_end = addr
if head == ".space" or head == ".zero":
addr_end = addr + to_int(rest)
elif head in (".asciz", ".string"):
s = rest.strip()
if not (s.startswith('"') and s.endswith('"')):
raise SimError(f"baris {lineno}: .asciz butuh string berkutip")
body = s[1:-1].encode("latin-1", "backslashreplace").decode("latin-1")
for ch in body + "\x00":
prog.data[addr_end] = ord(ch) & 0xFF
addr_end += 1
else:
width = 4 if head == ".word" else 1
items = [t.strip() for t in rest.split(",") if t.strip()]
for item in items:
value = to_int(item) & (MASK32 if width == 4 else 0xFF)
for k in range(width):
prog.data[addr_end] = (value >> (8 * k)) & 0xFF
addr_end += 1
if dataptr is None:
pc = addr_end
else:
dataptr = addr_end
continue
raise SimError(f"baris {lineno}: direktif tidak didukung `{head}`")
# instruksi
flush_labels()
if dataptr is not None:
raise SimError(f"baris {lineno}: instruksi setelah label data")
prog.instructions.append((pc, line, head))
pc += 4
return prog
def operands(rest: str) -> list[str]:
if not rest:
return []
return [t.strip() for t in rest.split(",") if t.strip()]
class Sim:
def __init__(self, prog: Program, trace: bool) -> None:
self.prog = prog
self.trace = trace
self.regs = [0] * 32
self.regs[2] = STACK_TOP
self.mem: dict[int, int] = dict(prog.data)
self.pc = 0
self.out: list[str] = []
def read_byte(self, addr: int) -> int:
return self.mem.get(addr & MASK32, 0)
def read(self, addr: int, width: int) -> int:
if addr % width:
raise SimError(f"akses tidak sejajar: alamat 0x{addr & MASK32:08x} lebar {width}")
value = 0
for k in range(width):
value |= self.read_byte(addr + k) << (8 * k)
return value
def write(self, addr: int, width: int, value: int) -> None:
if addr % width:
raise SimError(f"akses tidak sejajar: alamat 0x{addr & MASK32:08x} lebar {width}")
for k in range(width):
self.mem[(addr + k) & MASK32] = (value >> (8 * k)) & 0xFF
def label(self, name: str) -> int:
key = name.strip()
if key not in self.prog.labels:
raise SimError(f"label tidak dikenal `{name}`")
return self.prog.labels[key]
def emit(self, text: str) -> None:
self.out.append(text)
def do_ecall(self) -> bool:
service = signed(self.regs[17])
a0 = self.regs[10]
if service == 1:
self.emit(str(signed(a0)))
elif service == 4:
chars = []
addr = a0
while True:
byte = self.read_byte(addr)
if byte == 0:
break
chars.append(chr(byte))
addr += 1
self.emit("".join(chars))
elif service == 11:
self.emit(chr(a0 & 0xFF))
elif service == 10:
return True
else:
raise SimError(f"ecall layanan {service} tidak didukung")
return False
def step(self) -> bool:
"""Jalankan satu instruksi; kembalikan True bila program selesai."""
index = self.pc // 4
if index < 0 or index >= len(self.prog.instructions):
raise SimError(f"pc 0x{self.pc & MASK32:08x} berada di luar instruksi "
"(program berakhir tanpa ecall exit?)")
pc, text, mnem = self.prog.instructions[index]
if self.trace:
print(f"pc=0x{pc & MASK32:08x} {text}")
args = operands(text.split(None, 1)[1] if len(text.split(None, 1)) > 1 else "")
r = self.regs
def imm_load(rd: int, value: int) -> None:
r[rd] = u32(value)
if mnem in ("addi", "slti", "sltiu", "xori", "ori", "andi", "slli", "srli", "srai"):
rd, rs1 = reg(args[0]), reg(args[1])
imm = to_int(args[2])
if mnem == "addi":
imm_load(rd, r[rs1] + imm)
elif mnem == "slti":
imm_load(rd, 1 if signed(r[rs1]) < imm else 0)
elif mnem == "sltiu":
imm_load(rd, 1 if r[rs1] < u32(imm) else 0)
elif mnem == "xori":
imm_load(rd, r[rs1] ^ imm)
elif mnem == "ori":
imm_load(rd, r[rs1] | imm)
elif mnem == "andi":
imm_load(rd, r[rs1] & imm)
elif mnem == "slli":
imm_load(rd, r[rs1] << (imm & 31))
elif mnem == "srli":
imm_load(rd, r[rs1] >> (imm & 31))
else:
imm_load(rd, signed(r[rs1]) >> (imm & 31))
elif mnem in ("add", "sub", "sll", "slt", "sltu", "xor", "srl", "sra", "or", "and"):
rd, rs1, rs2 = reg(args[0]), reg(args[1]), reg(args[2])
if mnem == "add":
imm_load(rd, r[rs1] + r[rs2])
elif mnem == "sub":
imm_load(rd, r[rs1] - r[rs2])
elif mnem == "sll":
imm_load(rd, r[rs1] << (r[rs2] & 31))
elif mnem == "slt":
imm_load(rd, 1 if signed(r[rs1]) < signed(r[rs2]) else 0)
elif mnem == "sltu":
imm_load(rd, 1 if r[rs1] < r[rs2] else 0)
elif mnem == "xor":
imm_load(rd, r[rs1] ^ r[rs2])
elif mnem == "srl":
imm_load(rd, r[rs1] >> (r[rs2] & 31))
elif mnem == "sra":
imm_load(rd, signed(r[rs1]) >> (r[rs2] & 31))
elif mnem == "or":
imm_load(rd, r[rs1] | r[rs2])
else:
imm_load(rd, r[rs1] & r[rs2])
elif mnem == "lui":
imm_load(reg(args[0]), to_int(args[1]) << 12)
elif mnem == "auipc":
imm_load(reg(args[0]), pc + (to_int(args[1]) << 12))
elif mnem in ("beq", "bne", "blt", "bge", "bltu", "bgeu"):
rs1, rs2 = reg(args[0]), reg(args[1])
target = self.label(args[2])
taken = {"beq": r[rs1] == r[rs2], "bne": r[rs1] != r[rs2],
"blt": signed(r[rs1]) < signed(r[rs2]),
"bge": signed(r[rs1]) >= signed(r[rs2]),
"bltu": r[rs1] < r[rs2], "bgeu": r[rs1] >= r[rs2]}[mnem]
if taken:
self.pc = target
return False
elif mnem == "jal":
if len(args) == 1:
rd, target = 1, self.label(args[0])
else:
rd, target = reg(args[0]), self.label(args[1])
imm_load(rd, pc + 4)
self.pc = target
return False
elif mnem == "jalr":
if len(args) == 1:
rd, base, off = 1, reg(args[0]), 0
else:
rd, base = reg(args[0]), reg(args[1])
off = to_int(args[2]) if len(args) > 2 else 0
dest = (r[base] + off) & MASK32 & ~1
imm_load(rd, pc + 4)
self.pc = dest
return False
elif mnem in ("lb", "lh", "lw", "lbu", "lhu"):
rd, m = reg(args[0]), MEMOP_RE.match(args[1])
if not m:
raise SimError(f"operand memori tidak valid `{args[1]}`")
addr = u32(r[reg(m.group(2))] + to_int(m.group(1)))
width = {"lb": 1, "lbu": 1, "lh": 2, "lhu": 2, "lw": 4}[mnem]
value = self.read(addr, width)
if mnem in ("lb", "lh") and value & (1 << (width * 8 - 1)):
value -= 1 << (width * 8)
imm_load(rd, value)
elif mnem in ("sb", "sh", "sw"):
rs2, m = reg(args[0]), MEMOP_RE.match(args[1])
if not m:
raise SimError(f"operand memori tidak valid `{args[1]}`")
addr = u32(r[reg(m.group(2))] + to_int(m.group(1)))
self.write(addr, {"sb": 1, "sh": 2, "sw": 4}[mnem], r[rs2])
elif mnem == "ecall":
if self.do_ecall():
return True
elif mnem == "ebreak":
return True
else:
# pseudo-instruksi
if mnem == "nop":
pass
elif mnem == "li":
imm_load(reg(args[0]), to_int(args[1]))
elif mnem == "la":
imm_load(reg(args[0]), self.label(args[1]))
elif mnem == "mv":
imm_load(reg(args[0]), r[reg(args[1])])
elif mnem == "not":
imm_load(reg(args[0]), ~r[reg(args[1])])
elif mnem == "neg":
imm_load(reg(args[0]), -r[reg(args[1])])
elif mnem == "seqz":
imm_load(reg(args[0]), 1 if r[reg(args[1])] == 0 else 0)
elif mnem == "snez":
imm_load(reg(args[0]), 1 if r[reg(args[1])] != 0 else 0)
elif mnem == "sltz":
imm_load(reg(args[0]), 1 if signed(r[reg(args[1])]) < 0 else 0)
elif mnem == "j":
self.pc = self.label(args[0])
return False
elif mnem == "jr":
self.pc = r[reg(args[0])] & MASK32 & ~1
return False
elif mnem == "ret":
self.pc = r[1] & MASK32 & ~1
return False
elif mnem == "call":
target = self.label(args[0])
imm_load(1, pc + 4)
self.pc = target
return False
elif mnem in ("beqz", "bnez", "bgez", "bltz", "bgtz", "blez"):
rs1 = reg(args[0])
taken = {"beqz": r[rs1] == 0, "bnez": r[rs1] != 0,
"bgez": signed(r[rs1]) >= 0, "bltz": signed(r[rs1]) < 0,
"bgtz": signed(r[rs1]) > 0, "blez": signed(r[rs1]) <= 0}[mnem]
if taken:
self.pc = self.label(args[1])
return False
elif mnem in ("bgt", "ble", "bgtu", "bleu"):
rs1, rs2 = reg(args[0]), reg(args[1])
taken = {"bgt": signed(r[rs1]) > signed(r[rs2]),
"ble": signed(r[rs1]) <= signed(r[rs2]),
"bgtu": r[rs1] > r[rs2], "bleu": r[rs1] <= r[rs2]}[mnem]
if taken:
self.pc = self.label(args[2])
return False
else:
raise SimError(f"instruksi tidak dikenal `{mnem}`")
self.pc = u32(self.pc + 4)
return False
def run(self) -> None:
for _ in range(STEP_LIMIT):
if self.step():
return
raise SimError("batas langkah terlampaui (kemungkinan perulangan tak berujung)")
def main() -> None:
argv = sys.argv[1:]
trace = "--trace" in argv
argv = [a for a in argv if a != "--trace"]
if not argv:
print("Pemakaian: python3 sim.py [--trace] program.s")
return
try:
with open(argv[0], encoding="utf-8") as fh:
source = fh.read()
except OSError as exc:
print(f"sim: {exc}", file=sys.stderr)
raise SystemExit(1)
try:
prog = parse_source(source)
if not prog.instructions:
raise SimError("tidak ada instruksi untuk dijalankan")
sim = Sim(prog, trace)
sim.run()
for line in sim.out:
print(line)
except SimError as exc:
print(f"sim: {exc}", file=sys.stderr)
raise SystemExit(1)
if __name__ == "__main__":
main()Pemakaian: python3 sim.py [--trace] program.sProgram berikut memuat padanan RV32I dari fungsi yang sama. Nilai awal skor dan bonus diletakkan pada bagian .data, lalu badan programnya memuat keduanya ke register, menjumlahkan, dan menyimpan hasilnya kembali ke memori. Dua instruksi terakhir hanya melayani pencetakan hasil melalui ecall dan tidak menjadi bagian perhitungannya.
# tidak-dijalankan
# tambah-skor.s - padanan RV32I dari fungsi tambah_skor pada tambah-skor.c
.data
skor: .word 20 # nilai awal skor di memori
bonus: .word 7 # nilai bonus di memori
.text
.globl main
main:
la t0, skor # alamat sel skor ke t0
lw t1, 0(t0) # muat skor dari memori ke register
la t2, bonus # alamat sel bonus ke t2
lw t3, 0(t2) # muat bonus dari memori ke register
add t1, t1, t3 # jumlahkan keduanya di register
sw t1, 0(t0) # simpan hasil kembali ke memori
mv a0, t1 # hasil ke a0 untuk dicetak
li a7, 1 # layanan cetak bilangan
ecall # cetak 27
li a7, 10 # layanan keluar
ecall# menjalankan padanan RV32I melalui simulator
python3 sim.py tambah-skor.s27Bentuknya kini memperlihatkan pola load/store, yaitu prinsip yang memisahkan akses memori dari aritmetika sehingga instruksi add hanya menerima operand register. Instruksi lw memuat skor dari memori, add menjumlahkannya dengan bonus di register, dan sw menyimpan hasilnya kembali, sedangkan x86-64 menyelesaikan penambahan serupa hanya dengan satu instruksi add yang menyentuh memori. Baris la, li, dan mv adalah pseudo-instruksi, yaitu bentuk singkat yang dijabarkan assembler menjadi instruksi dasar, sehingga jumlah baris berkas tidak sama dengan jumlah instruksi mesinnya.
Panjang instruksi: bervariasi versus tetap#
Perbedaan encoding paling mudah dilihat dari jejak program counter. Opsi --trace pada simulator mencetak satu baris untuk setiap instruksi yang dieksekusi beserta alamatnya, sehingga langkah program counter dapat dibaca persis. Perintahnya tetap memakai berkas yang sama, hanya opsi pelacaknya ditambahkan.
# jalankan ulang dengan pelacakan program counter
python3 sim.py --trace tambah-skor.spc=0x00000000 la t0, skor
pc=0x00000004 lw t1, 0(t0)
pc=0x00000008 la t2, bonus
pc=0x0000000c lw t3, 0(t2)
pc=0x00000010 add t1, t1, t3
pc=0x00000014 sw t1, 0(t0)
pc=0x00000018 mv a0, t1
pc=0x0000001c li a7, 1
pc=0x00000020 ecall
pc=0x00000024 li a7, 10
pc=0x00000028 ecall
27Alamat pada keluaran ini naik dengan langkah tetap, yaitu 0x00, 0x04, 0x08, dan seterusnya sampai 0x28, karena setiap instruksi RV32I menempati tepat 4 byte. Kontrasnya dengan alamat disassembly x86-64 pada bagian sebelumnya yang melangkah 1, 2, dan 5 byte mengikuti panjang tiap instruksinya. Instruksi x86-64 dapat panjang 1 sampai 15 byte tanpa tanda batas antarinstruksi, sehingga dekoder yang besar diperlukan untuk menemukan awal setiap instruksinya. Prosesor x86-64 modern bahkan menerjemahkan hasilnya terlebih dahulu menjadi micro-op, yaitu operasi internal sederhana, sebelum dieksekusi. Dekoder RISC-V tidak memikul beban serupa karena batas instruksinya dapat ditemukan tanpa menafsirkan isinya.
Ukuran kode menjadi konsekuensi langsungnya. Badan tambah_skor versi x86-64 menempati 7 byte, sedangkan empat instruksi padanannya pada RV32I menempati 16 byte. Konstanta 100000 mempertajam perbedaan itu, karena x86-64 menanamkannya dalam satu instruksi 5 byte, tetapi RV32I yang hanya menyediakan immediate, yaitu konstanta yang menyertai instruksi, selebar 12 bit, sehingga konstanta itu harus disusun dari pasangan lui dan addi. Ekstensi C mempersempit jurang ini dengan memampatkan instruksi yang sering dipakai menjadi 2 byte. Perbandingan ukuran di atas berlaku untuk dua contoh kecil ini dan bukan tolok ukur umum, karena hasilnya berubah mengikuti compiler serta tingkat optimasinya.
Konsekuensi bagi pemrogram#
Bagi pemrogram C, kedua ISA berperilaku hampir serupa di atas permukaan. Kode sumber yang sama dikompilasi ke keduanya tanpa penyuntingan pada kasus umum, karena compiler yang menyesuaikan diri dengan kontrak setiap ISA. Perbedaan baru terasa ketika bahasa assembly dibaca atau ditulis, misalnya saat menganalisis disassembly atau menelusuri error tingkat rendah. Pada titik itu, kerja dengan x86-64 berarti berurusan dengan operand memori dan instruksi panjang bervariasi, sedangkan kerja dengan RISC-V berarti register eksplisit dan langkah 4 byte yang seragam.
Model bisnisnya juga berdampak pada pemrogram. Ekosistem x86-64 menyediakan hampir semua perangkat lunak desktop dan server yang sudah ada, sehingga jangkauan perangkatnya menjadi kekuatan utamanya, tetapi implementasinya terikat lisensi pemiliknya. Ekosistem RISC-V lebih muda dan belum menyediakan padanan untuk setiap aplikasi, meskipun dukungan Linux, gcc, dan LLVM terus bertambah. Sebagai gantinya, open standard membuka pintu bagi siapa pun untuk membuat inti sendiri, menambahkan ekstensi khusus, atau memakai inti kelas pendidikan tanpa perjanjian lisensi.
Karena itu, pilihan antara keduanya bukan soal yang menang, melainkan soal kecocokan dengan pekerjaannya. Program yang harus berjalan pada banyak komputer lama nyaman di x86-64, sedangkan perangkat yang membutuhkan inti sederhana, mudah dimodifikasi, atau bebas royalti menemukan jawabannya pada RISC-V. Kontrak keduanya sama sifatnya, yaitu menetapkan register, instruksi, dan perilaku memori, sehingga cara membaca bahasa mesin yang dipraktikkan pada topik ini berlaku pada keduanya.
Kesalahan umum#
Tiga kekeliruan berikut paling sering muncul ketika perbandingan ISA mulai dipraktikkan.
- Membaca operand gaya AT&T dengan urutan terbalik. Pada keluaran objdump, operand tujuan berada paling kanan, sehingga
mov %esi,%eaxmemindahkan isiesikeeaxdan bukan sebaliknya. Kebiasaan yang mencegah salah baca adalah memeriksa arah pada satu instruksi yang hasilnya sudah diketahui sebelum menafsirkan sisanya. - Menghitung ukuran kode dari jumlah baris berkas
.s. Barisla t0, skorpada daftar RV32I adalah pseudo-instruksi yang oleh assembler nyata dijabarkan menjadi instruksi dasar, dan pasanganluidenganaddiuntuk konstanta besar menambah instruksi yang tidak terlihat pada C-nya. Penghitungan baru sah setelah bentuk instruksi dasarnya jelas. - Menyamakan kebebasan royalti dengan kebebasan biaya total. Open standard menghapus biaya lisensi ISA, tetapi merancang, memverifikasi, dan memproduksi chip tetap pekerjaan yang mahal. Kesimpulan yang tepat menyangkut model bisnisnya yang terbuka, bukan perkiraan bahwa perangkat kerasnya pasti lebih murah.
Ringkasan#
- x86-64 mewarisi 8086 dan menuntut kompatibilitas penuh, sehingga instruksinya bervariasi 1 hingga 15 byte dan dekoder harus membelah deretan byte tanpa tanda batas.
- RISC-V RV32I memakai instruksi tetap 4 byte, memisahkan akses memori dari aritmetika dengan pola load/store, dan menyusun konstanta besar dari pasangan
luidanaddi. - Operand memori membuat satu instruksi x86-64 dapat menambahkan isi memori langsung ke register, sedangkan padanan RV32I membutuhkan pasangan
lwdanswyang mengapitnya. - x86-64 terikat lisensi dan memiliki ekosistem paling matang, sedangkan RISC-V berupa open standard bebas royalti yang dapat diperluas melalui ekstensi M, A, F, D, C, dan V.
Referensi#
- Computer Systems: A Programmer’s Perspective — Carnegie Mellon University — Buku ajar arsitektur komputer dari sudut pandang pemrogram yang membahas kode mesin x86-64 dan kontrak ISA.
- RISC-V ISA Manual — RISC-V International — Spesifikasi resmi set instruksi RISC-V yang memuat format encoding RV32I dan daftar ekstensi standar.
- Ratified Specifications — RISC-V International — Daftar spesifikasi RISC-V yang telah disahkan, termasuk basis I serta ekstensi M, A, F, D, dan C.