assembly x86 bootloader bios usb-drive

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