How to write your own Operating System (x86_64): Bootloader, 2nd Stage (long mode)

Πως να γράψετε το δικό σας Λειτουργικό Σύστημα (x86_64): Εκκινητής, 2o Στάδιο (μακρά κατάσταση)

· Coding Προγραμματισμός · assembly assembly os λειτουργικό σύστημα

IntroductionΕισαγωγή

In our previous article, we showed how to implement the first stage of the bootloader. Now, let's proceed to the second stage. First of all, in order for the execution to continue to the second stage, we have to remove the relevant comment from the jmp instruction in stage1/bootstage1.asm:

Σε προηγούμενο άρθρο μας, δείξαμε πως υλοποιούμε το πρώτο στάδιο του εκκινητή. Εδώ, θα εξετάσουμε το δεύτερο στάδιο. Πρώτα από όλα, για να προχωρήσει η εκτέλεση στο δεύτερο στάδιο, πρέπει να αφαιρέσουμε το σχετικό σχόλιο από την εντολή jmp στο stage1/bootstage1.asm:

    ...
    ; Jump to the entry point of stage 2
    jmp Stage2_entrypoint 
    ...

Stage 2 codeΚώδικας του 2ου σταδίου

In the x86_64 computer architecture, long mode is the architecture's intended primary mode of operation, where a 64-bit operating system can access 64-bit instructions and registers. Let's see some things we need in order to enter the long mode:

  1. We check if the long mode is supported.
  2. We activate the A20 gate / address line.
  3. We prepare the memory paging.
  4. We remap the PIC (Programmable Interrupt Controller).
  5. Then, we enter the long mode and pass the control to the Kernel.

Let's examine the code for the second stage:

Στην αρχιτεκτονική υπολογιστών x86_64, η προορισμένη κατάσταση λειτουργίας είναι η λεγόμενη μακρά κατάσταση (long mode) όπου ενα 64-bit λειτουργικό σύστημα έχει πρόσβαση στις 64-bit εντολές και τους 64-bit καταχωρητές. Ας δούμε τι χρειάζεται να κάνουμε ώστε να μπούμε στην λειτουργία μακράς κατάστασης:

  1. Ελέγχουμε αν η λειτουργία μακράς κατάστασης υποστηρίζεται.
  2. Ενεργοποιούμε την πύλη / γραμμή διεύθυνσης A20.
  3. Προετοιμάζομε τη σελιδοποίηση της μνήμης.
  4. Επανακαθορίζουμε τον Προγραμματιζόμενο Ελεγκτή Διακοπών (PIC).
  5. Τέλος, εισερχόμαστε στη λειτουργία μακράς κατάστασης και δίνουμε τον έλεγχο στον Πυρήνα.

Ας δούμε, λοιπόν, τον κώδικα για το δεύτερο στάδιο:

stage2/bootstage2.asm

BITS 16

;---Initialized data------------------------------------------------------------
stage2_message dw 19
db 'Entering Stage 2...'
longmode_supported_message dw 23
db 'Long mode is supported.'
longmode_not_supported_message dw 27
db 'Long mode is not supported.'

;---Code------------------------------------------------------------------------
Stage2_entrypoint:
    ; Print 'Entering Stage 2...' message
    mov si, stage2_message
    call Real_mode_println

    ; Check if long mode is supported
    call Is_longmode_supported
    test eax, eax
    jz .long_mode_not_supported
    mov si, longmode_supported_message
    call Real_mode_println

    ; Enable Gate A20
    call Enable_A20
    ; Prepare paging  
    call Prepare_paging
    ; Remap PIC
    call Remap_PIC
    ; Enter long mode
    call Enter_long_mode

   .long_mode_not_supported:
    mov si, longmode_not_supported_message
    call Real_mode_println
   .halt: hlt ; Infinite loop. 
    jmp .halt ; (It prevents us from going off in memory and executing junk).


; Include
%include "stage2/a20.asm"
%include "stage2/paging.asm"
%include "stage2/pic.asm"
%include "stage2/longmode.asm"

stage2/a20.asm

