Memahami operasi memory diadaptasi dari CS50x
Latihan membuat instruksi pembelajaran mengenai operasi memory saat menuliskan kode pemrograman, dengan implementasi bahasa pemrograman Rust.
Tujuan pembelajaran
Setelah selesai modul ini, peserta akan mampu:
Menjelaskan perbedaan stack dan heap, serta implikasinya (ukuran, lifetime, alokasi).
Menjelaskan apa itu pointer (alamat), dan perbedaan antara pointer C (
*
) dan referensi Rust (&
,&mut
) secara konseptual.Mengidentifikasi dan menjelaskan kesalahan memori umum: uninitialized reads, use-after-free, double free, buffer overflow.
Menunjukkan bagaimana Rust (ownership + borrow checker) mencegah kelas kesalahan tertentu, dan kapan perlu menggunakan
unsafe
.Mengonversi pola idiomatik C (mis. string copy, swap by pointer) ke idiom Rust yang aman.
Menggunakan alat bantu debugging memori di kedua ekosistem: Valgrind (C) dan Miri / sanitizers (Rust).
Prasyarat
Pengetahuan dasar C (variabel, pointer dasar) membantu karena contoh CS50 berbasis C.
Dasar Rust: variabel,
let
,mut
,struct
,fn
,String
,Vec
,Box
, referensi (&
), dan traitCopy
/Clone
.Terminal/basic tooling:
cargo
,rustc
,miri
(opsional),valgrind
(opsional).
Inti konsep: Stack vs Heap
Stack
Lokasi penyimpanan data untuk variabel lokal dengan lifetime yang jelas (scope).
Alokasi/dealokasi sangat cepat (LIFO).
Ukuran terbatas (stack overflow jika menaruh array besar di stack).
Contoh C:
int n = 50;
—n
berada di stack. (lihat slide dan contoh addresses).
Heap
Lokasi untuk data yang dialokasikan dinamis (runtime), mis.
malloc
di C,Box
/Vec
/String
di Rust.Alokasi/ dealokasi lebih lambat, memerlukan manajemen (free / drop).
Data heap hidup sampai di-free (atau sampai owner di Rust hilang).
Contoh C:
int *p = malloc(sizeof(int));
→*p
berada di heap. (lihat contoh memory.c / copyX.c).
Garis besar perbandingan
Stack: cepat, automatic, lifetime terikat scope.
Heap: fleksibel ukuran dan lifetime, memerlukan manajemen (manual di C; RAII/ownership di Rust).
Pointer & alamat: pemahaman intuitif
Pointer = alamat memori. Menyimpan alamat sebuah nilai. Dalam C:
int *p = &n;
. Anda bisa dereference pointer (*p
) untuk membaca/menulis nilai pada alamat tersebut. Contoh addresses1.c, addresses2.c.Pointer arithmetic di C:
*(s + 1)
untuk mengaksess[1]
. Ini raw dan kuat, sekaligus sumber banyak bug (out-of-bounds). Slide CS50 memperlihatkan string bytes dan alamatnya jelas.
Rust mapping (konseptual):
Rust menyediakan referensi
&T
(immutable) dan&mut T
(mutable) yang mirip pointer tetapi tidak memberi arithmetic. Referensi aman: tidak mungkin menjadi dangling atau double free semudah di C karena borrow checker.Untuk akses pointer-style/low-level ada raw pointers
*const T
/*mut T
danunsafe
— tapi pemula tidak perlu memulai dari sini.
Kesalahan memori klasik (dengan contoh dari CS50) dan cara Rust mencegahnya
1. Uninitialized reads / garbage values
C example: reading from stack array tanpa inisialisasi (garbage.c).
C bisa membaca memory yang belum diinisialisasi → hasil tak terduga.
Rust menolak akses ke variabel yang belum diinisialisasi; compiler menegakkan inisialisasi sebelum dipakai.
2. Use-after-free / dangling pointer
C pattern: free()
lalu masih menggunakan pointer → crash/UB. Banyak contoh di lecture slide yang menjelaskan malloc
/free
.
Rust ownership model: ketika owner (mis.
Box<T>
,Vec<T>
) keluar scope,drop
dipanggil; borrow checker memastikan tidak ada referensi hidup yang bisa mengakses memory setelah owner drop. Jadi use-after-free umum di C tidak muncul di Rust kecuali memakaiunsafe
.
3. Double free
C: memanggil free()
dua kali pada pointer sama → UB. Contoh copy5.c atau memory.c yang menunjukkan free handling.
Rust: ownership tunggal mencegah double free karena ownership pindah (move) atau borrow rules mencegah multi-owner. Untuk shared ownership gunakan
Rc
/Arc
yang menghitung refcount dan mengelola drop otomatis — tapi berhati-hati terhadap reference cycles.
4. Buffer overflow / out-of-bounds
C: scanf
atau strcpy
dapat menulis melewati batas (slides & get2.c/get3.c).
Rust: indexing
v[i]
panics on OOB in safe Rust; preferget()
returningOption
.String
/Vec
API membuat buffer overflow accidental lebih sulit. Namun diunsafe
atau konversi dari FFI, programmer harus hati-hati.
Alat bantu debugging memory
C world: Valgrind (memcheck) — memeriksa uninitialized reads, leaks, invalid frees. Slides menunjukkan valgrind dan contoh.
Rust world:
Miri — interpreter untuk mendeteksi undefined behaviour (use of uninitialized memory, invalid borrows) pada kode Rust.
AddressSanitizer (ASan) via compiler flags for detecting OOB, use-after-free when building with
-Z sanitizer
(nightly) or using clang sanitizers for C components.cargo clippy / static analyzers membantu menemukan pola bug.
Praktik: gunakan Miri (
cargo miri test
) untuk mengecek UB di kode Rust; di C gunakan Valgrind pada contoh copy5.c / memory.c.
Peta konsep: bagaimana pola C di CS50 diterjemahkan ke Rust
Di bagian source CS50 ada contoh-contoh yang sering dipakai: swap
(by value vs by pointer), copyX.c
(copy string with malloc and free), addresses*
(pointer and addresses), get
/scanf
pitfalls. Saya ringkas pola dan bagaimana mengonversinya ke idiom Rust.
Swap two ints
C (pass by value): tidak menukar:
void swap(int a, int b) { int tmp = a; a = b; b = tmp; }
C (by pointer): benar:
void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; }
Rust idiom:
Use mutable references:
fn swap(a: &mut i32, b: &mut i32) {
let tmp = *a;
*a = *b;
*b = tmp;
}
fn main() {
let mut x = 1;
let mut y = 2;
swap(&mut x, &mut y);
}
Clue: Rust references behave like pointers but checked by compiler — no raw pointer arithmetic.
String copy with allocation
C manual (copy4.c): allocate t = malloc(strlen(s)+1)
, strcpy
, then free(t)
. Rust idiom (safe, heap-allocated string copy):
fn main() {
let s = String::from("hello");
let t = s.clone(); // deep copy of heap string
// or if you need new allocation explicitly:
let t = String::from(&s);
println!("s: {}, t: {}", s, t);
}
Clue: String
owns heap memory; clone()
allocates new heap buffer and copies bytes.
Uninitialized array / garbage reads
C int scores[1024];
printing values yields garbage (memory.c). Rust disallows using uninitialized data. Use default initialization:
let scores: [i32; 1024] = [0; 1024];
Or heap allocate via Vec
:
let mut scores = vec![0i32; 1024];
Use-after-free demonstration -> Rust prevention
C misuse: keep pointer after free()
. Rust pattern: owner drop occurs when variable goes out of scope; borrow checker prevents referencing after owner moved. To show use-after-free in Rust you'd need unsafe
and raw pointers — avoid for beginners.
Contoh latihan terstruktur (praktik & exercises)
Berikut beberapa latihan (dari mudah ke menantang). Untuk tiap latihan saya sertakan clue singkat untuk membantu.
Latihan 1: alamat & referensi sederhana
Tujuan: lihat alamat variabel dan perbedaan stack vs heap.
Buat program Rust yang:
Membuat
let n = 50;
dan mencetak alamat&n
(println!("{:p}", &n);
).Membuat
let b = Box::new(50);
dan mencetak alamat&*b
(alamat heap) (println!("{:p}", &*b);
). Clue:Box<T>
menyimpan data di heap;&*b
deref box ke T lalu ambil alamatnya.
Latihan 2: swap (konversi dari swap1.c)
Tujuan: praktek mutable references.
Implementasikan fungsi
swap(a: &mut i32, b: &mut i32)
lalu panggilnya untuk menukar dua variabel. Clue: deref referensi untuk membaca/menulis*a
,*b
.
Latihan 3: salin string (konversi dari copy4.c)
Tujuan: praktik String
, clone()
.
Baca string dari stdin (pakai
read_line
), buat salinan (clone), ubah huruf pertama dari salinan menjadi uppercase, lalu cetak kedua string. Clue: gunakans.trim_end().to_string()
untuk membersihkan newline sebelum clone; gunakanchars()
/replace_range()
untuk modifikasi.
Latihan 4: mendeteksi OOB vs safe get
Tujuan: aman akses koleksi.
Buat
Vec<i32>
dengan 3 elemen. Coba aksesv[5]
vsv.get(5)
dan tanganiOption
. Clue:v[5]
panic (runtime),v.get(5)
returnNone
.
Latihan 5: menerjemahkan contoh CS50 copy5.c ke Rust (deteksi leak)
Tujuan: periksa perilaku alokasi/leak.
C:
int *x = malloc(3*sizeof(int)); x[1]=72; x[2]=73; x[3]=33;
→ out-of-bounds write.Tugas: buat contoh Rust yang menyerupai kesalahan (harus pakai unsafe raw pointer), hanya sebagai demonstrasi; jangan gunakan unsafe di aplikasi nyata. Clue: gunakan
Vec::with_capacity(3)
laluunsafe
block untuk menulis di luar bounds (HARUS dilakukan hanya untuk eksperimen di lingkungan terkendali), atau gunakan Valgrind / Miri untuk mendeteksi.
Catatan pedagogis: latihan 5 adalah opsional dan harus diawasi; tujuannya melihat alat pendeteksi kesalahan (Miri/Valgrind).
Jawaban singkat / clue untuk beberapa latihan
Latihan 1:
println!("stack addr: {:p}", &n); println!("heap addr: {:p}", &*b);
Latihan 2:
fn swap(a: &mut i32, b: &mut i32) { let tmp = *a; *a = *b; *b = tmp; }
Latihan 3: baca
let mut s = String::new(); std::io::stdin().read_line(&mut s).unwrap(); let mut t = s.trim_end().to_string(); t.replace_range(0..1, &t[0..1].to_uppercase());
Latihan 4:
match v.get(5) { Some(x) => println!("{}", x), None => println!("oob") }
Edge cases (perlu diberi perhatian dalam pembelajaran)
Stack overflow: membuat array sangat besar di stack (
let big: [u8; 10_000_000]
) akan crash; gunakanVec
atauBox<[T]>
. (CS50 meliput stack overflow / buffer overflow risks).Panic vs UB: Rust safe code panics on
v[index]
OOB; C may silently corrupt memory and continue. Perbedaan ini harus dipahami.Uninitialized reads are UB: membaca memory tanpa initialize (C) versus compile-time enforced initialization (Rust).
Ownership transfer pitfalls: memindahkan owner (
let b = a;
) membuata
invalid in Rust; di C, copy pointer tidak transfer ownership sehingga double-free possible.FFI boundaries: saat berinteraksi dengan C via FFI, Rust safety guarantees can be broken — treat FFI carefully and validate inputs.
Tools & workflow yang direkomendasikan
Untuk C examples: compile with
-g
and run Valgrind:gcc -g file.c && valgrind ./a.out
(Valgrind shows leaks, invalid reads/writes).For Rust: use
cargo miri test
(Miri) to detect undefined behaviour such as uninitialized reads. Usecargo +nightly run -Z sanitizer=address
for ASan if available.Use sanitizers and static linters early (Clippy, compiler warnings) to catch patterns.
Refleksi & pertanyaan pemicu diskusi (dengan clue)
Mengapa use-after-free lebih mudah terjadi di C daripada di Rust? Clue: C memisahkan pointer dari ownership; Rust mengikat ownership ke variable dan memaksa pemeriksaan pada compile time.
Apa trade-off antara kontrol penuh (C) dan safety-by-default (Rust)? Clue: C memberi kebebasan (dan risiko) untuk performance tuning; Rust memberi safety yang kuat namun masih bisa
unsafe
bila perlu.Kapan Anda tetap perlu
unsafe
di Rust? Clue: interoperabilitas FFI, optimasi low-level, atau interfacing dengan hardware; gunakanunsafe
hanya untuk batas wilayah kecil dan teruji.Bagaimana alat seperti Valgrind dan Miri membantu proses belajar? Clue: mereka mengungkap kelas bug yang sulit dideteksi manual, dan membantu memahami implikasi memory misuse.
Ringkasan praktis (cheat-sheet)
Stack
= local variables, limited size.Heap
= dynamic allocation (malloc/Box/Vec), flexible size.Pointer
di C = alamat (raw) → arithmetic allowed → risk.&T
/&mut T
di Rust = safe references → no pointer arithmetic.Use
Vec
for dynamic-sized collections; useBox
for single heap values or recursive types.Tools: Valgrind (C), Miri / sanitizers (Rust), Clippy for linting.
Referensi
Harvard University. CS50 — Week 4: Memory (lecture slides). Available: https://cs50.harvard.edu/x/weeks/4/ . (Also: available lecture PDF).
Harvard University. CS50 — Week 4 source code examples (addresses.c, copy*.c, memory.c, swap*.c, etc.). (Also available at the CS50 week 4).
The Rust Project. The Rust Programming Language (The Book) — chapters on Ownership and on References. URL: https://doc.rust-lang.org/book/
The Rust Project. Miri — an interpreter for Rust’s mid-level intermediate representation, for detecting undefined behavior in Rust code. URL: https://github.com/rust-lang/miri