
Jadi, langsung ke intinya. Kami akan menulis di Linux, di NASM dan menggunakan QEMU. Ini mudah dipasang, jadi lewati langkah ini.
Dapat dipahami bahwa pembaca sudah terbiasa dengan sintaksis NASM setidaknya pada tingkat dasar (namun, tidak akan ada sesuatu yang sangat rumit di sini) dan mengerti apa itu register.
Teori dasar
Hal pertama yang memulai prosesor ketika komputer dihidupkan adalah kode BIOS (atau UEFI, tetapi di sini saya hanya akan berbicara tentang BIOS), yang "terhubung" ke memori motherboard (khususnya, pada 0xFFFFFFF0).
Segera setelah menyalakan BIOS, Power-On Self-Test (POST) dimulai - pengujian mandiri setelah dihidupkan. BIOS memeriksa kesehatan memori, mendeteksi dan menginisialisasi perangkat yang terhubung, memeriksa register, menentukan ukuran memori, dan seterusnya dan seterusnya.
Langkah selanjutnya adalah mengidentifikasi disk boot dari mana Anda dapat mem-boot OS. Disk boot adalah disk (atau drive lain) yang memiliki 2 byte terakhir dari sektor pertama (sektor pertama berarti 512 byte pertama drive, karena 1 sektor = 512 byte) adalah 55 dan AA (dalam format heksadesimal). Segera setelah disk boot ditemukan, BIOS akan memuat 512 byte pertamanya ke dalam RAM di alamat 0x7c00 dan mentransfer kontrol ke prosesor di alamat ini.
Tentu saja, dalam 512 byte ini tidak akan berfungsi agar sesuai dengan sistem operasi yang lengkap. Oleh karena itu, biasanya di sektor ini dimasukkan loader utama, yang memuat kode OS utama ke dalam RAM dan mentransfer kontrol ke sana.
Sejak awal, prosesor telah berjalan dalam Mode Nyata (= mode 16-bit). Ini berarti bahwa itu hanya dapat bekerja dengan data 16-bit dan menggunakan pengalamatan memori tersegmentasi, dan juga hanya dapat mengatasi memori 1 MB. Tapi kita tidak akan menggunakan yang kedua di sini. Gambar di bawah ini menunjukkan keadaan RAM saat mentransfer kontrol ke kode kami (gambar diambil dari sini ).

