X86-16: INT 13h неправильно читает файл с диска

Я попытался загрузить ядро, хранящееся на дискете, отформатированной в FAT12. При попытке прочитать сектора, где находится ядро, у меня ошибок нет (флаг переноса не установлен), но в памяти, куда я пытаюсь загрузить ядро, стоят нули.

org 0x7C00
bits 16

%define endl            0x0D, 0x0A
%define dirEntrySize        32
%define fatLocation     0x7E00
%define rootdirLocation     0x9000
%define kernelLocation      0x9E00
%define filenameLength      11

jmp short main
nop

bpb_oemId:                  db "mkfs.fat"
bpb_bytesPerSector:         dw 512
bpb_sectorsPerCluster:      db 1
bpb_reservedSectors:        dw 1
bpb_fatCount:               db 2
bpb_rootdirEntries:         dw 224
bpb_sectorCount:            dw 2880
bpb_mediaDescriptor:        db 0xF0
bpb_sectorsPerFat:          dw 9
bpb_sectorsPerTrack:        dw 18
bpb_headCount:              dw 2
bpb_hiddenSectors:          dd 0
bpb_largeSectorCount:       dd 0
ebr_driveNumber:            db 0x00
ebr_ntFlags:                db 0
ebr_driveSignature:         db 0x29
ebr_volumeId:               dd 0
ebr_volumeLabel:            db "NO NAME    "
ebr_systemId:               db "FAT12   "

rootdirStart:           dw 0
dataStart:          dw 0

main:
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    mov [ebr_driveNumber], dl

    mov si, msg_loading
    call puts

    ; Read FAT
    mov ax, [bpb_reservedSectors]
    mov bx, fatLocation
    mov cx, [bpb_sectorsPerFat]
    call readSectors
    
    ; Read Root Directory
    mov ax, [bpb_rootdirEntries]
    mov bx, dirEntrySize
    mul bx
    div word [bpb_bytesPerSector]
    mov [dataStart], ax
    xchg ax, cx
    mul byte [bpb_fatCount]
    add ax, [bpb_reservedSectors]
    mov bx, rootdirLocation
    call readSectors
    mov [rootdirStart], ax
    add [dataStart], ax
    
    ; Find and read the kernel
    mov si, kernelFilename
    call findFile
    mov bx, kernelLocation
    call readFile

    ; Load kernel
    mov dl, [ebr_driveNumber]
    jmp kernelLocation

    cli
    hlt

putc:
    push ax
    push bx
    mov ah, 0x0E
    xor bx, bx
    int 0x10
    pop bx
    pop ax
    ret
    
puts:
    push ax
    push si
.loop:
    lodsb
    test al, al
    jz .done
    call putc
    jmp .loop
.done:
    pop si
    pop ax
    ret

lbaToChs:
    push ax
    push dx
    xor dx, dx
    div word [bpb_sectorsPerTrack]
    inc dx
    mov cx, dx
    xor dx, dx
    div word [bpb_headCount]
    mov dh, dl
    mov ch, al
    shl ah, 6
    or cl, ah
    pop ax
    mov dl, al
    pop ax
    ret

readSectors:
    push ax
    push cx
    push dx
    push si
    push di
    push cx
    call lbaToChs
    pop si
    mov di, 3
    mov dl, [ebr_driveNumber]
.loop:
    mov ah, 0x02
    int 0x13
    mov ah, 0x01
    int 0x13
    jnc .done
    mov ah, 0x00
    int 0x13
    dec di
    jz .fail
    dec si
    jnz .loop
.done:
    pop di
    pop si
    pop dx
    pop cx
    pop ax
    ret
.fail:
    mov si, msg_diskError
    call puts
    cli
    hlt

findFile:
    push bx
    push si
    push di
    mov ax, 0
    mov di, rootdirLocation
.loop:
    mov bx, filenameLength
.compare:
    cmpsb
    jnz .skip
    dec bx
    jz .done
    jmp .compare
.skip:
    sub si, filenameLength
    add di, dirEntrySize - filenameLength
    inc ax
    cmp ax, word [bpb_rootdirEntries]
    jnz .loop
.fail:
    mov si, msg_findFailed
    call puts
    cli
    hlt
.done:
    mov si, rootdirLocation
    mov bx, dirEntrySize
    mul dx
    add si, ax
    add si, 0x1A
    lodsw
    pop di
    pop si
    pop bx
    ret

readFile:
    push ax
    push bx
    push cx
    push dx