When IBM PC AT was released in 1984 (designed around the Intel 80286 microprocessor), for the sake of backward compatibility, a quirk in the 8086 microprocessor architecture (a memory wraparound) had to be replicated. You can read more about this issue here. Thus, the A20 line on the address bus was (and still is in many systems) disabled by default. This means that if we want to gain access to the full memory range, we have to enable the A20 line. There are several different ways (that may or may not be supported) and some of them may cause problems on some computers. Therefore, the recommended method is to try all of them, in the order of least risk, until one works:

  1. We test if A20 is already enabled. If it is, we don't need to do anything at all.
  2. We try to enable A20 using the BIOS function (we ignore the returned status).
  3. We test if A20 is enabled (to see if the BIOS function actually worked or not).
  4. We try to enable A20 using the keyboard controller method.
  5. We test if A20 is enabled in a loop with a time-out (as the keyboard controller method may work slowly).
  6. We try to enable A20 using the "Fast A20" method last.
  7. We test if A20 is enabled in a loop with a time-out (as the "Fast A20" method may work slowly).
  8. If none of the above worked, we give up.

Το 1984 κυκλοφόρησε ο IBM PC AT, σχεδιασμένος γύρω από τον μικροεπεξεργαστη 80286 της Intel. Για χάρη της προς τα πίσω συμβατότητας, μια ιδιοτροπία στην αρχιτεκτονική του μικροεπεξεργαστή 8086 (μια κυκλική επικάλυψη στην πρόσβαση της μνήμης) έπρεπε να αναπαραχθεί και σε αυτόν. Μπορείτε να διαβάσετε περισσότερα για αυτό το θέμα εδώ. Η ουσία είναι ότι η γραμμή Α20 στο δίαυλο διευθύνσεων ήταν (και εξακολουθεί να είναι σε πολλά συστήματα) αρχικά απενεργοποιημένη. Αυτό σημαίνει ότι αν θέλουμε να αποκτήσουμε πρόσβαση στο πλήρες εύρος της μνήνης, πρέπει να ενεργοποιήσουμε τη γραμμή Α20. Υπάρχουν αρκετοί διαφορετικοί τρόποι για να το επιτύχουμε αυτό (που μπορει να υποστηρίζονται ή όχι) και κάποιοι από αυτούς μπορεί να δημιουργήσουν προβλήματα σε κάποια συστήματα. Ως εκ τούτου, η συνιστώμενη μέθοδος είναι να δοκιμάσουμε όλους τους τρόπους κατά σειρά αυξανόμενου ρίσκου, μέχρι κάποιος να δουλέψει:

  1. Ελέγχουμε αν η A20 είναι ήδη ενεργοποιημένη. Αν είναι, δε χρειάζεται να κάνουμε κάτι άλλο.
  2. Δοκιμάζουμε να ενεργοποιήσουμε την Α20, χρησιμοποιώντας τη λειτουργία του BIOS (αγνοούμε την τιμή που μας γυρίζει).
  3. Ελέγχουμε αν η A20 ενεργοποιήθηκε (για να δούμε αν η λειτουργία του BIOS δούλεψε στην πράξη ή όχι).
  4. Δοκιμάζουμε να ενεργοποιήσουμε την Α20, χρησιμοποιώντας τον ελεκτή του πληκτρολογίου.
  5. Ελέγχουμε αν η A20 ενεργοποιήθηκε σε ένα βρόχο με κάποιο χρονικό τέλος (μιας και η μέθοδος με τον ελεκτή του πληκτρολογίου μπορεί να πάρει κάποιο χρόνο για να δουλέψει).
  6. Δοκιμάζουμε να ενεργοποιήσουμε την Α20, χρησιμοποιώντας τη μέθοδο "Fast A20" τελευταία.
  7. Ελέγχουμε αν η A20 ενεργοποιήθηκε σε ένα βρόχο με κάποιο χρονικό τέλος (μιας και η μέθοδος "Fast A20" μπορεί να πάρει κάποιο χρόνο για να δουλέψει).
  8. Αν δε δούλεψε κανένα από τα παραπάνω, τα παρατάμε.

BITS 16

