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:
- Parte I - Bootloader
- Parte II - Stage 1 <— Estás acá
- Parte III - Saltando a C
- Parte IV - Interrupciones
- Parte V - Sistema de Archivos y Driver de Disquetera
- Parte VI - Booteando en diferentes tipos de máquinas
Turbo 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.