简体   繁体   中英

Bootloader Loading Itself Rather than Kernel

I am currently working on a bootloader written in x86 NASM assembly, designed to load the kernel (R.BIN) from a FAT16 formatted disk. It has not been doing so, and after incorporating short messages for debugging (A, B, C, D, J, !; lines involving them marked with an asterisk), I discovered that the bootloader goes all the way through to the point at which it jumps to the loaded kernel perfectly, though it does not jump to the kernel, and instead seems to load itself again.

To test out the bootloader, I have a mounted blank image that I have been writing the bootloader to by using the following: dd if=LOADER.BIN of=/dev/loop0 (I also copied R.BIN to it) After this, I would save this new image, and test it out in Virtualbox.

This is my complete code:

BITS 16

jmp main                     ; Jump to main bootloader
nop                                 ; Pad out remaining bytes until boot descriptor

; Disk descriptor

OEM_name            db "MARSHMAL"   ; Disk label
bytes_sector        dw 0x0200       ; Bytes per sector
sectors_cluster     db 0x01         ; Sectors per cluster
sectors_record      dw 0x0001       ; Sectors reserved for boot record
fats                db 0x02         ; Number of file allocation tables
max_root_entries    dw 0x0200       ; Max number of root entries
sectors             dw 0x0B40       ; Number of sectors
medium_type         db 0xF0         ; Type of medium (removable or fixed?)
sectors_fat         dw 0x0009       ; Sectors per file allocation table
sectors_track       dw 0x0012       ; Sectors per track
heads               dw 0x0002       ; Number of heads
hidden_sectors      dd 0x00000000   ; Number of sectors before partition
total_sectors       dd 0x00000000   ; Number of sectors in medium (zero because 2B != 0)
drive_number        db 0x00         ; Drive number (for BIOS int 0x13)
drive_signature     db 0x00         ; NOT USED
ext_signature       db 0x29         ; Extended boot signature
volume_serial       dd 0x00000000   ; Volume's serial number
volume_label        db "MARSHMALLOW"; Volume label
fs_type             db "FAT16   "   ; Filesystem type

main:
    mov ax, 0x07C0
    add ax, 0x0220
    mov ss, ax
    mov sp, 0x1000                  ; 4K of stack
    mov ax, 0x07C0
    mov ds, ax
    mov byte [drive_num], dl        ; Save boot drive number

    mov bx, ds
    mov es, bx                      ; Set ES to Data Segment
    mov bx, disk_buffer             ; Set BX to disk buffer
    mov ax, 0x13                    ; Start of root = sectors_record + fats * sectors_fat = 1 + 2 * 9 = logical 19
    call ls_hts                     ; Convert logical 19 to head, track and sector
    mov al, 0x0E                    ; Number of sectors in root = max_root_entries * 32 / bytes_sector = 224 * 32 / 512 = 14
    mov si, a                       ; Read root dir message*
    call print_str                  ; Print!*

.read_disk:
    int 0x13                        ; BIOS disk interrupt
    jnc .search_init                ; If successful, get ready to search the disk
    call reset_disk                 ; Otherwise, reset the disk
    jmp .read_disk                  ; And retry

.search_init:
    mov si, success                 ; Success message*
    call print_str                  ; Print!*
    mov ax, ds
    mov es, ax                      ; Move data segment to extra segment
    mov di, disk_buffer             ; Location of disk buffer (ES:DI will be the location of the root entry we will be checking)
    mov si, r_name                  ; Location of filename of R (DS:SI will be the location of the string to compare to the root entry)
    mov bx, 0x00                    ; Start at root entry 0
    push si                         ; Push*
    mov si, b                       ; Search message*
    call print_str                  ; Print!
    pop si                          ; Pop*

.check_entry:
    mov cx, 0x0B                    ; Compare the first 11 bytes
    push si                         ; Push filename location to stack
    rep cmpsb                       ; Compare the two strings
    pop si                          ; Restore filename location to SI
    je .found_entry                 ; If equal, we found the root entry!
    add di, 0x15                    ; Otherwise, move to next entry
    inc bx                          ; Number of next entry
    cmp bx, max_root_entries        ; Have we gone through all root entries?
    jg .missing                     ; If so, R is missing
    jmp .check_entry                ; Otherwise, look at this next entry

