- Mengapa Saya Melakukan Ini
- Prasyarat
- Meng-clone Kernel
- Membuat Ruang Kerja yang Case-Sensitive
- Kesalahan Memilih allnoconfig
- Shim
- Binary Init 14 Baris
- Mengemas initramfs
- Boot Pertama yang Senyap
- Memulai dari Awal dengan defconfig
- Flag yang Hilang
- Image vs Image.gz
- Hello World yang Senyap
- Membaca init/main.c
- Menelusuri Boot dengan lldb
- Langkah Berikutnya
- Referensi
Cara Saya Mengompilasi dan Menjalankan Kernel Linux untuk RISC-V di Mac Apple Silicon
- Mengapa Saya Melakukan Ini
- Prasyarat
- Meng-clone Kernel
- Membuat Ruang Kerja yang Case-Sensitive
- Kesalahan Memilih allnoconfig
- Shim
- Binary Init 14 Baris
- Mengemas initramfs
- Boot Pertama yang Senyap
- Memulai dari Awal dengan defconfig
- Flag yang Hilang
- Image vs Image.gz
- Hello World yang Senyap
- Membaca init/main.c
- Menelusuri Boot dengan lldb
- Langkah Berikutnya
- Referensi
Saya menulis ini saat masih mempelajari topik ini. Mungkin saya belum sepenuhnya memahami semua yang saya tulis di sini, dan beberapa bagiannya mungkin keliru. Saat Kamu membacanya, mungkin saya sudah memahaminya lebih baik. Jika Kamu memiliki komentar atau masukan, saya akan sangat senang mendengarnya.
Mengapa Saya Melakukan Ini
Saya sudah menulis software selama bertahun-tahun, tetapi selalu di level tinggi. Web apps dan Node apps. Kernel selalu terasa seperti kotak hitam bagi saya. Saya mengetik di keyboard, menggerakkan mouse, sesuatu muncul di layar. Tetapi di antara semua itu, banyak hal terjadi di dalam kernel, dan saya tidak pernah memahami bagian itu.
Saya ingin membuka kotak itu.
Satu hal mendasar yang baru saya pahami dari video ini adalah Linux hanyalah sebuah kernel, dan kernel saja belum bisa menampilkan apa pun ke pengguna. Kernel mem-boot, menyiapkan dirinya, lalu menyerahkan kendali ke program userspace1 pertama yang diberikan. Program pertama itulah yang disebut init2. Linux sendiri tidak membawa init. Init bisa apa saja: systemd atau sysvinit pada desktop biasa, atau program C 14 baris buatan saya sendiri di artikel ini.
Dari situ saya mulai mencari cara mem-build kernel sendiri. Linux open source, jadi saya coba membacanya. Codebase-nya sangat besar dan saya tersesat. Saya juga sadar bahwa untuk mulai mem-build, saya harus memilih satu arsitektur CPU dulu sebagai target.
Pengetahuan saya tentang arsitektur CPU hampir nol. Yang saya tahu, Mac saya menggunakan Apple Silicon3, yang berarti ARM64, dan sebagian besar PC menggunakan x86. Saya mulai mencari arsitektur yang paling ramah untuk pemula, dan jawaban yang terus muncul adalah RISC-V4. Kecil, dirancang agar sederhana, dan tidak perlu membayar apa pun untuk membaca spesifikasinya. Akhirnya saya memilih RISC-V.
Masalahnya, saya tidak memiliki hardware RISC-V. Jadi saya harus cross-compile5: mem-build kernel untuk RISC-V di Mac ARM64 saya, lalu mem-boot-nya di dalam QEMU yang mengemulasi RISC-V.
Itu berarti saya butuh panduan yang sudah teruji untuk mem-build Linux di macOS untuk RISC-V. Saya menemukannya di Building Linux Kernel on macOS Natively oleh Seiya, yang menangani sisi build-nya dengan rapi. Saya melanjutkan dari sana: menjalankan kernel di QEMU, menulis binary init sendiri, dan menelusuri boot-nya dengan lldb.
Artikel ini menjelaskan perjalanan saya, error demi error. Sebagian error langsung terlihat jelas, sebagian lagi butuh waktu berjam-jam untuk dipahami. Build dan boot-nya sama-sama gagal di percobaan pertama. Program pertama yang saya tulis dalam bahasa C terkena compiler bug, dan saya harus menelusuri disassembly-nya cukup lama untuk menemukan penyebabnya. Tetapi pada akhirnya, saya memiliki kernel yang saya build sendiri yang mem-boot program userspace yang juga saya tulis sendiri, keduanya berjalan di hardware yang diemulasikan di laptop saya. Momen tersebutlah yang membuat semua ini layak dilakukan.
Berikut yang akan kita lakukan:
- Menyiapkan toolchain
- Mengunduh kode sumber kernel ke Mac
- Mengompilasi kernel
- Mem-boot-nya di QEMU
- Menulis program init kecil dengan bahasa C
- Menelusuri boot dari dalam menggunakan lldb
Prasyarat
Untuk membangun kernel di Mac, kita membutuhkan beberapa tool yang tidak ada secara bawaan di macOS. Sebagian besar dapat dipasang melalui Homebrew. Saya mengasumsikan Kamu sudah memiliki Homebrew. Jika belum, ikuti panduan pemasangan di brew.sh.
Berikut yang kita butuhkan dan alasannya:
- LLVM (
clangdanlld6): kernel dapat dibangun menggunakan clang dengan flagLLVM=1, sehingga kita tidak perlu memasang cross-gcc toolchain secara terpisah. macOS sudah memiliki clang bawaan dari Apple, namun versinya lebih lama dan mungkin belum mendukung semua fitur yang dibutuhkan build kernel. Homebrew memberi kita clang dan lld versi terbaru dalam satu paket. - GNU make: macOS hanya memiliki BSD make secara bawaan, sedangkan kernel membutuhkan GNU make. Setelah dipasang, kita akan memanggilnya sebagai
gmake. - coreutils: script build kernel menggunakan beberapa perintah seperti
nprocdanheadyang tidak tersedia di macOS, atau tersedia tetapi dalam versi BSD yang perilakunya sedikit berbeda. - gnu-sed: script kernel mengasumsikan sed versi GNU.
- findutils: script kernel menggunakan
find -printfyang tidak ada di BSD find. - libelf: dibutuhkan oleh beberapa tool kernel untuk mengurai file ELF7.
- QEMU: untuk menjalankan kernel yang sudah kita bangun.
Pasang semuanya sekaligus dengan satu perintah:
brew install llvm make coreutils libelf gnu-sed findutils qemuSetelah pemasangan selesai, kita perlu mengatur PATH agar llvm dari Homebrew dan versi GNU dari coreutils, gnu-sed, dan findutils didahulukan daripada versi bawaan macOS. Tambahkan baris berikut ke ~/.zshrc:
# ~/.zshrc
LLVM_PREFIX="$(brew --prefix llvm)"
COREUTILS_PREFIX="$(brew --prefix coreutils)"
GNU_SED_PREFIX="$(brew --prefix gnu-sed)"
FINDUTILS_PREFIX="$(brew --prefix findutils)"
export PATH="$LLVM_PREFIX/bin:$PATH"
export PATH="$COREUTILS_PREFIX/libexec/gnubin:$PATH"
export PATH="$GNU_SED_PREFIX/libexec/gnubin:$PATH"
export PATH="$FINDUTILS_PREFIX/libexec/gnubin:$PATH"Setelah itu, jalankan source ~/.zshrc atau buka terminal baru. Untuk memastikan semuanya sudah benar, jalankan clang --version dan find --version | head -1. Versi clang seharusnya menyebutkan “Homebrew” bukan Apple, sementara find menunjukkan “GNU findutils”.
Meng-clone Kernel
Sekarang kita sudah memiliki toolchain-nya. Saatnya mengunduh kode sumbernya. Kernel Linux berada di git.kernel.org dan di-mirror ke GitHub. Mari kita clone. Kita menggunakan flag --depth=1 agar yang diunduh hanya snapshot terbaru saja, bukan seluruh history-nya. Kita tidak membutuhkan history-nya untuk proyek ini.
git clone --depth=1 \
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git~/Learning/LINUX
❯ git clone --depth=1 \
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Cloning into 'linux'...
remote: Enumerating objects: 99198, done.
remote: Counting objects: 100% (99198/99198), done.
remote: Compressing objects: 100% (96420/96420), done.
remote: Total 99198 (delta 7850), reused 21828 (delta 1720), pack-reused 0 (from 0)
Receiving objects: 100% (99198/99198), 274.01 MiB | 6.43 MiB/s, done.
Resolving deltas: 100% (7850/7850), done.
Updating files: 100% (93697/93697), done.
warning: the following paths have collided (e.g. case-sensitive paths
on a case-insensitive filesystem) and only one from the same
colliding group is in the working tree:
'include/uapi/linux/netfilter/xt_CONNMARK.h'
'include/uapi/linux/netfilter/xt_connmark.h'
'include/uapi/linux/netfilter/xt_DSCP.h'
'include/uapi/linux/netfilter/xt_dscp.h'
'include/uapi/linux/netfilter/xt_MARK.h'
'include/uapi/linux/netfilter/xt_mark.h'
'include/uapi/linux/netfilter/xt_RATEEST.h'
'include/uapi/linux/netfilter/xt_rateest.h'
'include/uapi/linux/netfilter/xt_TCPMSS.h'
'include/uapi/linux/netfilter/xt_tcpmss.h'
'include/uapi/linux/netfilter_ipv4/ipt_ECN.h'
'include/uapi/linux/netfilter_ipv4/ipt_ecn.h'
'include/uapi/linux/netfilter_ipv4/ipt_TTL.h'
'include/uapi/linux/netfilter_ipv4/ipt_ttl.h'
'include/uapi/linux/netfilter_ipv6/ip6t_HL.h'
'include/uapi/linux/netfilter_ipv6/ip6t_hl.h'
'net/netfilter/xt_DSCP.c'
'net/netfilter/xt_dscp.c'
'net/netfilter/xt_HL.c'
'net/netfilter/xt_hl.c'
'net/netfilter/xt_RATEEST.c'
'net/netfilter/xt_rateest.c'
'net/netfilter/xt_TCPMSS.c'
'net/netfilter/xt_tcpmss.c'
'tools/memory-model/litmus-tests/Z6.0+pooncelock+poonceLock+pombonce.litmus'
'tools/memory-model/litmus-tests/Z6.0+pooncelock+pooncelock+pombonce.litmus'Proses clone selesai, namun di bagian akhir git memberi peringatan panjang: “the following paths have collided (e.g. case-sensitive paths on a case-insensitive filesystem) and only one from the same colliding group is in the working tree”.
Kernel memiliki file-file yang namanya hanya berbeda pada kapitalisasinya. Perhatikan daftar pada peringatan tersebut. xt_CONNMARK.h dan xt_connmark.h berada di direktori yang sama. Begitu juga dengan xt_DSCP.h dan xt_dscp.h. Perbedaan antara setiap pasangan file tersebut hanyalah kapitalisasinya. Di Linux, keduanya adalah dua file yang berbeda. Di APFS8 macOS, yang bersifat case-insensitive secara default, keduanya dianggap sebagai file yang sama. Hanya satu file dari setiap pasangan yang bertabrakan tersebut yang akhirnya tersimpan di disk.
Bahkan jika kita mengabaikan peringatan tersebut, hal ini tetap membuat build gagal. File header yang kita butuhkan tidak ada di disk karena sudah digantikan oleh pasangan case-collision-nya.
Kita membutuhkan filesystem yang case-sensitive.
Membuat Ruang Kerja yang Case-Sensitive
macOS tidak memungkinkan kita mengubah case-sensitivity pada disk yang sudah ada, namun kita masih bisa membuat volume terpisah yang bersifat case-sensitive. Perintah hdiutil bisa membuat sparse disk image yang kemudian dapat kita attach sebagai volume.
Saya membuat sparse image berukuran 20 GB di ~/Learning/LINUX/linuxkernel.dmg. Sparse artinya file-nya hanya akan membesar seiring penggunaannya. Kode sumber kernel beserta direktori build-nya cukup muat dalam 20 GB.
hdiutil create -size 20g -fs "Case-sensitive APFS" \
-volname linuxkernel ~/Learning/LINUX/linuxkernel.dmg
hdiutil attach ~/Learning/LINUX/linuxkernel.dmg~/Learning/LINUX
❯ hdiutil create -size 20g -fs "Case-sensitive APFS" \
-volname linuxkernel ~/Learning/LINUX/linuxkernel.dmg
created: /Users/jefrydco/Learning/LINUX/linuxkernel.dmg
~/Learning/LINUX took 5s
❯ hdiutil attach ~/Learning/LINUX/linuxkernel.dmg
/dev/disk6 GUID_partition_scheme
/dev/disk6s1 EFI
/dev/disk6s2 Apple_APFS
/dev/disk7 EF57347C-0000-11AA-AA11-0030654
/dev/disk7s1 41504653-0000-11AA-AA11-0030654 /Volumes/linuxkernelSetelah di-attach, volume-nya muncul di /Volumes/linuxkernel/. Mulai sekarang, kita bekerja di dalam volume tersebut. Mari kita clone ulang kernel di sana:
cd /Volumes/linuxkernel
git clone --depth=1 \
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.gitKali ini proses clone selesai tanpa peringatan case-collision. Semua file sudah berada di disk.
Kesalahan Memilih allnoconfig
Saya ingin memulai dari skala kecil. Kernel memiliki ribuan opsi, dan saya pikir akan belajar lebih cepat jika memulai dari nol dan menambahkan sesuatu hanya saat dibutuhkan. Jadi saya memilih allnoconfig, yang menonaktifkan setiap opsi yang dikenali oleh Kconfig9.
Ini adalah pilihan yang salah untuk build pertama, namun saat itu saya belum tahu.
gmake ARCH=riscv LLVM=1 allnoconfigHal pertama yang terjadi di luar dugaan: clang menolak dijalankan.
linux
❯ gmake ARCH=riscv LLVM=1 allnoconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
clang: unknown C compiler
scripts/Kconfig.include:45: Sorry, this C compiler is not supported.
gmake[2]: *** [scripts/kconfig/Makefile:85: allnoconfig] Error 1
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:755: allnoconfig] Error 2
gmake: *** [Makefile:248: __sub-make] Error 2Penyebabnya adalah sebuah file konfigurasi di ~/.config/clang/arm64-apple-darwin25.cfg yang saya gunakan untuk proyek lain. Clang secara otomatis memuat konfigurasi ini pada setiap pemanggilan. Berikut isinya:
# ~/.config/clang/arm64-apple-darwin25.cfg
-isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
-I/opt/homebrew/opt/llvm/include
-I/opt/homebrew/opt/boost/include
-L/opt/homebrew/opt/llvm/lib/c++
-L/opt/homebrew/opt/llvm/lib/unwind
-L/opt/homebrew/opt/boost/lib
-lunwind
-Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++
-std=c++26Ini adalah flag-flag C++ yang saya gunakan untuk proyek pribadi: include path Boost, link path libc++, dan -std=c++26. Ketika build kernel memanggil clang, clang secara otomatis mengambil flag-flag tersebut, mencoba mengompilasi kode C kernel dengan standar C++26, dan build-nya pun gagal.
Solusinya adalah memindahkan file tersebut:
mv ~/.config/clang/arm64-apple-darwin25.cfg \
~/.config/clang/arm64-apple-darwin25.cfg.bakSekarang gmake dapat memanggil clang. Saya menjalankan ulang allnoconfig untuk menghasilkan file .config:
gmake ARCH=riscv LLVM=1 allnoconfig❯ gmake ARCH=riscv LLVM=1 allnoconfig
#
# configuration written to .config
#Kali ini berhasil. Setelah itu saya mencoba build-nya:
gmake ARCH=riscv LLVM=1 -j$(nproc)Flag -j$(nproc) memberi tahu make berapa job kompilasi yang harus dijalankan secara paralel. nproc adalah bagian dari coreutils yang sudah kita pasang sebelumnya, dan akan mencetak jumlah processor di mesin. Di Mac dengan 8 core, ini menjadi -j8. Kernel memiliki ribuan file yang independen, jadi kompilasi paralel memangkas waktu build secara signifikan.
linux on master took 14s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc)
WRAP arch/riscv/include/generated/uapi/asm/errno.h
WRAP arch/riscv/include/generated/uapi/asm/fcntl.h
WRAP arch/riscv/include/generated/uapi/asm/param.h
[... truncated ...]
HOSTCC scripts/elf-parse.o
In file included from scripts/elf-parse.c:12:
In file included from scripts/elf-parse.hscripts/sorttable.c::535::
10:scripts/elf-parse.h:5: 10: fatal error: 'elf.h' file not found
5 | #include <elf.h>
fatal error: | ^~~~~~~'elf.h' file
not found
5 | #include <elf.h>
| ^~~~~~~
1 error generated.
1 error generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/elf-parse.o] Error 1
gmake[2]: *** Waiting for unfinished jobs....
gmake[2]: *** [scripts/Makefile.host:131: scripts/sorttable.o] Error 1
UPD include/config/kernel.release
UPD include/generated/utsrelease.h
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:1356: scripts] Error 2
gmake[1]: *** Waiting for unfinished jobs....
UPD include/generated/compile.h
gmake: *** [Makefile:248: __sub-make] Error 2Ini adalah error pertama yang berasal dari kernel. Build-nya memanggil host tools, program-program yang berjalan di Mac saya untuk menyiapkan kode sumber kernel agar dapat dikompilasi. Host tools tersebut menyertakan header yang tidak tersedia di macOS. Kita membutuhkan solusi yang berbeda.
Shim macos-include
Pendekatan shim di bagian ini mengikuti artikel Seiya yang saya rujuk di pendahuluan.
Error elf.h tersebut memberi tahu kita bahwa salah satu host tool mencari <elf.h>, namun macOS tidak menyediakan elf.h. Paket libelf yang sudah kita pasang sebelumnya menyediakan header yang setara, hanya saja path-nya berbeda: <libelf/gelf.h>.
Kita membutuhkan lapisan indirection. Buat direktori scripts/macos-include/ dan tambahkan stub elf.h yang mengarahkan ke header libelf:
// scripts/macos-include/elf.h
#pragma once
#include <libelf/gelf.h>
#define STT_SPARC_REGISTER 3
#define R_386_32 1Kemudian jalankan ulang gmake, dengan memberi tahu clang lokasi shim kita dan header libelf:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include"linux on master took 6s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include"
HOSTCC scripts/basic/fixdep
HOSTCC scripts/dtc/dtc.o
HOSTCC scripts/dtc/flattree.o
[... truncated ...]
HOSTCC scripts/elf-parse.o
In file included from scripts/elf-parse.cIn file included from :scripts/sorttable.c:3512:
:
scripts/elf-parse.hscripts/elf-parse.h::6262::2323:: error: incompatible pointer types passing 'Elf64_Off *' error: (aka 'unsigned long *') incompatible pointerto types parameter ofpassing type'Elf64_Off *' (aka 'unsigned long *') 'const uint64_t *'to
parameter (aka 'const unsigned long long *')of [-Wincompatible-pointer-types]type
'const uint64_t *'
(aka 'const unsigned long long *') [-Wincompatible-pointer-types]
62 | r e62t | urrentu renlf _eplafr_spearr.sre8r(.&re8h(d&re-h>der6-4>.ee6_4s.heo_fsfh)o;ff
) ;|
^~~~~~~~~~~~~~~~~~
[... truncated ...]
6 errors generated.
6 errors generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/elf-parse.o] Error 1
gmake[2]: *** Waiting for unfinished jobs....
gmake[2]: *** [scripts/Makefile.host:131: scripts/sorttable.o] Error 1
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:1356: scripts] Error 2
gmake: *** [Makefile:248: __sub-make] Error 2Build-nya berhasil melewati error elf.h, namun kini mendapatkan peringatan pointer types. Fungsi-fungsi gelf milik libelf mengembalikan tipe yang sedikit berbeda dari yang diharapkan host tool, dan build-nya memperlakukan peringatan ini sebagai error. Kita matikan dengan -Wno-incompatible-pointer-types:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types"Mengapa kita memilih mematikan peringatannya, bukan memperbaikinya? Ketidakcocokan ini hanyalah masalah kosmetik di level C type system. Pointer dari libelf menunjuk ke data dengan layout yang sama dengan yang diharapkan host tool, hanya dideklarasikan dengan tipe yang sedikit berbeda. Tool-nya tetap berjalan dengan benar. Memperbaikinya dengan benar akan membutuhkan patching kode sumber kernel itu sendiri, yang berarti memelihara fork lokal. Mematikan peringatan hanya berdampak pada build kita sendiri dan menjaga direktori kernel tetap bersih.
linux on master took 4s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types"
[... truncated ...]
HOSTCC scripts/mod/symsearch.o
In file included from scripts/mod/symsearch.c:8:
scripts/mod/modpost.h:2:10: fatal error: 'byteswap.h' file not found
2 | #include <byteswap.h>
| ^~~~~~~~~~~~
In file included from scripts/mod/sumversion.c:13:
scripts/mod/modpost.h:2:10: fatal error: 'byteswap.h' file not found
2 | #include <byteswap.h>
| ^~~~~~~~~~~~
In file included from scripts/mod/modpost.c:28:
scripts/mod/modpost.h:2:10: fatal error: 'byteswap.h' file not found
2 | #include <byteswap.h>
| ^~~~~~~~~~~~
In file included from scripts/mod/file2alias.c:19:
scripts/mod/modpost.h:2:10: fatal error: 'byteswap.h' file not found
2 | #include <byteswap.h>
| ^~~~~~~~~~~~
1 error generated.
1 error generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/symsearch.o] Error 1
gmake[2]: *** Waiting for unfinished jobs....
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/sumversion.o] Error 1
1 error generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/modpost.o] Error 1
1 error generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/file2alias.o] Error 1
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:1372: prepare0] Error 2
gmake: *** [Makefile:248: __sub-make] Error 2Header berikutnya yang hilang: byteswap.h. macOS tidak memilikinya, tetapi clang menyediakan builtin10 untuk byte-swapping. Kita tambahkan stub-nya:
// scripts/macos-include/byteswap.h
#pragma once
#define bswap_16 __builtin_bswap16
#define bswap_32 __builtin_bswap32
#define bswap_64 __builtin_bswap64Jalankan ulang gmake:
linux on master took 8s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types"
HOSTCC scripts/mod/modpost.o
HOSTCC scripts/mod/symsearch.o
HOSTCC scripts/mod/sumversion.o
HOSTCC scripts/mod/file2alias.o
scripts/mod/file2alias.c:112:3: error: typedef redefinition with different types ('struct uuid_t' vs '__darwin_uuid_t' (aka 'unsigned char[16]'))
112 | } uuid_t;
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk/usr/include/sys/_types/_uuid_t.h:31:25: note: previous definition is here
31 | typedef __darwin_uuid_t uuid_t;
| ^
scripts/mod/modpost.c:1177:7: error: use of undeclared identifier 'R_386_PC32'
1177 | case R_386_PC32:
| ^~~~~~~~~~
[... truncated ...]
17 errors generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/file2alias.o] Error 1
gmake[2]: *** Waiting for unfinished jobs....
16 errors generated.
gmake[2]: *** [scripts/Makefile.host:131: scripts/mod/modpost.o] Error 1
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:1372: prepare0] Error 2
gmake: *** [Makefile:248: __sub-make] Error 2Ada dua error berbeda yang muncul bersamaan di sini.
uuid_t redefinition di file2alias.c. Baik <unistd.h> milik macOS maupun host tool kernel mendefinisikan uuid_t, namun dengan bentuk yang berbeda. Versi macOS adalah unsigned char[16], sedangkan versi kernel adalah struct. Header macOS membungkus typedef-nya dengan pemeriksaan #ifndef _UUID_T, jadi jika kita mendefinisikan _UUID_T terlebih dahulu di command line, header tersebut akan menganggap _UUID_T sudah ada dan melewati typedef-nya. Hanya definisi versi kernel yang tersisa. Tambahkan -D_UUID_T ke HOSTCFLAGS.
Banyak konstanta relocation R_* yang hilang di modpost.c dan file2alias.c. Host tool kernel menangani relocation untuk berbagai arsitektur CPU, termasuk x86, ARM, MIPS, dan AArch64. Konstantanya berasal dari elf.h Linux. Shim kita baru berisi R_386_32 saja. Perbarui scripts/macos-include/elf.h dengan sisanya:
// scripts/macos-include/elf.h
#pragma once
#include <libelf/gelf.h>
#define STT_SPARC_REGISTER 3
#define R_386_32 1
#define R_386_PC32 2
#define R_MIPS_HI16 5
#define R_MIPS_LO16 6
#define R_MIPS_26 4
#define R_MIPS_32 2
#define R_ARM_ABS32 2
#define R_ARM_REL32 3
#define R_ARM_PC24 1
#define R_ARM_CALL 28
#define R_ARM_JUMP24 29
#define R_ARM_THM_JUMP24 30
#define R_ARM_THM_PC22 10
#define R_ARM_MOVW_ABS_NC 43
#define R_ARM_MOVT_ABS 44
#define R_ARM_THM_MOVW_ABS_NC 47
#define R_ARM_THM_MOVT_ABS 48
#define R_ARM_THM_JUMP19 51
#define R_AARCH64_ABS64 257
#define R_AARCH64_PREL64 260Jalankan ulang dengan kedua perbaikan: konstanta tambahan di elf.h dan -D_UUID_T di HOSTCFLAGS:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"linux on master took 10s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"
HOSTCC scripts/basic/fixdep
In file included from scripts/basic/fixdep.c:94:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk/usr/include/unistd.h:670:
/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk/usr/include/gethostuuid.h:41:25: error: expected identifier
41 | int gethostuuid(uuid_t, const struct timespec *) __API_AVAILABLE(macos(10.5)) __API_UNAVAILABLE(ios, tvos, watchos);
| ^
1 error generated.
gmake[2]: *** [scripts/Makefile.host:114: scripts/basic/fixdep] Error 1
gmake[1]: *** [/Volumes/linuxkernel/linux/Makefile:663: scripts_basic] Error 2
gmake: *** [Makefile:248: __sub-make] Error 2-D_UUID_T memblokir typedef uuid_t milik macOS, persis seperti yang kita inginkan. Tetapi <gethostuuid.h>, yang ikut di-include oleh <unistd.h>, juga merujuk ke uuid_t dan kini gagal. Ganti header tersebut dengan stub kosong:
// scripts/macos-include/gethostuuid.h
#pragma onceSekarang host tool berhasil di-build tanpa masalah. Direktori scripts/macos-include/ berisi tiga file kecil: elf.h, byteswap.h, dan gethostuuid.h, ditambah dua flag untuk HOSTCFLAGS: -Wno-incompatible-pointer-types dan -D_UUID_T.
linux on master took 2s
❯ gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"
HOSTCC scripts/basic/fixdep
HOSTCC scripts/dtc/dtc.o
[... truncated ...]
AR built-in.a
AR vmlinux.a
LD vmlinux.o
MODPOST vmlinux.symvers
CC .vmlinux.export.o
UPD include/generated/utsversion.h
CC init/version-timestamp.o
KSYMS .tmp_vmlinux0.kallsyms.S
AS .tmp_vmlinux0.kallsyms.o
LD .tmp_vmlinux1
NM .tmp_vmlinux1.syms
KSYMS .tmp_vmlinux1.kallsyms.S
AS .tmp_vmlinux1.kallsyms.o
LD .tmp_vmlinux2
NM .tmp_vmlinux2.syms
KSYMS .tmp_vmlinux2.kallsyms.S
AS .tmp_vmlinux2.kallsyms.o
LD vmlinux.unstripped
NM System.map
SORTTAB vmlinux.unstripped
OBJCOPY vmlinux
GEN modules.builtin.modinfo
GEN modules.builtin
OBJCOPY arch/riscv/boot/Image
Kernel: arch/riscv/boot/Image is ready
GZIP arch/riscv/boot/Image.gz
Kernel: arch/riscv/boot/Image.gz is readyBinary Init 14 Baris
Kernel membutuhkan sebuah program userspace1 untuk dijalankan sebagai PID 111 setelah boot-nya selesai. Pada sistem Linux yang sebenarnya, biasanya berupa systemd atau sysvinit. Kita hanya butuh sesuatu yang kecil untuk membuktikan kode kita sendiri sedang berjalan di userspace.
Bentuk paling kecilnya adalah program yang mencetak Hello, World! lalu melakukan loop tanpa henti. Ini programnya:
// /Volumes/linuxkernel/initramfs/init.c
void _start() {
const char msg[] = "Hello, World!\n";
__asm__ volatile (
"li a7, 64\n"
"li a0, 1\n"
"mv a1, %0\n"
"li a2, 14\n"
"ecall\n"
: : "r"(msg)
);
while(1);
}Mari kita telusuri tiap bagiannya.
_start bukan main. Kernel akan melompat ke alamat entry point yang ditunjuk oleh binary ELF7. Pada sistem normal, entry tersebut adalah _start. _start disediakan oleh kode startup libc12 bernama crt013, yang menyiapkan argc/argv lalu memanggil main kita. Karena kita tidak me-link libc, kita menamai fungsi kita _start secara langsung tanpa melalui crt0.
Keyword __asm__ adalah ekstensi GCC dan Clang yang memungkinkan kita menyematkan instruksi assembly di dalam kode C. Keyword volatile memberi tahu compiler agar tidak menghapus atau memindahkan blok ini. Syscall menulis byte ke file descriptor, dan efek samping ini tidak terlihat oleh compiler jika hanya membaca kode C-nya.
Blok ini memanggil syscall14 Linux menggunakan ABI15 RISC-V.
Bagian asm-nya menggunakan dua instruksi RISC-V: li (“load immediate”) menaruh konstanta ke dalam register16, dan mv (“move”) menyalin nilai sebuah register ke register lain. Pada mv a1, %0, %0 adalah placeholder yang digantikan compiler dengan register yang menampung msg. Jadi blok ini menyiapkan empat argumen pada register lalu menjalankan ecall:
a7 = 64: nomor syscall untukwritedi RISC-Va0 = 1: file descriptor17 stdouta1 = msg: pointer18 ke string yang ingin ditulisa2 = 14: jumlah byte dari “Hello, World!\n”
ecall19 adalah instruksi yang memicu trap20 dari U-mode21 ke S-mode untuk meminta kernel menjalankan syscall tersebut.
while(1) di akhir: PID 1 tidak boleh return. Tidak ada fungsi yang memanggilnya, jadi tidak ada tempat untuk kembali. Jika _start tetap return, kernel akan panic karena PID 1 telah berhenti. Karena itu kita melakukan loop tanpa henti.
Kompilasi dengan clang:
clang --target=riscv64-linux-gnu \
-static \
-nostdlib \
-fuse-ld=lld \
-o /Volumes/linuxkernel/initramfs/init \
/Volumes/linuxkernel/initramfs/init.cSetiap flag memiliki peran masing-masing:
--target=riscv64-linux-gnu: memberi tahuclangagar menghasilkan kode RISC-V Linux, bukan ARM64 milik host. Ini yang menentukan target cross-compile5-nya.-static: me-link binary secara statis. Tanpa dynamic loader, tanpa shared library. Kernel akan mengeksekusi binary ini secara langsung, tanpa mekanisme/lib/ld-linux*.soyang biasa, karena initramfs kita tidak berisi file-file tersebut.-nostdlib: melewati libc startup objects seluruhnya. Tanpa ini,clangakan mencoba me-link crt0 dan libc, yang akan bentrok dengan_startbuatan kita.-fuse-ld=lld: gunakan lld6 dari LLVM dan bukan linker bawaan host. Linker bawaan macOS hanya mengenal Mach-O22, format binary macOS. Kita membutuhkan binary ELF untuk Linux, dan lld menghasilkan ELF.
Mari kita disassemble binary-nya untuk melihat apa yang dihasilkan clang:
llvm-objdump -d /Volumes/linuxkernel/initramfs/init/Volumes/linuxkernel/initramfs via C v21.0.0-clang
❯ llvm-objdump -d /Volumes/linuxkernel/initramfs/init
/Volumes/linuxkernel/initramfs/init: file format elf64-littleriscv
Disassembly of section .text:
00000000000111fc <_start>:
111fc: 1101 addi sp, sp, -0x20
111fe: ec06 sd ra, 0x18(sp)
11200: e822 sd s0, 0x10(sp)
11202: 1000 addi s0, sp, 0x20
11204: 4501 li a0, 0x0
11206: fea40723 sb a0, -0x12(s0)
1120a: 6505 lui a0, 0x1
1120c: a2150513 addi a0, a0, -0x5df
11210: fea41623 sh a0, -0x14(s0)
11214: 646c7537 lui a0, 0x646c7
11218: 26f50513 addi a0, a0, 0x26f
1121c: fea42423 sw a0, -0x18(s0)
11220: fffff517 auipc a0, 0xfffff
11224: f7050513 addi a0, a0, -0x90
11228: 6108 ld a0, 0x0(a0)
1122a: fea43023 sd a0, -0x20(s0)
1122e: fe040513 addi a0, s0, -0x20
11232: 04000893 li a7, 0x40
11236: 4505 li a0, 0x1
11238: 85aa mv a1, a0
1123a: 4639 li a2, 0xe
1123c: 00000073 ecall
11240: a009 j 0x11242 <_start+0x46>
11242: a001 j 0x11242 <_start+0x46>Sebelum kita lanjut, sedikit pembahasan tentang alamat-alamat hex tersebut, karena akan terus muncul mulai dari sini. Memori adalah satu array besar yang berisi byte. Setiap byte memiliki indeks. Kita menuliskan indeks tersebut dalam hex, yaitu sistem bilangan basis 16. Hex dipetakan dengan rapi ke biner karena setiap digit hex setara dengan empat bit biner. Jadi 0x111fc sebenarnya hanya angka 70140 yang ditulis dalam hex. Angka yang sama, notasi yang berbeda.
Satu hal penting sebelum kita lanjut: alamat-alamat ini adalah virtual address23, bukan lokasi RAM fisik. Setiap program userspace memiliki ruang alamatnya sendiri. Kernel mengatur MMU agar virtual address 0x111fc di program kita dipetakan ke byte RAM fisik mana pun yang dialokasikan kernel untuk page tersebut. Dua program bisa sama-sama mengklaim virtual 0x10000 tanpa bentrok karena masing-masing memiliki page table-nya sendiri.
Mengapa virtual address inilah yang dipilih untuk _start? Karena lld, linker kita, yang menaruhnya di sana. Ketika linker membangun program, ia memilih sebuah image base24, yaitu alamat awal di ruang alamat program itu sendiri. Untuk binary static di RISC-V Linux, image base bawaan lld adalah 0x10000. Dari base tersebut, linker menata binary-nya secara berurutan. Pertama, header ELF7, sebuah blok kecil metadata yang menggambarkan file-nya. Setelah itu, program headers, yang memberi tahu kernel cara memuat setiap bagiannya. Berikutnya, section .text dimulai. .text berisi kode mesin kita, dan _start adalah fungsi pertama di dalamnya. Jadi setelah headers-nya selesai, _start ditempatkan di 0x111fc.
0x10000 +---------------------+ <-- image base bawaan lld
| ELF header |
| program headers |
| ... |
0x111fc +---------------------+ <-- _start (kode kita dimulai)
| addi sp, sp, -0x20 |
| sd ra, ... |
| ... |
| ecall |
| j . |
0x11242 +---------------------+ <-- akhir _startJika kita membuat binary-nya lebih panjang atau lebih pendek, setiap alamat di dalamnya akan bergeser. Menambahkan flag -Wl,--image-base=0x12345 ke clang saat linking akan memberi tahu lld agar memulai binary-nya dari base yang berbeda.
Kernel sendiri berada di alamat tinggi seperti 0xffffffff80c00360, ditentukan oleh linker script kernel. Stack25 di userspace berada di dekat puncak memori yang dapat diakses userspace, dipilih oleh kernel saat runtime ketika membuat proses kita. Tidak ada yang acak. Setiap alamat di artikel ini berasal dari tempat yang konkret: file konfigurasi, linker script, spesifikasi CPU, atau keputusan yang dibuat oleh kode saat runtime.
Sekilas, disassembly-nya terlihat baik: nilai-nilai dimuat lalu ecall dijalankan. Saya akan kembali ke bagian ini nanti, karena ada bug tersembunyi yang baru saya sadari jauh setelah kernel sudah berjalan dan program-nya tidak menghasilkan output apa pun.
Mengemas initramfs
Kernel dapat melakukan boot dari initramfs26, tetapi hanya jika kita menyediakannya. Initramfs adalah arsip cpio27 yang berisi file-file yang harus tersedia saat boot. Kernel sudah menyertakan tool bernama gen_init_cpio yang akan membangun arsip ini dari file spek sederhana. Kita perlu mengompilasinya sendiri lalu menjalankannya.
Percobaan kompilasi pertama mengalami masalah yang sudah tidak asing:
cc /Volumes/linuxkernel/linux/usr/gen_init_cpio.c \
-o /Volumes/linuxkernel/linux/usr/gen_init_cpiolinux on master [?]
❯ cc /Volumes/linuxkernel/linux/usr/gen_init_cpio.c \
-o /Volumes/linuxkernel/linux/usr/gen_init_cpio
/Volumes/linuxkernel/linux/usr/gen_init_cpio.c:460:16: error: call to undeclared function 'copy_file_range'; ISO C99 and later do not
support implicit function declarations [-Wimplicit-function-declaration]
460 | this_read = copy_file_range(file, NULL, outfd, NULL, size, 0);
| ^
/Volumes/linuxkernel/linux/usr/gen_init_cpio.c:677:31: error: use of undeclared identifier 'O_LARGEFILE'
677 | O_WRONLY | O_CREAT | O_LARGEFILE | O_TRUNC,
| ^~~~~~~~~~~
2 errors generated.O_LARGEFILE adalah fcntl flag khusus Linux. macOS tidak membutuhkannya karena semua operasi file secara default sudah 64-bit. Kita tambahkan shim yang membungkus <fcntl.h> milik macOS dan mendefinisikan flag yang hilang sebagai 0:
// scripts/macos-include/fcntl.h
#pragma once
#include_next <fcntl.h>
#define O_LARGEFILE 0Kompilasi ulang, kali ini sambil mengarahkan ke shim kita:
cc -I/Volumes/linuxkernel/linux/scripts/macos-include \
/Volumes/linuxkernel/linux/usr/gen_init_cpio.c \
-o /Volumes/linuxkernel/linux/usr/gen_init_cpiolinux on master
❯ cc -I/Volumes/linuxkernel/linux/scripts/macos-include \
/Volumes/linuxkernel/linux/usr/gen_init_cpio.c \
-o /Volumes/linuxkernel/linux/usr/gen_init_cpio
/Volumes/linuxkernel/linux/usr/gen_init_cpio.c:460:16: error: call to undeclared function 'copy_file_range'; ISO C99 and later do not
support implicit function declarations [-Wimplicit-function-declaration]
460 | this_read = copy_file_range(file, NULL, outfd, NULL, size, 0);
| ^
1 error generated.Berikutnya: copy_file_range. Ini adalah syscall Linux yang tidak dimiliki libc macOS. Kita buat stub-nya sebagai fungsi yang selalu gagal sehingga kode gen_init_cpio akan beralih ke read/write biasa:
// scripts/macos-include/unistd.h
#pragma once
#include_next <unistd.h>
#include <sys/types.h>
static inline ssize_t copy_file_range(int a, void *b, int c, void *d, size_t e, unsigned int f) { return -1; }Kompilasi ulang. Tanpa error. Sekarang kita memiliki gen_init_cpio. Tulis file spek yang menjelaskan isi arsipnya:
# /Volumes/linuxkernel/initramfs.txt
dir /dev 755 0 0
nod /dev/console 644 0 0 c 5 1
file /init /Volumes/linuxkernel/initramfs/init 755 0 0Baris nod inilah kuncinya. Dari kiri ke kanan: /dev/console adalah path-nya, 644 adalah mode file-nya, dua angka 0 mengatur owner dan group ke root, c menandakan ini adalah character device, dan 5 1 adalah major dan minor number yang Linux peruntukkan untuk konsol sistem. gen_init_cpio mencatat semua ini di dalam arsip cpio secara langsung, tanpa perlu device node-nya ada di filesystem macOS sama sekali. macOS sebenarnya menyertakan mknod28, tetapi pada Mac modern, perintah ini tidak bisa membuat device node di filesystem biasa. Pendekatan cpio kita tidak memerlukan mknod sama sekali.
Node-nya cukup ada di dalam arsip, yang akan Linux unpack ke tmpfs-nya sendiri saat boot. Kernel membutuhkan device file ini karena saat menjalankan proses init kita, ia akan membuka /dev/console untuk menyambungkan stdin, stdout, dan stderr. Tanpa device tersebut, syscall write ke fd 1 tidak punya tujuan untuk menulis.
Kemas:
/Volumes/linuxkernel/linux/usr/gen_init_cpio /Volumes/linuxkernel/initramfs.txt \
| gzip > /Volumes/linuxkernel/initramfs.cpio.gzPipeline ini menulis arsip cpio-nya ke /Volumes/linuxkernel/initramfs.cpio.gz dan tidak menghasilkan output di terminal. Kita siap mem-boot.
Boot Pertama yang Senyap
Kita sudah memiliki Image dan initramfs.cpio.gz dengan binary init kecil di dalamnya. Saatnya benar-benar menjalankannya.
QEMU mem-boot menggunakan virt machine29 RISC-V, dengan kernel dan initramfs kita, lalu mengarahkan semua I/O ke terminal:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image.gz \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "console=ttyS0"/Volumes/linuxkernel
❯ qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image.gz \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "console=ttyS0"
OpenSBI v1.7
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : riscv-virtio,qemu
Platform Features : medeleg
Platform HART Count : 1
Platform IPI Device : aclint-mswi
Platform Timer Device : aclint-mtimer @ 10000000Hz
Platform Console Device : uart8250
Platform HSM Device : ---
Platform PMU Device : ---
Platform Reboot Device : syscon-reboot
Platform Shutdown Device : syscon-poweroff
Platform Suspend Device : ---
Platform CPPC Device : ---
Firmware Base : 0x80000000
Firmware Size : 317 KB
Firmware RW Offset : 0x40000
Firmware RW Size : 61 KB
Firmware Heap Offset : 0x46000
Firmware Heap Size : 37 KB (total), 2 KB (reserved), 11 KB (used), 23 KB (free)
Firmware Scratch Size : 4096 B (total), 1400 B (used), 2696 B (free)
Runtime SBI Version : 3.0
Standard SBI Extensions : time,rfnc,ipi,base,hsm,srst,pmu,dbcn,fwft,legacy,dbtr,sse
Experimental SBI Extensions : none
Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W)
Domain0 Region01 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W)
Domain0 Region02 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: ()
Domain0 Region03 : 0x0000000080040000-0x000000008004ffff M: (R,W) S/U: ()
Domain0 Region04 : 0x0000000080000000-0x000000008003ffff M: (R,X) S/U: ()
Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W)
Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W)
Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087e00000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes
Domain0 SysSuspend : yes
Boot HART ID : 0
Boot HART Domain : root
Boot HART Priv Version : v1.12
Boot HART Base ISA : rv64imafdch
Boot HART ISA Extensions : sstc,zicntr,zihpm,zicboz,zicbom,sdtrig,svadu
Boot HART PMP Count : 16
Boot HART PMP Granularity : 2 bits
Boot HART PMP Address Bits : 54
Boot HART MHPM Info : 16 (0x0007fff8)
Boot HART Debug Triggers : 2 triggers
Boot HART MIDELEG : 0x0000000000001666
Boot HART MEDELEG : 0x0000000000f4b509OpenSBI30 mulai berjalan, mencetak banner-nya beserta info platform, lalu layarnya terdiam begitu saja. Tidak ada boot message kernel. Tidak ada “Hello, World!”. Tidak ada apa-apa.
Saya tidak tahu apa yang salah. Kernel seharusnya mengambil alih dari OpenSBI dan mencetak boot message-nya sendiri. Tetapi tidak ada yang terjadi. Saya menggali dokumentasi kernel dan Stack Overflow untuk mencari jawaban.
Polanya jelas: hampir tidak ada yang memulai build kernel dari allnoconfig. Dengan allnoconfig kita sudah mematikan semua yang diizinkan oleh Kconfig, termasuk bagian-bagian kernel yang dibutuhkan untuk sekadar berkomunikasi dengan pengguna. Tidak ada HVC_RISCV_SBI, jadi tidak ada output konsol. Tidak ada BLK_DEV_INITRD, jadi tidak ada dukungan initramfs. Tidak ada BINFMT_ELF, jadi tidak ada cara untuk menjalankan binary init yang sudah kita compile. Titik awal yang direkomendasikan adalah defconfig31, default config per arsitektur dengan semua opsi dasarnya sudah aktif.
Jadi saya memulai dari awal dengan defconfig.
Memulai dari Awal dengan defconfig
Beralih ke defconfig berarti menghapus direktori kernel saat ini dan memulai dari awal. Tetapi kita tidak ingin kehilangan direktori scripts/macos-include/ yang baru saja kita bangun. File-file shim tersebut adalah tambahan lokal, bukan bagian dari kode sumber kernel, sehingga akan hilang ketika kita melakukan clone ulang.
Sebelum menghapus direktorinya, saya menyalin direktori macos-include/ keluar dari sana ke ~/Learning/LINUX/macos-include/. Mulai sekarang, file-file shim disimpan di sana dan saya menghubungkannya kembali ke direktori kernel dengan symlink. Dengan begitu, kapan pun saya melakukan clone ulang, shim-nya akan tetap ada. init.c dan initramfs.cpio.gz yang sudah dikemas berada di /Volumes/linuxkernel/initramfs/ dan /Volumes/linuxkernel/, di luar direktori kernel, sehingga keduanya juga selamat dari reset ini. Hanya .config dan build artifact yang terhapus.
rm -rf /Volumes/linuxkernel/linux
cd /Volumes/linuxkernel
git clone --depth=1 https://github.com/torvalds/linux.git
ln -s ~/Learning/LINUX/macos-include linux/scripts/macos-include
cd linux
gmake ARCH=riscv LLVM=1 defconfigApa yang dilakukan perintah-perintah ini:
rm -rfmenghapus direktoriallnoconfig-nyagit clone --depth=1membawa kernel yang baruln -smem-symlink direktori shim yang kita simpan kembali kescripts/macos-include/gmake ... defconfigmenghasilkan.configbaru dari default config RISC-V
defconfig akan mem-build tools Kconfig9 terlebih dahulu, lalu menjalankannya untuk menghasilkan file .config:
linux on master
❯ gmake ARCH=riscv LLVM=1 defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#Flag defconfig yang Hilang
Setelah defconfig siap, saya mem-build ulang kernel dengan shim macos-include yang masih dirujuk melalui HOSTCFLAGS:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"Build ini memakan waktu jauh lebih lama dibandingkan build allnoconfig sebelumnya. defconfig mengaktifkan banyak driver dan fitur, sehingga ada lebih banyak kode yang harus dikompilasi. Saat yang tepat untuk istirahat sejenak.
Build-nya berjalan tanpa error. Image dan Image.gz sudah siap. Saatnya mem-boot:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image.gz \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "console=ttyS0"Masih senyap. Banner OpenSBI, lalu tidak ada apa-apa. Sama persis seperti sebelumnya.
Saya kembali mencari. Jawabannya muncul setelah membaca dokumentasi virt machine QEMU bersama file Kconfig HVC di kernel. Untuk virt machine RISC-V milik QEMU, driver konsol yang tepat adalah HVC_RISCV_SBI32, yang berkomunikasi dengan konsol debug SBI33 milik OpenSBI yang disebut DBCN34. Ternyata defconfig juga tidak mengaktifkan driver ini secara default, dan driver ini bergantung pada opsi konfigurasi bernama NONPORTABLE yang juga tidak diaktifkan secara default.
Aktifkan keduanya:
./scripts/config --enable NONPORTABLE
./scripts/config --enable HVC_RISCV_SBIVerifikasi keduanya sudah aktif di .config:
grep -E "^CONFIG_NONPORTABLE|^CONFIG_HVC_RISCV_SBI" .configlinux on master [?]
❯ grep -E "^CONFIG_NONPORTABLE|^CONFIG_HVC_RISCV_SBI" .config
CONFIG_NONPORTABLE=y
CONFIG_HVC_RISCV_SBI=yAda satu hal lagi yang harus dilakukan sebelum mem-build ulang. Mengaktifkan NONPORTABLE membuka sejumlah opsi konfigurasi baru yang harus dijawab. Jika kita langsung memulai build-nya, gmake akan menjeda build dan menanyakan tiap pertanyaan di terminal satu per satu. Untuk menerima default semuanya sekaligus, jalankan olddefconfig terlebih dahulu:
gmake ARCH=riscv LLVM=1 olddefconfigPerintah ini juga merupakan langkah pemulihan rutin setiap kali kita berpindah versi kernel, melakukan checkout ke commit lain, atau mengubah opsi konfigurasi. olddefconfig membaca .config yang sudah ada dan secara otomatis menerima nilai default untuk setiap opsi yang baru atau berubah, sehingga build berikutnya dimulai dari kondisi yang konsisten.
linux on master took 3m18s
❯ gmake ARCH=riscv LLVM=1 olddefconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
#
# configuration written to .config
#Sekarang bangun ulang kernel dengan konfigurasi yang baru:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"Image vs Image.gz
Dengan HVC_RISCV_SBI yang sudah aktif, kernel sekarang bisa berkomunikasi melalui konsol debug SBI. Kita ubah kernel command line QEMU untuk menggunakannya. console=hvc0 memilih konsol SBI sebagai konsol utama; earlycon=sbi35 menambahkan konsol tahap awal untuk pesan-pesan yang muncul sebelum konsol utama terinisialisasi, supaya kita tidak melewatkan apa pun di tahap awal boot.
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image.gz \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"Masih senyap. Banner OpenSBI, lalu tidak ada apa-apa.
Butuh waktu cukup lama untuk menemukan penyebabnya. Jawabannya ada di kode sumber QEMU. Fungsi yang memuat kernel untuk RISC-V adalah riscv_load_kernel di hw/riscv/boot.c. Fungsi ini mencoba tiga loader secara berurutan:
load_elf_ram_symmembaca byte pertama file dan memeriksa magic bytes36 ELF. FileImage.gzkita sudah di-gzip, sehingga magic bytes-nya adalah milik gzip, bukan ELF. Loader ELF menolaknya.load_uimage_asmemeriksa magic bytes format uImage milik U-Boot. File kita bukan uImage, jadi loader ini juga menolaknya.load_image_targphys_asadalah fallback tanpa syarat. Loader ini tidak memeriksa magic apa pun, hanya menyalin isi file-nya ke RAM yang diemulasikan di alamat load kernel.
Di langkah ketiga, file-nya berhasil dimuat, tetapi yang termuat adalah byte gzip yang belum di-decompress. Setelah OpenSBI menyerahkan kendali, CPU melompat ke alamat kernel dan mencoba mendekode gzip header sebagai instruksi RISC-V. Byte-byte itu tidak masuk akal sebagai instruksi. CPU mengalami fault, tetapi belum ada driver konsol yang aktif untuk memberi tahu kita. Jadi tidak ada apa pun yang muncul di layar.
Perbaikannya sederhana: gunakan Image yang tidak dikompres alih-alih Image.gz. Kedua file tersebut sama-sama dihasilkan oleh build. Image adalah kernel yang sama, hanya saja tidak di-gzip:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"linux on master took 12s
❯ qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"
OpenSBI v1.7
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : riscv-virtio,qemu
Platform HART Count : 1
[... truncated ...]
Runtime SBI Version : 3.0
Standard SBI Extensions : time,rfnc,ipi,base,hsm,srst,pmu,dbcn,fwft,legacy,dbtr,sse
[... truncated ...]
Boot HART ID : 0
Boot HART Base ISA : rv64imafdch
[... truncated ...]
[ 0.000000] Booting Linux on hartid 0
[ 0.000000] Linux version 7.1.0-rc3 (jefrydco@jefrydco-macbook-personal.local) (Homebrew clang version 22.1.4, Homebrew LLD 22.1.4) #1 SMP PREEMPT Mon May 11 08:01:53 WIB 2026
[ 0.000000] Machine model: riscv-virtio,qemu
[ 0.000000] SBI specification v3.0 detected
[ 0.000000] SBI DBCN extension detected
[ 0.000000] earlycon: sbi0 at I/O port 0x0 (options '')
[ 0.000000] printk: legacy bootconsole [sbi0] enabled
[ 0.000000] Kernel command line: earlycon=sbi console=hvc0
[... truncated ...]
[ 0.256876] Unpacking initramfs...
[... truncated ...]
[ 1.123813] Freeing unused kernel image (initmem) memory: 2484K
[ 1.124509] Run /init as init processOutput! Kernel mem-boot, mencetak pesan-pesan awalnya seperti “Booting Linux on hartid37 0”, me-mount initramfs-nya, menemukan /init, dan menjalankannya.
Hello World yang Senyap
Setelah Run /init as init process, layar tidak menampilkan apa pun lagi. Tidak ada “Hello, World!”, tidak ada crash, tidak ada panic. Binary init berjalan, tidak mengalami crash, dan melakukan loop tanpa henti di while(1), persis seperti yang saya tulis. Kernel tidak dapat memberi tahu saya apa yang salah karena memang tidak ada error yang bisa dicetak. Jadi saya mencoba melihat apa yang sebenarnya dihasilkan compiler untuk blok asm tersebut. Tool llvm-objdump38 mengubah binary kembali ke assembly yang dapat dibaca:
llvm-objdump -d /Volumes/linuxkernel/initramfs/init/Volumes/linuxkernel/initramfs via C v21.0.0-clang
❯ llvm-objdump -d /Volumes/linuxkernel/initramfs/init
/Volumes/linuxkernel/initramfs/init: file format elf64-littleriscv
Disassembly of section .text:
00000000000111fc <_start>:
111fc: 1101 addi sp, sp, -0x20
111fe: ec06 sd ra, 0x18(sp)
11200: e822 sd s0, 0x10(sp)
11202: 1000 addi s0, sp, 0x20
11204: 4501 li a0, 0x0
11206: fea40723 sb a0, -0x12(s0)
1120a: 6505 lui a0, 0x1
1120c: a2150513 addi a0, a0, -0x5df
11210: fea41623 sh a0, -0x14(s0)
11214: 646c7537 lui a0, 0x646c7
11218: 26f50513 addi a0, a0, 0x26f
1121c: fea42423 sw a0, -0x18(s0)
11220: fffff517 auipc a0, 0xfffff
11224: f7050513 addi a0, a0, -0x90
11228: 6108 ld a0, 0x0(a0)
1122a: fea43023 sd a0, -0x20(s0)
1122e: fe040513 addi a0, s0, -0x20
11232: 04000893 li a7, 0x40
11236: 4505 li a0, 0x1
11238: 85aa mv a1, a0
1123a: 4639 li a2, 0xe
1123c: 00000073 ecall
11240: a009 j 0x11242 <_start+0x46>
11242: a001 j 0x11242 <_start+0x46>Saya perlu beberapa saat untuk memahami isi disassembly-nya. Baris yang paling penting di asm saya adalah mv a1, %0. Saya kira %0 akan otomatis merujuk ke msg, tetapi sebenarnya %0 digantikan oleh compiler dengan register mana pun yang menampung msg.
Berikut alur yang sebenarnya terjadi:
- Constraint
"r"(msg)memberi tahu compiler agar meletakkanmsgke sebuah register, tanpa menentukan register yang mana. - Compiler memilih
a0. Lihataddi a0, s0, -0x20tepat sebelum bagian asm, instruksi ini yang meletakkan alamatmsgdi stack kea0. - Baris asm berjalan berurutan:
li a7, 64, laluli a0, 1. Instruksi kedua memuat angka 1 kea0, menimpa alamatmsg. mv a1, %0lalu digantikan menjadimv a1, a0, yang menyalina0(sekarang berisi 1) kea1. Jadia1menampung 1, bukan alamatmsg.
Saat ecall dijalankan, register menampung a0=1, a1=1, a2=14, a7=64. Kernel membaca ini sebagai write(1, (char *)1, 14), yaitu menulis 14 byte dari alamat 1 ke file descriptor 1. Alamat 1 bukan alamat memori yang valid. Syscall mengembalikan -EFAULT, kode error yang berarti “alamat yang salah”, dan tidak ada yang tercetak.
Yang saya butuhkan adalah cara untuk memberi tahu compiler secara tepat register mana yang harus diisi oleh setiap nilai. Saya mencari tahu cara melakukannya dan menemukan sintaks yang tepat. Keyword register di C adalah petunjuk bagi compiler agar menjaga variabel di dalam register. Jika berdiri sendiri, keyword ini tidak banyak berpengaruh; compiler mengatur register secara otomatis tanpanya. Ketika digabungkan dengan klausa asm(...) yang menamai register tertentu, seperti asm("a0"), setelah nama variabel, keduanya menjadi ekstensi GCC dan Clang yang disebut local register variables39. Kombinasi ini mengikat variabel ke register hardware tertentu. Keyword register dan klausa asm(...) keduanya harus ada bersamaan; salah satunya saja tidak akan bekerja.
Konvensi syscall RISC-V Linux memberi tahu kita register mana yang harus diisi setiap nilai. a7 menampung nomor syscall, dan a0 sampai a5 menampung argumennya. Untuk write(fd, buf, count), itu artinya a0 adalah fd, a1 adalah pointer ke buffer, dan a2 adalah jumlah byte. Dengan setiap nilai terikat ke register-nya, bagian asm hanya perlu menjalankan ecall:
// /Volumes/linuxkernel/initramfs/init.c
void _start() {
const char msg[] = "Hello, World!\n";
register long a0 asm("a0") = 1;
register const char *a1 asm("a1") = msg;
register long a2 asm("a2") = 14;
register long a7 asm("a7") = 64;
__asm__ volatile (
"ecall\n"
: "+r"(a0)
: "r"(a1), "r"(a2), "r"(a7)
: "memory"
);
while(1);
}Compile ulang, lalu disassemble kembali:
clang --target=riscv64-linux-gnu \
-static \
-nostdlib \
-fuse-ld=lld \
-o /Volumes/linuxkernel/initramfs/init \
/Volumes/linuxkernel/initramfs/init.cllvm-objdump -d /Volumes/linuxkernel/initramfs/init/Volumes/linuxkernel/initramfs via C v21.0.0-clang
❯ llvm-objdump -d /Volumes/linuxkernel/initramfs/init
/Volumes/linuxkernel/initramfs/init: file format elf64-littleriscv
Disassembly of section .text:
00000000000111fc <_start>:
111fc: 7139 addi sp, sp, -0x40
111fe: fc06 sd ra, 0x38(sp)
11200: f822 sd s0, 0x30(sp)
11202: 0080 addi s0, sp, 0x40
11204: 4501 li a0, 0x0
11206: fea40723 sb a0, -0x12(s0)
1120a: 6505 lui a0, 0x1
1120c: a2150513 addi a0, a0, -0x5df
11210: fea41623 sh a0, -0x14(s0)
11214: 646c7537 lui a0, 0x646c7
11218: 26f50513 addi a0, a0, 0x26f
1121c: fea42423 sw a0, -0x18(s0)
11220: fffff517 auipc a0, 0xfffff
11224: f7050513 addi a0, a0, -0x90
11228: 6108 ld a0, 0x0(a0)
1122a: fea43023 sd a0, -0x20(s0)
1122e: 4505 li a0, 0x1
11230: fca43c23 sd a0, -0x28(s0)
11234: fe040513 addi a0, s0, -0x20
11238: fca43823 sd a0, -0x30(s0)
1123c: 4539 li a0, 0xe
1123e: fca43423 sd a0, -0x38(s0)
11242: 04000513 li a0, 0x40
11246: fca43023 sd a0, -0x40(s0)
1124a: fd843503 ld a0, -0x28(s0)
1124e: fd043583 ld a1, -0x30(s0)
11252: fc843603 ld a2, -0x38(s0)
11256: fc043883 ld a7, -0x40(s0)
1125a: 00000073 ecall
1125e: fca43c23 sd a0, -0x28(s0)
11262: a009 j 0x11264 <_start+0x68>
11264: a001 j 0x11264 <_start+0x68>Lihat empat instruksi ld tepat sebelum ecall. Setiap instruksi memuat satu nilai ke register bernama tertentu: a0 untuk fd, a1 untuk pointer buffer, a2 untuk jumlah byte, a7 untuk nomor syscall. Pinning-nya berfungsi.
Sebelum mem-boot, kita perlu mengemas ulang initramfs supaya binary init yang baru masuk ke dalam arsip cpio yang dibaca kernel. Binary di disk memang sudah baru, tetapi arsip cpio-nya masih menyimpan binary yang lama sampai kita mengemasnya ulang:
/Volumes/linuxkernel/linux/usr/gen_init_cpio /Volumes/linuxkernel/initramfs.txt \
| gzip > /Volumes/linuxkernel/initramfs.cpio.gzBoot sekali lagi:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"linux on master took 4m28s
❯ qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"
OpenSBI v1.7
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : riscv-virtio,qemu
Platform HART Count : 1
[... truncated ...]
Runtime SBI Version : 3.0
Standard SBI Extensions : time,rfnc,ipi,base,hsm,srst,pmu,dbcn,fwft,legacy,dbtr,sse
[... truncated ...]
Boot HART ID : 0
Boot HART Base ISA : rv64imafdch
[... truncated ...]
[ 0.000000] Booting Linux on hartid 0
[ 0.000000] Linux version 7.1.0-rc3 (jefrydco@jefrydco-macbook-personal.local) (Homebrew clang version 22.1.4, Homebrew LLD 22.1.4) #1 SMP PREEMPT Mon May 11 08:01:53 WIB 2026
[ 0.000000] Machine model: riscv-virtio,qemu
[ 0.000000] SBI specification v3.0 detected
[ 0.000000] SBI DBCN extension detected
[ 0.000000] earlycon: sbi0 at I/O port 0x0 (options '')
[ 0.000000] printk: legacy bootconsole [sbi0] enabled
[ 0.000000] Kernel command line: earlycon=sbi console=hvc0
[... truncated ...]
[ 0.263436] Unpacking initramfs...
[... truncated ...]
[ 1.167737] Freeing unused kernel image (initmem) memory: 2484K
[ 1.168408] Run /init as init process
Hello, World!Program C pertama yang saya tulis berjalan di kernel buatan saya sendiri, di atas RISC-V yang diemulasi QEMU pada Mac ARM64 saya. Pesannya tercetak. Momen itulah yang membuat seluruh perjalanan ini layak dijalani.
Membaca init/main.c
Hello, World tercetak. Semuanya berjalan, dari saat CPU dinyalakan sampai 14 baris assembly kita. Di antara keduanya, kernel melakukan banyak pekerjaan, dan hampir semuanya ada di dalam init/main.c. Bagian ini menelusuri file tersebut. Dengan mengenali bagian-bagiannya saat semuanya berjalan normal, kita akan tahu di mana harus mencari ketika sesuatu rusak.
Semua yang terjadi antara firmware dan /init kita berada dalam satu file: init/main.c. File tersebut memiliki dua fungsi yang perlu kita ketahui: start_kernel dan kernel_init.
start_kernel adalah main-nya kernel. Fungsi ini melakukan sebagian besar pekerjaan penyiapan:
- Menyiapkan memori: mencari tahu RAM mana yang tersedia agar kernel bisa membagikan memori ke siapa saja yang memintanya nanti.
- Menyiapkan interrupt: memberi tahu CPU bagaimana menangani timer tick, page fault, dan system call.
- Menyiapkan scheduler: bagian yang memutuskan task mana yang akan berjalan di CPU selanjutnya.
- Menyiapkan konsol: alasan kita bisa melihat pesan boot di terminal kita.
Saat start_kernel selesai, kernel sudah berjalan dan bisa mencetak ke konsol. Namun /init belum berjalan; tugas itu diserahkan ke fungsi berikutnya, kernel_init.
kernel_init adalah fungsi yang berjalan setelah start_kernel. Fungsi ini adalah jembatan dari “kernel yang berjalan di atas hardware” menuju “program userspace kita mulai berjalan”. Fungsi ini:
- Menunggu sampai
initramfs.cpio.gzselesai diekstrak ke memori. - Membuka
/dev/consoleagar file descriptor 0, 1, 2 dapat digunakan oleh program apa pun yang dijalankannya. - Memanggil
run_init_process("/init"), fungsi yang mencetakRun /init as init processlalu menyerahkan kendali ke binary kita.
Berikut gejala-gejala umum, masing-masing beserta lokasinya di kode sumber:
- Tidak ada pesan kernel sama sekali, hanya banner OpenSBI: konsol awal belum aktif. Lihat
setup_archdiarch/riscv/kernel/setup.cdan parsingearlycon=sbidisetup_earlyconpadadrivers/tty/serial/earlycon.c. Ini perbaikan yang sama dengan yang kita lakukan saat menambahkanearlycon=sbipertama kali. - Boot berhenti di antara OpenSBI dan
Run /init: ada sesuatu distart_kernelataukernel_inityang panic atau macet. Baris terakhir yang tercetak sebelum berhenti adalah petunjuk kita. Bukainit/main.c, cari teks tersebut, lalu baca kode yang berjalan setelahnya. Run /init as init processlalu senyap: kernel sudah menyelesaikan tugasnya. Bug-nya ada di binary/initkita. Kita baru saja melihatnya di bagian sebelumnya.- Pesan
Warning: unable to open an initial console:console_on_rootfsgagal. Penyebabnya:/dev/consoletidak ada di initramfs, atau driver konsolnya tidak di-build ke dalam kernel. Pekerjaan kita denganHVC_RISCV_SBImengatasi kasus yang kedua. - Kernel panic dengan pesan
No working init found: kernel sudah menyelesaikan setup-nya, tetapi tidak menemukan/initdi initramfs. Pastikan binary-nya sudah masuk ke dalam initramfs.
Dengan petunjuk-petunjuk ini, “boot-nya rusak” tidak lagi terasa membingungkan, melainkan menjadi daftar pendek tempat yang harus diperiksa.
Kernel memiliki versi printf-nya sendiri yang bernama printk40. Kode kernel modern biasanya memanggilnya melalui wrapper seperti pr_info, pr_warn, dan pr_err. Setiap wrapper memanggil printk pada log level yang berbeda. Output-nya menuju ke konsol yang sama dengan boot log kita. Kita dapat menambahkan pr_info("hello from my code\n"); di mana saja dalam kode kernel, mem-build ulang, mem-boot, dan mencari baris tersebut. Inilah cara tercepat untuk memastikan bagian kode yang kita curigai benar-benar berjalan.
Mari kita coba ini di dua tempat yang baru saja kita sebutkan. Buka init/main.c dan temukan fungsi start_kernel. Menjelang akhir fungsi tersebut, tepat sebelum memanggil rest_init, tambahkan:
pr_info("hello from start_kernel\n");Sekarang temukan kernel_init di bagian bawah file yang sama. Di awal body fungsinya, tepat sebelum panggilan ke wait_for_completion(&kthreadd_done), tambahkan:
pr_info("hello from kernel_init\n");Build ulang kernel:
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"Boot:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"Boot log sekarang menampilkan dua baris baru, satu dari masing-masing fungsi:
linux on master took 22s
❯ qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0"
OpenSBI v1.7
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|
Platform Name : riscv-virtio,qemu
Platform HART Count : 1
[... truncated ...]
Runtime SBI Version : 3.0
Standard SBI Extensions : time,rfnc,ipi,base,hsm,srst,pmu,dbcn,fwft,legacy,dbtr,sse
[... truncated ...]
Boot HART ID : 0
Boot HART Base ISA : rv64imafdch
[... truncated ...]
[ 0.000000] Booting Linux on hartid 0
[ 0.000000] Linux version 7.1.0-rc3-dirty (jefrydco@jefrydco-macbook-personal.local) (Homebrew clang version 22.1.4, Homebrew LLD 22.1.4) #2 SMP PREEMPT Mon May 11 09:42:18 WIB 2026
[ 0.000000] Machine model: riscv-virtio,qemu
[ 0.000000] SBI specification v3.0 detected
[ 0.000000] SBI DBCN extension detected
[ 0.000000] earlycon: sbi0 at I/O port 0x0 (options '')
[ 0.000000] printk: legacy bootconsole [sbi0] enabled
[ 0.000000] Kernel command line: earlycon=sbi console=hvc0
[... truncated ...]
[ 0.031952] hello from start_kernel
[ 0.039693] hello from kernel_init
[... truncated ...]
[ 0.267852] Unpacking initramfs...
[... truncated ...]
[ 1.177287] Freeing unused kernel image (initmem) memory: 2484K
[ 1.178003] Run /init as init process
Hello, World!Kita dapat menambahkan pr_info di mana saja dan mengetahui apakah code path tersebut benar-benar berjalan. Itu inti tekniknya.
printk sudah cukup untuk menjawab “apakah ini berjalan?”. Untuk “nilai apa yang ada di register ini?” atau “kita ingin menjeda kernel di tengah boot”, bagian berikutnya akan menghubungkan lldb ke QEMU yang sedang berjalan.
Menelusuri Boot dengan lldb
printk menjawab pertanyaan “apakah ini berjalan?”. Untuk pertanyaan yang lebih dalam, seperti “berapa nilai register ini?” atau “kita ingin menjeda kernel dan melihat memori”, kita membutuhkan debugger. lldb bersama gdb stub milik QEMU memberikan kemampuan itu.
Setup-nya terdiri dari empat langkah:
- Mem-build kernel dengan debug symbols supaya debugger mengetahui nama fungsi dan variabel.
- Menjalankan QEMU dengan gdb stub-nya aktif dan CPU yang menunggu, supaya kita bisa menyambungkan debugger sebelum kode kernel berjalan.
- Menyambungkan
lldbke gdb stub. - Menelusuri boot-nya.
Untuk membuat lldb bisa memetakan alamat ke baris kode sumber, kernel harus di-build dengan DWARF symbols41. Ternyata defconfig secara bawaan mengaktifkan DEBUG_INFO_NONE, yang menghapus symbol dari kernel. Kita perlu mematikan opsi itu dan mengaktifkan DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT sebagai gantinya. Sekalian, GDB_SCRIPTS akan menambahkan script pendukung yang bisa dimuat oleh debugger yang kompatibel dengan gdb:
./scripts/config --disable DEBUG_INFO_NONE
./scripts/config --enable DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT
./scripts/config --enable GDB_SCRIPTSVerifikasi keduanya sudah aktif di .config:
grep -E "^CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT|^CONFIG_GDB_SCRIPTS" .configlinux on master [?]
❯ grep -E "^CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT|^CONFIG_GDB_SCRIPTS" .config
CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y
CONFIG_GDB_SCRIPTS=yLalu jalankan olddefconfig dan build ulang:
gmake ARCH=riscv LLVM=1 olddefconfig
gmake ARCH=riscv LLVM=1 -j$(nproc) \
HOSTCFLAGS="-Iscripts/macos-include -I$(brew --prefix libelf)/include -Wno-incompatible-pointer-types -D_UUID_T"olddefconfig menyesuaikan ketiga toggle tersebut dengan opsi-opsi lain yang bergantung kepadanya, sambil menerima nilai default untuk opsi baru. Build kemudian berjalan dengan .config yang konsisten.
Build ini menghasilkan file vmlinux42 baru di direktori kernel di samping file Image. vmlinux adalah versi lengkap binary ELF kernel, dengan tabel symbol yang masih utuh. lldb membaca vmlinux untuk mengetahui di mana setiap fungsi berada. File Image yang selama ini kita boot adalah vmlinux yang sudah dihilangkan debug info-nya dan wrapper ELF-nya dilepas.
Sekarang jalankan QEMU dengan dua flag baru:
qemu-system-riscv64 \
-M virt \
-kernel /Volumes/linuxkernel/linux/arch/riscv/boot/Image \
-initrd /Volumes/linuxkernel/initramfs.cpio.gz \
-nographic \
-append "earlycon=sbi console=hvc0" \
-s -SFlag barunya:
-smengaktifkan gdb stub43 milik QEMU di port TCP 1234. Debugger apa pun yang menggunakan GDB protocol dapat tersambung ke sana.-Sdengan huruf S besar memerintahkan QEMU agar memulai CPU guest dalam keadaan terhenti. Tanpa flag ini, kernel akan berjalan melewatistart_kerneldan seterusnya sebelum kita sempat menyambungkan debugger.
Terminal-nya diam menunggu. QEMU sudah memuat kernel ke RAM dan terhenti di reset vector44. Belum ada satu instruksi pun yang dieksekusi.
Buka terminal kedua. Jalankan lldb dengan versi lengkap binary kernel:
lldb /Volumes/linuxkernel/linux/vmlinuxlinux on master via 🐍 v3.14.5
❯ lldb /Volumes/linuxkernel/linux/vmlinux
(lldb) target create "/Volumes/linuxkernel/linux/vmlinux"
Current executable set to '/Volumes/linuxkernel/linux/vmlinux' (riscv64).lldb membaca vmlinux, mempelajari setiap fungsi di dalam kernel, lalu menampilkan prompt. lldb belum tersambung ke QEMU.
Sambungkan ke gdb stub milik QEMU dan pasang breakpoint45 di fungsi C pertama yang dijalankan kernel:
(lldb) gdb-remote localhost:1234
(lldb) breakpoint set --name start_kernel
(lldb) continuegdb-remote localhost:1234 membuka koneksi TCP ke QEMU. lldb memberi tahu QEMU “berhenti saat mencapai alamat ini” melalui GDB wire protocol. continue melepaskan CPU yang tadi terhenti. QEMU berjalan dari reset vector, melewati kode assembly di head.S yang detailnya masih saya pelajari, dan akhirnya berhenti di start_kernel:
(lldb) gdb-remote localhost:1234
Process 1 stopped
* thread #1, stop reason = signal SIGTRAP
frame #0: 0x0000000000001000
-> 0x1000: auipc t0, 0x0
0x1004: addi a2, t0, 0x28
0x1008: csrr a0, mhartid
0x100c: ld a1, 0x20(t0)
(lldb) breakpoint set --name start_kernel
Breakpoint 1: where = vmlinux`start_kernel + 12 at main.c:1019:8, address = 0xffffffff80c00360
(lldb) continue
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0xffffffff80c00360 vmlinux`start_kernel at main.c:1019:8
1016 asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
1017 void start_kernel(void)
1018 {
-> 1019 char *command_line;
1020 char *after_dashes;
1021
1022 set_task_stack_end_magic(&init_task);Kita sekarang berada di dalam fungsi C pertama kernel. Setiap nilai register, setiap variabel, setiap alamat memori dapat kita periksa dari titik ini.
Mari kita lompat ke tempat yang lebih menarik. run_init_process adalah fungsi yang sudah kita bahas di bagian sebelumnya, yaitu fungsi yang mencetak Run /init as init process lalu menyerahkan kendali ke binary kita. Pasang breakpoint di sana, lanjutkan, dan periksa argumennya:
(lldb) breakpoint set --name run_init_process
(lldb) continue
(lldb) register read a0
(lldb) memory read --format s --count 1 $a0Ketika breakpoint-nya tercapai, kita berada pada saat kernel akan memanggil binary init kita. Argumen pertama berada di a0 sesuai calling convention RISC-V15. run_init_process menerima satu argumen, yaitu const char *init_filename, sehingga a0 seharusnya berisi pointer ke string /init:
(lldb) breakpoint set --name run_init_process
Breakpoint 2: where = vmlinux`run_init_process + 18 at main.c:1507:15, address = 0xffffffff80002012
(lldb) continue
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 2.1
frame #0: 0xffffffff80002012 vmlinux`run_init_process(init_filename="/init") at main.c:1507:15
1504 {
1505 const char *const *p;
1506
-> 1507 argv_init[0] = init_filename;
1508 pr_info("Run %s as init process\n", init_filename);
1509 pr_debug(" with arguments:\n");
1510 for (p = argv_init; *p; p++)
(lldb) register read a0
a0 = 0xffffffff8134d295
(lldb) memory read --format s --count 1 $a0
0xffffffff8134d295: "/init"memory read --format s membaca alamat di $a0 dan menafsirkan byte-byte-nya sebagai string yang diakhiri dengan null. Kernel benar-benar akan menjalankan /init, bukan binary lain.
Satu pemberhentian lagi. start_thread adalah fungsi yang spesifik untuk arsitektur, yang kernel panggil untuk menyiapkan state untuk thread userspace baru sebelum CPU kembali ke U-mode21. Di RISC-V, fungsi ini menerima tiga argumen: pointer ke nilai-nilai register milik task yang tersimpan, program counter46 tempat userspace mulai dijalankan, dan stack pointer47 awalnya. Pasang breakpoint lalu baca ketiga register argumennya:
(lldb) breakpoint set --name start_thread
(lldb) continue
(lldb) register read a0 a1 a2(lldb) breakpoint set --name start_thread
Breakpoint 3: where = vmlinux`start_thread + 24 at process.c:147:15, address = 0xffffffff8001411c
(lldb) continue
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 3.1
frame #0: 0xffffffff8001411c vmlinux`start_thread(regs=0xff2000000000bee0, pc=70140, sp=140737414192480) at process.c:147:15
144 void start_thread(struct pt_regs *regs, unsigned long pc,
145 unsigned long sp)
146 {
-> 147 regs->status = SR_PIE;
148 if (has_fpu()) {
149 regs->status |= SR_FS_INITIAL;
150 /*
(lldb) register read a0 a1 a2
a0 = 0x0000000000000020
a1 = 0x00000000000111fc
a2 = 0x00007ffffb945d60a1 adalah program counter, yaitu alamat entry _start dari binary init ELF kita. a2 adalah puncak stack di userspace yang dialokasikan kernel untuk proses yang baru.
Setelah start_thread selesai dan kernel beralih kembali ke U-mode melalui sret48, program counter CPU menjadi nilai dari a1 dan eksekusi melompat ke 14 baris inline assembly kita. Hello World pun tercetak.
Kita baru saja menelusuri boot secara langsung: dari reset vector, melalui start_kernel, kernel_init, run_init_process, start_thread, sampai ke userspace. Setiap perpindahan yang kita baca di bagian sebelumnya barusan terjadi di depan mata, dengan breakpoint dan pembacaan register sebagai buktinya.
Langkah Berikutnya
Yang saya bangun di sini sudah berjalan, tetapi memahaminya sampai detail masih butuh perjalanan tersendiri. Berikut beberapa hal yang ingin saya jelajahi selanjutnya, semoga membantu Kamu memilih arah.
Menjalankan userspace yang sesungguhnya. Binary init 14 baris kita membuktikan kernel bisa menyerahkan kendali ke userspace, tetapi hanya bisa mencetak satu baris. Gantikan dengan BusyBox untuk mendapatkan shell yang berfungsi beserta ls, cat, dan utility lainnya. Mem-build BusyBox secara statis untuk RISC-V, masukkan binary-nya ke initramfs, arahkan /init ke init milik BusyBox, lalu reboot.
Referensi: https://busybox.net/
Menambahkan syscall buatan sendiri. Pilih nomor syscall yang belum dipakai, tambahkan entry di arch/riscv/kernel/syscall_table.c, implementasikan fungsi SYSCALL_DEFINE, lalu build ulang. Dari userspace, panggil dengan syscall(NR_my_call, ...). Ini cara paling bersih untuk merasakan kontrak antara userspace dan kernel dari kedua sisi.
Referensi: https://docs.kernel.org/process/adding-syscalls.html
Saya sendiri masih mempelajari dasar-dasarnya. Buku-buku yang masuk daftar bacaan saya:
- Computer Systems: A Programmer’s Perspective oleh Bryant dan O’Hallaron untuk memahami bagaimana register, memori, dan assembly saling terkait
- xv6 dari MIT, kernel yang cukup kecil untuk dibaca seluruhnya
- The RISC-V Reader oleh Waterman dan Patterson untuk ringkasan ISA yang padat
- The C Programming Language oleh Kernighan dan Ritchie untuk mempelajari bahasa C secara menyeluruh
Saya sudah sampai di sini. Bagian paling sulit sudah saya lewati: membuka kotak dan melihat ke dalam. Sisanya tinggal menjelajahi isinya satu per satu.
Referensi
- Building Linux Kernel on macOS Natively oleh Seiya
- BusyBox
- Homebrew
- Linux kernel: Adding a New System Call
- Linux kernel: Early Userspace Support
- Linux kernel: HVC Kconfig
- Linux kernel: Linux Allocated Devices
- QEMU: RISC-V virt Machine
- QEMU source: riscv_load_kernel in hw/riscv/boot.c
Footnotes
-
Tempat program yang kita jalankan berada, terpisah dari kernel. CPU memberlakukan pemisahan yang ketat: kode userspace berjalan di tingkat hak akses yang lebih rendah dan tidak dapat langsung menyentuh hardware atau memori milik kernel. Untuk melakukan sesuatu yang melewati batas tersebut, seperti membaca file, menulis ke layar, atau keluar dari program, userspace akan meminta kernel melalui syscall. Referensi: https://man7.org/linux/man-pages/man2/intro.2.html ↩ ↩2
-
Program userspace pertama yang dijalankan kernel setelah boot. Kernel mencari
/initdi dalam initramfs lalu menjalankannya. Implementasi umum pada sistem Linux nyata adalah systemd, sysvinit, dan OpenRC. Referensi: https://man7.org/linux/man-pages/man7/boot.7.html ↩ -
Sebutan dari Apple untuk Mac berbasis ARM mulai dari M1. Arsitektur CPU-nya adalah ARM64. Referensi: https://support.apple.com/en-us/HT211814 ↩
-
Arsitektur CPU yang open-source. Siapa saja dapat membuat chip-nya tanpa harus membayar lisensi. Instruction set-nya kecil dan mudah dibaca, sehingga populer untuk belajar. Referensi: https://riscv.org/about/ ↩
-
Mengompilasi binary untuk arsitektur yang berbeda dari mesin tempat kita melakukan kompilasi. Mengompilasi kernel RISC-V di Mac ARM adalah contoh cross-compilation. Referensi: https://clang.llvm.org/docs/CrossCompilation.html ↩ ↩2
-
Linker milik LLVM. Sudah disertakan bersama clang. Di macOS, lld otomatis terpasang setelah
brew install llvm. Referensi: https://lld.llvm.org/index.html ↩ ↩2 -
Executable and Linkable Format. Format binary standar di Linux. Program yang sudah dikompilasi, shared library, bahkan file object pun semuanya menggunakan format ELF. Referensi: https://man7.org/linux/man-pages/man5/elf.5.html ↩ ↩2 ↩3
-
Apple File System. Filesystem bawaan pada macOS modern. Bersifat case-insensitive secara default. Referensi: https://support.apple.com/guide/disk-utility/file-system-formats-dsku19ed921c/mac ↩
-
Sistem konfigurasi kernel. File-file yang bernama
Kconfigberisi daftar opsi-opsi yang tersedia. Tools discripts/kconfig/mengurai file tersebut dan menghasilkan file.config. Referensi: https://docs.kernel.org/kbuild/kconfig-language.html ↩ ↩2 -
Fungsi intrinsic yang disediakan compiler seperti clang dan gcc, yang langsung dipetakan ke instruksi CPU.
__builtin_bswap16,__builtin_bswap32, dan__builtin_bswap64membalik urutan byte pada bilangan bulat 16-, 32-, dan 64-bit secara berurutan. Referensi: https://clang.llvm.org/docs/LanguageExtensions.html#builtin-bswap16-builtin-bswap32-builtin-bswap64 ↩ -
Process ID 1. Proses userspace pertama yang dijalankan oleh kernel. Jika PID 1 mati, kernel akan panic. Referensi: https://man7.org/linux/man-pages/man7/boot.7.html ↩
-
C standard library. Menyediakan fungsi-fungsi seperti
printf,malloc,strcpy, beserta kode startup yang memanggilmainkita. Pada init kita tidak menggunakan libc agar binary-nya sekecil mungkin. Referensi: https://sourceware.org/glibc/manual/2.39/html_node/Introduction.html ↩ -
Kode startup kecil yang otomatis disertakan ke dalam binary. Kode ini menyiapkan stack, memanggil
main, kemudian memanggilexitsaatmainselesai. Tanpa crt0, kita harus menulis_startsendiri. Referensi: https://sourceware.org/git/?p=glibc.git;a=tree;f=csu ↩ -
System call. Cara sebuah program biasa meminta tolong kernel untuk mengerjakan hal yang tidak dapat dilakukannya sendiri, seperti membaca file atau menulis ke layar. Di RISC-V, kita meletakkan nomor syscall di register
a7, argumennya dia0sampaia5, kemudian menjalankan instruksiecall. Referensi: https://man7.org/linux/man-pages/man2/syscalls.2.html ↩ -
Application Binary Interface. Kalau API menjelaskan fungsi apa saja yang tersedia di tingkat kode sumber, ABI menjelaskan bagaimana fungsi itu dipanggil di tingkat mesin: argumen masuk ke register mana, nilai kembalian dibaca dari register mana, dan bagaimana stack disusun. Referensi: https://github.com/riscv-non-isa/riscv-elf-psabi-doc ↩ ↩2
-
Sel penyimpanan kecil yang tertanam langsung di dalam CPU. Membaca atau menulis register jauh lebih cepat daripada mengakses memori, karena register ada di dalam CPU sendiri sementara memori berada di luar. Sebagai analogi: memori seperti dapur besar yang harus kita datangi setiap kali butuh sesuatu, sedangkan register seperti meja kerja kecil tepat di samping kita yang langsung bisa diraih. RISC-V memiliki 32 general-purpose register, bernama
x0sampaix31, dengan nama ABI sepertia0-a7untuk argumen,t0-t6untuk temporary, dans0-s11untuk nilai yang disimpan. Referensi: spesifikasi RISC-V Unprivileged ISA di pada https://github.com/riscv/riscv-isa-manual ↩ -
Bilangan bulat kecil yang diberikan kernel saat program membuka sesuatu. Yang dibuka bisa berupa file biasa, driver perangkat, socket jaringan, pipe, atau konsol. Operator
|di shell bekerja karena kernel bisa menyambungkan output fd 1 dari satu program ke input fd 0 program berikutnya. Setiap proses dimulai dengan tiga fd yang sudah terbuka: 0 untuk input, 1 untuk output, 2 untuk error. Referensi: https://man7.org/linux/man-pages/man2/open.2.html ↩ -
Sebuah angka yang menyimpan alamat memori suatu data, bukan datanya itu sendiri. Bayangkan sticky note bertuliskan nomor loker: catatannya kecil, tetapi lokerlah yang menyimpan barangnya. Untuk mengambil barang itu, kita membaca nomornya, mendatangi lokernya, lalu membukanya. Dalam asm kita,
a1 = msgmenulis nomor loker tempat “Hello, World!\n” tersimpan ke dalama1. Kernel lalu memakai nomor itu untuk mendatangi loker tersebut dan membaca byte-nya. Referensi: https://en.cppreference.com/w/c/language/pointer ↩ -
Instruksi Environment Call. Instruksi trap di RISC-V. Dari U-mode, instruksi ini berpindah ke S-mode, tempat kernel berjalan. Beginilah cara syscall dimulai. Referensi: Unprivileged ISA spec pada https://github.com/riscv/riscv-isa-manual ↩
-
Perpindahan terkontrol dari level hak akses yang lebih rendah ke yang lebih tinggi. Dipicu oleh exception, interrupt, atau instruksi
ecall. Referensi: Privileged Architecture spec pada https://github.com/riscv/riscv-isa-manual ↩ -
RISC-V memiliki tiga level hak akses. M-mode adalah machine mode, digunakan oleh firmware. S-mode adalah supervisor mode, digunakan oleh kernel. U-mode adalah user mode, digunakan oleh program biasa. M paling tinggi, U paling rendah. Referensi: Privileged Architecture spec pada https://github.com/riscv/riscv-isa-manual ↩ ↩2
-
Format binary asli macOS dan iOS, berbeda dengan ELF yang dipakai di Linux. Binary Mach-O memiliki magic bytes dan struktur sendiri, sehingga linker untuk satu format tidak bisa menghasilkan format yang lain. Referensi: https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachORuntime/ ↩
-
Memory Management Unit. Perangkat keras yang menerjemahkan alamat virtual, yaitu alamat yang dilihat program, menjadi alamat fisik, yaitu lokasi data yang sebenarnya di RAM. Setiap program punya ruang alamat virtualnya sendiri, sehingga dua program bisa sama-sama memakai alamat
0x10000tanpa bentrok. MMU menerjemahkan masing-masing alamat virtual ke lokasi RAM fisik yang berbeda di balik layar, dan juga mengatur izin akses seperti read-only. Referensi: Privileged Architecture spec pada https://github.com/riscv/riscv-isa-manual ↩ -
Alamat awal tempat linker menaruh sebuah program di memori.
lldmemilih nilai default berdasarkan platform target. Default ini dapat diganti dengan flag-Wl,--image-base=ADDRsaat proses linking. Referensi: https://lld.llvm.org/ ↩ -
Daerah memori yang menyimpan data sementara dengan aturan LIFO, yaitu yang terakhir masuk akan keluar pertama. Bayangkan tumpukan piring bersih: setiap piring baru ditaruh di atas, dan saat kita butuh, kita mengambilnya dari atas juga. Program menggunakan stack untuk mencatat pemanggilan fungsi, variabel lokal, dan jalan kembali ke pemanggil. Stack pointer adalah penanda posisi paling atas: setiap push menggesernya turun, setiap pop menggesernya naik. Referensi: RISC-V calling convention pada https://github.com/riscv-non-isa/riscv-elf-psabi-doc ↩
-
Initial RAM Filesystem. Sebuah arsip cpio yang diekstrak oleh kernel ke memori saat boot. Kernel akan menjalankan
/initdari sini. Referensi: https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html ↩ -
Copy In, Out. Format arsip Unix yang sederhana. Digunakan untuk initramfs kernel karena unpacker-nya kecil dan belum membutuhkan filesystem sungguhan. Referensi: https://www.gnu.org/software/cpio/manual/cpio.html ↩
-
Perintah Unix untuk membuat file khusus seperti device node. Sebagai contoh,
mknod /dev/console c 5 1membuat character device bernama/dev/consoledengan major number 5 dan minor number 1. Referensi: https://man7.org/linux/man-pages/man1/mknod.1.html ↩ -
Platform virtual umum milik QEMU.
-M virtmemberi kita papan virtual dengan CPU, memori, sebuah UART, dan bus PCIe. Referensi: https://www.qemu.org/docs/master/system/riscv/virt.html ↩ -
Implementasi SBI yang open-source dan menjadi pilihan bawaan QEMU. OpenSBI berjalan paling awal saat boot, lalu menyerahkan kendali kepada kernel. Referensi: https://github.com/riscv-software-src/opensbi ↩
-
File
.configdefault untuk sebuah arsitektur, berlokasi diarch/<arch>/configs/defconfig. Menjalankanmake defconfigmengembalikan konfigurasi ke default tersebut. Referensi: https://docs.kernel.org/kbuild/kconfig.html ↩ -
Hypervisor Virtual Console. Framework konsol Linux untuk konsol-konsol yang tidak cocok dengan model UART biasa, termasuk konsol debug SBI di RISC-V. Referensi: https://github.com/torvalds/linux/blob/master/drivers/tty/hvc/Kconfig ↩
-
Supervisor Binary Interface. Aturan main antara kernel (di S-mode) dan firmware (di M-mode). Saat kernel membutuhkan sesuatu yang hanya dapat dikerjakan oleh firmware, kernel akan memanggil SBI. Referensi: https://github.com/riscv-non-isa/riscv-sbi-doc ↩
-
Debug Console Extension. Bagian dari spesifikasi SBI 2.0+ yang memungkinkan kernel mencetak karakter melalui firmware. Referensi: bab ekstensi DBCN pada https://github.com/riscv-non-isa/riscv-sbi-doc ↩
-
Parameter boot kernel yang mengaktifkan driver konsol minimalis di tahap paling awal boot, sebelum konsol utama siap. Berguna untuk menangkap pesan crash dan kesalahan konfigurasi yang tanpa earlycon tidak akan sempat tercetak. Referensi: https://docs.kernel.org/admin-guide/kernel-parameters.html ↩
-
Urutan byte unik di awal sebuah file yang mengidentifikasi formatnya. ELF dimulai dengan
7F 45 4C 46, gzip dengan1F 8B 08, PNG dengan89 50 4E 47. Tool yang harus membedakan berbagai format akan memeriksa byte ini terlebih dahulu untuk menentukan format file-nya. Referensi: https://man7.org/linux/man-pages/man1/file.1.html ↩ -
Hardware Thread ID. Cara RISC-V memberi nomor pada setiap core CPU. Hart adalah singkatan dari hardware thread. Hart 0 adalah core pertama. Referensi: https://github.com/riscv/riscv-isa-manual ↩
-
Membaca binary yang sudah di-compile dengan menerjemahkan kode mesinnya kembali ke instruksi assembly. Berguna untuk memeriksa apa yang sebenarnya dihasilkan compiler. Referensi: https://llvm.org/docs/CommandGuide/llvm-objdump.html ↩
-
Ekstensi GCC dan Clang yang mengikat sebuah variabel C ke register hardware tertentu. Ditulis sebagai
register long var asm("a0") = value;. Keywordregisterdan klausaasm(...)keduanya harus ada. Referensi: https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html ↩ -
Versi
printfmilik kernel. Menulis ke ring buffer di memori. Referensi: https://docs.kernel.org/core-api/printk-basics.html ↩ -
Format standar untuk menyimpan informasi debug tingkat source di dalam binary yang sudah di-compile: nama fungsi, tipe variabel, dan nomor baris. Dengan DWARF, debugger dapat memberi tahu posisi kita di baris 42 file
init/main.calih-alih hanya menampilkan alamat seperti 0xffffffff80123456. Referensi: https://dwarfstd.org/ ↩ -
Versi lengkap ELF kernel Linux hasil dari build, yang belum dihilangkan debug info-nya. Berisi seluruh tabel symbol dan debug info. File
Imageyang selama ini kita boot adalahvmlinuxyang sudah dihilangkan debug info-nya dan wrapper ELF-nya dilepas. Referensi: https://docs.kernel.org/admin-guide/bug-hunting.html ↩ -
Sepotong kode kecil yang berkomunikasi menggunakan GDB Remote Serial Protocol melalui socket TCP. QEMU sudah memilikinya secara built-in. Debugger seperti gdb atau lldb dapat tersambung ke sini dan menjalankan kode di QEMU langkah demi langkah. Referensi: https://www.qemu.org/docs/master/system/gdb.html ↩
-
Alamat pertama yang dieksekusi CPU saat dinyalakan atau di-reset. Instruksi pertama dari seluruh sistem berada di sini. Pada machine virt RISC-V milik QEMU, reset vector menunjuk ke ROM kecil berisi beberapa instruksi yang langsung menyerahkan kendali ke OpenSBI. Referensi: https://www.qemu.org/docs/master/system/riscv/virt.html ↩
-
Penanda yang memberi tahu debugger untuk menjeda eksekusi ketika program mencapai fungsi atau alamat tertentu. Saat dijeda, debugger dapat memeriksa register, memori, dan variabel sebelum membiarkan program berjalan kembali. Referensi: https://lldb.llvm.org/use/tutorial.html ↩
-
Program Counter. Register CPU khusus yang menampung alamat instruksi berikutnya yang akan dieksekusi. Setelah CPU selesai mengeksekusi satu instruksi, ia membaca instruksi berikutnya dari alamat yang ada di program counter. Referensi: RISC-V Unprivileged ISA spec pada https://github.com/riscv/riscv-isa-manual ↩
-
Stack Pointer. Register CPU yang menampung posisi puncak stack saat ini. Setiap push menggesernya turun, setiap pop menggesernya naik. Referensi: RISC-V calling convention pada https://github.com/riscv-non-isa/riscv-elf-psabi-doc ↩
-
Instruksi Supervisor Return. Kebalikan dari
ecall. Instruksi ini menurunkan hak akses dari S-mode kembali ke U-mode dan melanjutkan eksekusi di userspace. Referensi: Privileged Architecture spec pada https://github.com/riscv/riscv-isa-manual ↩