assembly - O bootloader personalizado inicializado via drive USB produz saída incorreta em alguns computadores




x86 bios (2)

O código de montagem funciona apenas em um dos meus dois processadores x86

Não são os processadores, mas os BIOSes:

A instrução int na verdade, é uma variante especial da instrução de call . A instrução chama alguma sub-rotina (normalmente escrita em assembler).

(Você pode até mesmo substituir essa sub-rotina por sua própria - que é realmente feita pelo MS-DOS, por exemplo).

Em dois computadores, você tem duas versões diferentes do BIOS (ou até mesmo fornecedores), o que significa que a sub-rotina chamada pela instrução int 10h foi escrita por diferentes programadores e, portanto, não faz exatamente o mesmo.

apenas para obter a seguinte saída

O problema que eu suspeito aqui é que a sub-rotina chamada por int 10h no primeiro computador não salva os valores de registro enquanto a rotina no segundo computador faz.

Em outras palavras:

No primeiro computador a rotina chamada por int 10h pode ser assim:

...
mov cl, 5
mov ah, 6
...

... então após o int 10h chamar o ah register não contém mais o valor 0Eh e pode até ser o caso que o registrador cl seja modificado (o qual terminará em um loop infinito então).

Para evitar o problema, você poderia salvar o registrador cl usando push (você tem que salvar todo o registro cx ) e restaurá-lo após a instrução int . Você também tem que definir o valor do registrador ah antes de cada chamada da sub-rotina int 10h porque você não pode ter certeza de que ele não foi modificado desde então:

push cx
mov ah, 0Eh
int 10h
pop cx

mov sp, ... ... ret

Por favor, pense no comentário de Peter Cordes:

Como funciona a instrução ret e como ela está relacionada aos registradores sp e ss ?

A instrução ret aqui definitivamente não fará o que você espera!

Em disquetes, os setores de inicialização normalmente contêm o seguinte código:

mov ax, 0  ; (may be written as "xor ax, ax")
int 16h
int 19h

int 19h faz exatamente o que você espera da instrução ret .

No entanto, o BIOS irá inicializar o computador novamente, o que significa que ele irá carregar o código do seu pendrive e executá-lo novamente.

Você obterá o seguinte resultado:

AAAAABAAAAABAAAAABAAAAAB ...

Portanto, a instrução int 16h é inserida. Isso irá esperar que o usuário pressione uma tecla no teclado quando o registrador ax tiver o valor 0 antes de chamar a sub-rotina int 16h .

Alternativamente, você pode simplesmente adicionar um loop infinito:

.endlessLoop:
    jmp .endlessLoop

mov ss, ...

Quando uma interrupção ocorre entre estas duas instruções:

mov ss, ax
    ; <--- Here
mov sp, 4096

... a combinação dos registradores sp e ss não representa uma representação "válida" de valores.

Se você não tiver sorte, a interrupção gravará dados em algum lugar na memória onde você não os deseja. Pode até sobrescrever o seu programa!

Portanto, você normalmente bloqueia as interrupções ao modificar o registrador ss :

cli          ; Forbid interrupts
mov ss, ax
mov sp, 4096
sti          ; Allow interrupts again

Eu sou relativamente novo na montagem, mas estou tentando mergulhar no mundo da computação de baixo nível. Eu estou tentando aprender como escrever código de assembly que seria executado como código de bootloader; tão independente de qualquer outro sistema operacional como Linux ou Windows. Depois de ler esta página e algumas outras listas de conjuntos de instruções x86, eu criei um código de montagem que deveria imprimir 10 A's na tela e depois 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

Então a saída deve ficar assim:

AAAAAAAAAAB

Eu montei o código usando o montador nasm rodando no programa Windows 10 Ubuntu Bash. Depois de produzir o arquivo .bin, abri-o usando um editor hexadecimal. Eu usei o mesmo editor hexadecimal para copiar o conteúdo desse arquivo .bin nos primeiros 512 bytes de uma unidade flash. Depois de escrever meu programa no pen drive, eu o desconectei e o conectei a um computador com um processador Intel Core i3-7100. Na inicialização, selecionei minha unidade flash USB como o dispositivo de inicialização, apenas para obter a seguinte saída:

A

Depois de mudar várias coisas no programa, eu finalmente me senti frustrado e tentei o programa em outro computador. O outro computador era um laptop com um i5-2520m. Eu segui o mesmo processo que mencionei antes. Com certeza, me deu a saída esperada:

AAAAAAAAAAB

Eu imediatamente tentei no meu computador original com o i3, mas ainda não funcionou.

Então, minha pergunta é: Por que meu programa funciona com um processador x86, mas não o outro? Ambos suportam o conjunto de instruções x86. O que da?

Solução:
Ok, consegui rastrear a solução real com alguma ajuda. Se você ler a resposta de Michael Petch abaixo, encontrará uma solução que solucionará meu problema e outro problema de um BIOS que esteja procurando por um BPB.

Aqui estava o problema com o meu código: Eu estava escrevendo o programa para os primeiros bytes do meu pen drive. Esses bytes foram carregados na memória, mas algumas interrupções do BIOS estavam usando esses bytes para si. Então meu programa estava sendo substituído pelo BIOS. Para evitar isso, você pode adicionar uma descrição do BPB conforme mostrado abaixo. Se o seu BIOS funciona da mesma maneira que o meu, ele simplesmente sobrescreve o BPB na memória, mas não no seu programa. Alternativamente, você pode adicionar o seguinte código ao topo do seu programa:

jmp start
resb 0x50

start: 
;enter code here

Este código (cortesia de Ross Ridge) enviará seu programa para o local de memória 0x50 (deslocamento de 0x7c00) para evitar que seja sobrescrito pelo BIOS durante a execução.

Também tenha em mente que sempre que você chamar qualquer sub-rotina, os valores dos registros que você estava usando poderiam ser sobrescritos. Certifique-se de usar push , pop ou salvar seus valores na memória antes de chamar uma sub-rotina. Veja a resposta de Martin Rosenau abaixo para ler mais sobre isso.

Obrigado a todos que responderam à minha pergunta. Agora tenho uma melhor compreensão de como esse material de baixo nível funciona.


Isso provavelmente poderia ser feito em uma resposta canônica sobre este assunto.

Problemas reais de hardware / USB / laptop

Se você está tentando usar o USB para inicializar em hardware real, você pode encontrar outro problema mesmo se ele funcionar no BOCHS e no QEMU . Se o seu BIOS estiver configurado para emulação de USB FDD (e não USB HDD ou qualquer outra coisa), talvez seja necessário adicionar um BPB (BIOS Parameter Block) ao início de seu bootloader. Você pode criar um falso como este:

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]

Ajustar a diretiva ORG para o que você precisa ou omiti-lo se você só precisa do padrão 0x0000.

Se você modificasse seu código para ter o layout acima do comando de file Unix / Linux, poderia despejar os dados do BPB que ele acha que compõem seu MBR na imagem do disco. Execute o file disk.img comando file disk.img e você poderá obter esta saída:

disk.img: Setor de boot DOS / MBR, offset de código 0x3c + 2, ID OEM "mkfs.fat", entradas raiz 224, setores 2880 (volumes <= 32 MB), setores / FAT 9, setores / faixa 18, serial número 0x2d7e5a1a, sem rótulo, FAT (12 bits)

Como o código nesta questão pode ser modificado

No caso deste código original do OP, ele poderia ter sido modificado para ficar assim:

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

Outras sugestões

Como foi apontado - você não pode ret para finalizar um bootloader. Você pode colocá-lo em um loop infinito ou parar o processador com cli seguido por hlt .

Se você alocar uma grande quantidade de dados na pilha ou começar a gravar dados fora dos 512 bytes do seu bootloader, você deve configurar seu próprio ponteiro de pilha ( SS: SP ) para uma região de memória que não interfira no seu próprio código . O código original nesta pergunta configura um ponteiro de pilha. Esta é uma observação geral para qualquer um que ler este Q / A. Eu tenho mais informações sobre isso na minha resposta que contém dicas gerais de bootloader .

Teste o código para ver se seu BIOS está sobrescrevendo o BPB

Se você quiser saber se o BIOS pode estar sobrescrevendo dados no BPB e determinar quais valores ele escreveu, você pode usar esse código de bootloader para despejar o BPB, pois o gerenciador de inicialização o vê depois que o controle é transferido para ele. Em circunstâncias normais, os primeiros 3 bytes devem ser EB 3C 90 seguidos por uma série de AA . Qualquer valor que não seja AA provavelmente foi substituído pelo BIOS. Este código está em NASM e pode ser montado em um gerenciador de inicialização com 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

A saída deve se parecer com isso para qualquer BIOS que não atualizou o BPB antes de transferir o controle para o código do gerenciador de inicialização:

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






usb-drive