;---Initialized data------------------------------------------------------------
a20_enabled_message dw 15
db 'A20 is enabled.'
a20_disabled_message dw 16
db 'A20 is disabled.' 
a20_trying_bios dw 34
db 'Trying to enable A20 using BIOS...'
a20_trying_keyb dw 49
db 'Trying to enable A20 using Keyboard Controller...'
a20_trying_io92 dw 40
db 'Trying to enable A20 using IO port 92...'

;---Code------------------------------------------------------------------------
Enable_A20:
    call Check_A20 ; Check if A20 is already enabled.
    test ax, ax
    jnz .end

    ; Try to enable A20 using BIOS.
    mov si, a20_trying_bios
    call Real_mode_println  
    call Enable_A20_using_BIOS 

    call Check_A20 ; Check if A20 is enabled.
    test ax, ax
    jnz .end

    ; Try to enable A20 using eEyboard Controller.
    mov si, a20_trying_keyb 
    call Real_mode_println
    call Enable_A20_using_Keyboard_Controller 

    call Check_A20 ; Check if A20 is enabled.
    test ax, ax
    jnz .end

    ; Try to enable A20 using IO port 92h (Fast A20 method).
    mov si, a20_trying_io92 
    call Real_mode_println
    call Enable_A20_using_Keyboard_Controller

    call Check_A20 ; Check if A20 is enabled.
    test ax, ax
    jnz .end
   .halt: hlt 
    jmp .halt ; Infinite loop.
   .end:
    ret


Check_A20:
;********************************************************************;
; Check the status of the A20 line                                   ;
;********************************************************************;
    call Real_mode_check_A20
    test ax, ax
    jnz .a20_enabled
        mov si, a20_disabled_message
        call Real_mode_println
        ret
   .a20_enabled:
        mov si, a20_enabled_message
        call Real_mode_println  
        ret
                

Real_mode_check_A20:
;**************************************************************************;
; Check the status of the A20 line (in real mode)                          ;
;--------------------------------------------------------------------------;
; Returns: ax = 0 if the a20 line is disabled (memory wraps around)        ;
;          ax = 1 if the a20 line is enabled (memory does not wrap around) ;
;**************************************************************************;
    pushf
    push ds
    push es
    push di
    push si
    cli ; clear interrupts
    
    xor ax, ax ; ax = 0
    mov es, ax ; es = 0
    not ax     ; ax = 0xFFFF
    mov ds, ax ; ds = 0xFFFF
    mov di, 0x0500 ; 0500 and 0510 are chosen since they are guaranteed to be free 
    mov si, 0x0510 ; for use at any point of time after BIOS initialization.

    ; save the original values found at these addresses.
    mov dl, byte [es:di]  
    push dx
    mov dl, byte [ds:si]
    push dx
    
    mov byte [es:di], 0x00 ; [es:di] is 0:0500
    mov byte [ds:si], 0xFF ; [ds:si] is FFFF:0510 
    cmp byte [es:di], 0xFF ; if the A20 line is disabled, [es:di] will contain 0xFF
                           ; (as the write to [ds:si] really occured to 00500).

    mov ax, 0 ; A20 disabled ([es:di] equal to 0xFF).
    je .a20_disabled
    mov ax, 1 ; A20 enabled.
   .a20_disabled:

    ; restore original values
    pop dx  
    mov byte [ds:si], dl
    pop dx
    mov byte [es:di], dl
   
    pop si
    pop di
    pop es
    pop ds
    popf
    sti ; Enable interrupts.
    ret


Enable_A20_using_BIOS:
;*************************************************************;
; Try to enable A20 gate using the BIOS (int 15h, ax = 2401h) ;
;-------------------------------------------------------------;
; Returns: ax = 0 (Failure)                                   ;
;          ax = 1 (Success)                                   ;
;*************************************************************;
    mov ax,2403h  ; Query A20 gate Support (later PS/2s systems)
    int 15h     
    jb .failure   ; INT 15h is not supported                
    cmp ah, 0
    jnz .failure  ; INT 15h is not supported
    mov ax, 2402h ; Get A20 gate Status
    int 15h
    jb .failure   ; Couldn't get status
    cmp ah, 0
    jnz .failure  ; Couldn't get status
    cmp al, 1     
    jz .success   ; A20 is already activated
    mov ax, 2401h ; Enable A20 gate 
    int 15h     
    jb .failure   ; Couldn't enable the A20 gate
    cmp ah, 0 
    jnz .failure  ; Couldn't enable the A20 gate
   .success:
        mov ax, 1
        ret
   .failure:
        mov ax, 0
        ret