.found_entry:
    mov si, success                 ; Success message*
    call print_str                  ; Print!*
    mov ax, word [es:di+0x0F]
    mov word [cluster], ax          ; Move starting cluster number to our spot in memory

    mov bx, disk_buffer             ; ES:BX points to disk buffer
    mov ax, 0x01                    ; 1st FAT begins at logical sector 1
    call ls_hts                     ; Convert to head, track and sector
    mov al, sectors_fat             ; Read all sectors in FAT
    mov si, c                       ; Read FAT message*
    call print_str                  ; Print!*

.read_fat:
    int 0x13                        ; BIOS disk interrupt
    jnc .read_cluster               ; If successful, load the first cluster of the file
    call reset_disk                 ; Otherwise, reset the disk
    jmp .read_fat                   ; And try again

.read_cluster:
    mov si, d                       ; Attempt to read cluster message*
    call print_str                  ; Print!*
    mov ax, 0x2000
    mov es, ax                      ; Segment into which we will load R
    mov bx, word [buffer_pointer]   ; Spot into which we will load this cluster
    mov ax, word [cluster]          ; Cluster to read
    add ax, 0x1F                    ; Convert to logical sector
    call ls_hts                     ; Convert to head, track and sector
    mov al, sectors_cluster         ; Read the number of sectors in 1 cluster
    int 0x13                        ; BIOS disk interrupt
    jnc .find_next_cluster          ; If successful, find the next cluster
    call reset_disk                 ; Otherwise, reset the disk
    jmp .read_cluster               ; And try again

.find_next_cluster:
    mov si, success                 ; Success message*
    call print_str                  ; Print!*
    mov ax, word [cluster]          ; Location of current cluster
    mov bx, 0x02                    ; There are two bytes per entry in FAT16
    mul bx                          ; The memory location of CLUSTER should fit in AL
    mov si, disk_buffer             ; Location of start of FAT
    add si, ax                      ; Add the number of bytes until current cluster
    mov ax, word [ds:si]            ; Number of next cluster
    mov word [cluster], ax          ; Store this
    cmp ax, 0xFFF8                  ; Check whether this next cluster is an end-of-file marker
    jae .jump                       ; If it is, we have fully loaded the kernel
    jge .jump
    add word [buffer_pointer], 0x0200 ; Otherwise, increment the buffer pointer a sector length
    jmp .read_cluster               ; And load it into memory

.jump:
    mov si, loaded                  ; Loaded kernel message
    call print_str                  ; Print!
    mov dl, byte [drive_num]        ; Make the boot drive number accessible to R
    jmp 0x2000:0x0000               ; Jump to R's location!

.missing:
    mov si, m_r_missing             ; Display the missing message
    call rsod                       ; Display it in a Red Screen of Death

reset_disk:
    pusha                           ; Push register states to stack
    mov ax, 0x00                    ; RESET disk
    mov dl, byte [drive_num]        ; Boot drive number
    int 0x13                        ; BIOS disk interrupt
    jc .disk_fail                   ; If failed, fatal error and reboot
    popa                            ; Restore register states
    ret                             ; And retry

.disk_fail:
    mov si, m_disk_error            ; Display the disk error message
    call rsod                       ; Display it in a Red Screen of Death

print_str:                          ; Prints string pointed to by REGISTER SI to cursor location (si=str)
    pusha                           ; Push register states to stack
    mov ah, 0x0E                    ; BIOS will PRINT

.repeat:
    lodsb                           ; Load next character from SI
    cmp al, 0x00                    ; Is this a null character?
    je .ret                         ; If it is, return to caller
    int 0x10                        ; Otherwise, BIOS interrupt
    jmp .repeat                     ; Do this again

.ret:
    mov ah, 0x00                    ; Read keyboard buffer
    int 0x16                        ; BIOS keyboard interrupt      
    popa                            ; Restore register states
    ret                             ; Return to caller