.loop:
    push ax
    sub ax, 2
    mul byte [bpb_sectorsPerCluster]
    add ax, word [dataStart]
    mov cx, 1
    call readSectors
    mov ax, [bpb_bytesPerSector]
    mul byte [bpb_sectorsPerCluster]
    add bx, ax
    pop ax
    mov cl, 3
    mul cx
    dec cl
    div cx
    mov si, fatLocation
    add si, ax
    mov ax, [si]
    or dx, dx
    jnz .odd
.even:
    and ax, 0x0FFF
    jmp .update
.odd:
    shr ax, 4
.update:
    cmp ax, 0xFF0
    jnl .loop
.done:
    pop dx
    pop cx
    pop bx
    pop ax
    ret
.fail:
    mov si, msg_loadFailed
    call puts
    cli
    hlt

kernelFilename: db "KERNEL  BIN"
msg_loading: db "Loading...", endl, 0
msg_findFailed: db "Cannot find kernel", 0
msg_loadFailed: db "Cannot load kernel", 0
msg_diskError: db "Disk error", 0

times 510+$$-$ db 0
dw 0xAA55

Я попробовал отладку с помощью GDB и проверил регистры, но их значения были правильными. Я также проверил память, в которой хранил FAT и корневой каталог, но эти данные тоже оказались верными. Только память, куда я хотел загрузить ядро, была заполнена нулями.

Значения регистров после преобразования lba-to-chs:

eax            0x21                33
ecx            0x10                16
edx            0x100               256
ebx            0x9e00              40448
esp            0x7be6              0x7be6
ebp            0x0                 0x0
esi            0x7d98              32152
edi            0x0                 0
eip            0x7ce6              0x7ce6
eflags         0x202               [ IOPL=0 IF ]
cs             0x0                 0
ss             0x0                 0
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0

Значение AX правильное, должно быть lba начала данных: reserved_sectors + sectors_per_fat * fat_count + root_dir_entries * dir_entry_size / bytes_per_sectors = 1 + 9 * 2 + 224 * 32 / 512 = 1 + 18 + 14 = 33. Образ дискеты, открытый в шестнадцатеричном редакторе, указывает на начало ядра.

Таблица размещения загруженных файлов:

0x7e00: 0xf0    0xff    0xff    0xff    0x0f    0x00    0x00    0x00
0x7e08: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
...

Загруженный корневой каталог:

0x9000: 0x4b    0x45    0x52    0x4e    0x45    0x4c    0x20    0x20
0x9008: 0x42    0x49    0x4e    0x20    0x18    0x00    0x28    0x61
0x9010: 0xaa    0x58    0xaa    0x58    0x00    0x00    0x28    0x61
0x9018: 0xaa    0x58    0x02    0x00    0x25    0x01    0x00    0x00
0x9020: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9028: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9030: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9038: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
...

*Загружено* ядро:

0x9e00: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9e08: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9e10: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x9e18: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
...

Кажется, вы пропустили переменные в разделе «Данные BPB и EBR здесь».

ecm 10.05.2024 18:12

Нет, я просто сократил это для краткости.

Grigaror 10.05.2024 23:52

Это то, что я заявил. Вы это пропустили. Я не могу запустить ваш код без этих переменных. Поэтому я не могу это проверить, даже если бы захотел. (Инструкции по сборке также будут полезны.)

ecm 11.05.2024 01:33

@ecm я вставил это обратно

Grigaror 11.05.2024 04:30
jnl .loop имеет неверный смысл: запись FAT >= 0FF7h (или 0FF0h, если мы не требовательны) означает конец, а не дальнейший цикл. Несвязанно с этим, в readFile.loop у вас есть mov cx, 1 раньше call readSectors, но вы хотите прочитать количество секторов на кластер, а не 1. Кроме того, вы делаете некоторые предположения о значениях, умещающихся в байтах или словах, и о том, что корневой каталог не нуждается в округлении. , и этот каталог, FAT, ядро ​​помещается в памяти. Кроме того, сканирование вашего каталога сбивается и не работает, если KERNEL.BIN не является первой записью. readSectors неправильно читает несколько секторов.
ecm 11.05.2024 13:33
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
org 0x7C00
bits 16

Вы можете добавить сюда cpu 8086, чтобы гарантировать, что условные переходы используют только кодировки коротких переходов.