Disable_A20_using_BIOS:
;**************************************************************;
; Try to disable A20 gate using the BIOS (int 15h, ax = 2400h) ;
;**************************************************************;
    mov ax, 2400h
    int 15h
    ret


Enable_A20_using_Keyboard_Controller:
;******************************************************************;
; Try to enable A20 line using the Keyboard Controller (chip 8042) ;
;------------------------------------------------------------------;
; Returns: ax = 0 (Failure)                                        ;
;          ax = 1 (Success)                                        ;
;******************************************************************;
    cli           ; Clear interrupts.
    call a20wait
    mov al, 0xAD  ; Disable keyboard.
    out 0x64, al
    call a20wait
    mov al, 0xD0  ; Read from input.
    out 0x64, al
    call a20wait2 
    in al,0x60
    push eax
    call a20wait
    mov al, 0xD1  ; Write to output.
    out 0x64, al
    call a20wait
    pop eax
    or al, 2
    out 0x60, al
    call a20wait
    mov al, 0xAE  ; Enable keyboard.
    out 0x64, al
    call a20wait
    sti           ; Enables interrupts.
    ret

a20wait:
    in      al, 0x64
    test    al, 2
    jnz     a20wait
    ret

a20wait2:
    in      al, 0x64
    test    al, 1
    jz      a20wait2
    ret


;*********************************************************************;
; Enable A20 Line via IO port 92h (Fast A20 method)                   ;
;---------------------------------------------------------------------;
; This method is quite dangerous because it may cause conflicts with  ;
; some hardware devices forcing the system to halt.                   ;
;=====================================================================;
; Bits of port 92h                                                    ;
;---------------------------------------------------------------------;
; Bit 0 - Setting to 1 causes a fast reset                            ;
; Bit 1 - 0: disable A20, 1: enable A20                               ;
; Bit 2 - Manufacturer defined                                        ;      
; Bit 3 - power on password bytes. 0: accessible, 1: inaccessible     ;
; Bits 4-5 - Manufacturer defined                                     ;
; Bits 6-7 - 00: HDD activity LED off, 01 or any value is "on"        ;
;*********************************************************************;
Enable_A20_using_IO_port_92:
    in al, 0x92  ; Read from port 0x92
    test al, 2   ; Check if bit 1 (i.e. the 2nd bit) is set.
    jnz .end     ; If bit 1 (i.e. the 2nd bit) is already set don't do anything.
    or al, 2     ; Activate bit 1 (i.e. the 2nd bit).
    and al, 0xFE ; Make sure bit 0 is 0 (it causes a fast reset).
    out 0x92, al ; Write to port 0x92
   .end:
    ret

stage2/paging.asm

Here is the code that sets the memory paging. Note that we will place the kernel in the high half of the virtual address space. This ensures that user space always occupies the lower canonical range, simplifying paging structures and context switching. This separation enforces stronger privilege boundaries, since user code cannot directly address high-half regions. It also enables all processes to map the same kernel space identically, reducing TLB flushes and improving overall memory management consistency.

Εδώ είναι ο κώδικας που καθορίζει τη σελιδοποίηση της μνήμης. Σημειώστε ότι θα τοποθετήσουμε τον πυρήνα στο υψηλό μισό του εικονικού χώρου διευθύνσεων. Αυτό εξασφαλίζει ότι ο χώρος χρήστη θα καταλαμβάνει πάντα το χαμηλό κανονικό εύρος, απλοποιώντας τις δομές σελιδοποίησης και την εναλλαγή διεργασιών. Ο διαχωρισμός αυτός επιβάλλει ισχυρότερα όρια προνομίων, καθώς ο κώδικας χρήστη δεν μπορεί να προσπελάσει άμεσα τις περιοχές του υψηλού μισού. Επιπλέον, επιτρέπει σε όλες τις διεργασίες να χαρτογραφούν τον χώρο του πυρήνα με τον ίδιο τρόπο, μειώνοντας τις εκκαθαρίσεις/ανανεώσεις του TLB και βελτιώνοντας τη συνοχή της διαχείρισης της μνήμης.