ls_hts:                             ; Convert logical sector to head, track, and sector configuration for int 0x13 (AX = logical sector)
    mov dx, 0x00                    ; Upper word of dividend is 0
    div word [sectors_track]        ; Divide to find the number of tracks before this
    mov cl, dl                      ; The remainder is the number of the sector within the track
    add cl, 0x01                    ; Sectors start at 1, not 0
    mov dx, 0x00                    ; Upper word of dividend is 0
    div word [heads]                ; Divide by number of heads/sides
    mov dh, dl                      ; The remainder is the head number (it should only take up the lower half of DX)
    mov ch, al                      ; The quotient is the track number (it should only take up the lower half of CX)
    mov dl, byte [drive_num]        ; Boot drive number
    mov ah, 0x02                    ; READ disk sectors
    ret                             ; Return to caller

rsod:                               ; Red Screen of Death (SI = line to print)
    mov al, 0x20                    ; SPACE
    mov bh, 0x00                    ; Page 0
    mov bl, 0x40                    ; Red background
    mov cx, 0x50                    ; Enough to fit the screen width

.repeat:
    mov ah, 0x09                    ; Write character and attribute
    int 0x10                        ; BIOS VGA interrupt
    mov ah, 0x03                    ; Get cursor position
    int 0x10                        ; BIOS VGA interrupt
    cmp dh, 0x1A                    ; Have we gone all the way down the screen?
    jge .write                      ; If we have, return to caller
    inc dh                          ; Otherwise, next row down
    mov ah, 0x02                    ; Set cursor position
    int 0x10                        ; BIOS VGA interrupt
    jmp .repeat                     ; Do this again for the next line

.write:
    mov ah, 0x02                    ; Set cursor position
    mov dh, 0x01                    ; Row 1
    mov dl, 0x03                    ; Col 3
    int 0x10                        ; BIOS VGA interrupt
    push si                         ; Push line to stack
    mov si, fatal                   ; Prepare to display "FATAL" message
    call print_str                  ; Print!
    pop si                          ; Restore line and prepare to print it
    call print_str                  ; Print!
    mov si, press_a_key             ; Prepare to display prompt
    call print_str                  ; Print!
    int 0x19                        ; Reboot

data:
    r_name          db "R       BIN"        ; Filename of R
    cluster         dw 0x0000               ; Cluster that we are working with
    buffer_pointer  dw 0x0000               ; Pointer to offset of buffer
    drive_num       db 0x00                 ; Boot drive number
    fatal           db "FATAL: ", 0x00      ; Fatal error message
    press_a_key     db "! Press a key", 0x00; Instruct the user to press a key and reboot
    m_r_missing     db "R missing", 0x00    ; Missing message
    m_disk_error    db "Disk failed", 0x00  ; Disk error message
    a               db "A", 0x00            ; About to read root dir*
    b               db "B", 0x00            ; About to search root dir*
    c               db "C", 0x00            ; About to read FAT*
    d               db "D", 0x00            ; About to attempt cluster read*
    success         db "!", 0x00            ; Success!*
    loaded          db "J", 0x00            ; Loaded R message*

    times 510-($-$$) db 0x00        ; Pad remainder of boot sector
    sig             dw 0xAA55       ; Boot signature

disk_buffer:                        ; Space in memory for loading disk contents

As I said, the bootloader seems to be doing everything as planned, until making the jump to the kernel, when it starts over at the beginning of the debugging messages and cycles again.

mov di, disk_buffer             ; Location of disk buffer (ES:DI will be the location of the root entry we will be checking)
mov si, r_name                  ; Location of filename of R (DS:SI will be the location of the string to compare to the root entry)
mov bx, 0x00                    ; Start at root entry 0
push si                         ; Push*
mov si, b                       ; Search message*
call print_str                  ; Print!
pop si                          ; Pop*

.check_entry:
mov cx, 0x0B                    ; Compare the first 11 bytes
push si                         ; Push filename location to stack
rep cmpsb                       ; Compare the two strings
pop si                          ; Restore filename location to SI
je .found_entry                 ; If equal, we found the root entry!
add di, 0x15      !!!!!!!!!!!!  ; Otherwise, move to next entry
inc bx                          ; Number of next entry
cmp bx, max_root_entries

It would be best to write repe cmpsb to better show that you're comparing for equality!

