Pengenalan Target Native

Posisi target native x86-64 dan LLVM IR dibandingkan stack VM.

Daftar isi

Topik ini memetakan posisi target native sebagai alternatif sasaran codegen terhadap stack VM yang sudah dibangun. Pembaca memperoleh demo langsung berupa kompilasi program C kecil menjadi assembly x86-64 melalui GCC, serta perkenalan konseptual terhadap LLVM IR sebagai target populer yang berdiri di antara compiler front end dan kode native. Setelah membaca topik ini, pembaca dapat menjelaskan perbedaan codegen ke register native dibandingkan codegen ke stack VM, menyebutkan peran calling convention dan alokasi register, serta menempatkan LLVM IR pada peta target codegen produksi.

Codegen ke target native berbeda dari codegen ke stack VM#

Codegen ke target native menargetkan mesin sasaran berbasis register, yaitu mesin dengan sejumlah register fisik bernama yang menjadi wadah operand. Berbeda dari stack VM yang menyimpan operand pada satu tumpukan dengan instruksi pendek tanpa operand, target native menyebut register sumber dan tujuan secara eksplisit pada tiap instruksi. Akibatnya, codegen ke target native memerlukan dua keputusan yang tidak ada pada codegen ke stack VM, yaitu pemilihan register untuk tiap temp, dan pemenuhan konvensi pemanggilan (calling convention) yang menetapkan bagaimana argumen dan nilai balik diteruskan antar fungsi.

Pemilihan register menjadi masalah karena jumlah register fisik terbatas. Pada x86-64 System V ABI, hanya ada enam belas register integer, yaitu rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, dan delapan register extended. Codegen harus menempatkan setiap temp ke salah satu register ini tanpa konflik. Bila temp lebih banyak daripada register yang tersedia, beberapa temp harus ditumpahkan ke memori (spill) pada stack. Keputusan alokasi register menjadi tahap tersendiri pada modul optimasi, biasanya dengan pewarnaan graf interferensi. Pada stack VM, masalah ini tidak ada sebab operand disimpan pada tumpukan yang ukurannya tidak terbatas.

Konvensi pemanggilan menetapkan bagaimana argumen diteruskan ke fungsi dan bagaimana nilai balik dikembalikan. Pada x86-64 System V ABI, enam argumen integer pertama diteruskan lewat register rdi, rsi, rdx, rcx, r8, dan r9, sedangkan argumen berikutnya diteruskan lewat stack. Nilai balik integer diletakkan di register rax. Codegen ke target native harus mengikuti konvensi ini agar fungsi yang dikompilasi dapat dipanggil oleh kode lain yang mengikuti konvensi yang sama. Pada stack VM, konvensi pemanggilan tidak ada sebab semua operasi memakai tumpukan yang sama.

Demo: kompilasi C ke assembly x86-64 via GCC#

Untuk melihat codegen ke target native secara langsung, demo berikut memakai GCC untuk mengompilasi program C kecil menjadi assembly x86-64. Program tersebut berisi fungsi hitung yang menjumlahkan bilangan dari 1 hingga n dengan perulangan while, dan fungsi main yang memanggil hitung(10). Pemakaian -O0 mematikan optimasi sehingga pemetaan antara sumber dan assembly tetap dekat, sedangkan -S menghasilkan assembly alih-alih berkas objek.

Berkas native_demo.py menulis sumber C ke berkas sementara, lalu memanggil GCC melalui subprocess.run dengan argumen yang sesuai. Assembly yang dihasilkan diteruskan ke stdout dan ditangkap oleh skrip.

python native_demo.py
"""Demo codegen ke target native: kompilasi C ke assembly x86-64 via GCC.

Skrip ini menulis program C kecil ke berkas sementara, lalu memanggil
`gcc -S -O0 -o - small.c` untuk menghasilkan assembly dan mencetaknya
ke stdout. Berkas sementara dibersihkan setelah selesai.

Tujuan skrip adalah menunjukkan secara langsung bagaimana compiler
produksi (GCC) menurunkan program C menjadi deret instruksi x86-64.
Berkas sengaja kecil agar keluaran assembly dapat dibaca tanpa
terpotong. Opsi `-S` menghasilkan assembly alih-alih berkas objek,
sedangkan `-O0` mematikan optimasi sehingga pemetaan antara sumber
dan assembly tetap dekat satu sama lain. Opsi `-o -` mengarahkan
assembly ke stdout agar dapat ditangkap oleh skrip.

Pemakaian:
    python3 native_demo.py
"""

import subprocess
import sys
import tempfile
from pathlib import Path


SMALL_C = """\
int hitung(int n) {
    int s = 0;
    int i = 1;
    while (i <= n) {
        s = s + i;
        i = i + 1;
    }
    return s;
}

int main(void) {
    return hitung(10);
}
"""


def main() -> None:
    with tempfile.TemporaryDirectory() as d:
        src = Path(d) / "small.c"
        src.write_text(SMALL_C)
        # gcc -S menghasilkan assembly; -O0 mematikan optimasi;
        # -o - mengarahkan assembly ke stdout.
        result = subprocess.run(
            ["gcc", "-S", "-O0", "-o", "-", str(src)],
            capture_output=True, text=True, check=True,
        )
    print("=== Sumber small.c ===")
    print(SMALL_C, end="")
    print()
    print("=== Perintah ===")
    print("gcc -S -O0 -o - small.c")
    print()
    print("=== Assembly x86-64 (gcc -S -O0) ===")
    sys.stdout.write(result.stdout)


if __name__ == "__main__":
    main()

Keluaran berikut diperoleh dengan menjalankan python3 native_demo.py pada sandbox dengan GCC 14.

