Para descargar el Sistema Operativo tOSh, podés hacerlo desde la sección Resources de este mismo sitio.

Partes

Esta serie de artículos tiene las siguientes partes:

Turbo Debugger - Not Enough MemoryTurbo Debugger - Not Enough Memory

Stage I - De Modo Real a Modo Protegido

Estamos en el Real Mode, lo último que hicimos fue saltar a otra posición de memoria donde deberíamos haber podido copiar otra parte de código binario que llamaremos stage1 que tomamos de un sector del Floppy donde estamos compilando y guardando todo el bootloader como vimos en la primera parte de esta serie y ahora toca ver que hacemos con el control de nuestra CPU. Esto me hace recordar algunas sextillas gauchescas de nuestro querido Diego Silicio, gaucho del cátodo:

Atendiendo la obligada,
no se haga el distraído
no se olvide la pavada,
ni se pierda el protegido,
que es cuestión de buen formato,
¡memorí laiaut bendito!

Para poder entender mejor que es lo que vamos a hacer, y como vamos a configurar el CPU para que cambie de Modo tenemos que entender nuestro Memory Layout (memorí laiaut) y donde estamos decidiendo casi arbitrariamente –y esto es mucho muy importante, lo ‘casi’ arbitrario– donde vamos a copiar todo en memoria, este mismo stage1.bin el cual cargamos del disco a memoria y saltamos y empezamos a correr con la última instrucción del bootloader: jmp 0x9000.

Primero cabe explicar que el Memory Layout de maquinas x86 ya está definido y hay direcciones de memoria que tienen overlays con componentes y periféricos los cuales usamos para comunicarnos con ellos. Esto significa que no podemos simplemente usar secciones de memoria ya que están siendo usadas por otros dispositivos y la CPU para comunicarse entre si, el BIOS en 0040:0000h y también interfasear con el usuario. Vamos a tomar como ejemplo la tabla que podemos encontrar en el documento Physical Memory Layout of the PC y mezclarla con nuestro layout. La idea es en primer lugar evitar sobreescribir estas regiones de memoria que no nos pertenecen y por el otro, encontrar un hueco donde nos podamos meter nosotros a escribir memoria para nuestro código para el stage1.bin y en un futuro, nuestro kernel.bin. Deberíamos tener suficiente espacio para todo sin sobreescribir nada.

rango de memoria lineal rango de direccion modo real tipo de memoria uso
0- 3FF 0000:0000-0000:03FF RAM real-mode interrupt vector table (IVT)
400- 4FF 0040:0000-0040:00FF RAM BIOS data area (BDA)
500- 9FBFF 0050:0000-9000:FBFF RAM free conventional memory (below 1 meg)
0x9000 + (512bytes * 4 sectores) 0900:0000 – 0900:07FF RAM (stage1.bin) nos metemos por aca, debajo el primer mega de memoria
0x10000 + (512bytes * n sectores) ¡NOS CHUPA UN HUEVO! RAM (kernel.bin) a estas alturas, ya deberiamos estar en Modo Protegido
0x80000 ¡NOS CHUPA UN HUEVO! RAM (stack) El stack que va a ser usado por el kernel y nuestros programas
9FC00- 9FFFF 9000:FC00-9000:FFFF RAM extended BIOS data area (EBDA)
A0000- BFFFF A000:0000-B000:FFFF Video RAM VGA framebuffers
C0000- C7FFF C000:0000-C000:7FFF ROM video BIOS (32K is typical size)
C8000- EFFFF C800:0000-E000:FFFF NOTHING  
F0000- FFFFF F000:0000-F000:FFFF ROM motherboard BIOS (64K is typical size)
100000- FEBFFFFF   RAM free extended memory (1 meg and above)
FEC00000- FFFFFFFF various   motherboard BIOS, PnP NVRAM, ACPI, etc.

Teniendo todo esto en cuenta, podemos empezar a ver un poco de código, stage1.asm:

; ----------------------------------------------------
; tOSh Operating System, Huh?                 (c) 2019
; stage1.asm                                  by toshi   
; $ nasm -D KERNEL_FIRST_SECTOR=$KERNEL_FIRST_SECTOR \
;   -D KERNEL_SECTORS=$kernel_sectors \
;   -f bin -l stage1.lst stage1.asm -o stage1.bin
; ----------------------------------------------------

bits 16
org 0x9000                  ; we copied sector 2+ here!
[map symbols stage1.map]    ; create a stage1.map file for offsets checking

