How to write your own Operating System (x86_64): Bootloader, 1st Stage
Πως να γράψετε το δικό σας Λειτουργικό Σύστημα (x86_64): Εκκινητής, 1o Στάδιο
IntroductionΕισαγωγή
First of all, we need a bootloader which will prepare the environment and will load our kernel. On the x86, the BIOS loads only one sector (a.k.a. the boot sector) from disk into RAM. This means that we are limited to 512 bytes for the bootloader. To bypass this restriction, we are going to use a two-stage bootloader. The first stage will be very small (less than 512 bytes) with the sole purpose of loading the second stage. The second stage will contain all the code we need in order to prepare the environment and load our kernel.
Πρώτα από όλα, χρειαζόμαστε έναν εκκινητή (bootloader) ο οποίος θα προετοιμάσει το περιβάλλον και θα φορτώσει τον πυρήνα μας. Στην αρχιτεκτονική x86, το BIOS φορτώνει μόνο έναν τομέα δίσκου (γνωστό ως boot sector) από τον δίσκο στη μνήμη RAM. Αυτό σημαίνει ότι θα πρέπει να χωρέσουμε τον εκκινητή μας σε λιγότερα από 512 bytes. Για να ξεπεράσουμε αυτόν τον περιορισμό, θα χρησιμοποιήσουμε έναν εκκινητή δύο σταδίων. Το πρώτο στάδιο θα είναι πολύ μικρό (μικρότερο από 512 bytes) και θα έχει ως μοναδικό σκοπό να φορτώσει το δεύτερο στάδιο. Το 2ο στάδιο θα εμπεριέχει όλο τον κώδικα που χρειαζόμαστε για να προετοιμάσουμε το περιβάλλον και να φορτώσουμε τον πυρήνα μας.
For this project, we are going to use the Netwide Assembler (NASM). Our initial Makefile is like that:
Για αυτό το έργο, θα χρησιμοποιήσουμε τον Netwide Assembler (NASM). Το αρχικό μας Makefile είναι ως εξής:
.PHONY: clean, .force-rebuild
all: bootloader.bin
bootloader.bin: os.asm .force-rebuild
nasm -fbin os.asm -o os.bin
clean:
rm *.bin
The contents of os.asm are:
Τα περιεχόμενα του os.asm είναι:
stage1_start:
times 90 db 0 ; BPB (BIOS Parameter Block) will go here
%include "stage1/bootstage1.asm"
stage1_end:
stage2_start:
%include "stage2/bootstage2.asm"
align 512, db 0
stage2_end:
kernel_start:
%include "kernel/kernel.asm"
align 512, db 0
kernel_end:
Stage 1 of the BootloaderΣτάδιο 1 του Εκκινητή
stage1/bootstage1.asm
Let's see the code for the first stage:
Ας δούμε τον κώδικα για το πρώτο στάδιο:
BITS 16 ; On the x86, the BIOS (and consequently the bootloader) runs in 16-bit Real Mode.
ORG 0x7C00 ; We are loaded/booted by BIOS into this memory address.
Stage1_entrypoint: ; Main entry point where BIOS leaves us. Some BIOS may load us at 0x0000:0x7C00 while others at 0x07C0:0x0000. We do a far jump to accommodate for this issue (CS is reloaded to 0x0000).
jmp 0x0000:.setup_segments
.setup_segments:
; Next, we set all segment registers to zero.
xor ax, ax
mov ss, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; We set up a temporary stack so that it starts growing below Stage1_entrypoint (i.e. the stack base will be located at 0:0x7c00).
mov sp, Stage1_entrypoint
; Clear direction flag (go forward in memory when using instructions like lodsb).
cld
; Loading stage 2 from disk into RAM
mov [disk], dl ; Storing disk number. BIOS loads into dl the "drive number" of the booted device.
mov ax, (stage2_start-stage1_start)/512 ; ax: start sector
mov cx, (kernel_end-stage2_start)/512 ; cx: number of sectors (512 bytes) to read
mov bx, stage2_start ; bx: offset of buffer
xor dx, dx ; dx: segment of buffer
call Real_mode_read_disk
; Print "Stage 1 finished." message.
mov si, stage1_message
call Real_mode_println
; Jump to the entry point of stage 2 (commented out for now)
;;; jmp Stage2_entrypoint
.halt: hlt ; Infinite loop.
jmp .halt ; (It prevents us from going off in memory and executing junk).
; Include
%include "stage1/disk.asm"
%include "stage1/print.asm"
times 510-($-$$) db 0 ; Padding
dw 0xAA55 ; The last two bytes of the boot sector should have the 0xAA55 signature.
; Otherwise, we'll get an error message from BIOS that it didn't find a bootable disk.
; This signature is represented in binary as 1010101001010101. The alternating bit
; pattern was thought to be a protection against certain (drive or controller) failures.
stage1/disk.asm
Here is some code for reading disk sectors (i.e. loading them into RAM). We use the BIOS interrupt call INT 13h, service AH=42h, for the extended reading (i.e. LBA addressing) of the disk sectors:
Εδώ βρίσκεται ο κώδικας για την ανάγνωση τομέων από τον δίσκο (δηλαδή τη φόρτωσή τους στη RAM). Χρησιμοποιούμε κλήση στη διακοπή INT 13h του BIOS, υπηρεσία AH=42h, που αφορά εκτεταμένη ανάγνωση (LBA addressing) των τομέων ενός δίσκου:
BITS 16
;---Initialized data------------------------------------------------------------
disk db 0x80
disk_error_message dw 11
db 'Disk error!'
DAP:
;*******************************************************************************;
; Disk Address Packet ;
;-------------------------------------------------------------------------------;
; Offset Size Description ;
; 0 1 size of packet (16 bytes) ;
; 1 1 always 0 ;
; 2 2 number of sectors to load (max = 127 on some BIOS) ;
; 4 2 16-bit offset of target buffer ;
; 4 2 16-bit segment of target buffer ;
; 8 4 lower 32 bits of 48-bit starting LBA ;
; 12 4 upper 32 bits of 48-bit starting LBA ;
;*******************************************************************************;
db 0x10 ; size of packet = 16 bytes
db 0 ; always 0
.num_sectors: dw 127 ; number of sectors to load (max = 127 on some BIOS)
.buf_offset: dw 0x0 ; 16-bit offset of target buffer
.buf_segment: dw 0x0 ; 16-bit segment of target buffer
.LBA_lower: dd 0x0 ; lower 32 bits of 48-bit starting LBA
.LBA_upper: dd 0x0 ; upper 32 bits of 48-bit starting LBA
;---Code------------------------------------------------------------------------
Real_mode_read_disk:
;**********************************************************;
; Load disk sectors to memory (int 13h, function code 42h) ;
;----------------------------------------------------------;
; ax: start sector ;
; cx: number of sectors (512 bytes) to read ;
; bx: offset of buffer ;
; dx: segment of buffer ;
;**********************************************************;
.start:
cmp cx, 127 ; (max sectors to read in one call = 127)
jbe .good_size
pusha
mov cx, 127
call Real_mode_read_disk
popa
add eax, 127
add dx, 127 * 512 / 16
sub cx, 127
jmp .start
.good_size:
mov [DAP.LBA_lower], ax
mov [DAP.num_sectors], cx
mov [DAP.buf_segment], dx
mov [DAP.buf_offset], bx
mov dl, [disk]
mov si, DAP
mov ah, 0x42
int 0x13
jc .print_error
ret
.print_error:
mov si, disk_error_message
call Real_mode_println
.halt: hlt
jmp .halt; Infinite loop. We cannot recover from disk error.
stage1/print.asm
Here is our code for printing messages. We use the BIOS interrupt call INT 10h, service AH=0Eh to print each character. Note that we do not use null-terminated strings. Instead, we opted to store the string length in the first 16 bits (2 bytes) of the string structure:
Εδώ έχουμε τον κώδικά μας για την εκτύπωση μηνυμάτων. Χρησιμοποιούμε κλήση στη διακοπή INT 10h του BIOS, υπηρεσία AH=0Eh για να εκτυπώσουμε τον κάθε χαρακτήρα. Σημειώστε ότι δεν χρησιμοποιούμε το '\0' για να δηλώσουμε το τέλος των αλφαριθμητικών σειρών χαρακτήρων. Αντίθετα, επιλέξαμε να αποθηκεύουμε τον αριθμό των χαρακτήρων στα πρώτα 16 bits (2 bytes) της δομής της σειράς χαρακτήρων:
BITS 16
;---Initialized data------------------------------------------------------------
newline dw 2
db 13,10 ; \r\n
stage1_message dw 17
db 'Stage 1 finished.'
;---Code------------------------------------------------------------------------
Real_mode_print:
;*********************************************************************************;
; Prints a string (in real mode) ;
;---------------------------------------------------------------------------------;
; si: pointer to string (first 16 bits = the number of characters in the string.) ;
;*********************************************************************************;
push ax
push cx
push si
mov cx, word [si] ; first 16 bits = the number of characters in the string
add si, 2
.string_loop: ; print all the characters in the string
lodsb
mov ah, 0eh
int 10h
loop .string_loop, cx
pop si
pop cx
pop ax
ret
Real_mode_println:
;***********************************************************;
; Prints a string (in real mode) and a newline (\r\n) ;
;-----------------------------------------------------------;
; si: pointer to string ;
; (first 16 bits = the number of characters in the string.) ;
;***********************************************************;
push si
call Real_mode_print
mov si, newline
call Real_mode_print
pop si
ret
Running our codeΕκτέλεση του κώδικά μας
We can test our code very easily using an emulator like QEMU:
Μπορούμε να δοκιμάσουμε τον κώδικά μας πολύ εύκολα, χρησιμοποιώντας έναν εξομοιωτή όπως ο QEMU:
$ make
$ qemu-system-x86_64 -drive format=raw,file=os.bin
Full codeΠλήρης κώδικας
You can download the full code from here: stage 1 code
Μπορείτε να κατεβάσετε τον πλήρη κώδικα απο εδώ: κώδικας για το πρώτο στάδιο