text Keluaran
=== Sumber small.c ===
int hitung(int n) {
    int s = 0;
    int i = 1;
    while (i <= n) {
        s = s + i;
        i = i + 1;
    }
    return s;
}

int main(void) {
    return hitung(10);
}

=== Perintah ===
gcc -S -O0 -o - small.c

=== Assembly x86-64 (gcc -S -O0) ===
	.file	"small.c"
	.text
	.globl	hitung
	.type	hitung, @function
hitung:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	$0, -4(%rbp)
	movl	$1, -8(%rbp)
	jmp	.L2
.L3:
	movl	-8(%rbp), %eax
	addl	%eax, -4(%rbp)
	addl	$1, -8(%rbp)
.L2:
	movl	-8(%rbp), %eax
	cmpl	-20(%rbp), %eax
	jle	.L3
	movl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	hitung, .-hitung
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$10, %edi
	call	hitung
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Debian 14.2.0-19) 14.2.0"
	.section	.note.GNU-stack,"",@progbits

Assembly yang dihasilkan memperlihatkan dua ciri khas codegen ke register native. Pertama, setiap variabel lokal dialokasikan ke slot stack frame, bukan ke register. Pada -O0, GCC memilih alokasi memori untuk semua variabel lokal, sehingga s berada di -4(%rbp), i di -8(%rbp), dan n di -20(%rbp). Instruksi movl -8(%rbp), %eax memuat nilai i ke register eax sebelum dipakai pada cmpl, sedangkan addl %eax, -4(%rbp) menambahkan eax ke s langsung di memori. Pola muat-hitung-simpan ini muncul sebab GCC tidak mengoptimalkan penggunaan register pada -O0. Kedua, pemanggilan hitung(10) di main menempatkan argumen 10 di register edi sebelum call hitung, sesuai System V ABI yang menetapkan edi sebagai register argumen pertama. Nilai balik hitung diletakkan di eax dan menjadi nilai balik main juga.

Direktif .cfi_* dan .LFB0, .LFE0 adalah penanda debug dan bukan instruksi mesin. Direktif tersebut dipakai oleh debugger untuk melacak panggilan fungsi dan penanganan exception. Codegen produksi memancarkan direktif ini bersama instruksi mesin agar berkas objek dapat dipakai bersama peralatan debug. Pada codegen proyek kursus yang menargetkan stack VM, direktif semacam ini tidak ada sebab bytecode stack VM tidak memiliki konsep stack frame ABI.

LLVM IR sebagai target populer#

LLVM IR adalah bentuk tengah antara front end dan back end yang menjadi target populer pada compiler modern. Front end seperti Clang menurunkan C, C++, dan Objective-C menjadi LLVM IR, lalu back end LLVM menurunkan IR tersebut menjadi kode native untuk berbagai arsitektur, termasuk x86-64, ARM, dan RISC-V. Pemisahan ini mengikuti pola front end dan back end dengan IR sebagai batas, sebagaimana dibahas pada modul representasi-antara, tetapi dengan cakupan back end yang jauh lebih luas daripada codegen ke satu mesin.

Bentuk LLVM IR berada di antara bytecode stack VM dan kode native. LLVM IR berbasis register virtual dengan jumlah tak terbatas, sehingga codegen front end tidak perlu mengalokasikan register fisik. Setiap nilai adalah nama virtual dengan tipe statis, misalnya %result = add i32 %a, %b menyatakan penjumlahan dua nilai i32 ke %result. Pass alokasi register pada back end LLVM mengubah register virtual menjadi register fisik, lengkap dengan spill ke memori bila perlu. Pendekatan ini membebaskan front end dari keputusan register, sebagaimana halnya stack VM, tetapi tetap menghasilkan kode native yang efisien setelah pass back end.

Karena clang tidak tersedia pada sandbox ini, keluaran LLVM IR berikut tidak ditangkap dari mesin, melainkan disusun mengikuti sintaks yang ditetapkan oleh LLVM Language Reference. Keluaran ini menggambarkan bentuk IR untuk fungsi add(a, b) = a + b sebagai contoh kanonik yang muncul pada dokumentasi resmi.

llvm Keluaran yang diharapkan
; Sumber: int add(int a, int b) { return a + b; }
define i32 @add(i32 %a, i32 %b) {
  %result = add i32 %a, %b
  ret i32 %result
}

Fragmen IR di atas memperlihatkan tiga ciri khas LLVM IR. Pertama, fungsi dideklarasikan dengan define yang menyebut tipe kembalian i32, nama fungsi @add, dan daftar parameter bertipe i32. Kedua, instruksi add i32 %a, %b adalah instruksi tiga alamat dengan tipe eksplisit, sehingga verifikasi tipe dapat dilakukan pada level IR. Ketiga, nama register virtual ditulis dengan prefiks %, sedangkan nama global seperti fungsi ditulis dengan prefiks @. Konvensi penamaan ini membedakan keduanya secara visual pada pembacaan IR. Pada compiler yang menargetkan LLVM, front end cukup membangkitkan IR dalam bentuk ini, dan back end LLVM menyelesaikan sisa pipeline.

Ringkasan#

  • Codegen ke target native menargetkan mesin berbasis register dengan instruksi yang menyebut register sumber dan tujuan secara eksplisit.
  • Dua keputusan yang tidak ada pada codegen stack VM adalah alokasi register dan pemenuhan calling convention, yaitu bagaimana argumen dan nilai balik diteruskan antar fungsi.
  • Demo GCC mengonfirmasi pola codegen ke x86-64 secara langsung, termasuk muat-hitung-simpan pada -O0 dan penempatan argumen di edi sesuai System V ABI.
  • LLVM IR adalah bentuk tengah berbasis register virtual yang menjadi target populer karena membebaskan front end dari alokasi register fisik.

Referensi#