KERNEL_OFFSET equ 0x10000
STACK_ADDRESS equ 0x80000

Todo lo que se ensamble va a ser en 16 bits relativo a org 0x9000. Adicionalmente vamos a crear un mapa de símbolos que nos va a ayudar a ver los offsets de las funciones en caso que los necesitemos en el desarrollo con la directiva map symbols.

Entre varias ventajas, la que más útil me parece a mi es poder llevar el apunte de los equ, los nombres de función y sus posiciones de memoria al haber sido ensambladas inclusive cuando stage1.asm tiene varios %include.

map symbols stage1.map produce un archivo que tiene la siguiente pinta:

- NASM Map file ---------------------------------------------------------------

Source file:  stage1.asm
Output file:  stage1.bin

-- Symbols --------------------------------------------------------------------

---- No Section ---------------------------------------------------------------

Value     Name
00010000  KERNEL_OFFSET
00080000  STACK_ADDRESS
00000008  CODE_SEGMENT
00000010  DATA_SEGMENT
000B8000  VRAM


---- Section .text ------------------------------------------------------------

Real              Virtual           Name
            9023              9023  prepare_protected_mode
            903B              903B  load_kernel
            9052              9052  load_kernel.next_sector
            9070              9070  load_kernel.destination_ok
            908A              908A  load_kernel.done
            908F              908F  load_error
            909A              909A  kernel_sectors_remaining
            909C              909C  error_hang
            90A7              90A7  init_protected_mode
            90E7              90E7  protected_mode_msg
            9108              9108  kernel_load_fail_msg
            9121              9121  kernel_load_msg
            9148              9148  gdt_start
            9150              9150  gdt_code_segment
            9158              9158  gdt_data_segment
            9160              9160  gdt_descriptor
            9166              9166  gdt_end
            9166              9166  enable_A20
            9196              9196  a20wait
            919D              919D  a20wait2
            91A4              91A4  asm_print_protected
            91AA              91AA  asm_print_protected.loop
            91BD              91BD  asm_print_protected_end
            91BF              91BF  asm_print_hex
            91D0              91D0  asm_print_hex.loop
            91E2              91E2  asm_print_hex.digit
            91E5              91E5  asm_print_hex.write

Después de cambiar el fondo de pantalla a gris -podés ver esto en el código fuente del OS en la sección Resources - hacemos una call a load_kernel que escencialmente lo que hace es copiar el binario del kernel (que todavía está en veremos) a la posición de memoria arbitraria 0x10000, definida en el KERNEL_OFFSET equ 0x10000 más arriba.

load_kernel:
    ; ---------------------------------------------------------
    ; Load KERNEL_SECTORS sectors from floppy into 0x10000
    ;
    ; Floppy geometry:
    ;   80 cylinders
    ;   2 heads
    ;   18 sectors per track
    ;
    ; Kernel starts at KERNEL_FIRST_SECTOR
    ; check build.sh for sector calculations
    ; ---------------------------------------------------------

    push ax
    push bx
    push cx
    push dx

    ; Destination = 0x10000 + n bytes, ES = 0x1000, BX = copy n bytes offset
    xor bx, bx
    mov ax, 0x1000
    mov es, ax

Una cosa para destacar en este listado (phuá, "listado" le mandé, volvimos a los 80s!) es el cuidado que hay que tener haciendo algo que voy a llamar ‘aritméticas de segmentos’ porque no se bien como llamarlo. Cada vez que, en 16 bits, tenés que escribir algo en memoria, tenés que tener en cuenta siempre en el segmento en el que vas a operar y como las instrucciones los usan o no y tener en cuenta la relacion que hay entre la dirección de memoria "logica" y la física teniendo en mente siempre la siguiente fórmula:

Dirección Física = (Segmento * 16) + Desplazamiento

Todo esto tiene que ver con como poder direccionar más de 64 kilobytes (o sea, 2^16 son 16 bits) cuando físicamente tenés uno, dos o más megabytes. Bueno, los muchachos de intel lo resolvieron asi y esto trae bastante dolores de cabeza porque esta aritmética hace que las direcciones no sean únicas y se generen solapamientos. La copia del Kernel a memoria la hago asi y después salto a Modo Protegido donde este problema desaparece y en el momento que estaba tratando de copiar un binario a memoria use las funciones del BIOS para copiar del diskette a memoria el kernel, porque quería darle soporte a 286 que tiene un modo protegido falopa y después se me complicó y quedó asi.

load_kernel funciona y cuando logré darle la vuelta para copiar el kernel y que funcione, no lo quise tocar más.