BITS 16

;---Constants-------------------------------------------------------------------
; Paging flag bits
PAGE_PRESENT equ (1 << 0)               ; Bit 0 => Page present
PAGE_WRITE   equ (1 << 1)               ; Bit 1 => Page writable
PAGE_HUGE_PS equ (1 << 7)               ; Bit 7 => 2MB page size, ignore PT

; Addresses to store the paging structures
PML4         equ 0xB000
PDPT_LOW     equ (PML4 + 0x1000)
PD_LOW       equ (PML4 + 0x2000)
PDPT_HIGH    equ (PML4 + 0x3000)
PD_HIGH      equ (PML4 + 0x4000)

; Kernel virtual address
KERNEL_VIRT_BASE equ 0xFFFFFFFF80000000 ; High-half base (48-bit address)

; Extract indices from KERNEL_VIRT_BASE for page table structure
PML4_INDEX   equ ((KERNEL_VIRT_BASE >> 39) & 0x1FF)  ; Bits 47-39
PDPT_INDEX   equ ((KERNEL_VIRT_BASE >> 30) & 0x1FF)  ; Bits 38-30
PD_INDEX     equ ((KERNEL_VIRT_BASE >> 21) & 0x1FF)  ; Bits 29-21
PT_INDEX     equ ((KERNEL_VIRT_BASE >> 12) & 0x1FF)  ; Bits 20-12

; Segment Selector = (Index * 8) + (TI * 4) + RPL = (1 * 8) + (0 * 4) + 0 = 8   
; RPL (Requestor Privilege Level - Bits 1-0): 0 is highest, 3 is lowest 
; TI (Table Indicator - Bit 2): 0 => GDT, 1 => LDT
; Index (Bits 15-3): The index of the segment descriptor within the GDT.                   
CODE_SEL equ 8
DATA_SEL equ 16


;---Initialized data------------------------------------------------------------

;******************************************************************************;
; Global Descriptor Table (GDT)                                                ;
;******************************************************************************;
; The Global Descriptor Table (GDT) is a data structure used by x86-family     ;
; processors (starting with the 80286) in order to define the characteristics  ;
; of the various memory areas (segments) used during program execution,        ;
; including the base address, the size, and access privileges like             ;
; executability and writability.                                               ;
;******************************************************************************;
GDT:
  .Null:
    dq 0x0000000000000000  ; Null Descriptor (should be present).
  .Code:
    dq 0x00209A0000000000  ; 64-bit code descriptor (exec/read).
    dq 0x0000920000000000  ; 64-bit data descriptor (read/write).
  ALIGN 4                  ; Align the pointer field on a 4-byte boundary.
    dw 0                   ; Padding.
  GDTR:
  .Length dw $ - GDT - 1   ; 16-bit Size (Limit) of GDT.
  .Base   dd GDT           ; 32-bit Base Address of GDT 
                           ; (CPU will zero extend to 64-bit)

