Membuat Aplikasi UEFI Dengan EDK2
Bootstrap loader adalah komponen sistem operasi yang dikerjakan paling awal pada saat komputer dinyalakan. Pada era BIOS, bootstrap loader dapat menggunakan BIOS interrupt call seperti 10h
untuk mencetak tulisan di layar dan 13h
untuk membaca disk. Karena layanan yang tersedia pada kondisi ini masih sangat terbatas, bootstrap loader biasanya akan mempersiapkan perangkat input/output dan dukungan file system (seperti FAT32, ext4, NTFS dan sebagainya) untuk membaca file kernel sistem operasi yang tersimpan di disk. Ini adalah permasalahan “telur dan ayam” karena kode program bootstrap loader yang lumayan kompleks tersebut tidak akan dipakai lagi setelah kernel sistem operasi dijalankan. Unified Extensible Firmware Interface (UEFI) berusaha menyelesaikan permasalahan ini dengan melakukan standarisasi layanan (dalam bentuk boot services dan runtime services) serta dukungan file system FAT32 secara bawaan. Hampir semua perangkat PC baru saat ini sudah menggunakan UEFI dan sistem operasi populer untuk PC seperti Windows & Linux juga sudah mendukung UEFI.
Bila kode program bootstrap loader era BIOS biasanya di-simpan di sektor pertama yang disebut Master Boot Record (MBR), kode program bootstrap loader di UEFI disimpan dalam bentuk file EFI di sebuah partisi FAT32 dengan nama seperti BOOTX64.EFI
. File EFI ini menggunakan format Portable Executable (PE) sama seperti yang dipakai oleh file EXE di sistem operasi Windows. Selain itu, kebanyakan motherboard PC juga dilengkapi dengan UEFI Shell yang memungkinkan pengguna untuk menjalankan file EFI secara interaktif saat komputer dinyalakan (sebelum sistem operasi dikerjakan). Pada tulisan ini, saya akan mencoba membuat aplikasi UEFI sederhana yang dapat dijalankan dari UEFI Shell.
EFI Development Kit (EDK) II
Walaupun UEFI terdengar lebih mudah di-program, proses awal untuk menulis sebuah aplikasi UEFI membutuhkan usaha yang cukup banyak bila dibandingkan menulis kode Assembly untuk MBR yang hasilnya dapat langsung disalin lewat dd
. Untuk membuat aplikasi UEFI, saya akan menggunakan Tianocore EDK II yang merupakan platform referensi resmi untuk UEFI dari Intel. Saya akan mulai dengan men-clone kode program EDK II dari GitHub dengan menggunakan perintah berikut ini:
Sampai disini, saya akan memperoleh folder /home/developer/edk2
dengan struktur folder yang terlihat seperti berikut ini:
Directoryhome/developer/edk2
DirectoryOvmfPkg
- …
DirectoryMdeModulePkg
- …
DirectoryMdePkg
- …
- edksetup.sh
- …
Untuk menghindari kesalahan versi dan tools yang tidak ter-install, saya akan menjalankan proses building dari dalam container Docker. Tianocore memiliki beberapa image siap pakai yang dapat dilihat https://github.com/tianocore/containers. Sebagai latihan, saya akan menggunakan image ghcr.io/tianocore/containers/ubuntu-22-test:latest
. Container ini sudah dilengkapi dengan QEMU yang dapat saya pakai untuk menjalankan file EFI pada saat development. Karena file EFI hanya bisa dijalankan oleh UEFI, bukan oleh sistem operasi seperti aplikasi biasanya, saya tentu saja tidak ingin me-restart PC setiap kali ingin melihat hasil perubahan kode program.
Untuk menjalankan container secara interaktif, saya dapat memberikan perintah seperti berikut ini:
Pada perintah di atas, saya menjalankan edksetup.sh
untuk melakukan inisialisasi proyek EDK2.
Open Virtual Machine Firmware (OVMF)
OVMF adalah implementasi EDK2 untuk QEMU. Dengan OVMF, mesin virtual QEMU akan memiliki dukungan UEFI. Langkah pertama yang perlu saya lakukan untuk memakai OVMF adalah men-build proyek tersebut. Saya akan mulai dengan mengubah beberapa nilai di Conf/target.txt
dengan isi seperti berikut ini:
Berikutnya, saya akan memulai proses building dengan perintah seperti berikut ini:
Pada perintah di atas, setelah proses building selesai, saya akan memperoleh file Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd
yang merupakan hasil akhir yang dapat dipakai oleh QEMU. Karena akan sering dipakai dan jarang berubah, saya akan men-copy file ini sebagai bios.bin
dengan perintah seperti:
Sekarang, saya dapat menggunakan file bios.bin
tersebut di QEMU dengan perintah seperti berikut ini:
Untuk keluar dari QEMU di modus teks, tekan tombol Ctrl+A
diikuti dengan X
.
Menulis Aplikasi UEFI
Sekarang saatnya untuk menulis kode program aplikasi UEFI. Proyek EDK2 dikelompokkan ke dalam satuan yang disebut sebagai package. Sebagai contoh, pada saat men-build OVMF, saya bekerja dengan package OvmfPkg
. Untuk aplikasi UEFI latihan ini, saya akan membuat sebuah package baru di folder tersendiri yang berada di luar folder EDK2. Folder package yang saya buat memiliki struktur seperti berikut ini:
Directory/home/developer/MyAppPkg
DirectoryApplication
DirectorySampleApp
- SampleApp.c
- SampleApp.inf
- MyAppPkg.dec
- MyAppPkg.dsc
File MyAppPkg.dec
merupakan file yang selalu ada di package EDK2 yang berisi informasi tentang package tersebut. Sebagai contoh, saya akan menambahkan isi berikut ini pada file MyAppPkg.dec
:
Pada definisi di atas, saya memberikan nama package di PACKAGE_NAME
, versi package di PACKAGE_VERSION
dan juga sebuah GUID unik sebagai identitas package di PACKAGE_GUID
. Selain file DEC, sebuah package di EDK2 juga memiliki file DSC. Sebagai contoh, saya menambahkan isi berikut ini pada file MyAppPkg.dsc
:
Sama seperti di file DEC, pada file DSC, setiap package memiliki nilai PLATFORM_NAME
, PLATFORM_GUID
dan OUTPUT_DIRECTORY
yang unik. Saya dapat mengatur target arsitektur di SUPPORTED_ARCHITECTURES
. Karena hanya akan dipakai di PC pribadi, saya menggunakan nilai X64
untuk SUPPORTED_ARCHITECTURES
.
File DSC dapat menyertakan file DSC lain dengan menggunakan !include
. Sebagai contoh, pada deklarasi di atas, saya menggunakan !include MdePkg/MdeLibs.dsc.inc
untuk menambahkan deklarasi [LibraryClasses]
yang umum dipakai. Nilai pada [LibraryClasses]
menggunakan format seperti NamaLibrary|LokasiLibrary
. File INF di package ini nantinya dapat merujuk ke library dengan menggunakan NamaLibrary
. Sebagai contoh, karena akan membuat aplikasi UEFI, saya menambahkan UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
. Di file INF untuk aplikasi nanti, saya cukup menggunakan UefiApplicationEntryPoint
sebagai nilai di [LibraryClasses]
.
Sebuah package mengandung satu atau lebih komponen. Pada EDK2, komponen berupa aplikasi UEFI biasanya diletakkan di folder Application
, komponen library di folder Library
, dan sebagainya. Definisi komponen untuk sebuah package perlu ditambahkan di bagian [Components]
di file DSC. Sebagai contoh, karena saya hanya akan membuat sebuah aplikasi tunggal, isi [Components]
di file DSC saya hanya berupa Application/SampleApp/SampleApp.inf
.
Masing-masing komponen memiliki file INF-nya sendiri. Sebagai latihan, saya akan menggunakan isi berikut ini untuk file SampleApp.inf
:
Pada deklarasi di atas, saya menambahkan nama komponen di BASE_NAME
, versi komponen di VERSION_STRING
dan sebuah GUID unik untuk komponen di FILE_GUID
. Saya menggunakan nilai UEFI_APPLICATION
di MODULE_TYPE
karena ini adalah aplikasi UEFI. Beberapa nilai lain yang mungkin untuk MODULE_TYPE
adalah UEFI_DRIVER
bila ini adalah driver UEFI, USER_DEFINED
untuk logo, BASE
untuk dipakai dimana saja, dan sebagainya.
Khusus untuk aplikasi UEFI, saya perlu mengisi nilai ENTRY_POINT
dengan nama function yang akan dikerjakan saat aplikasi dijalankan. Pada deklarasi di atas, karena nilai ENTRY_POINT
adalah SampleAppMain
, maka saya perlu membuat sebuah function dengan nama SampleAppMain
. Saya perlu mendaftarkan seluruh file kode program yang dipakai di bagian [Sources]
.
Bila kode program bergantung pada package lain, saya perlu menyertakan file DEC package tersebut di bagian [Packages]
. MdePkg
dibutuhkan oleh build system EDK2 sehingga saya perlu menyertakan package tersebut. Nilai [LibraryClasses]
adalah nama library yang dibutuhkan oleh komponen sesuai dengan yang dideklarasikan di file DSC.
Sekarang saatnya menulis kode program. Sebagai contoh, saya akan membuat sebuah kode program yang men-cetak tulisan ke layar, menunggu hingga sebuah tombol di keyboard ditekan dan menghapus layar sebelum keluar:
Dengan hanya 10 baris, kode program di atas terlihat jauh lebih sederhana bila dibandingkan dengan kode program MBR yang memanggil BIOS interrupt call. EDK2 memiliki styling kode program yang agak berbeda dari kode program C pada umumnya. Sebagai contoh, pada EDK2, variabel global diawali huruf kecil sementara untuk variabel lokal menggunakan camel case yang diawali huruf kapital. Kode program EDK2 juga tidak menggunakan tipe data C melainkan macro yang disediakan seperti UINTN
. Kode program di atas sendiri sebenarnya tidak sepenuhnya mengikuti code style EDK2 seperti selalu menambahkan spasi sebelum tanda kurung buka (termasuk pada pemanggilan function).
Menjalankan Aplikasi UEFI
Untuk men-build package yang telah saya buat sebelumnya, saya perlu men-mount dua folder berbeda di container: satu untuk folder package dan satu lagi untuk folder yang berisi EDK2. Saya kemudian menggunakan variabel PACKAGES_PATH
yang berisi lokasi kedua folder tersebut (di dalam container) sehingga EDK2 tahu bahwa kedua folder akan dipakai bersamaaan. Untuk itu, saya bisa memberikan perintah seperti berikut ini:
Saya kemudian memanggil tool build
dari EDK2 untuk men-build package aplikasi saya dengan menyertakan nama file DSC seperti berikut ini:
Hasil akhir dari perintah di atas adalah file Build/MyAppPkg/RELEASE_GCC5/X64/SampleApp.efi
. Saya dapat menjalankan file ini di QEMU dengan menggunakan perintah seperti berikut ini:
QEMU dapat mensimulasikan sebuah folder menjadi sebuah disk FAT32 dengan menggunakan -drive file=fat:rw:lokasi_folder
. Dengan fitur ini, QEMU akan membuat sebuah disk yang berisi folder output dimana SampleApp.efi
berada. Setelah masuk ke dalam UEFI Shell, saya memberikan perintah fs0:
untuk beralih ke disk virtual tersebut. Untuk menjalankan aplikasi UEFI, saya cukup mengetikkan nama file-nya seperti SampleApp.efi
. Pada hasil di atas, terlihat bahwa aplikasi UEFI bekerja sesuai dengan yang diharapkan.
Untuk menjalankan file EFI pada perangkat nyata, saya perlu menyiapkan sebuah USB flash drive dengan partisi FAT32 (atau NTFS bila UEFI pada PC mendukung NTFS) dan menyalin file SampleApp.efi
ke partisi tersebut. Pada saat komputer dinyalakan, saya dapat menekan tombol DEL (tombol ini bisa berbeda tergantung pada motherboard yang dipakai) untuk masuk ke halaman pengaturan BIOS/UEFI, lalu saya perlu memilih menu seperti Boot to UEFI Shell. Tidak lama kemudian saya akan menjumpai UEFI Shell sama seperti di QEMU dimana saya bisa memberikan perintah yang sama untuk menjalankan aplikasi UEFI.
Tidak semua UEFI pada perangkat PC dilengkapi dengan UEFI Shell. Bila UEFI pada perangkat keras tidak dilengkapi dengan UEFI Shell, saya dapat menjalankan aplikasi UEFI sama seperti bootloader sistem operasi pada umumnya. Sebagai contoh, notebook yang saya pakai tidak memiliki UEFI Shell, tetapi tetap mendeteksi file EFI di USB flash drive seperti yang terlihat pada gambar berikut ini:
Setelah memilih file EFI yang saya buat, ia tetap bekerja seperti yang diharapkan:
Konfigurasi IDE CLion
IDE CLion yang saya pakai tidak mendukung EDK2 sehingga proses pembuatan kode program terasa sangat sulit tanpa content assist, syntax highlightning dan sebagainya. Untuk mengakali ini, saya bisa membuat sebuah file CMakeLists.txt
dengan isi seperti berikut ini:
File ini murni dipakai oleh IDE saja dan sama sekali tidak dipakai pada proses build (CMake) karena EDK2 memiliki build tool-nya tersendiri. Saya mungkin perlu mengubah CMakeLists.txt
setiap kali file DSC dan INF berubah, namun ini pantas diperjuangkan demi fitur IDE yang layak seperti yang terlihat pada gambar berikut ini: