assembly - El cargador de arranque personalizado que se inicia a través de una unidad USB produce una salida incorrecta en algunas computadoras
x86 bootloader (2)
El código de ensamblaje solo funciona en uno de mis dos procesadores x86
No son los procesadores sino las BIOS:
La instrucción
int
realidad es una variante especial de la instrucción de
call
.
La instrucción llama a alguna subrutina (típicamente escrita en ensamblador).
(Incluso puede reemplazar esa subrutina por la suya propia, que en realidad es realizada por MS-DOS, por ejemplo).
En dos computadoras tiene dos versiones diferentes de BIOS (o incluso proveedores), lo que significa que la subrutina llamada por la instrucción
int 10h
ha sido escrita por diferentes programadores y, por lo tanto, no hace exactamente lo mismo.
solo para obtener el siguiente resultado
El problema que sospecho aquí es que la subrutina llamada por
int 10h
en la primera computadora no guarda los valores de registro mientras que la rutina en la segunda computadora sí.
En otras palabras:
En la primera computadora, la rutina llamada por
int 10h
puede verse así:
...
mov cl, 5
mov ah, 6
...
... así que después de la llamada
int 10h
el registro
ah
ya no contiene el valor
0Eh
e incluso puede darse el caso de que se modifique el registro
cl
(que terminará en un bucle sin fin).
Para evitar el problema, puede guardar el registro
cl
mediante
push
(debe guardar todo el registro
cx
) y restaurarlo después de la instrucción
int
.
También debe establecer el valor del registro
ah
antes de cada llamada de la subrutina
int 10h
porque no puede estar seguro de que no se haya modificado desde entonces:
push cx
mov ah, 0Eh
int 10h
pop cx
mov sp, ...
...ret
Piensa en el comentario de Peter Cordes:
¿Cómo funciona la instrucción
ret
y cómo se relaciona con los registros
sp
y
ss
?
¡Las instrucciones de
ret
aquí definitivamente no harán lo que esperas!
En los disquetes, los sectores de arranque suelen contener el siguiente código:
mov ax, 0 ; (may be written as "xor ax, ax")
int 16h
int 19h
int 19h
hace exactamente lo que espera de la instrucción
ret
.
Sin embargo, el BIOS reiniciará la computadora nuevamente, lo que significa que cargará el código de su memoria USB y lo ejecutará nuevamente.
Obtendrás el siguiente resultado:
AAAAABAAAAABAAAAABAAAAAB ...
Por lo tanto, se inserta la instrucción
int 16h
.
Esto esperará a que el usuario presione una tecla en el teclado cuando el registro
ax
tenga el valor 0 antes de llamar a la subrutina
int 16h
.
Alternativamente, puede simplemente agregar un bucle sin fin:
.endlessLoop:
jmp .endlessLoop
mov ss, ...
Cuando se produce una interrupción entre estas dos instrucciones:
mov ss, ax
; <--- Here
mov sp, 4096
... la combinación de los registros
sp
y
ss
no representa una representación "válida" de valores.
Si no tiene suerte, la interrupción escribirá datos en algún lugar de la memoria donde no los desee. ¡Incluso puede sobrescribir su programa!
Por lo tanto, normalmente bloquea las interrupciones al modificar el registro
ss
:
cli ; Forbid interrupts
mov ss, ax
mov sp, 4096
sti ; Allow interrupts again
Soy bastante nuevo en el ensamblaje, pero estoy tratando de sumergirme en el mundo de la informática de bajo nivel. Estoy tratando de aprender a escribir código de ensamblaje que se ejecute como código del gestor de arranque; tan independiente de cualquier otro sistema operativo como Linux o Windows. Después de leer esta página y algunas otras listas de conjuntos de instrucciones x86, se me ocurrió un código de ensamblaje que supuestamente imprime 10 A en la pantalla y luego 1 B.
BITS 16
start:
mov ax, 07C0h ; Set up 4K stack space after this bootloader
add ax, 288 ; (4096 + 512) / 16 bytes per paragraph
mov ss, ax
mov sp, 4096
mov ax, 07C0h ; Set data segment to where we''re loaded
mov ds, ax
mov cl, 10 ; Use this register as our loop counter
mov ah, 0Eh ; This register holds our BIOS instruction
.repeat:
mov al, 41h ; Put ASCII ''A'' into this register
int 10h ; Execute our BIOS print instruction
cmp cl, 0 ; Find out if we''ve reached the end of our loop
dec cl ; Decrement our loop counter
jnz .repeat ; Jump back to the beginning of our loop
jmp .done ; Finish the program when our loop is done
.done:
mov al, 42h ; Put ASCII ''B'' into this register
int 10h ; Execute BIOS print instruction
ret
times 510-($-$$) db 0 ; Pad remainder of boot sector with 0s
dw 0xAA55
Entonces la salida debería verse así:
AAAAAAAAAAB
Ensamblé el código usando el ensamblador nasm que se ejecuta en el programa Ubuntu 10 Bash de Windows 10. Después de que produjo el archivo .bin, lo abrí usando un editor hexadecimal. Usé el mismo editor hexadecimal para copiar el contenido de ese archivo .bin en los primeros 512 bytes de una unidad flash. Una vez que escribí mi programa en la unidad flash, lo desconecté y lo enchufé a una computadora con un Intel Core i3-7100. En el arranque, seleccioné mi unidad flash USB como dispositivo de arranque, solo para obtener el siguiente resultado:
A
Después de cambiar varias cosas en el programa, finalmente me frustré y probé el programa en otra computadora. La otra computadora era una laptop con un i5-2520m. Seguí el mismo proceso que mencioné antes. Efectivamente, me dio el resultado esperado:
AAAAAAAAAAB
Inmediatamente lo probé en mi computadora original con el i3, pero todavía no funcionó.
Entonces mi pregunta es: ¿por qué mi programa funciona con un procesador x86 pero no con el otro? Ambos admiten el conjunto de instrucciones x86. ¿Lo que da?
Solución:
Ok, he podido localizar la solución real con algo de ayuda.
Si lee la respuesta de Michael Petch a continuación, encontrará una solución que solucionará mi problema y otro problema de un BIOS que busca un BPB.
Aquí estaba el problema con mi código: estaba escribiendo el programa en los primeros bytes de mi unidad flash. Esos bytes se cargaron en la memoria, pero algunas interrupciones del BIOS estaban usando esos bytes para sí mismo. Así que mi BIOS estaba sobrescribiendo mi programa. Para evitar esto, puede agregar una descripción de BPB como se muestra a continuación. Si su BIOS funciona de la misma manera que el mío, simplemente sobrescribirá el BPB en la memoria, pero no su programa. Alternativamente, puede agregar el siguiente código a la parte superior de su programa:
jmp start
resb 0x50
start:
;enter code here
Este código (cortesía de Ross Ridge) empujará su programa a la ubicación de memoria 0x50 (desplazamiento de 0x7c00) para evitar que el BIOS lo sobrescriba durante la ejecución.
También tenga en cuenta que siempre que llame a cualquier subrutina, los valores de los registros que estaba utilizando podrían sobrescribirse.
Asegúrese de usar
push
,
pop
o guardar sus valores en la memoria antes de llamar a una subrutina.
Mire la respuesta de Martin Rosenau a continuación para leer más sobre eso.
Gracias a todos los que respondieron a mi pregunta. Ahora entiendo mejor cómo funciona este material de bajo nivel.
Esto probablemente podría convertirse en una respuesta canónica sobre este tema.
Problemas de hardware real / USB / computadora portátil
Si está intentando usar USB para arrancar en hardware real, entonces puede encontrar otro problema incluso si funciona en BOCHS y QEMU . Si su BIOS está configurado para hacer emulación USB FDD (y no USB HDD u otra cosa), es posible que deba agregar un Bloque de parámetros de BIOS (BPB) al comienzo de su gestor de arranque. Puedes crear una falsa como esta:
org 0x7c00
bits 16
boot:
jmp main
TIMES 3-($-$$) DB 0x90 ; Support 2 or 3 byte encoded JMPs before BPB.
; Dos 4.0 EBPB 1.44MB floppy
OEMname: db "mkfs.fat" ; mkfs.fat is what OEMname mkdosfs uses
bytesPerSector: dw 512
sectPerCluster: db 1
reservedSectors: dw 1
numFAT: db 2
numRootDirEntries: dw 224
numSectors: dw 2880
mediaType: db 0xf0
numFATsectors: dw 9
sectorsPerTrack: dw 18
numHeads: dw 2
numHiddenSectors: dd 0
numSectorsHuge: dd 0
driveNum: db 0
reserved: db 0
signature: db 0x29
volumeID: dd 0x2d7e5a1a
volumeLabel: db "NO NAME "
fileSysType: db "FAT12 "
main:
[insert your code here]
Ajuste la directiva
ORG
a lo que necesita u omítala si solo necesita el 0x0000 predeterminado.
Si tuviera que modificar su código para tener el diseño sobre el comando de
file
Unix / Linux
file
podría volcar los datos de BPB que cree que forman su MBR en la imagen del disco.
Ejecute el
file disk.img
comando
file disk.img
y puede obtener este resultado:
disk.img: sector de arranque DOS / MBR, desplazamiento de código 0x3c + 2, ID de OEM "mkfs.fat", entradas raíz 224, sectores 2880 (volúmenes <= 32 MB), sectores / FAT 9, sectores / pista 18, serie número 0x2d7e5a1a, sin etiquetar, FAT (12 bits)
Cómo se podría modificar el código en esta pregunta
En el caso de este código original de OP, podría haberse modificado para tener este aspecto:
bits 16
boot:
jmp main
TIMES 3-($-$$) DB 0x90 ; Support 2 or 3 byte encoded JMPs before BPB.
; Dos 4.0 EBPB 1.44MB floppy
OEMname: db "mkfs.fat" ; mkfs.fat is what OEMname mkdosfs uses
bytesPerSector: dw 512
sectPerCluster: db 1
reservedSectors: dw 1
numFAT: db 2
numRootDirEntries: dw 224
numSectors: dw 2880
mediaType: db 0xf0
numFATsectors: dw 9
sectorsPerTrack: dw 18
numHeads: dw 2
numHiddenSectors: dd 0
numSectorsHuge: dd 0
driveNum: db 0
reserved: db 0
signature: db 0x29
volumeID: dd 0x2d7e5a1a
volumeLabel: db "NO NAME "
fileSysType: db "FAT12 "
main:
mov ax, 07C0h ; Set up 4K stack space after this bootloader
add ax, 288 ; (4096 + 512) / 16 bytes per paragraph
mov ss, ax
mov sp, 4096
mov ax, 07C0h ; Set data segment to where we''re loaded
mov ds, ax
mov cl, 10 ; Use this register as our loop counter
mov ah, 0Eh ; This register holds our BIOS instruction
.repeat:
mov al, 41h ; Put ASCII ''A'' into this register
int 10h ; Execute our BIOS print instruction
cmp cl, 0 ; Find out if we''ve reached the end of our loop
dec cl ; Decrement our loop counter
jnz .repeat ; Jump back to the beginning of our loop
jmp .done ; Finish the program when our loop is done
.done:
mov al, 42h ; Put ASCII ''B'' into this register
int 10h ; Execute BIOS print instruction
ret
times 510-($-$$) db 0 ; Pad remainder of boot sector with 0s
dw 0xAA55
Otras sugerencias
Como se ha señalado, no puede
ret
para finalizar un gestor de arranque.
Puede ponerlo en un bucle infinito o detener el procesador con
cli
seguido de
hlt
.
Si alguna vez asigna una gran cantidad de datos en la pila o comienza a escribir en datos fuera de los 512 bytes de su gestor de arranque, debe configurar su propio puntero de pila ( SS: SP ) en una región de memoria que no interfiera con su propio código . El código original en esta pregunta configura un puntero de pila. Esta es una observación general para cualquiera que lea este Q / A. Tengo más información sobre eso en mi respuesta de que contiene consejos generales para el cargador de arranque .
Código de prueba para ver si su BIOS está sobrescribiendo el BPB
Si desea saber si el BIOS podría estar sobrescribiendo datos en el BPB y para determinar qué valores escribió, puede usar este código del gestor de arranque para volcar el BPB tal como lo ve el gestor de arranque después de transferirle el control.
En circunstancias normales, los primeros 3 bytes deben ser
EB 3C 90
seguidos de una serie de
AA
.
Cualquier valor que no sea
AA
probablemente fue sobrescrito por el BIOS.
Este código está en
NASM
y puede ensamblarse en un gestor de arranque con
nasm -f bin boot.asm -o boot.bin
; Simple bootloader that dumps the bytes in the BIOS Parameter
; Block BPB. First 3 bytes should be EB 3C 90. The rest should be 0xAA
; unless you have a BIOS that wrote drive geometry information
; into what it thinks is a BPB.
; Macro to print a character out with char in BX
%macro print_char 1
mov al, %1
call bios_print_char
%endmacro
org 0x7c00
bits 16
boot:
jmp main
TIMES 3-($-$$) DB 0x90 ; Support 2 or 3 byte encoded JMPs before BPB.
; Fake BPB filed with 0xAA
TIMES 59 DB 0xAA
main:
xor ax, ax
mov ds, ax
mov ss, ax ; Set stack just below bootloader at 0x0000:0x7c00
mov sp, boot
cld ; Forward direction for string instructions
mov si, sp ; Print bytes from start of bootloader
mov cx, main-boot ; Number of bytes in BPB
mov dx, 8 ; Initialize column counter to 8
; So first iteration prints address
.tblloop:
cmp dx, 8 ; Every 8 hex value print CRLF/address/Colon/Space
jne .procbyte
print_char 0x0d ; Print CRLF
print_char 0x0a
mov bx, si ; Print current address
call print_word_hex
print_char '':'' ; Print '': ''
print_char '' ''
xor dx, dx ; Reset column counter to 0
.procbyte:
lodsb ; Get byte to print in AL
call print_byte_hex ; Print the byte (in BL) in HEX
print_char '' ''
inc dx ; Increment the column count
dec cx ; Decrement number of bytes to process
jnz .tblloop
cli ; Halt processor indefinitely
.end:
hlt
jmp .end
; Print the character passed in AL
bios_print_char:
push bx
xor bx, bx ; Attribute=0/Current Video Page=0
mov ah, 0x0e
int 0x10 ; Display character
pop bx
ret
; Print the 16-bit value in AX as HEX
print_word_hex:
xchg al, ah ; Print the high byte first
call print_byte_hex
xchg al, ah ; Print the low byte second
call print_byte_hex
ret
; Print lower 8 bits of AL as HEX
print_byte_hex:
push bx
push cx
push ax
lea bx, [.table] ; Get translation table address
; Translate each nibble to its ASCII equivalent
mov ah, al ; Make copy of byte to print
and al, 0x0f ; Isolate lower nibble in AL
mov cl, 4
shr ah, cl ; Isolate the upper nibble in AH
xlat ; Translate lower nibble to ASCII
xchg ah, al
xlat ; Translate upper nibble to ASCII
xor bx, bx ; Attribute=0/Current Video Page=0
mov ch, ah ; Make copy of lower nibble
mov ah, 0x0e
int 0x10 ; Print the high nibble
mov al, ch
int 0x10 ; Print the low nibble
pop ax
pop cx
pop bx
ret
.table: db "0123456789ABCDEF", 0
; boot signature
TIMES 510-($-$$) db 0
dw 0xAA55
La salida debería tener este aspecto para cualquier BIOS que no haya actualizado el BPB antes de transferir el control al código del cargador de arranque:
7C00: EB 3C 90 AA AA AA AA AA 7C08: AA AA AA AA AA AA AA AA 7C10: AA AA AA AA AA AA AA AA 7C18: AA AA AA AA AA AA AA AA 7C20: AA AA AA AA AA AA AA AA 7C28: AA AA AA AA AA AA AA AA 7C30: AA AA AA AA AA AA AA AA 7C38: AA AA AA AA AA AA