Fungsi memecah program besar menjadi potongan yang dapat dipanggil berulang, dan stack menyimpan konteks yang membuat pemanggilan itu aman. Topik ini menulis fungsi RISC-V yang utuh, yaitu fungsi perkalian dengan prolog, epilog, dan calling convention RV32I, kemudian mengamati isi frame-nya di memori pada saat panggilan berlangsung. Setelah membacanya, alamat kembali, register yang dipertahankan, serta aliran argumen dan hasil dapat dijelaskan baris demi baris.
Simulator dan berkas topik#
Simulator RV32I yang dipakai sepanjang modul ini tersedia di bundle topik ini sebagai sim.py, dan program dijalankan dengan python3 sim.py program.s. Layanan ecall mengikuti konvensi RARS, yaitu nomor layanan pada a7 dan argumen pada a0, dengan layanan 1 untuk bilangan bertanda, 4 untuk string, 11 untuk satu karakter, dan 10 untuk keluar. Register sp diawali simulator pada alamat 0x10000, sehingga stack tumbuh dari sana menuju alamat yang lebih kecil.
#!/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.sStack adalah wilayah memori yang dipakai dengan prinsip terakhir masuk, pertama keluar, dan tumbuh ke arah alamat menurun. Instruksi addi sp, sp, -12 memindahkan batasnya turun dua belas byte, sehingga ruang di atas sp milik fungsi yang sedang berjalan. Ukuran stack pada simulator cukup untuk contoh modul ini, dan setiap fungsi mengembalikan sp ke posisi semula sebelum kembali, sehingga panggilan berikutnya menimpa wilayah yang sama dengan rapi. Simulator juga menolak program yang berakhir tanpa ecall keluar, sehingga kebiasaan menutup program perlu dijaga sejak fungsi pertama ditulis.
Masalah alamat kembali#
Instruksi call kali melakukan dua hal sekaligus, yaitu menaruh alamat instruksi berikutnya pada register ra dan melompat ke label kali. Di akhir badan fungsi, ret memuat ulang program counter dari ra, sehingga eksekusi kembali tepat setelah baris pemanggilnya. Untuk satu tingkat pemanggilan, ra bekerja tanpa bantuan apa pun.
Masalah muncul ketika fungsi memanggil fungsi lain. Pemanggilan kedua menimpa ra dengan alamat kembali yang baru, sehingga alamat kembali milik pemanggil pertama hilang dan ret tidak pernah bisa kembali ke tempat yang benar. Stack menyediakan tempat penyimpanannya, yaitu menulis ra ke memori sebelum nilai itu tertimpa, lalu membacanya kembali saat fungsi selesai.
Fungsi kali pada topik ini tidak memanggil fungsi lain, tetapi prolognya tetap menyimpan ra. Kebiasaan itu membuat fungsi tetap benar bila badannya kelak diperluas, dan biayanya hanya satu instruksi simpan plus satu instruksi baca. Kebalikannya jauh lebih mahal, karena alamat kembali yang hilang tidak selalu terlihat saat pengujian awal, dan kehilangan jenis ini sulit diperbaiki setelah program menjadi besar.
Prolog dan epilog#
Prolog adalah rangkaian pembuka yang menyediakan frame, yaitu wilayah stack milik satu panggilan fungsi, dan menyimpan register yang wajib dipertahankan. Epilog adalah cerminannya, yaitu memulihkan register, melepas frame, lalu kembali. Assembly tidak menyediakannya secara otomatis, sehingga keduanya ditulis dengan tangan di setiap fungsi.
# tidak-dijalankan
kali:
addi sp, sp, -12 # prolog: sediakan frame 12 byte
sw ra, 8(sp) # simpan alamat kembali
sw s0, 4(sp) # simpan s0 yang dipertahankan
sw s1, 0(sp) # simpan s1 yang dipertahankan
# ... badan fungsi ...
lw ra, 8(sp) # epilog: pulihkan dalam urutan terbalik
lw s0, 4(sp)
lw s1, 0(sp)
addi sp, sp, 12 # lepas frame
ret # kembali ke pemanggilUrutan penyimpanan dan pemulihan harus berkebalikan satu sama lain karena stack bekerja terakhir masuk, pertama keluar. Register yang disimpan paling akhir dibaca paling awal, dan sp harus kembali persis ke nilai semula. Salah urutan membuat register bertukar isi secara diam-diam, sedangkan lupa mengembalikan sp membuat semua panggilan berikutnya menulis ke tempat yang salah.
Offset dihitung dari sp yang sudah digeser, yaitu ra pada 8(sp), s0 pada 4(sp), dan s1 pada 0(sp), sehingga tiga word menempati persis dua belas byte yang disediakan. Register yang disimpan dipilih dari konvensi, bukan dari selera, karena s0 sampai s11 dijanjikan tetap utuh setelah panggilan selesai. Register ra masuk daftar yang sama karena dapat ditimpa oleh pemanggilan berikutnya.
Fungsi kali dari pemanggilan sampai kembali#
Program utama topik ini memanggil fungsi kali dua kali untuk menghitung luas dua kamar, yaitu 3 × 4 dan 5 × 2. Fungsi kali mengalikan dengan penjumlahan berulang karena RV32I tidak memiliki instruksi perkalian, dan hasilnya selalu dikembalikan pada a0.
# tidak-dijalankan
# program.s - fungsi kali dengan stack dan calling convention
.data
teks_a: .asciz "Luas kamar A"
teks_b: .asciz "Luas kamar B"
.text
.globl main
main:
# panggilan pertama: kali(3, 4)
la a0, teks_a
li a7, 4 # layanan cetak string
ecall # cetak judul kamar
li a0, 3 # argumen pertama pada a0
li a1, 4 # argumen kedua pada a1
call kali # kembali dengan a0 = 12
li a7, 1 # layanan cetak bilangan
ecall # cetak 12
# panggilan kedua: kali(5, 2)
la a0, teks_b
li a7, 4
ecall # cetak judul kamar
li a0, 5
li a1, 2
call kali # kembali dengan a0 = 10
li a7, 1
ecall # cetak 10
li a7, 10 # layanan keluar
ecall
# kali: menghitung a0 * a1 dengan penjumlahan berulang
# hasil dikembalikan pada a0, register s0 dan s1 dipakai sebagai pekerja
kali:
addi sp, sp, -12 # prolog: sediakan frame 12 byte
sw ra, 8(sp) # simpan alamat kembali
sw s0, 4(sp) # simpan s0
sw s1, 0(sp) # simpan s1
mv s1, zero # akumulasi mulai dari nol
mv s0, a1 # pengali tersimpan
ulang:
beqz s0, selesai # pengali habis, berhenti
add s1, s1, a0 # akumulasi += a0
addi s0, s0, -1
j ulang
selesai:
mv a0, s1 # hasil ke a0
lw ra, 8(sp) # epilog: pulihkan register
lw s0, 4(sp)
lw s1, 0(sp)
addi sp, sp, 12 # lepas frame
ret # kembali ke pemanggil# menjalankan program fungsi kali
python3 sim.py program.sLuas kamar A
12
Luas kamar B
10Sebelum setiap panggilan, main menyiapkan argumen sesuai konvensi, yaitu a0 untuk operand pertama dan a1 untuk operand kedua. Setelah call kali selesai, hasil perkaliannya sudah menunggu di a0 dan dicetak dengan layanan 1. Urutan keluarannya empat baris, yaitu judul kamar, hasilnya, judul kamar kedua, dan hasil keduanya.
Badan kali menyalin a1 ke s0 sebagai pengali, lalu menambahkan a0 ke akumulasi s1 sebanyak a1 kali. Perulangannya memakai pola penghitung yang sudah dikenal, yaitu register yang dikurangi satu per satu dan beqz yang keluar saat nol. Ketika penghitung habis, hasilnya dipindahkan ke a0, epilog memulihkan ra, s0, dan s1, lalu ret kembali ke pemanggil. Uji ulang dengan li a1, 0 untuk satu panggilan, dan eksekusinya langsung mendarat di label selesai saat pengalinya nol, tanpa satu pun penjumlahan, sehingga hasilnya tetap 0.
Isi frame saat panggilan berlangsung#
Diagram berikut menggambar isi stack pada saat kali sedang berjalan, tepat setelah prolognya selesai.
Frame kali berukuran dua belas byte dan menempati alamat 0xFFF4 sampai 0xFFFF. Slot 0xFFFC menyimpan ra yang berisi alamat kembali ke main, slot 0xFFF8 menyimpan s0 yang menampung pengali, dan slot 0xFFF4 menyimpan s1 yang menampung akumulasi. Panah sp menunjuk 0xFFF4, yaitu slot terakhir yang sudah dipakai, sesuai kebiasaan RISC-V.
Pada panggilan kedua, main telah kembali dari kali pertama lewat epilog yang memulihkan sp ke 0x10000, sehingga frame baru menempati alamat yang sama persis. Isi frame lama tidak dibersihkan, tetapi prolog menulis ulang setiap slotnya sebelum dibaca. Karena itu stack tetap benar tanpa proses pembuangan yang eksplisit.
Struktur ini juga menjadi dasar rekursi, yaitu fungsi yang memanggil dirinya sendiri. Setiap panggilan menyediakan frame sendiri di wilayah stack yang masih kosong, sehingga data setiap tingkat panggilan tidak saling menimpa. Syaratnya satu, yaitu ada kasus dasar yang pasti berhenti sebelum stack habis.
Calling convention RV32I#
Calling convention adalah kesepakatan pemakaian register antara pemanggil dan fungsi yang dipanggil. Tanpa kesepakatan itu, dua fungsi saling merusak data di register tanpa ada yang salah secara sintaks. Tabel berikut merangkum bagian yang dipakai topik ini.
| Register | Peran | Penanggung jawabnya |
|---|---|---|
a0, a1 | argumen pertama dan kedua | pemanggil memasang sebelum call |
a0 | hasil fungsi | fungsi mengisi sebelum ret |
ra | alamat kembali | ditulis call, disimpan fungsi yang ikut memanggil |
s0 sampai s11 | data lintas panggilan | callee-saved, wajib dipulihkan fungsi |
t0 sampai t6 | pekerjaan sementara | caller-saved, dapat tertimpa oleh panggilan |
Istilah callee-saved berarti register yang wajib dipulihkan oleh fungsi yang dipanggil, sedangkan caller-saved berarti register yang boleh hilang setelah satu panggilan sehingga pemanggil menyimpannya bila masih dibutuhkan. Fungsi kali menerapkan keduanya secara langsung, yaitu menyimpan s0 dan s1 karena memakainya, dan tidak menjanjikan apa pun tentang register t.
Konvensi inilah yang membuat fungsi dapat dipanggil tanpa membaca isinya. Bagian main menyiapkan a0 dan a1, memanggil kali, lalu membaca hasil di a0, tanpa pernah memeriksa register mana yang dipakai di dalamnya. Selama kedua sisi patuh, bagian yang berbeda dapat ditulis dan diuji secara terpisah.
Tabel ini juga menjelaskan mengapa main pada program topik ini tidak menulis prolog. Tidak ada fungsi di atasnya yang menuntut isi register tertentu, dan tidak ada data yang perlu dilindungi sebelum call kali yang pertama. Fungsi di dalam badan program tetap wajib patuh, sedangkan kebebasan semacam itu hanya milik titik masuk program.
Kesalahan umum#
- Lupa melepas frame. Tanpa
addi sp, sp, 12di epilog,spterus turun setiap panggilan sehingga stack bergeser dan program error saat akses memori. Pastikan jumlah yang digeser turun sama dengan yang dinaikkan, karena selisih satu byte pun sudah cukup untuk menggeser semua frame berikutnya. - Memakai register
tuntuk data yang harus hidup melewati panggilan. Registertboleh ditimpa oleh fungsi mana pun, sehingga nilainya pindahkan ke registersmelalui prolog atau ke memori. - Menyimpan
rasetelah melakukancall. Alamat kembali sudah tertimpa pada saat itu, sehingga penyimpanannya harus berada di awal prolog, sebelumcallpertama yang dilakukan fungsi. - Offset epilog tidak mencerminkan prolog. Menyimpan
rapada8(sp)tetapi membacanya dari4(sp)menukar isi dua register secara diam-diam. Cocokkan setiap pasanganswdanlw. - Memulihkan
spsebelum register dibaca. Penukaran urutan antaralwdanaddi sp, sp, 12membuat semua offset seperti8(sp)menunjuk alamat yang salah. Baca dulu seluruh register, lalu gesersppaling akhir sebagai cermin dari prolog.
Ringkasan#
callmenulis alamat kembali keralalu melompat, danretkembali lewatra, sehingga fungsi yang memanggil pihak lain wajib menyimpanrake stack terlebih dahulu.- Prolog menyediakan frame dengan menggeser
spdan menyimpan register yang dipertahankan, sedangkan epilog memulihkannya dalam urutan terbalik lalu melepas frame. - Calling convention RV32I menempatkan argumen pada
a0dana1, hasil padaa0, data lintas panggilan padas0sampais11, dan pekerjaan sementara padat0sampait6. - Setiap panggilan menulis ulang frame pada alamat yang sama karena
spselalu dipulihkan, dan pola itulah yang membuat rekursi mungkin.
Referensi#
- Computer Systems: A Programmer’s Perspective — Carnegie Mellon University — Buku pegangan tingkat mesin yang membahas prosedur, stack frame, dan calling convention secara mendalam.
- RISC-V Instruction Set Manual — RISC-V International — Spesifikasi instruksi jal dan jalr yang menjadi dasar call serta ret pada RV32I.