Si entendés mas o menos que estoy haciendo exactamente en:

    xor bx, bx
    mov ax, 0x1000
    mov es, ax

donde estoy preparando la copia, vas a ver que en vez de pararme en 0x10000 que sería lo lógico, me paro en <--> Segmento:Desplazamiento <--> 0x1000:0x0000 <--> ES:BX estoy haciendo absuso de notación para que se "NOTE BIEN" lo que estoy diciendo porque esto es fundamental para entender porque las cosas nunca caen en memoria donde uno espera.

O sea, por todo lo dicho:

Memoria Fisica: 0x10000 (cinco ceros) <—> 1000:0000 (cuatroceros, cuatroceros)

Y porque el segmento va en ES? cuando llamas a INTerrupciones o algunas instrucciones usan SEGmentos para operar. Es convención y los detalles estan en los manuales de intel y Ralf Brown’s Interrupt List,

pues le digo, mi ingeniero
que tiene la segmentación
pa' que no falte ocasión
y no se le pase el viaje
a esos bites ni el anclaje
ni una horrible excepción

Lo que sigue en la función load_kernel es una rutina estándar de copia de de sectores del disquette a memoria:

    ; Current CHS
    xor ch, ch                  ; cylinder = 0
    mov dh, 0                   ; head = 0
    mov cl, KERNEL_FIRST_SECTOR ; sector

    ; Number of sectors remaining
    mov word [kernel_sectors_remaining], KERNEL_SECTORS

.next_sector:

    cmp word [kernel_sectors_remaining], 0
    je .done

    ; BIOS read: exactly ONE sector
    mov ah, 0x02
    mov al, 1
    mov dl, 0                   ; floppy drive A:

    int 0x13
    jc load_error

    ; ---------------------------------------------------------
    ; Advance destination by 512 bytes
    ; ---------------------------------------------------------

    add bx, 512

    ; If BX wrapped around, advance ES by 0x1000
    ; because 0x1000:0000 -> 0x2000:0000
    jnc .destination_ok

    mov ax, es      ; yuck! 
    add ax, 0x1000  ; we are going to leave all this 
    mov es, ax      ; segment:offset madness very soon!

.destination_ok:

    ; One less sector to load
    dec word [kernel_sectors_remaining]

    ; ---------------------------------------------------------
    ; Advance CHS
    ; ---------------------------------------------------------

    inc cl                      ; next sector

    cmp cl, 19                  ; floppy has sectors 1..18
    jb .next_sector

    ; End of track -> next head
    mov cl, 1
    inc dh

    cmp dh, 2                   ; heads 0 and 1
    jb .next_sector

    ; End of cylinder -> next cylinder
    mov dh, 0
    inc ch

    jmp .next_sector


.done:
    pop dx
    pop cx
    pop bx
    pop ax
    ret


load_error:
    ; BIOS sets carry flag (CF) on error
    ; AX contains BIOS error information

    mov ebx, kernel_load_fail_msg
    call asm_print_protected

    jmp error_hang


kernel_sectors_remaining dw 0


error_hang:
        mov ebx, kernel_load_fail_msg
        call asm_print_protected
        jmp $       ; loop forever 
                    ; Instruction Pointer is self (JMP IP)

Preparando el Modo Protegido

El Real Mode en el que estamos es una porquería de los 70's/80's y en pleno 2019 no deberíamos estar escribiendo este tipo de código, asi que vamos a configurar el CPU para pasar de modo 16 bits a modo 32 bits.

Para esto, tenemos que hacer varias cosas:

  • Armar un descriptor en la Global Descriptor Table o GDT
  • Habilitar la línea del bus A20
  • Armar un Interrupt Descriptor Table IDT (Update: era un bardo mantenerlo en assembly, asi que lo pasé al Kernel en C y esto está en otro artículo)
  • Habilitar el Modo Protegido después de configurar todo esto a través del registro de control cr0
  • Saltar a código 32 Bits