;---Code------------------------------------------------------------------------
Prepare_paging:
;******************************************************************************;
; Prepare paging                                                               ;
;------------------------------------------------------------------------------;
; ES:EDI Should point to a valid 4096-aligned 16KiB buffer.                    ;
; SS:ESP Should point to memory that can be used as a small stack.             ;
;******************************************************************************;
  
    ; Zero out the 20KiB buffer (PML4, PDPT_LOW, PD_LOW, PDPT_HIGH, PD_HIGH)
    mov edi, PML4            ; Point to 20KiB buffer for the paging structures.
    push di                  ; Store di (rep stosd alters di).
    mov ecx, 5*0x1000/4      ; Count should be 5*4096/4 = 5120 dwords
    xor eax, eax             ; eax = 0
    cld                      ; Set forward direction
    rep stosd                ; Fill with 0s  
    pop di                   ; Restore di
    
    ;************************* Low (identity) mapping **************************
    ; Build the PML4 (Page Map Level 4): PML4[0] -> PDPT_LOW
    lea eax, [PDPT_LOW]                  ; eax = address of the PDPT_LOW
    or eax, PAGE_PRESENT | PAGE_WRITE    ; Set the flags (present and writable)
    mov [PML4], eax                      ; PML4E[0] = eax
    ; Build the PDPT_LOW (Page Directory Pointer Table): PDPT_LOW[0] -> PD_LOW
    lea eax, [PD_LOW]                    ; eax = address of the PD_LOW
    or  eax, PAGE_PRESENT | PAGE_WRITE   ; Set the flags (present and writable)
    mov [PDPT_LOW], eax                  ; PDPT_LOW[0] = eax  
    ; Fill the PD_LOW (Page Directory) with entries (each maps a 2 MiB page)
    mov eax, 0                                       ; eax = 0 (0-2MiB identity)
    or eax, PAGE_PRESENT | PAGE_WRITE | PAGE_HUGE_PS ; Set flags (2MiB page)
    mov [PD_LOW], eax                                ; PD_LOW[0] = eax 
    ; ...
    
    ; ************************ High-half kernel mapping ************************
    ; PML4[PML4_INDEX] -> PDPT_HIGH
    lea eax, [PDPT_HIGH]                ; eax = PDPT_HIGH
    or eax, PAGE_PRESENT | PAGE_WRITE   ; Set the flags (present and writable)
    mov [PML4+PML4_INDEX*8], eax        ; PML4[PML4_INDEX] = eax
    ; PDPT_HIGH[PDPT_INDEX] -> PD_HIGH
    lea eax, [PD_HIGH]                  ; eax = PD_HIGH
    or eax, PAGE_PRESENT | PAGE_WRITE   ; Set the flags (present and writable)
    mov [PDPT_HIGH+PDPT_INDEX*8], eax   ; PDPT_HIGH[PDPT_INDEX] = eax
    ; Fill the PD_HIGH (Page Directory) with entries (each maps a 2 MiB page)
    mov di, PD_HIGH
    xor eax, eax                        ; eax = 0
    mov ecx, 512                        ; Fill 512 entries (i.e. map 1 GiB)
   .MapLoop:        
        ; Set flags: Present, Writable, 2 MiB page
        or eax, PAGE_PRESENT | PAGE_WRITE | PAGE_HUGE_PS
        mov [di], eax                   ; Store low 32 bits (entry is 8 bytes) 
        mov [di + 4], dword 0           ; Store high 32 bits 
        add di, 8                       ; Advance to next PD_HIGH entry 
        add eax, 0x200000               ; Increment address by 2MiB
        loop .MapLoop
    ret

stage2/pic.asm

The Programmable Interrupt Controller (PIC) is one of the most important chips in the x86 architecture. Its function is to manage hardware interrupts and send them to the appropriate system interrupts. This allows the system to respond to devices without the need for polling them all the time. In protected mode and long mode, the Interrupt Requests (IRQs) 0-15 are in conflict with the CPU exceptions which reserve the first 32 positions (0-31 or 0x00-0x1F). Thus, prior to switching to the long mode, we have to change the PIC's offsets (a.k.a. remapping the PIC) so that IRQs use non-reserved vectors:

O Προγραμματιζόμενος Ελεγκτής Διακοπών (Programmable Interrupt Controller, PIC) είναι ένα από τα πιο σημαντικά ολοκληρωμένα κυκλώματα στην αρχιτεκτονικη x86. Η λειτούργια του είναι να διαχειρίζεται τις διακοπές υλικού (hardware interrupts) και να τις στέλνει στις κατάλληλες διακοπές συστήματος. Αυτό επιτρέπει στο σύστημα να ανταποκρίνεται στις συσκευές χωρίς την ανάγκη να τις βολιδοσκοπεί περιοδικά (polling) όλη την ώρα. Στην προστατευμένη κατάσταση λειτουργίας (protected mode) και στην μακρά κατάσταση λειτουργίας (long mode), οι αιτήσεις διακοπών (Interrupt Requests, IRQs) 0-15 συγκρούονται με τις εξαιρέσεις του επεξεργαστή (CPU exceptions) που δεσμεύουν τις πρώτες 32 θέσεις (0-31 ή 0x00-0x1F). Ως εκ τούτου, πριν τη μετάβαση στην λειτουργία μακράς κατάστασης, πρέπει να μετατοπίσουμε τα IRQs σε μη δεσμευμένες θέσεις στον PIC (αυτή η διαδικασία είναι γνωστή και ως "remapping the PIC"):