In .check_entry you always add 21 to DI . This is incorrect! If repe cmpsb reports NotEqual then DI could be anywhere. You need to put DI back at the start of the 32-byte record and then add 32 to go to the next root record.

.check_entry:
  mov cx, 0x0B               ; Compare the first 11 bytes
  push si di                 ; Push filename location to stack
  repe cmpsb                 ; Compare the two strings
  pop di si                  ; Restore filename location to SI
  je .found_entry            ; If equal, we found the root entry!
  add di, 0x20               ; Otherwise, move to next entry
  inc bx                     ; Number of next entry
  cmp bx, max_root_entries
  jg .missing                ; If so, R is missing
  jmp .check_entry           ; Otherwise, look at this next entry

.found_entry:
  mov si, success            ; Success message*
  call print_str             ; Print!*
  mov ax, [es:di+0x1A]
  mov [cluster], ax

Take care to update all references within the root record that you make hereafter.
Above I've changed [es:di+0x0F] into the now correct form [es:di+0x1A] .

 cmp bx, max_root_entries ; Have we gone through all root entries? mov al, sectors_fat ; Read all sectors in FAT mov al, sectors_cluster ; Read the number of sectors in 1 cluster 

All of the above are wrong. You're using NASM and therefore you need to write the square brackets in order to fetch something from memory. Without the brackets you obtain the address itself and not the content.

cmp bx, [bpbRootEntries]
...

My FAT File System Specification mentions:

There is no such thing as a FAT16 volume that has less than 4085 clusters... ...If you try to make a FAT volume that violates this rule, Microsoft operating systems will not handle them correctly because they will think the volume has a different type of FAT than what you think it does.

The disk that you use has 2880 sectors and consequently too few clusters to be recognized as FAT16. Perhaps your Linux operating system will recognize it fine but maybe your mount command then should use an option like fat=16 .

Those same 2880 sectors and seeing that bpbSectorsPerCluster=0x01, will require the FATs to be each 12 sectors. Nonetheless I see that bpbFATSize16=0x0009 which is the value I would expect for FAT 12 .


The Boot Sector and BPB structure has a lot of information that your program should use instead of relying on constants that you fail to initialize correctly. However in my answer I will continue to use these (corrected) constants for the purpose of simplicity!

FirstFATSecNum = bpbHiddenSectors + bpbReservedSectors
               = 0x00000000 + 0x0001
               = 1

FirstRootDirSecNum = FirstFATSecNum + bpbNumberOfFATs * bpbFATSize16
                   = 1 + 0x02 * 0x0009
                   = 19

RootDirSectors = ((bpbRootEntries * 32) + (bpbBytesPerSector - 1)) / bpbBytesPerSector
               = ((0x0200 * 32) + (0x200 - 1)) / 0x200
               = 32

FirstDataSecNum = FirstRootDirSecNum + RootDirSectors
                = 19 + 32
                = 51

It's important to see that the RootDir occupies 32 sectors or 16384 bytes! Your program has setup a DiskBuffer of only 8192 bytes. This was based on your wrong assumption that the RootDir contains 224 entries, the normal count for FAT 12 .


The routine that converts the logical sector number could be more aptly named "SetupCHS". This better expresses that it does more than a simple conversion and it also emphasizes the relative significance between Cylinder, Head, and Sector. eg Compare it to HMS for Hours, Minutes, and Seconds. So going from most significant (C) to least significant (S).

; IN (ax) OUT (cx,dx) MOD (ax)
SetupCHS:
    cwd
    div  word [bpbSectorsPerTrack]
    mov  cl, dl
    inc  cx                        ; Sector number
    cwd
    div  word [bpbNumberOfHeads]
    mov  dh, dl                    ; Head number
    mov  ch, al                    ; Cylinder number
    mov  dl, [drive_num]           ; Drive number
    ret

For clarity you should not setup the function number in AH here, but close to where you have the int 0x13 instruction!


Many BIOSes fail to read/write multiple sectors at once especially if they have to cross some boundary, be it another head or another cylinder.
That's why most prudent programmers will use a loop of 1-sector reads/writes.