%define endl            0x0D, 0x0A
%define dirEntrySize        32
%define fatLocation     0x7E00
%define rootdirLocation     0x9000
%define kernelLocation      0x9E00
%define filenameLength      11

FAT на образе дискеты размером 1440 КиБ имеет 9 секторов по 1200 байт. Так что это просто соответствует выделенной вами памяти. Это может привести к сбою, если вы захотите загрузить файловую систему большего размера.

Корневому каталогу с 224 записями требуется 1C00h байт. Однако на самом деле вам больше не нужен корневой каталог после того, как вы нашли файл для поиска, поэтому перекрытие между расположением каталога и расположением ядра не является проблемой.

jmp short main
nop

bpb_oemId:                  db "mkfs.fat"
bpb_bytesPerSector:         dw 512
bpb_sectorsPerCluster:      db 1
bpb_reservedSectors:        dw 1
bpb_fatCount:               db 2
bpb_rootdirEntries:         dw 224
bpb_sectorCount:            dw 2880
bpb_mediaDescriptor:        db 0xF0
bpb_sectorsPerFat:          dw 9
bpb_sectorsPerTrack:        dw 18
bpb_headCount:              dw 2
bpb_hiddenSectors:          dd 0
bpb_largeSectorCount:       dd 0
ebr_driveNumber:            db 0x00
ebr_ntFlags:                db 0
ebr_driveSignature:         db 0x29
ebr_volumeId:               dd 0
ebr_volumeLabel:            db "NO NAME    "
ebr_systemId:               db "FAT12   "

rootdirStart:           dw 0
dataStart:          dw 0

Для оптимизации размера вы не хотите помещать их в загрузчик как байты с нулевой инициализацией, потому что вам никогда не понадобится их инициализация нулями. Однако, хотя это и не оптимально, это не является неправильным.

main:
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    mov [ebr_driveNumber], dl

    mov si, msg_loading
    call puts

    ; Read FAT
    mov ax, [bpb_reservedSectors]
    mov bx, fatLocation
    mov cx, [bpb_sectorsPerFat]
    call readSectors
    
    ; Read Root Directory
    mov ax, [bpb_rootdirEntries]
    mov bx, dirEntrySize
    mul bx
    div word [bpb_bytesPerSector]

Это должно округлить секторы для каждого корневого каталога. Однако многие загрузчики и драйверы допускают эту ошибку.

    mov [dataStart], ax

Это то, что я указал для переменных, вы безоговорочно перезаписываете их, поэтому их не нужно инициализировать нулями.

    xchg ax, cx
    mul byte [bpb_fatCount]

Предполагается, что количество секторов на FAT <256.

    add ax, [bpb_reservedSectors]

Предполагается, что начало корневого каталога < 65_536.

    mov bx, rootdirLocation
    call readSectors
    mov [rootdirStart], ax

Кажется, вы нигде не читаете эту переменную, так зачем ее устанавливать?

    add [dataStart], ax
    
    ; Find and read the kernel
    mov si, kernelFilename
    call findFile

Поскольку файл find никогда не вызывается с другим именем, вы можете жестко запрограммировать адрес имени файла ядра в функции поиска файла.

    mov bx, kernelLocation
    call readFile

    ; Load kernel
    mov dl, [ebr_driveNumber]
    jmp kernelLocation

Комментарий некорректен, на этом этапе ядро ​​уже загружено (если код работает корректно). Здесь вы пытаетесь передать управление ядру.

Вы предполагаете, что cs:ip равно 0:7C00h в начале вашего загрузчика. Вместо этого было бы лучше сделать большой прыжок, например jmp 0:kernelLocation, чтобы гарантировать, что cs сбрасывается в ноль, потому что вместо этого предыдущий загрузчик может ввести ваш код по адресу 7C0h:0. (Немедленные короткие и близкие вызовы и прыжки всегда выполняются с использованием rel8 или rel16, поэтому эта разница не влияет на них.)

    cli
    hlt

Это мертвый код, который так и не был достигнут. Удалите его, чтобы сэкономить место.

putc:
    push ax
    push bx
    mov ah, 0x0E
    xor bx, bx
    int 0x10
    pop bx
    pop ax
    ret

Если вы использовали bp, вам также следует нажать и вставить его сюда , потому что эта функция может уничтожить bp .

puts:
    push ax
    push si
.loop:
    lodsb
    test al, al
    jz .done
    call putc
    jmp .loop
.done:
    pop si
    pop ax
    ret