; PIC (Programmable Interrupt Controller)

BITS 16

;---Constants----------------------------------------------------------------------
PIC1_COMMAND    equ 0x20 ; Command port of 1st PIC
PIC1_DATA       equ 0x21 ; Data port of 1st PIC
PIC2_COMMAND    equ 0xA0 ; Command port of 2nd PIC
PIC2_DATA       equ 0xA1 ; Data port of 2nd PIC
PIC_EOI         equ 0x20 ; EOI (End of interrupt) command (= 0x20)

ICW1_ICW4       equ 0x01 ; Initialization Command Word 4 is needed
ICW1_SINGLE     equ 0x02 ; Single mode (0: Cascade mode)
ICW1_INTERVAL4  equ 0x04 ; Call address interval 4 (0: 8)
ICW1_LEVEL      equ 0x08 ; Level triggered mode (0: Edge mode)
ICW1_INIT       equ 0x10 ; Initialization - required!

ICW4_8086       equ 0x01 ; 8086/88 mode (0: MCS-80/85 mode)
ICW4_AUTO_EOI   equ 0x02 ; Auto End Of Interrupt (0: Normal EOI)
ICW4_BUF_SLAVE  equ 0x08 ; Buffered mode/slave
ICW4_BUF_MASTER equ 0x0C ; Buffered mode/master
ICW4_SFNM       equ 0x10 ; Special Fully Nested Mode


;---Code---------------------------------------------------------------------------
Remap_PIC:
;***************************************************************************;
; In protected / long mode, the IRQs 0-15 conflict with the CPU exceptions  ;
; (which are reserved up until 0x1F). It is thus recommended to change the  ;
; PIC's offsets (remapping the PIC) so that IRQs use non-reserved  vectors. ; 
; A common choice is to move them to the beginning of the available range:  ;
; IRQs 0..15 -> INT 0x20..0x2F (30..47). For that, we need to set the 1st   ;
; PIC's offset to 0x20 (32) and the 2nd's to 0x28 (40).                     ;  
;***************************************************************************;
    push ax

    ; Disable IRQs
    mov al, 0xFF ; Out 0xFF to 0xA1 and 0x21 to mask/disable all IRQs.
    out PIC1_DATA, al
    out PIC2_DATA, al
    nop
    nop

    ; Remap PIC
    mov al, ICW1_INIT | ICW1_ICW4 ; ICW1: Send initialization command (= 0x11) to both PICs 
    out PIC1_COMMAND, al    
    out PIC2_COMMAND, al     
    mov al, 0x20       ; ICW2: Set vector offset of 1st PIC to 0x20 (i.e. IRQ0 => INT 32) 
    out PIC1_DATA, al
    mov al, 0x28       ; ICW2: Set vector offset of 2nd PIC to 0x28 (i.e. IRQ8 => INT 40)    
    out PIC2_DATA, al
    mov al, 4          ; ICW3: tell 1st PIC that there is a 2nd PIC at IRQ2 (= 00000100)
    out PIC1_DATA, al       
    mov al, 2          ; ICW3: tell 2nd PIC its "cascade" identity (= 00000010) 
    out PIC2_DATA, al        
    mov al, ICW4_8086  ; ICW4: Set mode to 8086/88 mode
    out PIC1_DATA, al
    out PIC2_DATA, al

    mov al, 0xFF       ; OCW1: We mask all interrupts (until we set a proper IDT in Kernel)
    out PIC1_DATA, al
    out PIC2_DATA, al

    pop ax
    ret

stage2/longmode.asm

Here is the code for checking if the long mode is supported and the code for entering the long mode:

Εδώ βρίσκεται ο κώδικας που έχει να κάνει με τον έλεγχο υποστήριξης και τη μετάβαση στην λειτουργία μακράς κατάστασης (long mode):

