Sesi Assembly Kedua

Fungsi, stack, dan calling convention RISC-V beserta isi frame saat panggilan terjadi.

Daftar isi

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.

python sim.py
#!/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()
text Keluaran
Pemakaian: python3 sim.py [--trace] program.s

Stack 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.

asm Contoh: prolog dan epilog fungsi kali
# 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 pemanggil

Urutan 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.

asm program.s
# 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
bash
# menjalankan program fungsi kali
python3 sim.py program.s
text Keluaran
Luas kamar A
12
Luas kamar B
10

Sebelum 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.

0x10000puncak stack, belum dipakai0xFFFCra, alamat kembali ke main0xFFF8s0, pengali tersimpan0xFFF4s1, akumulasi hasil0xFFF0belum dipakaispTiga kotak berwarna adalah frame 12 byte milik fungsi kali, dari 0xFFF4 sampai 0xFFFF.
Isi stack saat fungsi kali berjalan dengan frame 12 byte

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.

RegisterPeranPenanggung jawabnya
a0, a1argumen pertama dan keduapemanggil memasang sebelum call
a0hasil fungsifungsi mengisi sebelum ret
raalamat kembaliditulis call, disimpan fungsi yang ikut memanggil
s0 sampai s11data lintas panggilancallee-saved, wajib dipulihkan fungsi
t0 sampai t6pekerjaan sementaracaller-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, 12 di epilog, sp terus 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 t untuk data yang harus hidup melewati panggilan. Register t boleh ditimpa oleh fungsi mana pun, sehingga nilainya pindahkan ke register s melalui prolog atau ke memori.
  • Menyimpan ra setelah melakukan call. Alamat kembali sudah tertimpa pada saat itu, sehingga penyimpanannya harus berada di awal prolog, sebelum call pertama yang dilakukan fungsi.
  • Offset epilog tidak mencerminkan prolog. Menyimpan ra pada 8(sp) tetapi membacanya dari 4(sp) menukar isi dua register secara diam-diam. Cocokkan setiap pasangan sw dan lw.
  • Memulihkan sp sebelum register dibaca. Penukaran urutan antara lw dan addi sp, sp, 12 membuat semua offset seperti 8(sp) menunjuk alamat yang salah. Baca dulu seluruh register, lalu geser sp paling akhir sebagai cermin dari prolog.

Ringkasan#

  • call menulis alamat kembali ke ra lalu melompat, dan ret kembali lewat ra, sehingga fungsi yang memanggil pihak lain wajib menyimpan ra ke stack terlebih dahulu.
  • Prolog menyediakan frame dengan menggeser sp dan menyimpan register yang dipertahankan, sedangkan epilog memulihkannya dalam urutan terbalik lalu melepas frame.
  • Calling convention RV32I menempatkan argumen pada a0 dan a1, hasil pada a0, data lintas panggilan pada s0 sampai s11, dan pekerjaan sementara pada t0 sampai t6.
  • Setiap panggilan menulis ulang frame pada alamat yang sama karena sp selalu dipulihkan, dan pola itulah yang membuat rekursi mungkin.

Referensi#