Функция put-символа не обязательно должна быть функцией, если вы вызываете ее только из одного места. Однако это не проблема корректности.

lbaToChs:
    push ax
    push dx
    xor dx, dx
    div word [bpb_sectorsPerTrack]
    inc dx
    mov cx, dx
    xor dx, dx
    div word [bpb_headCount]
    mov dh, dl
    mov ch, al
    shl ah, 6
    or cl, ah
    pop ax
    mov dl, al
    pop ax
    ret

push dx и сопутствующие pop и mov не нужны как таковые, потому что вы всегда перезаписываете dl после вызова этой функции.

readSectors:
    push ax
    push cx
    push dx
    push si
    push di
    push cx
    call lbaToChs
    pop si
    mov di, 3
    mov dl, [ebr_driveNumber]
.loop:
    mov ah, 0x02
    int 0x13

Вы не инициализировали al количество секторов для чтения! Наивным решением было бы инициализировать общее количество секторов, которые вы хотите прочитать за один вызов функции. Однако если вы пересечете границу 64 КиБ или границу «дорожки», чтение более 1 сектора может завершиться неудачно. Поэтому в моих загрузчиках загрузочных секторов функция readSectors считывает один сектор безоговорочно. Он выполняет расчет LBA в CHS для каждого сектора, который он хочет прочитать, а не один раз за непрерывное чтение. Вызывающие программы должны позаботиться о зацикливании, если они хотят прочитать несколько секторов.

Итак, если вы хотите сделать это, вам следует выполнить расчет LBA в CHS в функции чтения одного сектора и вызвать это в цикле, который увеличивает LBA и добавляет к read_sector после чтения каждого отдельного сектора. (Это также означает, что вам не нужно вычислять количество байтов на кластер и добавлять их к указателю позже в bx.)

    mov ah, 0x01
    int 0x13
    jnc .done

Этот звонок в лучшем случае бесполезен. readFile подскочит, если чтение прошло успешно. Но если это произойдет, вы пропустите цикл jnc и никогда не продолжите читать последующие сектора.

    mov ah, 0x00
    int 0x13
    dec di
    jz .fail
    dec si
    jnz .loop

Петля dec si очень запутана. Цикл dec si, похоже, предназначен для повторной попытки ошибки. Но зачем зацикливаться на следующем секторе, только если происходит повторная попытка с ошибкой? Кроме того, если вы выполняете цикл для dec di > 1, вы не вычисляете следующий кортеж CHS и не передаете адрес, поэтому вы не сможете правильно прочитать несколько секторов.

.done:
    pop di
    pop si
    pop dx
    pop cx
    pop ax
    ret
.fail:
    mov si, msg_diskError
    call puts
    cli
    hlt

Лучше запустить si функцию 00h, а затем int 16h вернуть управление предыдущему загрузчику. Или используйте int 19h \ sti \ halt: \ hlt, что позволяет перезагрузиться, например, с помощью Ctrl-Alt-Del.

findFile:
    push bx
    push si
    push di
    mov ax, 0
    mov di, rootdirLocation
.loop:
    mov bx, filenameLength
.compare:
    cmpsb
    jnz .skip
    dec bx
    jz .done
    jmp .compare

Этот цикл работает, если (SFN) KERNEL.BIN находится в самой первой записи каталога.

.skip:
    sub si, filenameLength
    add di, dirEntrySize - filenameLength

Это очень сбивает с толку. Вы не знаете, на какую точку jmp halt и di указывают (например, если si обнаруживается как несоответствие, то KERNEL.SYS будет указывать на di в «SYS», а Y на si в «BIN»), поэтому эти жестко запрограммированные настройки не могут быть выполнены. правильный. Если я вставлю удаленную запись каталога (первый байт не совпадает), ваши инструкции здесь также не сработают.

    inc ax
    cmp ax, word [bpb_rootdirEntries]
    jnz .loop

Это странно, но не неправильно. Я предпочитаю инициализировать записи корневого каталога и вести обратный отсчет в регистре.

.fail:
    mov si, msg_findFailed
    call puts
    cli
    hlt

Вы можете поделиться большей частью кода с другой функцией отображения ошибок.


.done:
    mov si, rootdirLocation
    mov bx, dirEntrySize
    mul dx

Это кажется неправильным, возможно, вы имели в виду I ?

    add si, ax
    add si, 0x1A
    lodsw
    pop di
    pop si
    pop bx
    ret

readFile:
    push ax
    push bx
    push cx
    push dx
.loop:
    push ax
    sub ax, 2
    mul byte [bpb_sectorsPerCluster]

Предполагается, что ваш текущий кластер < 256. Это не обязательно верно для образа дискеты размером 1440 КиБ.

    add ax, word [dataStart]
    mov cx, 1
    call readSectors

Если бы mul bx работало, это бы жестко запрограммировало размер кластера в 1 сектор.

    mov ax, [bpb_bytesPerSector]
    mul byte [bpb_sectorsPerCluster]
    add bx, ax

Предполагается, что количество байтов на сектор < 256, что неверно для типичного изображения. Таким образом, вы добавите ноль к readSectors, поэтому позже перезапишете первый сектор ядра.

    pop ax
    mov cl, 3
    mul cx
    dec cl
    div cx
    mov si, fatLocation
    add si, ax
    mov ax, [si]
    or dx, dx
    jnz .odd
.even:
    and ax, 0x0FFF
    jmp .update
.odd:
    shr ax, 4

О, это 186 инструкция. Конечно, для qemu это не проблема.

.update:
    cmp ax, 0xFF0
    jnl .loop

Это должно быть bx (или jb .loop). Если следующий кластер ниже 0FF0h, выполните цикл.

.done:
    pop dx
    pop cx
    pop bx
    pop ax
    ret
.fail:
    mov si, msg_loadFailed
    call puts
    cli
    hlt

Это мертвый код, который так и не был достигнут.

kernelFilename: db "KERNEL  BIN"
msg_loading: db "Loading...", endl, 0
msg_findFailed: db "Cannot find kernel", 0
msg_loadFailed: db "Cannot load kernel", 0
msg_diskError: db "Disk error", 0

times 510+$$-$ db 0
dw 0xAA55

Наконец, я отладил это с помощью загрузочного lDebug , работающего в qemu. Я создал образ дискеты размером 1440 КиБ, используя мой скрипт bootimg. А вот командный скриптлет, который я использовал для отладки вашего загрузчика:

jl .loop

Я изменил переключатель на test$ nasm kernel.asm -o kernel.bin && nasm test.asm -o test.bin -l test.lst && nasm -I ~/proj/lmacros/ -I ~/proj/bootimg/ ~/proj/bootimg/bootimg.asm -D_PAYLOADFILE = "kernel.bin" -D_BOOTPATCHFILE = "test.bin" -o diskette.img && nasm -I ~/proj/lmacros/ -I ~/proj/ldosboot/ ~/proj/ldosboot/boot.asm -o boot12.bin -D_LOAD_NAME = "'LDEBUG'" && nasm -I ~/proj/lmacros/ -I ~/proj/ldosmbr/ ~/proj/ldosmbr/oldmbr.asm -o oldmbr.bin && nasm -I ~/proj/ldebug/bin/ -I ~/proj/lmacros/ -I ~/proj/bootimg/ ~/proj/bootimg/bootimg.asm -D_PAYLOADFILE = "ldebug.com,extlib.eld" -D_BOOTPATCHFILE = "boot12.bin" -D_MBR -D_MBRPATCHFILE = "oldmbr.bin" -o hdimage.img && qemu-system-i386 -hda hdimage.img -fda diskette.img -boot order=c -display curses -chardev serial,id=serial2,path=/tmp/vptty-dos -serial null -serial chardev:serial2; stty sane, чтобы вставить удаленную запись каталога, что приводит к сбою вашего кода даже после исправления ошибки -D_PAYLOADFILE = "::directorypad,1,kernel.bin".

Конечно, findFile и readFile также вызываются только один раз, поэтому их можно встроить.

ecm 11.05.2024 15:03

В lbaToChs есть shl ah, 6, а в ReadFile есть shr ax, 4, о котором вы упомянули: «О, это инструкция 186. Конечно, для qemu это не проблема». Это по-прежнему справедливо, если ОП будет использовать cpu 8086, как вы предложили, чтобы гарантировать, что условные переходы используют только кодировки коротких переходов?

Sep Roland 12.05.2024 16:52

@SepRoland Если вы попытаетесь собрать более 186 инструкций (хороший улов для конвертации CHS) с действующей директивой cpu 8086, сборка завершится неудачей во время сборки. В этом случае, чтобы избежать 386+ около условных прыжков, но при этом разрешить 186 сдвигов, вы можете использовать cpu 186 или cpu 286.

ecm 12.05.2024 18:20

Другие вопросы по теме