BITS 16

;---Code------------------------------------------------------------------------
Is_longmode_supported:
;********************************************************************;
; Check if Long mode is supported                                    ;
;--------------------------------------------------------------------;
; Returns: eax = 0 if Long mode is NOT supported, else non-zero.     ;
;********************************************************************;
    mov eax, 0x80000000 ; Test if extended processor info in available.  
    cpuid                
    cmp eax, 0x80000001 
    jb .not_supported     
    mov eax, 0x80000001 ; After calling CPUID with EAX = 0x80000001, 
    cpuid               ; all AMD64 compliant processors have the longmode-capable-bit
    test edx, (1 << 29) ; (bit 29) turned on in the EDX (extended feature flags).
    jz .not_supported   ; If it's not set, there is no long mode.
    ret
   .not_supported:
    xor eax, eax
    ret


Enter_long_mode:
;********************************************************************;
; Enter long mode                                                    ;
;********************************************************************;
    mov edi, PML4         ; Point edi at the PML4 
    mov eax, 10100000b    ; Set the PAE and PGE bit.
    mov cr4, eax
    mov edx, edi          ; Point CR3 at the PML4.
    mov cr3, edx
    mov ecx, 0xC0000080   ; Read from the EFER MSR. 
    rdmsr    
    or eax, 0x00000100    ; Set the LME bit.
    wrmsr
    mov ebx, cr0          ; Activate long mode
    or ebx,0x80000001     ; by enabling paging and protection simultaneously.
    mov cr0, ebx                    
    lgdt [rel GDTR]       ; Load GDT.Pointer
    jmp CODE_SEL:LONGMODE ; Mode-Switch Jump (Load CS & flush instruction cache) 
LONGMODE: 
    BITS 64               ; We have entered the long mode! :)          
    mov ax, DATA_SEL      ; Set all segments registers to DATA_SEL.
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov rax, KERNEL_VIRT_BASE + HIGH_KERNEL      
    jmp rax               ; Jump to high-address mapped kernel 
HIGH_KERNEL:
    lea rax, [rel GDT]          ; Load our high-address GDT
    mov [rel GDTR.Base], rax
    lgdt [rel GDTR]          
    add rsp, KERNEL_VIRT_BASE   ; Point rsp (stack) to its high address
    mov qword [abs KERNEL_VIRT_BASE + PML4], 0 ; Clear PML4[0] (identity entry)
    mov rax, cr3                ; Flush the TLB (Translation Lookaside Buffer)
    mov cr3, rax                ; Reload CR3 to flush TLB
    jmp Kernel_entrypoint       ; Jump to Kernel entrypoint

kernel/kernel.asm

Here is some dummy code that write a message directly into the video RAM, just to test if our bootloader is functional:

Εδώ έχουμε έναν απλό κώδικα, ο οποίος γράφει έναν μήνυμα απευθείας στην περιοχή της μνήμης των γραφικών, απλά για να δούμε αν ο εκκινητή μας είναι λειτουργικός:

BITS 64   ; We have entered the long mode! :)

;---Constants-------------------------------------------------------------------
VRAM equ KERNEL_VIRT_BASE + 0xB8000  

;---Code------------------------------------------------------------------------
Kernel_entrypoint:
;********************************************************************;
; Just some dummy code for now                                       ;
;********************************************************************;
    ; Set RDI to point to Video RAM (KERNEL_VIRT_BASE + 0xB8000)
    mov rdi, VRAM
 
    ; Print "Hello World!"
    mov rax, 0x1F6C1F6C1F651F48    
    mov [rdi], rax
    mov rax, 0x1F6F1F571F201F6F
    mov [rdi + 8], rax
    mov rax, 0x1F211F641F6C1F72
    mov [rdi + 16], rax
 
   .halt: hlt 
    jmp .halt ; Infinite loop.

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

Running Bootloader

Full codeΠλήρης κώδικας

You can download the full code from here: bootloader code

Μπορείτε να κατεβάσετε τον πλήρη κώδικα απο εδώ: κώδικας εκκινητή

ReferencesΑναφορές

See also...

Δείτε επίσης...