Kompilasi Kode Rust
Latihan membuat instruksi pembelajaran mengenai tahapan kompilasi serta topik-topik yang terkait dalam bahasa pemrograman Rust, dimana fokus utamanya adalah menjelaskan untuk pemula.
Memahami apa yang terjadi di balik layar ketika kode Rust dikompilasi menjadi program yang bisa dijalankan. Peserta akan belajar istilah (parsing, macro expansion, MIR, LLVM IR, codegen, assembly, object, linking), perbedaan build debug dan release, bagaimana melakukan inspeksi sederhana terhadap binary, dasar debugging, serta pengantar singkat ke decompiling / reverse-engineering dan teknik obfuscation.
Tujuan Pembelajaran
Setelah membaca dan mengerjakan latihan singkat ini, Anda dapat:
Menjelaskan setiap tahap kompilasi Rust dari kode sumber ke executable.
Menjalankan
rustc
(Rust compiler) dengan opsi untuk mengeluarkan (emit) antara lain LLVM IR dan assembly, lalu membaca output sederhana.Mengerti perbedaan antara debug build dan release build, serta imbas optimization (optimisasi) pada debugging.
Menggunakan alat sederhana untuk memeriksa binary:
nm
,objdump
,readelf
,strings
.Menjelaskan secara umum apa itu decompiling / reverse engineering dan mengapa hasilnya tidak sama persis dengan sumber.
Mengetahui konsep obfuscation (pengacakan) vs encryption (enkripsi) dan keterbatasannya.
Menyadari edge cases umum seperti error linking, missing toolchain saat cross-compile, dan kendala debug pada code yang dioptimisasi.
Prasyarat
Memiliki Rust toolchain terinstal (
rustup
,cargo
,rustc
).Mengetahui dasar Rust:
fn main()
,cargo new
,cargo build
,cargo run
.Nyaman menjalankan perintah di terminal (Linux/macOS/WSL).
Jika belum, jalankan: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
lalu ikuti instruksi.
Gambaran Analogis untuk pemula
Bayangkan Anda menulis resep makanan (kode sumber). Agar bisa dimasak mesin (CPU), resep itu harus melalui beberapa tahap:
Membersihkan/menyusun resep (macro expansion / parsing), menggantikan kata² yang bersifat template.
Memeriksa aturan dapur (type checking, borrow checking), apakah langkah-langkah logis dan aman?
Membuat instruksi mesin tingkat menengah (IR, Intermediate Representation), resep diterjemahkan ke “bahasa dapur universal”.
Mengubah ke instruksi spesifik oven (assembly) menjadi paket masak (object file).
Menggabungkan semua paket masak menjadi satu paket lengkap yang bisa dieksekusi (linking).
Setiap tahap punya peran berbeda, dan beberapa alat bisa melihat hasil antar-tahap agar kita tahu apa yang terjadi.
Penjelasan Tahap demi Tahap, dengan catatan untuk pemula
(1) Macro expansion & parsing, mirip preprocessor di C
Apa yang terjadi: Rust menampilkan macro (mis.
println!
,macro_rules!
, procedural macros) menjadi kode Rust murni — ini disebut macro expansion. Lalu compiler melakukan lexing (memecah teks menjadi token) dan parsing (membuat struktur pohon, AST = Abstract Syntax Tree).Mengapa pemula perlu tahu: berbeda dengan C yang memiliki preprocessor berbasis teks (
#include
,#define
), Rust tidak melakukan substitusi teks mentah; macro Rust lebih terstruktur dan aman. Jadi Anda tidak akan “men-debug” hasil substitusi teks yang tersembunyi — pesan error akan merujuk ke tempat yang masuk akal di kode Anda biasanya.Singkatan / istilah: AST = Abstract Syntax Tree.
(2) Analisis tipe, borrow checking, lifetimes
Apa yang terjadi: compiler memeriksa tipe, aturan ownership/borrowing (borrow checker), dan lifetime (jangka hidup referensi). Banyak kesalahan memori ditangkap di sini — sebelum program dijalankan.
Mengapa ini sulit: pesan borrow checker kadang panjang; pemula perlu membaca pesan lengkap karena sering memberi saran perbaikan (hint).
Tip: gunakan
cargo check
untuk cepat melihat error tanpa menghasilkan binary.
(3) MIR: Mid-level Intermediate Representation
Apa yang terjadi: compiler Rust menurunkan AST menjadi MIR (Mid-level IR). MIR adalah representasi tingkat menengah yang cocok untuk analisis bahasa-spesifik (mis. borrow checker, optimisasi awal).
Analogi: MIR adalah resep yang sudah dipecah menjadi langkah-langkah detail (bukan instruksi oven).
Catatan: Anda jarang perlu melihat MIR, tapi saat mempelajari borrow checker lanjutan, MIR berguna.
(4) LLVM IR: Low-level Intermediate Representation
Apa itu LLVM: LLVM adalah sebuah compiler infrastructure (koleksi alat) yang menyediakan format IR (Intermediate Representation) untuk optimisasi dan code generation. Singkatan: LLVM = Low Level Virtual Machine (nama sejarah; sekarang lebih dikenal sebagai proyek LLVM).
Apa yang terjadi: Rust menurunkan MIR menjadi LLVM IR. LLVM melakukan optimisasi seperti inlining fungsi, loop unrolling, dan lain-lain.
Mengapa penting: optimisasi pada tahap ini mempengaruhi performa dan struktur akhir kode mesin, dan juga memengaruhi seberapa mudah Anda bisa mendebug.
(5) Codegen → Assembly → Object (assembling)
Codegen: LLVM menghasilkan assembly (kode set instruksi CPU spesifik).
Assembling: assembler (mis.
as
) mengubah assembly menjadi object file (.o
) berisi instruksi mesin.Analogi: memasak tiap komponen makanan menjadi paket yang bisa disimpan (object file).
(6) Linking
Apa itu linking: linker menggabungkan satu atau lebih object files dan library (crate dependencies) menjadi executable tunggal. Linker juga menyelesaikan simbol (nama fungsi/variabel) sehingga pemanggilan fungsi bekerja.
Masalah umum: error linking muncul bila ada simbol yang hilang (missing symbol), ABI mismatch (Application Binary Interface — aturan bagaimana fungsi/parameter disusun di memori), atau versi library yang tidak cocok.
Tool: linker populer:
ld
,gold
,lld
(LLVM linker).
(7) Strip / LTO / Last steps
Strip: menghapus symbol table/debug info sehingga binary lebih kecil dan menyulitkan reverse engineering (
strip program
).LTO (Link Time Optimization): optimisasi yang dilakukan selama linking; dapat meningkatkan performa tetapi memperpanjang waktu build. LTO = Link Time Optimization.
Debug info & DWARF
Debug info: informasi tambahan yang memungkinkan debugger menampilkan nama fungsi, variabel, dan lokasi sumber dalam binary. Disimpan di format seperti DWARF (Debugging With Attributed Record Formats).
Perintah: untuk melihat debug sections:
readelf --debug-dump=info target/debug/your_binary
.Catatan: release builds sering menghilangkan debug info untuk mengecilkan binary; Anda bisa mengaktifkannya jika perlu.
Perintah praktis yang berguna untuk pemula
Gunakan di folder proyek Rust:
cargo build
— build debug (default).cargo run
— build & run.cargo build --release
— build release (optimasi).cargo check
— periksa compile errors cepat tanpa membuat binary.rustc --emit=llvm-ir src/main.rs
— keluarkan LLVM IR (file.ll
).rustc --emit=asm src/main.rs
— keluarkan assembly (file.s
).rustc --emit=obj src/main.rs
— keluarkan object file (file.o
).nm -C target/debug/your_binary
— lihat symbol (demangleC++
/Rust
names).objdump -d target/debug/your_binary
— disassembly.strings target/debug/your_binary
— tampilkan string literal yang tersisa di binary.strip target/release/your_binary
— hapus symbol/debug info.
Petunjuk praktis: gunakan
cargo build
dulu; jika ingin melihat assembly, pakairustc
langsung terhadap file sumber (cargo menyembunyikan beberapa detail build).
Debugging, menjelaskan untuk pemula
Debug build (
cargo build
) cocok untuk debugging karena compiler tidak mengoptimalkan banyak hal; stack frames sejalan dengan kode sumber, variabel tetap ada.Release build (
cargo build --release
) mengaktifkan optimisasi (mis. inlining) sehingga variabel bisa hilang atau langkah eksekusi berubah — ini membuat stepping di debugger membingungkan.Debugger tools:
gdb
(GNU Debugger),lldb
(LLVM Debugger). Rust menyediakan wrapperrust-gdb
/rust-lldb
yang men-setup environment. Visual tools: VSCode + Rust Analyzer + CodeLLDB extension.Trik pemula: gunakan macro
dbg!(expr)
untuk cepat melihat nilai selama debugging;RUST_BACKTRACE=1
menampilkan stack trace pada panic.
Decompiling / Reverse Engineering, pengenalan untuk pemula
Decompiling mencoba memulihkan kode “tingkat tinggi” dari binary. Hasilnya sering hilang detail: nama variabel, komentar, struktur high-level.
Tools: Ghidra (open source), IDA Pro (komersial), radare2. Untuk Rust, symbol names dimangle — gunakan demangler (
rustfilt
) untuk memperlihatkan nama fungsi Rust asli.Keterbatasan: generics, inlining, dan optimisasi membuat pseudo-code hasil decompiler sulit dimengerti; procedural macros dan compile-time code generation mengaburkan asal kode.
Etika/Legal: hanya lakukan reverse engineering pada kode yang Anda miliki izin atau untuk tujuan pembelajaran yang sah.
Obfuscation vs Encryption, penjelasan sederhana
Encryption (Enkripsi): membuat data tidak terbaca tanpa kunci. Jika Anda ingin menjalankan program, executable harus didekripsi oleh mesin — kunci harus tersedia sehingga encryption di executable client tidak menjadi proteksi efektif.
Obfuscation (Pengacakan): teknik untuk mempersulit pembacaan/analisis kode/binary (menghapus symbol names, menambahkan dead code, mengubah alur kontrol). Obfuscation hanya menaikkan biaya analisis, bukan mencegahnya.
Praktik:
strip
menghapus symbol table sehingga analis harus kerja lebih keras, dan LTO/optimisasi membuat struktur assembly berbeda dari source.
Perbedaan Build Profiles & Pengaruhnya
Profiles (Profil build) di Cargo:
dev
(default debug),release
. Anda dapat mengkonfigurasiCargo.toml
untuk mengubahopt-level
(optimization level),debug
(apakah debug info disertakan), dll.Tip: Jika ingin debug performa release tetapi tetap ingin debug info, set
debug = true
di profile.release — ini membuat binary besar tetapi bisa di-debug.
Contoh sederhana, langkah demi langkah
(1) Buat project:
cargo new compile_stages_demo
cd compile_stages_demo
(2) Isi src/main.rs
:
fn main() {
println!("hello from Rust");
}
(3) Build debug:
cargo build
Binary di target/debug/compile_stages_demo
.
(4) Emit assembly:
rustc --emit=asm src/main.rs -C opt-level=0
# lihat src/main.s (assembly) atau rustc membuat hello.s
(5) Emit LLVM IR:
rustc --emit=llvm-ir src/main.rs
# lihat src/main.ll
(6) Disassemble executable:
objdump -d target/debug/compile_stages_demo | less
(7) Debug:
rust-gdb target/debug/compile_stages_demo
# di gdb: break main; run; step; print var
Penjelasan singkat: -C opt-level=0
menonaktifkan optimisasi sehingga assembly lebih dekat ke source; berguna untuk belajar.
Edge Cases & Hal yang Sering Membingungkan Pemula
Pesan borrow checker yang panjang: baca keseluruhan pesan, compiler sering memberi
help:
dan contoh perbaikan. Gunakancargo check
cepat untuk iterasi.Optimized code menghilangkan variabel: saat release, beberapa variabel bisa tidak tampak di debugger karena compiler mengoptimalkan atau inlining. Solusi: debug build atau set
debug = true
untuk release.Linker errors saat cross-compile: saat menarget platform lain (mis. build Windows di Linux), linker untuk target tidak ada. Anda perlu toolchain/ linker cross-compile.
Procedural macro errors: errors di hasil macro mungkin menunjuk ke kode yang dihasilkan — kadang sulit membaca; panduan: periksa definisi macro atau ekspand macro (tooling tertentu dapat menunjukkan expanded macro).
Large binary size: LTO + debugging info meningkatkan size. Gunakan strip dan compress jika perlu.
Undefined Behavior (UB) dari
unsafe
:unsafe
membebaskan sebagian jaminan compiler — kesalahan bisa menyebabkan crash sporadis dan sulit direproduksi.
Latihan singkat untuk pemula
(1) Melihat IR dan assembly
Buat project hello dan jalankan:
rustc --emit=llvm-ir,asm src/main.rs
Buka file .ll
(LLVM IR) dan .s
(assembly). Tuliskan satu baris pada .ll
yang kira-kira memetakan println!
.
Clue: cari string
"hello from Rust"
di file.ll
atau.s
.
(2) Perbedaan Debug vs Release
Buat fungsi loop sederhana (mis. sum 0..n) dan build debug & release. Bandingkan ukuran binary (
ls -lh target/debug/... target/release/...
) dan lihat perbedaanobjdump -d
kedua binary.Clue: release lebih kecil/lebih cepat dan assembly lebih "padat" (lebih banyak optimisasi).
(3) Inspect symbol table
Jalankan
nm -C target/debug/your_binary | less
dan cari symbolmain
.Clue:
-C
demangles nama Rust sehingga lebih mudah dibaca.
(4) Simple reverse outline
Jalankan
strings target/release/your_binary | less
dan lihat apa saja teks yang tersisa.Clue: pesan literal (seperti
"hello"
) biasanya tetap ada; nama fungsi (symbol) bisa hilang setelahstrip
.
Refleksi dengan jawaban-clue singkat
Mengapa Rust tidak memakai preprocessor tekstual seperti C?
Clue: macro Rust lebih terstruktur;
use
/mod
menggantikan include.Jawaban singkat: untuk mengurangi bug yang timbul dari substitusi teks dan menyediakan macro yang lebih aman/struktur.
Apa yang membuat debugging release sulit?
Clue: optimisasi seperti inlining dapat menghapus variabel atau mengubah kontrol flow.
Jawaban singkat: compiler mengubah struktur program untuk performa — menyebabkan variabel/stack tidak sesuai sumber.
Apakah strip membuat program “aman” dari reverse engineering?
Clue: strip hanya menghapus symbol names; instruksi mesin masih ada.
Jawaban singkat: tidak aman sepenuhnya — hanya menyulitkan analis, bukan mencegah analisis.
Kenapa perlu memahami MIR/LLVM IR?
Clue: untuk memahami transformasi compiler dan mengapa sejumlah bug/performa muncul.
Jawaban singkat: membantu debugging tingkat lanjut dan memahami optimisasi.
Referensi
CS50 Lecture 2 — Compiling, Linking, Debugging (materi dasar tentang tahapan kompilasi dan inspeksi).
The Rust Programming Language — The Rust Book (bab tentang compilation, ownership, modules, and FFI).
LLVM Project — LLVM Language Reference Manual (untuk pembaca lanjut yang ingin memahami LLVM IR).
Ghidra / IDA / radare2 documentation — pengenalan reverse engineering tools.