Hal terakhir yang dikatakan sebelum bagian praktisnya adalah interupsi. Interupsi adalah sinyal khusus (misalnya, dari perangkat input, seperti keyboard atau mouse) ke prosesor yang mengatakan bahwa perlu untuk segera menghentikan eksekusi kode saat ini dan menjalankan kode penangan interrupt. Semua alamat penangan interupsi terletak di Interrupt Descriptor Table (IDT) di memori utama. Setiap interrupt memiliki handler interrupt sendiri. Misalnya, ketika tombol keyboard ditekan, interupsi dipanggil, prosesor berhenti, mengingat alamat instruksi yang terputus, menyimpan semua nilai registernya (pada tumpukan) dan melanjutkan untuk mengeksekusi penangan interrupt. Segera setelah eksekusi berakhir, prosesor mengembalikan nilai register dan melompat kembali ke instruksi yang terputus dan melanjutkan eksekusi.
Misalnya, untuk menampilkan sesuatu di layar, BIOS menggunakan interupsi 0x10 (format heksadesimal), dan interupsi 0x16 digunakan untuk menunggu penekanan tombol. Sebenarnya, ini semua adalah interupsi yang akan kita butuhkan di sini.
Juga, setiap interupsi memiliki subfungsi sendiri yang menentukan keunikan perilakunya. Untuk menampilkan sesuatu dalam format teks (!), Anda harus memasukkan nilai 0x0e dalam register AH. Selain itu, interupsi memiliki parameter sendiri. 0x10 mengambil nilai dari ah (mendefinisikan subfungsi tertentu) dan al (karakter yang akan dicetak). Dengan cara ini
mov ah, 0x0e mov al, 'x' int 0x10
menampilkan karakter 'x'. 0x16 mengambil nilai dari ah (subfungsi spesifik) dan memuat nilai kunci yang dimasukkan ke register al. Kami akan menggunakan fungsi 0x0.
Bagian praktis
Mari kita mulai dengan kode pembantu. Kita akan membutuhkan fungsi membandingkan dua garis dan fungsi menampilkan garis di layar. Saya mencoba menggambarkan operasi fungsi-fungsi ini dalam komentar sejelas mungkin.
str_compare.asm:
compare_strs_si_bx: push si ; push bx push ax comp: mov ah, [bx] ; , cmp [si], ah ; ah jne not_equal ; , cmp byte [si], 0 ; , je first_zero ; inc si ; bx si inc bx jmp comp ; first_zero: cmp byte [bx], 0 ; bx != 0, , jne not_equal ; , not_equal mov cx, 1 ; , cx = 1 pop si ; pop bx pop ax ret ; not_equal: mov cx, 0 ; , cx = 0 pop si ; pop bx pop ax ret ;
Fungsi menerima register SI dan BX sebagai parameter. Jika garis sama, maka CX diatur ke 1, jika tidak 0.
Perlu dicatat juga bahwa register AX, BX, CX dan DX dibagi menjadi dua bagian byte tunggal: AH, BH, CH, dan DH untuk byte tinggi, dan AL, BL, CL, dan DL untuk byte rendah.
Awalnya, diasumsikan bahwa dalam bx dan si ada pointer (!) (Yaitu, menyimpan alamat dalam memori) ke beberapa alamat dalam memori di mana awal baris berada. Operasi [bx] akan mengambil pointer dari bx, ia akan pergi ke alamat ini dan mengambil beberapa nilai dari sana. inc bx berarti bahwa sekarang pointer akan merujuk ke alamat segera setelah alamat asli.
print_string.asm:
print_string_si: push ax ; ax mov ah, 0x0e ; ah 0x0e, call print_next_char ; pop ax ; ax ret ; print_next_char: mov al, [si] ; cmp al, 0 ; si jz if_zero ; int 0x10 ; al inc si ; jmp print_next_char ; ... if_zero: ret
Sebagai parameter, fungsi mengambil register SI dan byte demi byte mencetak sebuah string.
Sekarang mari kita beralih ke kode utama. Pertama, mari kita definisikan semua variabel (kode ini ada di bagian paling akhir file):
; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
Karakter carriage return memindahkan carriage ke tepi kiri layar, yaitu ke awal baris.
input: times 64 db 0
berarti bahwa kami mengalokasikan 64 byte di bawah buffer untuk input dan mengisinya dengan nol.
Sisa variabel diperlukan untuk menampilkan beberapa informasi, lebih jauh ke bawah kode Anda akan mengerti mengapa mereka semua diperlukan.
times 510 - ($-$$) db 0 dw 0xaa55
berarti bahwa kita secara eksplisit mengatur ukuran file output (dengan ekstensi .bin) menjadi 512 byte, isi 510 byte pertama dengan nol (tentu saja, mereka diisi sebelum seluruh kode dieksekusi), dan dua byte terakhir dengan "sihir" yang sama byte 55 dan AA . $ berarti alamat dari instruksi saat ini, dan $$ adalah alamat dari instruksi pertama dari kode kami.
Mari kita beralih ke kode aktual:
org 0x7c00 ; (1) bits 16 ; (2) jmp start ; start %include "print_string.asm" ; %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ; (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ; (4) mov si, greetings ; call print_string_si ; mainloop
(1) Perintah ini menjelaskan kepada NASM bahwa kita menjalankan kode mulai dari 0x7c00. Ini memungkinkannya untuk secara otomatis mem-bias semua alamat relatif ke alamat itu sehingga kami tidak secara eksplisit melakukan ini.
(2) Perintah ini menginstruksikan NASM bahwa kami beroperasi dalam mode 16-bit.
(3) Saat diluncurkan, QEMU mencetak banyak informasi yang tidak kita butuhkan. Untuk melakukan ini, atur ke ah 0x00, ke al 0x03 dan panggil 0x10 untuk menghapus layar segalanya.
(4) Untuk menyimpan register pada stack, Anda harus menentukan di mana alamat vertexnya akan terletak menggunakan pointer stack SP. SP akan menunjukkan area dalam memori di mana nilai selanjutnya akan ditulis. Tambahkan nilai ke tumpukan - SP menurunkan memori sebesar 2 byte (karena kita berada dalam Mode Nyata, di mana semua operan register adalah 16-bit, mis. Nilai byte ganda,). Kami menentukan 0x7c00, sehingga nilai pada stack akan disimpan tepat di sebelah kode kami di memori. Sekali lagi - tumpukan tumbuh turun (!). Ini berarti bahwa semakin banyak nilai yang ada pada stack, semakin sedikit memori yang ditunjukkan oleh pointer dari stack SP.
mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop...
Loop utama. Di sini, dengan setiap iterasi, kita mencetak simbol ">", setelah itu kita memanggil fungsi get_input, yang mengimplementasikan kerja dengan gangguan keyboard.
get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ;
(1) [input + bx] berarti bahwa kita mengambil alamat awal input buffer input dan menambahkan bx padanya, yaitu, kita bisa mendapatkan bx + elemen pertama buffer.
stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $
Semuanya sederhana di sini - jika Anda menekan Ctrl + C, komputer hanya menjalankan fungsi jmp $ tanpa henti.
backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ;
Agar tidak menghapus karakter '>' saat menekan backspace, kami memeriksa apakah input kosong. Jika tidak, maka jangan lakukan apa pun.
check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!"
Di sini, saya pikir semuanya jelas dari komentar.
equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done
Bergantung pada apa yang dimasukkan, kami menampilkan teks dari variabel help_desc atau teks dari variabel wrong_command.
; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret
Sebenarnya, seluruh kode adalah:
prompt.asm:
org 0x7c00 bits 16 jmp start ; start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ; , ; mov ah, 0x00 ; mov al, 0x03 int 0x10 mov sp, 0x7c00 ; mov si, greetings ; call print_string_si ; mainloop mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop... get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ; stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $ backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ; check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret ; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
Untuk mengkompilasi semua ini, masukkan perintah:
nasm -f bin prompt.asm -o bootloader.bin
Dan kami mendapatkan biner dengan kode kami di output. Sekarang jalankan emulator QEMU dengan file ini (-ditor stdio memungkinkan Anda untuk menampilkan nilai register kapan saja menggunakan perintah print $ reg):
qemu-system-i386 bootloader.bin -monitor stdio
Dan kami mendapatkan output:
