assembly - El cargador de arranque no salta al código del núcleo
virtualbox nasm (1)
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é el gestor de arranque que restablece la unidad y luego carga el sector después del gestor de arranque.
El problema es porque después de la función
jmp
, nada sucede realmente.
No estoy tratando de cargar el siguiente sector en 0x7E00 (no estoy totalmente seguro de cómo apuntar la dirección usando es: bx, por lo que puede ser un problema, creo que es Address: 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 se muestra, así como el mensaje que debería imprimirse.
Los principales problemas con este código fueron:
- ES: BX apuntaba al segmento incorrecto: desplazamiento para cargar el núcleo en
- Se estaba cargando un sector incorrecto, por lo que el núcleo no era lo que se esperaba
El primero estaba en este código:
mov bx,0x7E00
mov es,bx
xor bx,bx
La pregunta quiere cargar el sector del disco a
0x0000:0x7E00
(
ES: BX
).
Este código establece
ES: BX
en
0x7E00:0x0000
que 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
here
.
Multiplicar el segmento por 16 es lo mismo que desplazarlo a la izquierda 4 bits.
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 bloque 512 en el disco es 2, no 1. Entonces, para arreglar el código anterior, debe configurar CL en consecuencia:
mov cl,0x2 ;sector number
Consejos generales para el desarrollo del cargador de arranque
Otros problemas que pueden tropezar con el código en ejecución 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. Solo se le puede garantizar 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 sabe que no entrará en conflicto con la operación de su propio código. El BIOS puede haber colocado su puntero de pila predeterminado en cualquier lugar en el primer megabyte de RAM utilizable y direccionable. No hay garantía de dónde es eso y si será adecuado para el código que escriba.
-
El indicador de dirección utilizado por
lodsb
,lodsb
, etc. podría establecerse omovsb
. Si el indicador de dirección está configurado incorrectamente, los registros SI / DI pueden ajustarse en la dirección incorrecta. UseSTD
/CLD
para configurarlo en la dirección que desee (CLD = hacia adelante / STD = hacia atrás). En este caso, el código supone un movimiento hacia adelante, por lo que uno debe usarCLD
. 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 usar FAR JMP para que establezca correctamente CS: IP en los valores esperados. Esto puede evitar problemas con el código del kernel que puede hacer JMPs y CALLs absolutos cerca del mismo segmento.
- Si apunta a su cargador de arranque para código de 16 bits que funciona en procesadores 8086/8088 (Y superior), evite el uso de registros de 32 bits en el código de ensamblaje. 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, ha sido un problema para otros 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.
- Los registros de segmento FS y GS se agregaron a 80386+ CPU. Evítalos si tienes la intención de apuntar a 8086/8088/80286.
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 un
MOV
), se supone que el procesador apaga las interrupciones para esa instrucción y las mantiene apagadas hasta
después de
la siguiente instrucción.
Normalmente no necesita preocuparse por deshabilitar las interrupciones si actualiza
SS
seguido inmediatamente por una actualización de
SP
.
Hay un error en los procesadores 8088 muy tempranos en el que esto no se respetaba, 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 en un buggy 8088, las instrucciones
CLI
/
STI
se pueden eliminar en el código anterior.
Conozco este error de primera mano con el 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 muchas 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 pila (
SP
) a
0
.
Cuando
empuja 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 en 0x0000-2 = 0xFFFE (-2).
Entonces tendría un
SS: SP
que se parece a
0x8000:0xFFFE
.
En este caso, la pila se ejecuta desde
0x8000:0x0000
a
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 establecer el puntero de pila ( SP ) en un número par. En el 8086 original, si configura el 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. Como 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 8086 tomó 4 ciclos de reloj, mientras que tomó 8 ciclos de reloj en el 8088 (dos lecturas de memoria de 8 bits).
Por último, si desea establecer
CS: IP de
manera explícita para que
CS
esté configurado correctamente para cuando el
JMP
esté completo (en su núcleo), se recomienda hacer un
JMP FAR
(
consulte Operaciones que afectan a los registros de segmento
/
Salto FAR
).
En la sintaxis NASM, el
JMP
se vería así:
jmp 0x07E0:0x0000
Algunos ensambladores (es decir, MASM / MASM32) no tienen soporte directo para codificar un FAR Jmp, por lo que una forma de hacerlo es manualmente de esta manera:
db 0x0ea ; Far Jump instruction
dw 0x0000 ; Offset
dw 0x07E0 ; Segment
Si usa el ensamblador GNU se vería así:
ljmpw $0x07E0,$0x0000