Pemrograman Bare Metal dalam Raspberry Pi dengan Rust
(Post in Indonesian) Belajar bare metal programming + Rust
Dari kemarin sebenernya bingung mau nulis ini dalam Bahasa Inggris atau enggak, tapi kalau dipikir-pikir resource bare metal programming/OS enggak begitu banyak dalam Bahasa Indonesia. Setelah ngoding C/C++ dan sempat rame juga tentang Rust (bahkan ada Zig sekarang), jadinya pengen coba nulis Rust sekalian belajar tentang hardware.
Intro
Bare metal programming artinya kita membuat program yang sangat dekat dengan hardware, seperti microcontroller, SoC. Apa bedanya sama pemrograman sistem biasa? Disini, kita bener-bener bikin program yang CPU langsung eksekusi tanpa bantuan OS. Ini artinya:
- Tidak ada system call
- Tidak ada library dasar seperti
libc
(karena system callnya pun tidak ada) - Tidak ada memory management: Tidak ada dynamic data structures seperti dynamic array, malloc, etc
- Program bekerja dalam physical memory by default, bukan virtual memory.
- Kita cuma punya instruksi CPU dan hardware-hardware yang ada di SoC.
Kalau programming menggunakan Linux, Bedanya Linux sudah menyediakan interface system call yang sudah mature, dan banyak library-library yang sudah men-support Linux. Kali ini, kita akan meninggalkan Linux dan bisa dibilang membuat sistem operasi sendiri. Kita akan menulis program tersebut menggunakan Rust.
Persiapan
Kali ini kita akan membuat program bare metal untuk Raspberry Pi 3 Model B (yang saya punya). Dilihat dari dari spesifikasinya, Raspberry PI 3 Model B menggunakan Quad Core 1.2GHz Broadcom BCM2837 64bit CPU
. BCM2837 sendiri pakai ARM Cortex-A53 sebagai core-nya, dengan ARMv8-A. Artinya, kita harus membuat program yang menggunakan instruksi ARMv8 yang ditambah dengan peripheral (hardware tambahan) dari BCM2837.
Kita bisa dapatkan datasheet dari BCM2837 disini. Atau di link yang sudah saya mirrorkan disini
Dalam PDF tersebut sudah disediakan cara berinteraksi untuk setiap komponen, dan alamat dari register tersebut.
Datasheet tersebut dibuat dari dokumen BCM2835 namun beberapa di-edit manual untuk disesuaikan kepada BCM2837 (+ beberapa errata). Gambar di PDF mungkin tidak akurat. {: .prompt-warning}
Target
Kita akan membuat program yang bisa mengontrol GPIO, dan membuat LED berkedip. Kita belum akan membuat interface yang bisa menampilkan tulisan, karena hal tersebut cukup kompleks dan masih sulit untuk memastikan correctness-nya. Untuk sekarang setidaknya kita ingin tahu apakah program kita berjalan dengan benar.
Implementasi
Tutorial ini sebagian besar mengambil dari artikel wiki OSDev - Raspberry Pi Bare Bones. Link disini
Buat folder project menggunakan cargo init --bin --edition 2021
. Kita akan membuat beberapa komponen dari program, dimulai dari entry point sampai API untuk mengakses GPIO.
Entry point
Di dalam src/
, kita akan membuat entry point dari program kita dalam assembly, dengan filename boot.S
. CPU akan pertama kali mengeksekusi kode ini.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// AArch64 mode
// To keep this in the first portion of the binary.
.section ".text.boot"
// Make _start global.
.globl _start
// Entry point for the kernel. Registers:
// x0 -> 32 bit pointer to DTB in memory (primary core only) / 0 (secondary cores)
// x1 -> 0
// x2 -> 0
// x3 -> 0
// x4 -> 32 bit kernel entry point, _start location
_start:
// https://forums.raspberrypi.com/viewtopic.php?t=273010
// read cpu id, stop slave cores
mrs x1, mpidr_el1
and x1, x1, #3
cbz x1, 2f
// cpu id > 0, stop
b halt
2: // cpu id == 0
// set stack before our code
ldr x5, =_start
mov sp, x5
// clear bss
ldr x5, =__bss_start
ldr w6, =__bss_size
1: cbz w6, 2f
str xzr, [x5], #8
sub w6, w6, #1
cbnz w6, 1b
// jump to C code, should not return
2: bl kernel_main
// for failsafe, halt this core
halt:
wfe
b halt
Untuk penjelasan instruction sendiri bisa look-up sendiri lewat Google, disini saya akan menjelaskan garis besar apa yang dilakukan.
Assembly Directive
Ada 2 assembly directive di atas: .section
, dan .globl
. Tujuan directive pertama untuk memunculkan section .text.boot
yang dimulai dari instruksi pertama dibawahnya. Kedua, untuk memberitahu compiler bahwa _start
adalah sebuah symbol yang public. Nanti section dan symbol di atas akan berguna pada saat proses linking
, yang akan dijelaskan di bawah nanti.
Single-core only
Pertama, semua CPU akan menyala dan mengeksekusi program yang sama (program kita). Agar tidak terjadi bentrok, kita mau “mematikan” CPU ID #1, #2, #3 dan biarkan CPU 0 yang berjalan.
Stack Pointer
Kedua, kita mengatur posisi stack pointer kita tepat di atas alamat instruksi pertama kita, yang ditandai oleh symbol_start
.
Karena sistem stack di ARM itu arahnya “ke-atas” (saat push, value alamat di
sp
akan dikurangi), makanya kita ambil posisi kita di atas alamat instruksi pertama, biar tidak tiba-tiba menimpa instruksi selanjutnya.
Clear .bss
Ketiga, kita ingin meng-nol-kan segmen memori yang ditandai dengan bss
. Segmen bss
nantinya akan ditempati oleh variabel-variabel yang statically allocated, mirip seperti kita membuat variabel static int my_value
. Jadi, ini untuk memastikan bahwa variable static kita value-nya 0.
“Segmen memori” disini sama saja dengan segmen program kita, karena nanti semua program kita akan ditaruh ke memori dalam proses booting.
Jump to main function
Terakhir, kita akan jump ke alamat yang ditandai kernel_main
, yang nanti kita akan implementasi.
Dari assembly di atas, ada beberapa symbol: _start
, __bss_start
, __bss_size
, kernel_main
. Pada saat linking process
nantinya, baru symbol ini akan di-resolve dan diganti ke value yang sebenarnya.
Karena compilation Rust hanya melihat file yang di-“depend” oleh main.rs
, maka kita buat source file baru rs
bernama boot.rs
yang isinya:
1
core::arch::global_asm!(include_str!("boot.S"));
Main Function
Kita akan membuat main “entry-point” kita yang menggunakan kode Rust. Ingat, program kita tidak akan bisa menggunakan library tambahan maupun syscall apapun, maka kita tidak bisa menggunakan library std
, hanya core
saja. Kita akan menambahkan crate attribute no_std
dan no_main
karena main function kita akan dipanggil oleh assembly di atas.
File main.rs
akan berisi sbb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#![no_std]
#![no_main]
mod boot;
#[no_mangle]
extern "C" fn kernel_main() {
}
#[panic_handler]
fn on_panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
External Linkage dan no_mangle
no_mangle
dan extern "C"
di atas sangat penting untuk memastikan fungsi kernel_main
memiliki external linkage dan nama symbolnya tidak mangled (jadi tetap kernel_main
, yang bisa dibaca oleh assembly yang kita buat di atas).
Secara default, fungsi yang di-compile Rust akan melewati proses name mangling. Mudahnya, nama fungsi yang muncul setelah meng-compile
kernel_main()
tidak akan menjadikernel_main
, tetapi seperti_ZN5osdev10main14kernel_main17h6443fc27a975a06bE
. Source file lain yang ingin memanggilkernel_main
harus menggunakan nama/symbol hasil mangling, atau melewati proses name mangling yang sama, agar linker bisa me-“nyambungkan” dengan benar.
Panic Handler
Rust sendiri memiliki panic handler yang akan dipanggil apabila ada unrecoverable error. Sayangnya, fitur ini tidak bisa langsung kita pakai. Kita akan mematikan default panic handler dan menggantinya di main.rs
di fungsi yang ditandai #[panic_handler]
.
Tambahkan informasi berikut di Cargo.toml
:
1
2
3
4
5
6
7
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
Penjelasannya bisa dilihat disini dan disini.
Implementasi GPIO
Mulai dari sini, kita bisa ngoding “hampir sepenuhnya” dalam Rust. Untuk mengoperasikan GPIO kita harus:
- Memahami cara mengakses register yang mengontrol pin GPIO tersebut
- Memahami berada di region memory mana register tersebut berada.
Dari halaman 6 dari datasheet,
Physical addresses range from 0x3F000000 to 0x3FFFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x3Fnnnnnn.
Dari kalimat pertama, kita dapat mengetahui base address dari peripheral dimulai dari 0x3F000000
.
Semua address yang ditunjukkan di dalam datasheet ditulis dalam format “bus address” (alamat yang dipahami oleh VideoCore), jadi kita perlu berhati-hati karena kita bekerja dalam CPU, bukan GPU. Contoh, register
GPSET0
ada di alamat0x7E20001C
, maka dalam CPU sebenarnya alamat tersebut adalah0x3F20001C
.
MMIO
Kita akan mempersiapkan interface untuk menulis data ke dalam memori segmen peripheral. Kita namakan MMIO (Memory mapped IO) karena CPU mengakses peripheral melalui interface memori.
File mmio.rs
berisi sbb:
1
2
3
4
5
6
7
8
9
10
11
12
// Each model has different base addresses.
static BASE_ADDR: u64 = 0x3F000000;
#[inline(always)]
pub fn write(addr: u64, data: u32) {
unsafe { core::ptr::write_volatile((BASE_ADDR + addr) as *mut u32, data) }
}
#[inline(always)]
pub fn read(addr: u64) -> u32 {
unsafe { core::ptr::read_volatile((BASE_ADDR + addr) as *mut u32) }
}
write_volatile
dan read_volatile
disini sangat penting untuk menghindari optimization yang bisa dilakukan oleh compiler. Karena value di dalam bagian memori tersebut tidak sepenuhnya dikontrol oleh program kita, maka compiler tidak boleh melakukan asumsi apapun terhadap pointer tersebut.
Kontrol GPIO
Untuk mengontrol GPIO, kita harus mengubah mode GPIO tersebut menjadi mode output, baru kita bisa menyalakan output tersebut.
Mengacu ke halaman 90, ada 3 jenis register yang harus kita kontrol:
GPFSEL
: Mengatur mode GPIO. Kita akan mengubah mode GPIO menjadi mode output.GPSET
: Menyalakan GPIO output.GPCLR
: Mematikan GPIO output.
Untuk penjelasan GPSET
dan GPCLR
sepertinya sudah cukup jelas dalam datasheet. Untuk GPFSEL
, yang perlu diperhatikan adalah setiap GPIO diwakili oleh 3 bit.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
use crate::io::mmio;
// Number of GPIOs
const NUM_GPIOS: u8 = 54;
// Registers
struct Reg;
#[allow(dead_code)]
impl Reg {
const BASE: u64 = 0x0020_00_00;
const GPFSEL0: u64 = Reg::BASE + 0x00; // GPIO Function Select 0
const GPFSEL1: u64 = Reg::BASE + 0x04; // GPIO Function Select 1
const GPFSEL2: u64 = Reg::BASE + 0x08; // GPIO Function Select 2
const GPFSEL3: u64 = Reg::BASE + 0x0C; // GPIO Function Select 3
const GPFSEL4: u64 = Reg::BASE + 0x10; // GPIO Function Select 4
const GPFSEL5: u64 = Reg::BASE + 0x14; // GPIO Function Select 5
// Each register represents floor(32bit / 3bit) = 10 GPIOs
// bit 30-31 is reserved
const GPFSEL_BANK: [u64; 6] = [Reg::GPFSEL0, Reg::GPFSEL1, Reg::GPFSEL2,
Reg::GPFSEL3, Reg::GPFSEL4, Reg::GPFSEL5];
const GPSET0: u64 = Reg::BASE + 0x1C; // GPIO Pin Output Set 0
const GPSET1: u64 = Reg::BASE + 0x20; // GPIO Pin Output Set 1
const GPCLR0: u64 = Reg::BASE + 0x28; // GPIO Pin Output Clear 0
const GPCLR1: u64 = Reg::BASE + 0x2C; // GPIO Pin Output Clear 1
}
#[allow(dead_code)]
pub enum Function {
Input,
Output,
Func0,
Func1,
Func2,
Func3,
Func4,
Func5,
}
// Set function of GPIOs in which position the bit is set.
// For example, set_function(1 << 5 | 1 << 10, Function::Output)
// sets GPIO5 and GPIO10 as output.
pub fn set_function(mut gpios: u64, function: Function) {
assert!(NUM_GPIOS as usize <= 10 * Reg::GPFSEL_BANK.len());
// Make sure only 0..53 are set
gpios = gpios & ((1 << NUM_GPIOS) - 1);
let function_bits: u8 = {
match function {
Function::Input => 0b000,
Function::Output => 0b001,
Function::Func0 => 0b100,
Function::Func1 => 0b101,
Function::Func2 => 0b110,
Function::Func3 => 0b111,
Function::Func4 => 0b011,
Function::Func5 => 0b010,
}
};
let mut gpfsel_val: [u32; Reg::GPFSEL_BANK.len()] =
[0; Reg::GPFSEL_BANK.len()];
for (idx, reg) in Reg::GPFSEL_BANK.iter().enumerate() {
gpfsel_val[idx] = mmio::read(*reg);
}
for i in 0..(NUM_GPIOS - 1) {
if gpios & (1 << i) != 0 {
// Where to place the function bit
let shift = (i % 10) * 3;
let bank_idx = i / 10;
// zero out the bits first
gpfsel_val[bank_idx as usize] &= !((0b111 << shift) as u32);
gpfsel_val[bank_idx as usize] |= (function_bits as u32) << shift;
}
}
for (reg, val) in core::iter::zip(Reg::GPFSEL_BANK, gpfsel_val) {
mmio::write(reg, val);
}
}
// Set the output of GPIO in which position the bit is set.
// For example, output_set(1 << 5 | 1 << 10) sets GPIO 5, and 10.
pub fn output_set(mut gpios: u64) {
// Make sure only 0..53 is set
gpios = gpios & ((1 << NUM_GPIOS) - 1);
let gpios_0: u32 = gpios as u32;
let gpios_1: u32 = (gpios >> 32) as u32;
mmio::write(Reg::GPSET0, gpios_0);
mmio::write(Reg::GPSET1, gpios_1);
}
// Set the output of GPIO in which position the bit is set.
// For example, output_set(1 << 5 | 1 << 10) clears GPIO 5, and 10.
pub fn output_clear(mut gpios: u64) {
// Make sure only 0..53 is set
gpios = gpios & ((1 << NUM_GPIOS) - 1);
let gpios_0: u32 = gpios as u32;
let gpios_1: u32 = (gpios >> 32) as u32;
mmio::write(Reg::GPCLR0, gpios_0);
mmio::write(Reg::GPCLR1, gpios_1);
}
Saya membuat fungsi diatas yang menerima 1 value u64
yang mewakili 1 bit sebagai 1 GPIO. Contohnya, untuk mengontrol GPIO4, saya akan memasang value 0b10000
(GPIO dimulai dari 0).
“Sleep function”
Karena kita ingin membuat blinking LED, maka kita perlu sesuatu untuk men-“delay” program kita berjalan. Kalau pemrograman biasa, biasanya kita sudah diberikan system call sleep
yang di dalamnya sudah dibuat dengan cukup akurat menggunakan hardware timer dan interrupt. Dalam implementasi kita, belum ada yang bisa melakukan hal tersebut. Jadi untuk sekarang simplenya kita cukup membuat for loop yang menghitung 1 sampai N saja.
1
2
3
4
// Loop <delay> times in a way that the compiler won't optimize away
pub fn sleep(count: i32) {
core::hint::black_box((|mut cnt: i32| while cnt > 0 { cnt -= 1; } ) (count));
}
Kita perlu menghindari optimisasi compiler disini, karena compiler bisa saja melihat program kita sebenarnya tidak melakukan apa-apa (hanya loop tanpa hasil), kemudian meng-skip kode tersebut Rust menyediakan fungsi black_box
untuk membuat compiler-nya tidak melakukan optimisasi appun.
Menyusun yang sudah dibangun
Kita sudah melengkapi apa yang dibutuhkan untuk blinking LED. Sekarang kita tinggal menyusunnya di main function yang kita buat sebelumnya.
Untuk percobaan kali ini, saya akan menghubungkan GPIO4 dengan LED. Saya menggunakan GPIO Extension Board agar lebih mudah koneksinya.
Jika ingin menggunakan GPIO lain, perlu diketahui dalam Raspberry Pi ada 2 sistem penomoran GPIO yang berbeda: BCM dan GPIO. Yang dipahami oleh SoC adalah penomoran BCM. Sebagai contoh, di website pinout.xyz tulisan “GPIO X” yang diwarnai putih, X-nya menggunakan penomoran BCM
Kemudian, pada file main.rs
, kita akan menaruh logic-nya:
1
2
3
4
5
6
7
8
9
10
#[no_mangle]
extern "C" fn kernel_main() {
gpio::set_function(1 << 4, gpio::Function::Output);
loop {
gpio::output_set(1 << 4);
synchronization::sleep(500_000);
gpio::output_clear(1 << 4);
synchronization::sleep(500_000);
}
}
Logic di atas cukup simple berkat bantuan interface yang kita buat. Pertama, kita mengubah mode GPIO4 menjadi mode output, kemudian kita nyalakan dan matikan output tersebut dengan jeda menggunakan delay(500_000)
.
Build
Untuk build project ini, tidak bisa langsung menjalankan cargo build
karena kita akan build untuk hardware tanpa OS apapun (bare metal). Kita perlu menentukan sendiri struktur binary program kita. Sebelumnya kita menyinggung linking process di beberapa bagian, sekarang kita akan mengatur linking process tersebut.
Linking
Sebenarnya yang terjadi dalam hasil kompilasi, biasanya source file akan dikompilasi menjadi object file. Object file belum tentu bisa dijalankan, karena bisa saja ada symbol yang belum di-resolve. Sebagai contoh, di atas kita membuat boot.S
yang berisi instruksi untuk jump ke address yang dilabeli kernel_main
. boot.S
sendiri belum tahu apa maksud dari kernel_main
, dan main.rs
hanya membuat fungsi kernel_main
tanpa tahu siapa yang menggunakannya. Linking process inilah yang menghubungkan symbol kernel_main
tersebut.
Linker juga bertanggung jawab mengatur isi struktur program binary akhir kita, seperti mengatur section .text.boot
harus berada di paling atas (ini penting agar CPU bisa eksekusi), dan section sisanya berada di bawahnya.
Perlu diketahui juga bahwa CPU Raspberry Pi mengeksekusi instruksi pertama pada alamat memory 0x8000
(untuk 32-bit), atau 0x80000
untuk (64-bit). Lebih lengkapnya bisa dilihat di post Stack Overflow berikut.
Berikut adalah linker script yang akan kita pakai, kita namakan aarch64-raspi3b.ld
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. */
. = 0x80000;
/* For arm32, use . = 0x8000; */
__start = .;
__text_start = .;
.text :
{
KEEP(*(.text.boot))
*(.text*)
}
. = ALIGN(4096); /* align to page size */
__text_end = .;
__rodata_start = .;
.rodata :
{
*(.rodata*)
}
. = ALIGN(4096); /* align to page size */
__rodata_end = .;
__data_start = .;
.data :
{
*(.data*)
}
. = ALIGN(4096); /* align to page size */
__data_end = .;
__bss_start = .;
.bss :
{
bss = .;
*(.bss*)
}
. = ALIGN(4096); /* align to page size */
__bss_end = .;
__bss_size = __bss_end - __bss_start;
__end = .;
}
Script di atas diambil dari Wiki OSDev, dan disana ada penjelasan lebih detail tentang setiap perintahnya. Intinya, section .text.boot
akan ditaruh tepat di alamat 0x80000
dan sisanya akan mengikuti, termasuk alamat variabel dan function akan ditaruh setelah 0x80000
.
Cross Compile
Semua sudah siap, sekarang kita hanya tinggal compile. Sayangnya, kita harus melakukan step khusus untuk compile, karena target hardware kita bukan komputer kita sendiri, melainkan sebuah sistem AArch64 yang berjalan tanpa OS (bare metal). Kita harus mengubah target kita ke aarch64-unknown-none
(Platform AArch64 dengan vendor unknown
dan OS none
).
Siapkan toolchain yang dibutuhkan untuk cross compile ke target berikut:
1
2
3
rustup target add aarch64-unknown-none
cargo install cargo-binutils
rustup component add llvm-tools
Kemudian, buat file .cargo/config.toml
dan sisipkan konfigurasi berikut:
1
2
3
[build]
target = "aarch64-unknown-none"
rustflags = ["-C", "link-arg=-Tsrc/aarch64-raspi3b.ld"]
Konfigurasi di atas memastikan bahwa compile akan dilakukan ke target aarch64-unknown-none
dengan linker script src/aarch64-raspi3b.ld
. Silakan ubah file linker scriptnya apabila berbeda.
Semua sudah lengkap, tinggal dijalankan saja build seperti biasa:
1
2
3
4
> cargo build
Compiling osdev v0.0.1 (/Users/firmanhp/Code/firmanhp.github.io/references/2024-10-04-osdev)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.72s
Hasil kompilasi akan berbentuk ELF format yang belum langsung bisa dijalankan di mesin.
1
2
file target/aarch64-unknown-none/debug/osdev
target/aarch64-unknown-none/debug/osdev: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped
ELF sendiri bisa dibilang sebagai format binary dengan tambahan metadata. Untuk menghapus metadata agar isi binary-nya hanya instruksi dan data saja, kita bisa menggunakan objcopy
.
1
2
3
> cargo objcopy -- -O binary osdev.img
> file osdev.img
osdev.img: data
Verifikasi/Sanity check
Debugging bare metal program tidaklah mudah karena kita tidak ada feedback yang jelas, karena kitapun belum implementasi sistem serial maupun display yang bisa menampilkan sesuatu.
Ada sedikit tips yang bisa digunakan untuk memastikan apakah binary akan berjalan dengan benar atau bukan, kita bisa melakukan sedikit inspeksi pada ELF file yang baru kita buat menggunakan objdump
. Karena binary hasil kompilasi Rust cukup besar untuk source yang kecil ini (karena somehow ada data yang dipakai mungkin untuk exception handling?), kita taruh saja hasil outputnya ke dalam file.
1
objdump -dt target/aarch64-unknown-none/debug/osdev > objdump_out
Jika dilihat hasil outputnya, harusnya symbol _start
kita berada di alamat 0x80000
seperti berikut:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Disassembly of section .text:
0000000000080000 <_start>:
80000: d53800a1 mrs x1, MPIDR_EL1
80004: 92400421 and x1, x1, #0x3
80008: b4000041 cbz x1, 0x80010 <_start+0x10>
8000c: 1400000a b 0x80034 <halt>
80010: 58000185 ldr x5, 0x80040 <$d.2>
80014: 910000bf mov sp, x5
80018: 58000185 ldr x5, 0x80048 <$d.2+0x8>
8001c: 180001a6 ldr w6, 0x80050 <$d.2+0x10>
80020: 34000086 cbz w6, 0x80030 <_start+0x30>
80024: f80084bf str xzr, [x5], #8
80028: 510004c6 sub w6, w6, #1
8002c: 35ffffa6 cbnz w6, 0x80020 <_start+0x20>
80030: 94000460 bl 0x811b0 <kernel_main>
...
Kita bisa lihat juga instruksi di dalamnya yang sama seperti isi kode boot.S
kita. Artinya, kita cukup yakin bahwa di atas akan menjadi instruksi pertama yang dieksekusi oleh CPU kita. Apabila alamat 0x80000
diisi data yang lain, artinya hasil kompilasi kita tidak benar.
Dari hasil tersebut kita juga bisa lihat hasil name mangling Rust, dan section yang kita singgung di dalam linker script kita: .rodata
, .data
, .bss
.
Menjalankan di Mesin
Misalkan binary (tanpa metadata ELF) kita bernama osdev.img
, kita akan pindahkan file tersebut ke SD Card mesin kita. Asumsi kita sudah pernah menginstall Linux sebelumnya ke dalam SD Card tersebut, kita cukup mengganti file kernel8.img
di dalam partisi bootfs
, atau tetap menggunakan nama osdev.img
, dan mengatur config.txt
dan menambahkan konfigurasi berikut:
1
2
3
4
5
# For more options and information see
# http://rptl.io/configtxt
# Some settings may impact device functionality. See link above for details
kernel=osdev.img
Eject SD cardnya, masukkan ke Raspberry Pi, dan nyalakan!
Conclusion
Sampel source code bisa dilihat disini.
Kita baru saja membuat program yang berjalan di atas bare metal Raspberry Pi. Cara di atas bisa saja diadaptasi untuk platform yang lain, tentunya dengan memahami hardware yang dituju.
Selanjutnya mungkin bisa dikembangkan lagi menjadi operating system yang sesungguhnya (bisa menjalankan process, mengakses filesystem, menggunakan keyboard/mouse, dll), tentunya step-by-step. Dari sini kita jadi memahami bahwa operating system adalah project yang sangat besar dan memakan waktu yang lama, dan perlu kerjasama dari banyak orang, terutama vendor untuk mengimplementasikan driver hardware mereka.