; Ok, we now try to switch to protected mode
prepare_protected_mode:
    cli                     ; disable interrupts

    lgdt [gdt_descriptor]   ; load the gdt_descriptor table in gdt.asm
                            ; with the lgdt (load GDT) instruction

    ; Fast A20 Gate 286+ ? 
    ; using this Fast A20 gate resets the computer on 286
    ;in al, 0x92
    ;or al, 2
    ;out 0x92, al
    call enable_A20         ; so we try to use this neat function instead

    ; lidt instruction execution moved to C kernel! 
    ;lidt [idt_descriptor]

    ;xchg bx, bx             ; magic bochs breakpoint

    ; protected mode 286+ ?
    ; we are NOT supporting 286!!!!!111 t_t
    ; tOSh it's 386+ -ONLY-
    ;    lmsw ax        ; pre-cr0 register is the 'msw' register in 286
    ;    or ax, 0x1     ; and this way you enable
    ;    smsw ax        ; protected mode, but cannot make it work,
                        ; don't know what I missed

    mov eax, cr0
    or  eax, 0x1            ; set the 32-bit mode (Protected Mode)...
    mov cr0, eax            ; into cr0

    ; the CODE_SEGMENT is defined in gdt.asm
    jmp CODE_SEGMENT:init_protected_mode        ; far jump! 

Lo que voy a explicar aca es una exagerada superultrasobresimplificación sobre lo que habría que leer al menos dos volúmenes de los seis que componen los manuales de Intel donde se explican con lujo de detalles todo lo necesario para entender como configurar el CPU.

GDT

La GDT es una estructura de datos usada por los procesadores Intel (32/64 bits) para definir segmentos de memoria y permisos para poder escribir, leer y ejecutar en todas sus combinaciones posibles cada uno de esos segmentos. Es un array de 8 bytes que tiene la siguiente pinta en [tOSh]:

; https://wiki.osdev.org/GDT_Tutorial
; https://wiki.osdev.org/Global_Descriptor_Table
bits 32
db 'GDT'    ; GDT mark for bin
gdt_start:
    ; the first entry of the GDT must start with
    ; 8 null bytes

    dd 0x0
    dd 0x0

gdt_code_segment:
    ; this is the entry for the kernel address space
    dw 0xFFFF   ; limit 0:15 - 16 bits - 2 bytes
                ; (all will be pages of 4KiB, so take this into account)
    dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
    db 0x00     ; base  16:23 - 8 bits - 1 byte
                ; Base address will be 0
    db 10011010b ; access byte - 8 bits - 1 byte
                 ; present bit: 1
                 ; privilege, 2 bits, 0 = kernel space, 3 = userspace
                 ; S: Descriptor type: enable for code/data segments, 0 for system segments
                 ; executable bit: 1 
                 ; Direction bit/Conforming bit: 0, code only exec by priv lvl
                 ; RW (code segments read access, data segments always read, set for writing) : 1
                 ; Ac: accessed bit, set to 0 as recommended by osdev
    db 11001111b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
                 ; FLAGS (1st most significant nibble): 
                 ; Granularity: 0 = 1 byte blocks, 1= 4KiB blocks (pages)
                 ; Size bit: 0 = 16 bit protected mode, 1 = 32 bit protected mode
                 ; next to bytes in the nibble must be zero
                 ; 2nd nibble: limit 16:19
                 ; 
    db 00000000b ; base 24:31 - 8 bits - 1 byte

gdt_data_segment:
    ; this is the entry for the kernel address space
    dw 0xffff   ; limit 0:15 - 16 bits - 2 bytes
    dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
    db 0x00     ; base  16:23 - 8 bits - 1 byte
    db 10010010b ; access byte - 8 bits - 1 byte
    db 11001111b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
    db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_tss_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_ldt_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_user_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

gdt_end:

; constants to be used by stage1.asm

CODE_SEGMENT equ gdt_code_segment - gdt_start
DATA_SEGMENT equ gdt_data_segment - gdt_start
;TSS_SEGMENT  equ gdt_tss_segment  - gdt_start
;LDT_SEGMENT  equ gdt_ldt_segment  - gdt_start
;USER_SEGMENT equ gdt_user_segment - gdt_start

A20

La línea A20 es una representación física del bit número 21 de cualquier dirección de memoria. Por una cuestión de compatibilidad con procesadores anteriores, los diseñadores de los CPUs de Intel "la ataron con alambre". Hasta el dia de hoy, en procesadores modernos basados en Intel, hay que seguir configurando esta chanchada que se hizo hace como 50 años.

Hacele click a A20 y buscá en internet la historia completa de toda esa subtrama de diseño de procesadores y de Intel que no tiene desperdicio. Nosotros podríamos skippear poskippeamos todo el lore subiendo la vara de nuestro Sistema Operativo para computadoras PC Compatibles IBM PS/2+ usando el metodo mencionado en osdev: Fast A20 Gate:

in al, 0x92
or al, 2
out 0x92, al

