Saat kita menulis program C dan menjalankannya, sistem operasi tidak sekadar meletakkan seluruh kode dan data ke satu kantong memori yang datar. Sistem operasi memetakan executable ke beberapa segment memori dengan izin akses dan tujuan berbeda—ada segment yang hanya bisa dibaca, ada yang bisa ditulis, ada yang tumbuh ke alamat rendah, dan ada yang tumbuh ke alamat tinggi. Memahami layout ini bukan sekadar teori akademis: pemilihan tempat sebuah variabel diletakkan (global, lokal, atau malloc) menentukan lifetime, kecepatan akses, dan potensi bug. Pada chapter ini kita akan memetakan lima segment utama—text, rodata, data, bss, heap, dan stack—lalu melihat langsung alamat masing-masing melalui program C yang nyata.
Lima Segmen Utama Memori Program #
Sebuah proses yang baru dijalankan oleh kernel Linux memiliki virtual address space yang dibagi menjadi beberapa wilayah. Berikut ringkasannya:
- Text segment — berisi machine code hasil kompilasi fungsi-fungsi program. Bersifat read-only dan biasanya shared antar proses yang menjalankan executable yang sama.
- Rodata segment — berisi constant seperti string literal dan variabel bertipe
constglobal. Bersifat read-only. - Data segment — berisi variabel global yang terinisialisasi dengan nilai bukan nol (misalnya
int counter = 42;). - BSS segment — berisi variabel global yang tak terinisialisasi atau diinisialisasi ke nol. Kernel Linux menyiapkan wilayah ini sebagai halaman-halaman nol secara lazy; executable tidak perlu menyimpan nol di dalamnya.
- Heap — wilayah yang tumbuh ke alamat tinggi, dipakai untuk memori yang dialokasikan secara dinamis lewat
malloc,calloc, danrealloc. - Stack — wilayah yang tumbuh ke alamat rendah, dipakai untuk automatic storage seperti variabel lokal dan parameter fungsi.
Mekanisme kerja di balik layar: ketika kernel menjalankan executable, kernel memuat ELF (format executable di Linux), memetakan segment text dan rodata sebagai halaman read-only, menyiapkan segment data dan bss, lalu memberi satu stack frame awal untuk fungsi main. Setiap kali kita memanggil malloc, allocator glibc meminta halaman dari kernel (lewat mmap atau memperluas brk) dan mengembalikan pointer ke wilayah heap. Setiap kali kita memanggil fungsi, stack pointer berkurang (karena stack tumbuh ke bawah) untuk membuat ruang bagi variabel lokal baru.
Visualisasi Konsep #
Diagram berikut memetakan enam segment utama memori program C dan arah pertumbuhan stack versus heap:
Pada diagram di atas, virtual address space sebuah proses disusun dari alamat rendah (text) ke alamat tinggi (stack). Wilayah text dan rodata bersifat read-only; wilayah data dan bss menampung variabel global; heap tumbuh ke alamat tinggi mengikuti permintaan malloc; stack tumbuh ke arah berlawanan (alamat rendah) setiap kali fungsi dipanggil. Karena kedua wilayah bertumbuh saling mendekati, program yang banyak memanggil malloc atau rekursi sangat dalam bisa menyebabkan stack dan heap saling menabrak.
Menampilkan Alamat Tiap Segmen #
Kita akan memverifikasi teori di atas dengan program yang mencetak alamat dari entitas yang berasal dari masing-masing segmen. Karena Address Space Layout Randomization (ASLR) aktif di sistem modern, nilai heksadesimal pasti berubah setiap kali program dijalankan—yang penting bagi kita adalah pola relatifnya.
#include <stdio.h>
#include <stdlib.h>
/* Variabel global terinisialisasi -> segmen data */
int data_var = 42;
/* Variabel global tak terinisialisasi -> segmen bss */
int bss_var;
/* Pointer global menunjuk string literal di rodata */
const char *rodata_str = "Belajar C";
int main(void) {
/* Variabel lokal -> stack */
int stack_var = 7;
/* Alokasi dinamis -> heap */
int *heap_ptr = malloc(sizeof(int));
printf("Alamat fungsi main : %p\n", (void*)main);
printf("Alamat data_var : %p\n", (void*)&data_var);
printf("Alamat bss_var : %p\n", (void*)&bss_var);
printf("Alamat rodata_str : %p\n", (void*)&rodata_str);
printf("Alamat stack_var : %p\n", (void*)&stack_var);
printf("Alamat heap_ptr (target) : %p\n", (void*)heap_ptr);
free(heap_ptr);
return 0;
}Output yang Dihasilkan:
Alamat fungsi main : 0x557d2c921159
Alamat data_var : 0x557d2c924028
Alamat bss_var : 0x557d2c92403c
Alamat rodata_str : 0x557d2c924030
Alamat stack_var : 0x7ffe06e2f1b4
Alamat heap_ptr (target) : 0x557d2e4552a0Perhatikan pola alamatnya. Alamat main, data_var, bss_var, dan rodata_str semua berada di kisaran 0x557d2c92xxxx—itulah wilayah executable yang dipetakan kernel ke memori proses. Alamat stack_var jauh di atas sana di kisaran 0x7ffe..., dan heap_ptr di 0x557d2e45...—sedikit lebih tinggi dari data segment, sesuai arah pertumbuhan heap ke alamat tinggi.
Mari kita telaah baris-baris kode di atas:
- Baris 6 (
int data_var = 42;): variabel global terinisialisasi ke nilai bukan nol. Compiler menempatkannya di segmen data, yang diisi langsung dari berkas executable. - Baris 9 (
int bss_var;): variabel global tanpa inisialisasi. Compiler menempatkannya di segmen bss; berkas executable tidak menyimpan nilai nol, kernel menyiapkan halaman nol saat program dimuat. - Baris 12 (
const char *rodata_str = "Belajar C";): pointer itu sendiri di segmen data, tetapi string literal"Belajar C"disimpan di segmen rodata yang bersifat read-only. - Baris 17 (
int stack_var = 7;): variabel lokal di dalam fungsimain. Compiler menghasilkan instruksi yang menyesuaikan stack pointer dan menulis7ke memori stack. - Baris 20 (
int *heap_ptr = malloc(sizeof(int));):mallocmeminta 4 byte dari heap ke allocator glibc. Allocator mengembalikan alamat di wilayah heap yang ditumbuhkan denganbrkataummap. - Baris 22 (
printf("... %p", (void*)main)): alamat fungsimainberada di text segment. Standar C mengharuskan argumen%pberupavoid*, sehingga kita melakukan castmainkevoid*. - Baris 28 (
free(heap_ptr);): mengembalikan blok heap ke allocator agar bisa dipakai ulang di alokasi berikutnya.
Heap dan Stack Bertumbuh dari Arah Berlawanan #
Stack tumbuh ke alamat rendah; heap tumbuh ke alamat tinggi. Hal ini terlihat jelas jika kita membandingkan alamat sebuah variabel lokal dengan alamat yang dikembalikan malloc.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int a = 10;
int *p = malloc(100 * sizeof(int));
printf("Stack variabel a alamat : %p\n", (void*)&a);
printf("Heap alamat (malloc) : %p\n", (void*)p);
printf("Selisih heap - stack : %ld byte\n",
(long)((char*)p - (char*)&a));
free(p);
return 0;
}Output yang Dihasilkan:
Stack variabel a alamat : 0x7fffec18bad4
Heap alamat (malloc) : 0x559386b792a0
Selisih heap - stack : -46645045700660 byteNilai selisih negatif yang besar menunjukkan bahwa &a (alamat tinggi di stack) jauh lebih besar daripada p (alamat lebih rendah di heap). Inilah mengapa stack overflow (misalnya akibat rekursi terlalu dalam) bisa “menabrak” wilayah heap bila kernel tidak menjaga batas stack guard page—kasus yang klasik pada buffer overflow attack.