Dalam pengembangan sistem operasi (OS Development), langkah pertama yang paling krusial bukanlah membuat antarmuka grafis, melainkan memastikan kita memiliki kendali penuh atas perangkat keras. Artikel ini mendokumentasikan proses pembuatan kernel bare metal sederhana untuk Raspberry Pi (BCM2835) yang berfungsi mengedipkan LED, serta bagaimana melakukan verifikasi eksekusi kode tersebut tanpa perangkat keras fisik menggunakan emulator QEMU dan GDB pada Fedora Linux.
1. Persiapan Lingkungan Pengembangan (Environment)
Pada sistem operasi Fedora Linux, paket toolchain untuk arsitektur ARM dan emulator QEMU dapat diinstal dengan mudah. Berbeda dengan distro lain, Fedora menyediakan GDB yang mendukung multi-architecture secara bawaan, sehingga kita tidak memerlukan paket khusus gdb-multiarch.
Perintah instalasi:
sudo dnf install arm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-newlib qemu-system-arm gdb
2. Struktur Kode Sumber
Proyek ini terdiri dari empat berkas utama yang bekerja secara sinergis untuk menghasilkan image kernel.
A. main.c: Kernel Utama
Kode ini ditulis murni dalam C tanpa ketergantungan pada pustaka standar (<stdint.h> atau libc). Kita mengakses register GPIO secara langsung melalui Memory Mapped I/O untuk mengontrol Pin 16 (LED Status pada model Raspberry Pi lama).
/*
* main.c - Kode Kernel Bare Metal Sederhana
* Tujuan: Mengedipkan LED pada GPIO 16 (Active Low)
*/
void main() {
/* Definisi Register GPIO (BCM2835 Base: 0x20000000 + Offset GPIO: 0x200000) */
volatile unsigned int* GPFSEL1 = (volatile unsigned int*)0x20200004;
volatile unsigned int* GPSET0 = (volatile unsigned int*)0x2020001C;
volatile unsigned int* GPCLR0 = (volatile unsigned int*)0x20200028;
/* Konfigurasi GPIO 16 sebagai OUTPUT
* Membersihkan bit 18-20, lalu set bit 18 ke 1 (Biner 001)
*/
*GPFSEL1 &= ~(7 << 18);
*GPFSEL1 |= (1 << 18);
/* Loop Utama */
while (1) {
/* Nyalakan LED (Active Low -> Tulis ke Register Clear) */
*GPCLR0 = (1 << 16);
/* Delay Manual */
for (volatile int i = 0; i < 500000; i++);
/* Matikan LED (Tulis ke Register Set) */
*GPSET0 = (1 << 16);
/* Delay Manual */
for (volatile int i = 0; i < 500000; i++);
}
}
B. boot.S: Assembly Startup
Kode assembly ini bertugas menyiapan Stack Pointer (SP) di alamat 0x8000 sebelum memanggil fungsi main di C.
Cuplikan kode
.section ".text.boot"
.global _start
_start:
mov sp, #0x8000 /* Set Stack Pointer */
bl main /* Lompat ke kode C */
halt:
b halt /* Loop selamanya jika main kembali */
C. linker.ld: Skrip Linker
Skrip ini memastikan kode dimuat di alamat memori 0x8000, sesuai dengan spesifikasi bootloader Raspberry Pi.
Cuplikan kode
SECTIONS
{
. = 0x8000;
.text : { *(.text.boot) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
D. Makefile: Otomatisasi Kompilasi
Berkas ini mengatur proses assemble, compile, dan link. Perhatikan penggunaan flag -g untuk menyertakan simbol debug, yang sangat penting untuk analisis di GDB.
Makefile
# --- Toolchain ---
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
# --- Definisi Folder ---
SRC_DIR = source
BUILD_DIR = build
# --- Flags Kompilasi (Compile Time) ---
CFLAGS = -g -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles
# --- Flags Linker (Link Time) ---
# PERBAIKAN DISINI: Kita tambahkan -nostdlib dan -nostartfiles
LDFLAGS = -T linker.ld -nostdlib -nostartfiles
# --- Target Utama ---
all: kernel.img
# 1. Membuat Binary Image
kernel.img: $(BUILD_DIR)/kernel.elf
$(OBJCOPY) $(BUILD_DIR)/kernel.elf -O binary kernel.img
# 2. Linker
$(BUILD_DIR)/kernel.elf: $(BUILD_DIR)/boot.o $(BUILD_DIR)/main.o
$(CC) $(LDFLAGS) $(BUILD_DIR)/boot.o $(BUILD_DIR)/main.o -o $(BUILD_DIR)/kernel.elf
# 3. Compile Assembly
$(BUILD_DIR)/boot.o: $(SRC_DIR)/boot.S
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -c $(SRC_DIR)/boot.S -o $(BUILD_DIR)/boot.o
# 4. Compile C
$(BUILD_DIR)/main.o: $(SRC_DIR)/main.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -c $(SRC_DIR)/main.c -o $(BUILD_DIR)/main.o
# --- Bersih-bersih ---
clean:
rm -rf $(BUILD_DIR) kernel.img
3. Proses Kompilasi dan Emulasi
Setelah semua berkas siap, jalankan perintah make untuk menghasilkan berkas kernel.elf dan kernel.img. Selanjutnya, kita jalankan kernel tersebut menggunakan QEMU dengan mode pause (-S) dan membuka port gdb server (-s).
qemu-system-arm -M raspi0 -kernel kernel.elf -nographic -S -s
Catatan: Parameter -M raspi0 digunakan karena memiliki arsitektur yang sama dengan Raspberry Pi 1 (BCM2835).
4. Analisis dan Verifikasi dengan GDB
Tahap ini membuktikan bahwa kode benar-benar berjalan di level register CPU. Kita menggunakan GDB untuk terhubung ke QEMU.
A. Koneksi dan Eksekusi Awal
Pada terminal terpisah, jalankan GDB:
gdb kernel.elf
Di dalam prompt GDB, hubungkan ke target dan pasang breakpoint:
Cuplikan kode
(gdb) target remote localhost:1234
(gdb) break main
(gdb) continue
Hasilnya nampak seperti ini
Reading symbols from kernel.elf...
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
_start () at source/boot.S:5
5 mov sp, #0x8000 /* Set Stack Pointer */
(gdb) break main
Breakpoint 1 at 0x800c: file source/main.c, line 15.
(gdb) continue
Continuing.
Breakpoint 1, main () at source/main.c:15
15 *GPFSEL1 &= ~(7 << 18);
(gdb)
B. Inspeksi Instruksi Assembly
Gunakan mode visual untuk melihat instruksi mesin yang sedang dijalankan:
Cuplikan kode
(gdb) layout asm
Gunakan perintah ni (Next Instruction) untuk menelusuri kode baris demi baris hingga melewati instruksi str (Store Register) yang menulis ke alamat GPIO.
C. Verifikasi Memori (Bukti Keberhasilan)
Untuk membuktikan bahwa LED telah dinyalakan atau dimatikan, kita memeriksa alamat register GPIO Level 0 (0x20200034). Register ini menampung status tegangan pin saat ini.
Setelah instruksi “Matikan LED” dijalankan, kita memeriksa memori:
Cuplikan kode
(gdb) x /4x 0x20200034
Hasil yang diperoleh:
0x20200034: 0x00010000 0x00000000 0x00000000 0x00000000
Nilai 0x00010000 menunjukkan bahwa bit ke-16 bernilai 1 (HIGH). Ini mengonfirmasi bahwa CPU telah berhasil mengeksekusi logika program untuk mengontrol periferal perangkat keras secara akurat.
Kesimpulan
Melalui eksperimen ini, kita telah berhasil membangun fondasi sistem operasi dasar. Kita tidak hanya menulis kode, tetapi juga memvalidasi perilaku hardware virtual secara presisi menggunakan GDB. Langkah selanjutnya dalam pengembangan ini adalah implementasi driver UART untuk memungkinkan komunikasi serial (cetak teks) dari kernel ke terminal.