Pero mejor incluimos y llamamos un a20.asm que copié y pegué de algún lugar de la mancha el cual no puedo acordarme.

cr0

    mov eax, cr0
    or  eax, 0x1            ; set the 32-bit mode (Protected Mode)...
    mov cr0, eax            ; into cr0

    ; the CODE_SEGMENT is defined in gdt.asm
    jmp CODE_SEGMENT:init_protected_mode        ; far jump! 

Setiamos con un OR el registro de control cr0 el bit 1 para habilitar el Modo Protegido y saltamos directamente al código 32 bits.

Hay banda de cosas más que se pueden configurar en el CPU pero dejamos las otras funcionalidades y configuraciones disponibles como ejercicio para el lector ;-).

Un Mundo de 32 Bits

Llegamos (¡finalmente!) al mundo de los 32 bits y el realm de las direcciones de memoria lineal. Copiamos un "kernel misterioso hecho en C" a memoria pero antes de poder usarlo, tenemos que obviamente seguir configurando el CPU, sus segmentos en el modo 32 bits y además el stack.

pHUN pHAKT: Al stack en criollo le decimos "La Pila".

; ---------------------------------------------------------------------
; 32 bit code! now we are able to use 32 bit instructions at this point
; ---------------------------------------------------------------------
bits 32         
init_protected_mode:
    ; and we need to setup the segments again
    ; with our previously configured GDT configuration addresses
    ; find gdt.asm and other %includes further in this listing
    mov ax, DATA_SEGMENT        ; defined in gdt.asm
    mov ds, ax                  ; data segment
    mov es, ax                  ; extended segment
    mov fs, ax                  ; fuck you segment
    mov ss, ax                  ; stack segment
    ;mov ax, USER_SEGMENT       ; in the future, maybe...
    mov gs, ax                  ; gorgeous segment <3
    ;mov ax, CODE_SEGMENT
    ;mov cs, ax                 ; TODO: why i didn't setup the code segment? 
                                ;       while reviewing the code for the article
                                ;       I found this commented like years ago
                                ;       and I got startled. Maybe for another
                                ;       time...

    ;ret ; !!!!!
    ; set up the stack
    ;mov ebp, 0xf90000        ; 16MB RAM machine! this breaks old 86box / PCem Machines!
    mov ebp, STACK_ADDRESS    ; equ 0x80000 --> stack ~ 576KiB
    mov esp, ebp

Una de las grandes sesiones de debug que tuve que spawnearon pedazos de ISABugger-Related-Code fue entender porque volaba por los aires nuestro kernel y era exactamente setear el stack correctamente. Como se puede apreciar en el asm, estaba usando la dirección 0xf90000 como extended base pointer y extended stack pointer para definir donde iba a vivir nuestro stack y en las VMs que estaba probando, no tenia TANTA memoria, haciendo que me vaya fuera de los boundaries y el CPU me tire una excepción irrecuperable. Solo una instrucción y bastante tiempo de debugging con bochs me hicieron dar cuenta del error. Entonces, el stack ahora vive aproximadamente alrededor de los 576KiB.

    ; [ a lot of debugging stuff ]

    ; PIC Remapping ! (now handled by kernel at pic.c)

    ; mov al, 0x11
    ; out 0x20, al        ; restart pic1
    ; out 0xa0, al        ; restart pic2

    ; mov al, 0x20
    ; out 0x21, al        ; pic1 now starts at 32
    ; mov al, 0x28
    ; out 0xa1, al        ; pic2 now starts at 40

    ; mov al, 0x04
    ; out 0x21, al        ; setup cascading
    ; mov al, 0x02
    ; out 0xa1, al

    ; mov al, 0x01
    ; out 0x21, al
    ; out 0xa1, al        ; listo!


    ; ; IRQ1 = keyboard
    ; mov al, 0xFD
    ; out 0x21, al

    ; ; mask slave PIC
    ; mov al, 0xFF
    ; out 0xA1, al
    
    ; the kernel is the responsible for enabling
    ; the interrupts!
    ; the kernel now initializes IDT --> THEN STI()
    ;sti ; enable interrupts!

    jmp  KERNEL_OFFSET ; execute kernel code
  

Y el último jmp. Todo lo demás fue comentado porque lo pasé al kernel hecho en C el cual vamos a saltar, el próximo artículo. Recapitulando, prácticamente lo que hicimos fue copiar el kernel que todavía no tenemos, saltar a Modo Protegido, una vez ahí, preparar el stack en 32 bits y finalmente saltar al Kernel C Entrypoint.

Referencias