El cargador de arranque no salta al código del kernel
Estoy escribiendo un pequeño sistema operativo, para practicar. Empecé con el gestor de arranque.
Quiero crear un pequeño sistema de comando que se ejecute en modo real de 16 bits (por ahora).
Creé un gestor de arranque que reinicia la unidad y luego carga el sector tras el gestor de arranque.
El problema es que después de jmp
la función no sucede nada.
No estoy intentando cargar el siguiente sector en 0x7E00 (no estoy totalmente seguro de cómo señalar la dirección usando es:bx, por lo que puede ser un problema, creo que es Dirección:offset), justo después del gestor de arranque.
Este es el código:
;
; SECTOR 0x0
;
;dl is number of harddrive where is bootloader
org 0x7C00
bits 16
;reset hard drive
xor ah,ah
int 0x13
;read sectors
clc
mov bx,0x7E00
mov es,bx
xor bx,bx
mov ah,0x02 ;function
mov al,0x1 ;sectors to read
mov ch,0x0 ;tracks
mov cl,0x1 ;sector
mov dh,0x0 ;head
int 0x13
;if not readed jmp to error
jc error
;jump to 0x7E00 - executed only if loaded
jmp 0x7E00
error:
mov si,MSGError
.loop:
lodsb
or al,al
jz .end
mov ah,0x0E
int 0x10
jmp .loop
.end:
hlt
MSGError db "Error while booting", 0x0
times 0x1FE - ($ - $$) db 0x0
db 0x55
db 0xAA
;
; SECTOR 0x1
;
jmp printtest
;definitions
MSGLoaded db "Execution successful", 0x0
;
; Print function
; si - message to pring (NEED TO BE FINISHED WITH 0x0)
printtest:
mov si,MSGLoaded
.loop:
lodsb
or al,al
jz .end
mov ah,0x0E
int 0x10
jmp .loop
.end:
hlt
times 0x400 - ($-$$) db 0x0
He estado probando este código usando VirtualBox pero en realidad no sucede nada. El error de lectura no aparece, así como el mensaje que debería imprimirse.
Los principales problemas con este código fueron:
- ES:BX estaba apuntando al segmento incorrecto: desplazamiento para cargar el kernel
- Se estaba cargando un sector incorrecto por lo que el kernel no era lo esperado
El primero estaba en este código:
mov bx,0x7E00
mov es,bx
xor bx,bx
La pregunta quiere cargar el sector desde el disco a 0x0000:0x7E00
( ES:BX ). Este código configura el ES:BX que 0x7E00:0x0000
se resuelve en una dirección física de 0x7E000
((0x7E00<<4)+0x0000). Creo que la intención era cargar 0x07E0
en ES lo que produciría una dirección física de 0x7E00
((0x07E0<<4)+0x0000). Puede obtener más información sobre los cálculos de direccionamiento de memoria 16:16 aquí . Multiplicar el segmento por 16 es lo mismo que desplazarlo 4 bits a la izquierda.
El segundo problema en el código está aquí:
mov ah,0x02 ;function
mov al,0x1 ;sectors to read
mov ch,0x0 ;tracks
mov cl,0x2 ;sector number
mov dh,0x0 ;head
int 0x13
El número para el segundo sector de 512 bloques en el disco es 2, no 1. Entonces, para corregir el código anterior, debe configurar CL en consecuencia:
mov cl,0x2 ;sector number
Consejos generales para el desarrollo del gestor de arranque
Otros problemas que pueden dificultar la ejecución de código en varios emuladores, máquinas virtuales y hardware físico real que deben abordarse son:
- Cuando el BIOS salta a su código, no puede confiar en que los registros CS , DS , ES , SS , SP tengan valores válidos o esperados. Deben configurarse adecuadamente cuando se inicia el gestor de arranque. Sólo puede tener la garantía de que su gestor de arranque se cargará y ejecutará desde la dirección física 0x00007c00 y que el número de la unidad de arranque se cargará en el registro DL .
- Configure SS:SP en la memoria que sepa que no entrará en conflicto con la operación de su propio código. Es posible que el BIOS haya colocado su puntero de pila predeterminado en cualquier lugar del primer megabyte de RAM utilizable y direccionable. No hay garantía de dónde está y si será adecuado para el código que escriba.
- El indicador de dirección utilizado por
lodsb
,movsb
etc. podría establecerse o borrarse. Si el indicador de dirección está configurado incorrectamente, es posible que los registros SI / DI se ajusten en la dirección incorrecta. UtiliceSTD
/CLD
para configurarlo en la dirección que desee (CLD=adelante/STD=atrás). En este caso, el código supone un movimiento hacia adelante, por lo que se debe utilizarCLD
. Puede encontrar más información sobre esto en una referencia del conjunto de instrucciones. - Al saltar a un kernel, generalmente es una buena idea utilizar FAR JMP para que establezca correctamente CS:IP en los valores esperados. Esto puede evitar problemas con el código del kernel que puede funcionar de manera absoluta cerca de JMP y CALL dentro del mismo segmento.
- Si apunta a su cargador de arranque para código de 16 bits que funcione en procesadores 8086/8088 (Y superiores), evite el uso de registros de 32 bits en el código ensamblador. Utilice AX / BX / CX / DX / SI / DI / SP / BP en lugar de EAX / EBX / ECX / EDX / ESI / EDI / ESP / EBP . Aunque no es un problema en esta pregunta, lo ha sido para otras personas que buscan ayuda. Un procesador de 32 bits puede utilizar registros de 32 bits en modo real de 16 bits, pero un 8086/8088/80286 no puede porque eran procesadores de 16 bits sin acceso a registros extendidos de 32 bits.
- Se agregaron registros de segmentos FS y GS a más de 80386 CPU. Evítelos si tiene intención de apuntar al 8086/8088/80286.
- Nota: Este es un problema muy común sobre el que se pregunta en Stackoverflow : si tiene la intención de iniciar como medio USB usando emulación de disquete (FDD) en hardware real, es posible que necesite tener un bloque de parámetros de BIOS (BPB) en su sector de inicio. Puede encontrar información más detallada en esta respuesta relacionada de Stackoverflow que proporciona un BPB de ejemplo y una herramienta para ver si su BIOS sobrescribe los datos en el BPB después de cargar su sector de arranque en la memoria.
Para resolver el primer y segundo elemento, este código se puede utilizar cerca del inicio del cargador de arranque:
xor ax,ax ; We want a segment of 0 for DS for this question
mov ds,ax ; Set AX to appropriate segment value for your situation
mov es,ax ; In this case we'll default to ES=DS
mov bx,0x8000 ; Stack segment can be any usable memory
cli ; Disable interrupts to circumvent bug on early 8088 CPUs
mov ss,bx ; This places it with the top of the stack @ 0x80000.
mov sp,ax ; Set SP=0 so the bottom of stack will be @ 0x8FFFF
sti ; Re-enable interrupts
cld ; Set the direction flag to be positive direction
Un par de cosas a tener en cuenta. Cuando cambia el valor del registro SS (en este caso a través de a MOV
), se supone que el procesador desactiva las interrupciones para esa instrucción y las mantiene desactivadas hasta después de la siguiente instrucción. Normalmente no necesita preocuparse por deshabilitar las interrupciones si actualiza SS seguido inmediatamente de una actualización de SP . Hay un error en los primeros procesadores 8088 donde esto no se cumplió, por lo que si se dirige a los entornos más amplios posibles, es una apuesta segura deshabilitarlos y volver a habilitarlos explícitamente. Si no tiene la intención de trabajar nunca en un 8088 con errores, las instrucciones CLI
/ STI
se pueden eliminar en el código anterior. Conozco este error de primera mano gracias al trabajo que hice a mediados de los 80 en la PC de mi casa.
La segunda cosa a tener en cuenta es cómo configuro la pila. Para las personas nuevas en el ensamblaje de 16 bits 8088/8086, la pila se puede configurar de varias maneras. En este caso configuré la parte superior de la pila (la parte más baja de la memoria) en 0x8000
( SS ). Luego configuré el puntero de la pila ( SP ) en 0
. Cuando inserta algo en la pila en modo real de 16 bits, el procesador primero disminuye el puntero de la pila en 2 y luego coloca una PALABRA de 16 bits en esa ubicación. Por lo tanto, el primer impulso a la pila sería 0x0000-2 = 0xFFFE (-2). Entonces tendrías un SS:SP que se parece a 0x8000:0xFFFE
. En este caso la pila va desde 0x8000:0x0000
hasta 0x8000:0xFFFF
.
Cuando se trata de la pila que se ejecuta en un 8086 (no se aplica a los procesadores 80286,80386+), es una buena idea configurar el puntero de la pila ( SP ) en un número par. En el 8086 original, si configura SP en un número impar, incurriría en una penalización de 4 ciclos de reloj por cada acceso al espacio de la pila. Dado que el 8088 tenía un bus de datos de 8 bits, esta penalización no existía, pero cargar una palabra de 16 bits en el 8086 requirió 4 ciclos de reloj, mientras que en el 8088 tomó 8 ciclos de reloj (dos lecturas de memoria de 8 bits).
Lastly, If you want to explicitly set CS:IP so that CS is properly set by the time the JMP is complete (to your kernel) then it is recommended to do a FAR JMP (See Operations that affect segment registers/FAR Jump). In NASM syntax the JMP
would look like this:
jmp 0x07E0:0x0000
Some (ie MASM/MASM32) assemblers don't have direct support to encode a FAR Jmp so one way it can be done is manually like this:
db 0x0ea ; Far Jump instruction
dw 0x0000 ; Offset
dw 0x07E0 ; Segment
If using GNU assembler it would look like:
ljmpw $0x07E0,$0x0000