Contoh yang bisa dijalankan
Mari kita buat dan jalankan beberapa program dunia bare metal yang sangat kecil yang berjalan tanpa OS di:
Kami juga akan mencobanya pada emulator QEMU sebanyak mungkin, karena lebih aman dan nyaman untuk dikembangkan. Tes QEMU telah berada di host Ubuntu 18.04 dengan QEMU 2.11.1 yang telah dikemas sebelumnya.
Kode semua contoh x86 di bawah ini dan lainnya ada pada repo GitHub ini .
Cara menjalankan contoh di perangkat keras x86 nyata
Ingatlah bahwa menjalankan contoh pada perangkat keras nyata bisa berbahaya, misalnya Anda dapat menghapus disk atau merusak perangkat keras secara tidak sengaja: hanya lakukan ini pada mesin lama yang tidak berisi data penting! Atau bahkan lebih baik, gunakan devboards semi-sekali pakai murah seperti Raspberry Pi, lihat contoh ARM di bawah ini.
Untuk laptop x86 biasa, Anda harus melakukan sesuatu seperti:
Bakar gambar ke stik USB (akan menghancurkan data Anda!):
sudo dd if=main.img of=/dev/sdX
pasang USB di komputer
Hidupkan
katakan itu untuk boot dari USB.
Ini berarti membuat firmware memilih USB sebelum hard disk.
Jika itu bukan perilaku default mesin Anda, terus tekan Enter, F12, ESC atau kunci aneh lainnya setelah dinyalakan hingga Anda mendapatkan menu boot di mana Anda dapat memilih untuk boot dari USB.
Seringkali dimungkinkan untuk mengonfigurasi urutan pencarian di menu-menu itu.
Misalnya, pada T430 saya, saya melihat yang berikut ini.
Setelah dihidupkan, inilah saatnya saya harus menekan Enter untuk masuk ke menu boot:
Kemudian, di sini saya harus menekan F12 untuk memilih USB sebagai perangkat boot:
Dari sana, saya dapat memilih USB sebagai perangkat boot seperti ini:
Atau, untuk mengubah urutan boot dan memilih USB untuk memiliki prioritas lebih tinggi sehingga saya tidak harus memilihnya secara manual setiap kali, saya akan menekan F1 pada layar "Startup Interrupt Menu", dan kemudian arahkan ke:
Sektor boot
Pada x86, level paling sederhana dan terendah yang dapat Anda lakukan adalah membuat Master Boot Sector (MBR) , yang merupakan jenis sektor boot , dan kemudian instal ke disk.
Di sini kita membuat satu dengan satu printf
panggilan:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Hasil:
Perhatikan bahwa bahkan tanpa melakukan apa pun, beberapa karakter sudah dicetak di layar. Itu dicetak oleh firmware, dan berfungsi untuk mengidentifikasi sistem.
Dan pada T430 kita hanya mendapatkan layar kosong dengan kursor yang berkedip:
main.img
berisi yang berikut ini:
\364
dalam oktal == 0xf4
dalam hex: pengkodean untuk hlt
instruksi, yang memberitahu CPU untuk berhenti bekerja.
Karenanya program kami tidak akan melakukan apa-apa: hanya memulai dan berhenti.
Kami menggunakan oktal karena \x
nomor hex tidak ditentukan oleh POSIX.
Kami dapat memperoleh enkode ini dengan mudah dengan:
echo hlt > a.S
as -o a.o a.S
objdump -S a.o
yang keluaran:
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: f4 hlt
tetapi ini juga didokumentasikan dalam manual Intel.
%509s
menghasilkan 509 ruang. Diperlukan untuk mengisi file hingga byte 510.
\125\252
dalam oktal == 0x55
diikuti oleh 0xaa
.
Ini adalah 2 byte sihir yang diperlukan yang harus berupa byte 511 dan 512.
BIOS memeriksa semua disk kami untuk mencari yang dapat di-boot, dan BIOS hanya mempertimbangkan yang dapat di-boot yang memiliki dua byte ajaib itu.
Jika tidak ada, perangkat keras tidak akan memperlakukan ini sebagai disk yang dapat di-boot.
Jika Anda bukan seorang printf
master, Anda dapat mengkonfirmasi konten main.img
dengan:
hd main.img
yang menunjukkan yang diharapkan:
00000000 f4 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 |. |
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
*
000001f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 55 aa | U.|
00000200
di mana 20
ruang di ASCII.
Firmware BIOS membaca 512 byte tersebut dari disk, memasukkannya ke dalam memori, dan menetapkan PC ke byte pertama untuk mulai menjalankannya.
Halo sektor boot dunia
Sekarang kita telah membuat program minimal, mari kita pindah ke dunia halo.
Pertanyaan yang jelas adalah: bagaimana cara melakukan IO? Beberapa pilihan:
minta firmware, misalnya BIOS atau UEFI, untuk melakukannya untuk kita
VGA: wilayah memori khusus yang akan dicetak ke layar jika ditulis. Dapat digunakan dalam Mode Terlindungi.
menulis driver dan berbicara langsung ke perangkat keras layar. Ini adalah cara yang "tepat" untuk melakukannya: lebih kuat, tetapi lebih kompleks.
port serial . Ini adalah protokol standar yang sangat sederhana yang mengirim dan menerima karakter dari terminal host.
Di desktop, tampilannya seperti ini:
Sumber .
Sayangnya ini tidak terpapar pada kebanyakan laptop modern, tetapi merupakan cara umum untuk menggunakan papan pengembangan, lihat contoh ARM di bawah ini.
Ini benar-benar memalukan, karena antarmuka seperti itu sangat berguna untuk men-debug kernel Linux misalnya .
gunakan fitur debug chip. ARM menyebut semihosting mereka misalnya. Pada perangkat keras nyata, ini memerlukan beberapa perangkat keras dan dukungan perangkat lunak tambahan, tetapi pada emulator itu bisa menjadi pilihan nyaman gratis. Contoh .
Di sini kita akan melakukan contoh BIOS karena lebih sederhana pada x86. Tetapi perhatikan bahwa ini bukan metode yang paling kuat.
utama
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
GitHub hulu .
link.ld
SECTIONS
{
/* The BIOS loads the code from the disk to this location.
* We must tell that to the linker so that it can properly
* calculate the addresses of symbols we might jump to.
*/
. = 0x7c00;
.text :
{
__start = .;
*(.text)
/* Place the magic boot bytes at the end of the first 512 sector. */
. = 0x1FE;
SHORT(0xAA55)
}
}
Kumpulkan dan tautkan dengan:
as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img
Hasil:
Dan pada T430:
Diuji pada: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disk dihasilkan pada host Ubuntu 18.04.
Selain instruksi perakitan pengguna darat standar, kami memiliki:
.code16
: memberitahu GAS untuk mengeluarkan kode 16-bit
cli
: nonaktifkan interupsi perangkat lunak. Itu bisa membuat prosesor mulai berjalan lagi setelahhlt
int $0x10
: melakukan panggilan BIOS. Inilah yang mencetak karakter satu per satu.
Bendera tautan penting adalah:
--oformat binary
: output kode rakitan biner mentah, jangan bungkus di dalam file ELF seperti halnya untuk executable userland biasa.
Untuk lebih memahami bagian skrip linker, biasakan diri Anda dengan langkah relokasi menghubungkan: Apa yang dilakukan linker?
Cooler x86 program bare metal
Berikut adalah beberapa pengaturan logam telanjang yang lebih kompleks yang telah saya capai:
Gunakan C sebagai ganti perakitan
Rangkuman: gunakan GRUB multiboot, yang akan menyelesaikan banyak masalah menjengkelkan yang tidak pernah Anda pikirkan. Lihat bagian di bawah ini.
Kesulitan utama pada x86 adalah bahwa BIOS hanya memuat 512 byte dari disk ke memori, dan Anda cenderung meledakkan 512 byte tersebut saat menggunakan C!
Untuk mengatasinya, kita bisa menggunakan bootloader dua tahap . Ini membuat panggilan BIOS lebih lanjut, yang memuat lebih banyak byte dari disk ke dalam memori. Berikut ini adalah contoh perakitan tahap 2 minimal dari awal menggunakan panggilan BIOS int 0x13 :
Kalau tidak:
- jika Anda hanya perlu bekerja di QEMU tetapi bukan perangkat keras nyata, gunakan
-kernel
opsi, yang memuat seluruh file ELF ke dalam memori. Ini adalah contoh ARM yang saya buat dengan metode itu .
- untuk Raspberry Pi, firmware default menangani pemuatan gambar untuk kita dari file ELF bernama
kernel7.img
, seperti halnya QEMU -kernel
.
Hanya untuk tujuan pendidikan, berikut adalah contoh minimal C satu tahap :
main.c
void main(void) {
int i;
char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
for (i = 0; i < sizeof(s); ++i) {
__asm__ (
"int $0x10" : : "a" ((0x0e << 8) | s[i])
);
}
while (1) {
__asm__ ("hlt");
};
}
entri
.code16
.text
.global mystart
mystart:
ljmp $0, $.setcs
.setcs:
xor %ax, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov $__stack_top, %esp
cld
call main
linker.ld
ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: /programming/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* /programming/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
.sig : AT(ADDR(.text) + 512 - 2)
{
SHORT(0xaa55);
}
/DISCARD/ : {
*(.eh_frame)
}
__stack_bottom = .;
. = . + 0x1000;
__stack_top = .;
}
Lari
set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw
C library standar
Akan lebih menyenangkan jika Anda juga ingin menggunakan pustaka standar C, karena kami tidak memiliki kernel Linux, yang mengimplementasikan banyak fungsi pustaka standar C melalui POSIX .
Beberapa kemungkinan, tanpa masuk ke sistem operasi lengkap seperti Linux, termasuk:
Tulis milikmu sendiri. Itu hanya sekelompok header dan file C pada akhirnya, kan? Baik??
Newlib
Contoh terperinci di: /electronics/223929/c-standard-libraries-on-bare-metal/223931
Mengimplementasikan Newlib semua hal yang membosankan non-OS khusus untuk Anda, misalnya memcmp
, memcpy
, dll
Kemudian, ia memberikan beberapa potongan untuk Anda menerapkan syscalls yang Anda butuhkan sendiri.
Sebagai contoh, kita dapat mengimplementasikan exit()
ARM melalui semihosting dengan:
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
seperti yang ditunjukkan pada contoh ini .
Misalnya, Anda bisa mengarahkan ulang printf
ke sistem UART atau ARM, atau menerapkannya exit()
dengan semihosting .
sistem operasi tertanam seperti FreeRTOS dan Zephyr .
Sistem operasi seperti itu biasanya memungkinkan Anda untuk mematikan penjadwalan pre-emptive, sehingga memberi Anda kendali penuh atas runtime program.
Mereka dapat dilihat sebagai semacam Newlib pra-implementasi.
GNU GRUB Multiboot
Sektor boot sederhana, tetapi tidak terlalu nyaman:
- Anda hanya dapat memiliki satu OS per disk
- kode muat harus sangat kecil dan masuk ke dalam 512 byte
- Anda harus melakukan banyak startup sendiri, seperti pindah ke mode terlindungi
Karena alasan itulah GNU GRUB membuat format file yang lebih nyaman yang disebut multiboot.
Contoh kerja minimal: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Saya juga menggunakannya pada repo contoh GitHub saya untuk dapat dengan mudah menjalankan semua contoh pada perangkat keras nyata tanpa membakar USB satu juta kali.
Hasil QEMU:
T430:
Jika Anda menyiapkan OS Anda sebagai file multiboot, GRUB kemudian dapat menemukannya di dalam sistem file biasa.
Inilah yang dilakukan sebagian besar distro, menempatkan gambar OS di bawah /boot
.
File multiboot pada dasarnya adalah file ELF dengan header khusus. Mereka ditentukan oleh GRUB di: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Anda dapat mengubah file multiboot menjadi disk yang dapat di-boot dengan grub-mkrescue
.
Firmware
Sebenarnya, sektor boot Anda bukan perangkat lunak pertama yang berjalan pada CPU sistem.
Apa yang sebenarnya berjalan pertama adalah apa yang disebut firmware , yang merupakan perangkat lunak:
- dibuat oleh produsen perangkat keras
- biasanya sumber tertutup tetapi kemungkinan berbasis C
- disimpan dalam memori hanya-baca, dan karenanya lebih sulit / tidak mungkin untuk dimodifikasi tanpa persetujuan vendor.
Firmwares yang terkenal meliputi:
- BIOS : firmware x86 lama semua sekarang. SeaBIOS adalah implementasi open source default yang digunakan oleh QEMU.
- UEFI : penerus BIOS, lebih terstandarisasi, tetapi lebih mampu, dan sangat kembung.
- Coreboot : upaya open source lintas lengkung mulia
Firmware melakukan hal-hal seperti:
lewati setiap hard disk, USB, jaringan, dll. hingga Anda menemukan sesuatu yang dapat di-boot.
Ketika kami menjalankan QEMU, -hda
mengatakan itu main.img
adalah hard disk yang terhubung ke perangkat keras, dan hda
itu yang pertama kali dicoba, dan itu yang digunakan.
muat 512 byte pertama ke alamat memori RAM 0x7c00
, letakkan RIP CPU di sana, dan biarkan berjalan
menunjukkan hal-hal seperti menu boot atau panggilan cetak BIOS pada tampilan
Firmware menawarkan fungsionalitas mirip OS di mana sebagian besar OS bergantung. Misalnya subset Python telah porting untuk dijalankan di BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Dapat dikatakan bahwa firmware tidak dapat dibedakan dari OS, dan bahwa firmware adalah satu-satunya pemrograman bare metal yang "benar" yang dapat dilakukan.
Seperti yang dikatakan CoreOS dev ini :
Bagian yang sulit
Saat Anda menyalakan PC, chip yang membentuk chipset (northbridge, southbridge dan SuperIO) belum diinisialisasi dengan benar. Meskipun ROM BIOS sejauh mungkin dihapus dari CPU, ini dapat diakses oleh CPU, karena harus demikian, jika tidak, CPU tidak akan memiliki instruksi untuk mengeksekusi. Ini tidak berarti bahwa ROM BIOS sepenuhnya dipetakan, biasanya tidak. Tapi cukup dipetakan untuk memulai proses boot. Perangkat lain, lupakan saja.
Ketika Anda menjalankan Coreboot di bawah QEMU, Anda dapat bereksperimen dengan lapisan Coreboot yang lebih tinggi dan dengan muatan, tetapi QEMU menawarkan sedikit peluang untuk bereksperimen dengan kode startup tingkat rendah. Untuk satu hal, RAM hanya berfungsi sejak awal.
Poskan keadaan awal BIOS
Seperti banyak hal dalam perangkat keras, standardisasi lemah, dan salah satu hal yang tidak boleh Anda andalkan adalah keadaan awal register ketika kode Anda mulai berjalan setelah BIOS.
Jadi bantulah diri Anda sendiri dan gunakan beberapa kode inisialisasi seperti berikut: https://stackoverflow.com/a/32509555/895245
Mendaftar suka %ds
dan %es
memiliki efek samping yang penting, jadi Anda harus membidiknya walaupun Anda tidak menggunakannya secara eksplisit.
Perhatikan bahwa beberapa emulator lebih bagus daripada perangkat keras asli dan memberikan Anda kondisi awal yang bagus. Kemudian ketika Anda menjalankannya pada perangkat keras nyata, semuanya rusak.
El Torito
Format yang dapat dibakar ke CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Dimungkinkan juga untuk menghasilkan gambar hibrida yang bekerja pada ISO atau USB. Ini dapat dilakukan dengan grub-mkrescue
( contoh ), dan juga dilakukan oleh kernel Linux saat make isoimage
menggunakan isohybrid
.
LENGAN
Dalam ARM, ide-ide umumnya sama.
Tidak ada firmware pra-instal semi-standar yang tersedia secara luas seperti BIOS untuk kami gunakan untuk IO, jadi dua jenis IO paling sederhana yang dapat kami lakukan adalah:
- serial, yang banyak tersedia di devboards
- berkedip LED
Saya telah mengunggah:
Beberapa perbedaan dari x86 termasuk:
IO dilakukan dengan menulis ke alamat sihir secara langsung, tidak ada in
dan out
instruksi.
Ini disebut memori yang dipetakan IO .
untuk beberapa perangkat keras nyata, seperti Raspberry Pi, Anda dapat menambahkan sendiri firmware (BIOS) ke gambar disk.
Itu adalah hal yang baik, karena itu membuat memperbarui firmware itu lebih transparan.
Sumber daya