; Loading the RootDir
    mov  bx, DiskBuffer            ; ES:BX
    mov  ax, 19                    ; FirstRootDirSecNum
    call SetupCHS                  ; -> CX DX (AX)
    mov  bp, 32                    ; RootDirSectors
.Next:
    call ReadOneSector             ; -> (AX DI)
    add  bx, [bpbBytesPerSector]
    dec  bp
    jnz  .Next

Because sometimes disk access can fail for some harmless reason, we repeat the operation a limited number of times. 5 is a good choice. Your program chose to repeat at infinitum which I find questionable.

; IN (es:bx,cx,dx) OUT () MOD (ax,di)
ReadOneSector:
    mov  di, 5
.ReTry:
    mov  ax, 0x0201                ; BIOS.ReadSector
    int  0x13                      ; -> CF
    jnc  .OK
    dec  di
    jz   DiskFail
    call ResetDisk
    jmp  .Retry
.OK:
    ret

Loading the first FAT is of course a similar process.
And for loading a file cluster you're in luck since each cluster has only one sector on this disk. No loop required.


Searching through the RootDir. Fifoernik already told you about the danger of using rep cmpsb . Using rep instead of repe has made you think that the DI register would always have advanced by 11 where in fact the repetition can end earlier.
Additional issues here are:

  • You don't test the first byte of the RootDir entry. It has vital info that you must inspect. If it is 0, you've reached the end of the RootDir and it's useless to continue evaluating entries. If it is 0xE5 the entry is free and you should just skip it.

  • You don't test the attribute byte. The entry could well be for a directory of for the volume id. Neither can be the file that you seek, so skip the entry!

Next applies the above:

    mov  di, DiskBuffer          ; ES:DI but knowing that ES=DS
    mov  bx, [bpbRootEntries]
.CheckEntry:
    cmp  byte [di], 0
    je   .FileNotFound
    cmp  byte [di], 0xE5
    je   .SkipEntry
    test byte [di+11], 00011000b ; DIR | VOL
    jnz  .SkipEntry              ; Is not a file
    mov  si, r_name
    mov  cx, 11
    push di
    repe cmpsb
    pop  di
    je   .FileFound
.SkipEntry:
    add  di, 32
    dec  bx                      ; Counting downward is easier
    jnz  .CheckEntry
.FileNotFound
    jmp  Missing
.FileFound:
    mov  ax, [di+0x1A]
    mov  [cluster], ax

Following the cluster chain. Below is the formula to convert a cluster number N into the sector number for its first/only sector:

FirstSectorOfCluster = FirstDataSecNum + (N - 2) * bpbSectorsPerCluster
                     = 51 + (N - 2) * 0x01
                     = N + 49

This part of your program will never load more than 1 cluster of the file because of the strange jge .jump . All of the good cluster numbers will be Greater . The cmp ax, 0xFFF8 instruction when interpreted in the signed fashion (which is what jge does) reads cmp ax, -8 . All cluster numbers from 2 to a few thousand will thus be greater.

Tip: to make it possible to load a big file (bigger than 64KB), you should change the ES segment register and keep the offset BX at 0.

    mov  ax, 0x2000
    xor  bx, bx
LoadNextCluster:
    add  ax, bx
    mov  es, ax
    xor  bx, bx
    mov  ax, [cluster]
    add  ax, 49
    call SetupCHS                ; -> CX DX (AX)
    call ReadOneSector           ; -> (AX DI)
    mov  si, [cluster]
    shl  si, 1                   ; You don't need MUL to calculate x2
    mov  ax, [DiskBuffer+si]     ; FAT gives number of next cluster
    mov  [cluster], ax
    cmp  ax, 0xFFF8              ; End-of-file marker ?
    mov  ax, es
    mov  bx, 512 / 16
    jb   LoadNextCluster
Loaded:                          ; We have fully loaded the kernel

The parts of your program that deal with the BIOS display functions have a few issues of their own but I think that is not what is important for now.
If you manage to edit all of these changes in, and still don't get results, you could post a follow-up question with the amended program. (If you do it today I can take another peek at it...)
Never ever incorporate the info you get from an answer into your original question, unless of course you write it in a separate section, but still. Major changes ask for a follow-up question (separate post).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM