6
\$\begingroup\$

Typically I use DOS file services for disk access, so that I don't have to care if the files are on a network drive, or CD-ROM, or in a mounted DOSBox folder. In the future, however, I would like to write my 8086 assembly programs (for MS-DOS era machines) to run at boot without DOS, so I've been looking into accessing FAT16A/B (non-LBA) partitions using low level BIOS service int 13h (non-extended).

Near the bottom of this question, just above the code example, I mention a few issues with robustness that I have considered. They will be corrected in the near future. I'm asking this question in the hope that others with more experience will notice potential bugs that I haven't considered.

The goal of this code is to give higher level procedures quick, and easy access to four data files:

  • FIELDS.DAT
  • TILES.DAT
  • AREAS.DAT
  • SPRITES.DAT

This is accomplished by determining, and storing as variables:

  • The size of the clusters in sectors (clstSize)
  • The first sector of the FAT (fatSect)
  • The first sector of the disk data area (datSect)
  • The cluster numbers of the four data files mentioned above (aClst, fClst, tClst, sClst)

The code expects that certain conditions be met (for the code to execute without errors):

  • It expects a primary master, or slave ATA hard drive to be installed.
  • It expects the variables drv and part to be set to the desired drive and partition number.
  • It expects the partition number in part to reference a FAT16A/B (non-LBA) partition which must exist entirely in the first 8 gigabytes of the selected hard drive (starts and ends before C/H/S 1023/254/63).
  • It expects to find in the root directory, a sub-directory named ENGINE, within that sub directory it expects to find the four data files mentioned above.

The main procedure INIT_DISK_VARS takes as input:

  • The output of int 13h function 8, which returns the (possibly BIOS translated) disk geometry.
  • Two byte variables drv, and part. drv holds either 80h (master drive number) or 81h (slave drive number). part holds the partition number (0-3).
  • The file names of the files that are being searched for in the form of an array of strings. (fileNames)

This code has been tested on several of my old machines (a 386, Pentium MMX, and early Pentium 3) and I have encountered no issues.

This code (at the moment) has several issues with robustness that will be corrected before the program is completed and released.

  • Handling error if the selected drive isn't installed, or not working. (Error codes returned by int 13h when attempting to load the MBR)
  • Handling error if the MBR doesn't load correctly. (aa55h not last word in secBuff)
  • Handling error if the selected partition is not of type 0x4 (FAT16A), or 0x6 (FAT16B)
  • Handling error if the boot sector doesn't load correctly, or isn't correctly located. (aa55h not last word in secBuff)
  • No support for long, mixed-case file names.
  • Handling error if the asset sub-directory named ENGINE is not found.
  • Handling error if the files FIELDS.DAT, TILES.DAT, AREAS.DAT, and SPRITES.DAT are not found in the sub-directory ENGINE.
  • Confirming that the FAT lies at the stored sector number (fatSect) by testing it for a copy of the media descriptor byte. (The root directory, and disk data area are not so easy to confirm.)
  • Handling error if the calculated cylinder number is greater than 1023.
  • Handling error if an attempt is made to access a sector that lies beyond the last sector of the partition.
  • Confirming that the file cluster numbers point to the files. The four .DAT files are in my own file format. Their structure includes unique marker bytes used to ensure they load correctly.

INIT_DISK_VARS is typically called as an external procedure from a library at the top of my program before the main loop. You can see the current form of my main assembly file in this Stack Overflow question I asked.

Here I've packaged INIT_DISK_VARS into a complete program that can be directly assembled by MASM 5.10.

;----------------------------------------------------------------------------|                                                                     
 theStack SEGMENT STACK                                                     ;|
;____________________________________________________________________________|
 db 16 dup ('THESTACK')   
;----------------------------------------------------------------------------|
 theStack ENDS                                                              ;|
;____________________________________________________________________________|










;----------------------------------------------------------------------------|   
 varData SEGMENT
;____________________________________________________________________________|

 bytPerSec  equ 512d              ;Number of bytes assumed to be in sectors
                                  ;(other sector sizes not supported)

 drv       db  80h                ;selected hard drive 80h=master,81h=slave 
 part      db  0                  ;selected partition of selected hdd (0-3)
 secPerHd  db  ?                  ;number of sectors per head (per track)
 hdPerCyl  db  ?                  ;number of heads (tracks) per cylinder

 varSect   dd  0                  ;32-bit arbitrary Sector number. 


 clstSize  db  ?                  ;the size of a cluster in sectors
 fatSect   dd  ?                  ;sector num of 1st sector of FAT 
 datSect   dd  ?                  ;sector num of 1st sector of disk data area              


 secBuff   db bytPerSec dup ('$') ;A buffer for holding a loaded sector.

 fileName  db "ENGINE     "       ;the name of the asset sub-directory
           db "AREAS   DAT"       ;the name of the areas data file
           db "TILES   DAT"       ;the name of the tiles data file 
           db "FIELDS  DAT"       ;the name of the fields data file
           db "SPRITES DAT"       ;the name of the sprites data file 

 aClst     dw ?                   ;the cluster number of AREAS.DAT    
 tClst     dw ?                   ;the cluster number of TILES.DAT 
 fClst     dw ?                   ;the cluster number of FIELDS.DAT 
 sClst     dw ?                   ;the cluster number of SPRITES.DAT

;----------------------------------------------------------------------------|
 varData ENDS                                                               ;|
;____________________________________________________________________________|










;----------------------------------------------------------------------------|   
 code SEGMENT

 assume cs:code,ds:varData   

 main PROC                                                                  

 start:                                                                      
;____________________________________________________________________________|


   mov ax, varData                            
   mov ds, ax               ;Load the variable segment into ds                                            
   cld                      ;ensure that string operations auto-increment

   call INIT_DISK_VARS      ;Setup file access variables 

   mov ax, 4c00H              
   int 21H                  ;Return to DOS





INIT_DISK_VARS PROC

;INITIALIZES THE VARIABLES NEEDED TO LOAD FILES FROM THE HARD DRIVE.

;EXPECTS DS TO BE LOADED WITH varData

    push ax
    push bx
    push cx
    push dx
    push bp
    push es
    push di
    push si  






    ;// INITIALIZE THE DISK GEOMETRY VARIABLES //


    mov ah, 8                  ;function 8, return disk parameters
    mov dl, drv                ;dl -> 80h=master or 81h=slave
    int 13h                    ;return the disk parameters as follows:
                               ;All of ch & upper 2 bits of cl form the  
                               ;drive's total number of cylinders. (0 based) 
                               ;Low 6 bits of cl is num of sectors per head   
                               ;dh -> number of heads per cyl (0 based)
    inc dh                     ;dh -> number of heads (counting from 1) 
    mov hdPerCyl, dh           ;initialize hdPerCyl
    and cl, 3fh                ;cl -> sector number
    mov secPerHd, cl           ;initialize secPerHd







    ;// LOAD THE MBR. WE'LL USE THE INFORMATION IN THE SELECTED // 
    ;// PARTITION'S ENTRY TO NAVIGATE TO IT'S BOOT SECTOR.      //   


    call LOAD_SECTOR           ;varSect points to the MBR by default. Load  
                               ;the MBR into secBuff

    lea bx, secBuff            ;bx -> MBR in memory
    add bx, 446d               ;1st part. entry is at byte 446 in the MBR
    xor ah, ah    
    mov al, part               ;ax -> current partition number
    mov cl, 4                  ;multiply ax by 16 (length of part. entry)
    shl al, cl                 ;ax -> offset to start of desired partition
    add bx, ax                 ;bx -> start of desired partition entry
    mov dx, WORD PTR [bx+10]    
    mov ax, WORD PTR [bx+8]    ;dx:ax -> boot sector offset
    call UPDATE_SECTOR         ;varSect -> boot sector             








    ;// LOAD THE BOOT SECTOR. IT CONTAINS ALL THE INFORMATION WE NEED TO    //
    ;// INITIALIZE clstSize, fatSect, datSect, AND LOAD THE ROOT DIRECTORY. //  


    call LOAD_SECTOR           ;load the boot sector into secBuff

    lea bx, secBuff
    mov al, BYTE PTR [bx+13]   ;al -> cluster size in sectors
    mov clstSize, al           ;clstSize initialized


    xor dx, dx
    mov ax, WORD PTR [bx+14]   ;ax -> number of reserved sectors.
    call UPDATE_SECTOR         ;offset of boot sector + number of reserved
                               ;sectors = first sector of FAT.

    mov dx, WORD PTR varSect+2
    mov ax, WORD PTR varSect   ;dx:ax -> sector num of 1st sector of the FAT
    mov WORD PTR fatSect+2, dx
    mov WORD PTR fatSect, ax   ;fatSect initialized


    xor ah, ah
    mov al, BYTE PTR [bx+16]   ;ax -> number of copies of the FAT
    mov dx, WORD PTR [bx+22]   ;dx -> number of sectors per FAT
    mul dx                     ;dx:ax -> total number of sectors taken up
                               ;by all the copies of the FATs 
    call UPDATE_SECTOR         ;varSect -> first sector of root directory

    mov di, WORD PTR varSect+2
    mov bp, WORD PTR varSect   ;Store the 1st sector of the root dir for now.
                               ;datSect needs to be initialized before we
                               ;load the root directory. 



    mov ax, WORD PTR [bx+17]   ;ax -> number of root directory entries
    mov cl, 5                  ;multiplying ax by 32 (num bytes per entry)
    shl ax, cl                 ;ax -> size of root directory in bytes
    mov cl, 9                  ;div ax by 512 (number of bytes per sector)
    xor dx, dx
    shr ax, cl                 ;dx:ax -> root directory size in sectors
    mov cx, ax                 ;store it, we'll reuse it as the loop cntr 
                               ;when searching the root dir for the asset dir

    call UPDATE_SECTOR         ;varSect now points to the first sector of 
                               ;the disk data area

    mov dx, WORD PTR varSect+2
    mov ax, WORD PTR varSect   ;dx:ax -> sector num of 1st sector of the FAT
    mov WORD PTR datSect+2, dx
    mov WORD PTR datSect, ax   ;datSect initialized


    mov WORD PTR varSect+2, di
    mov WORD PTR varSect, bp   ;varSect -> root directory 







    ;// LOAD THE ROOT DIRECTORY AND THEN SEARCH FOR THE SUB-DIRECTORY // 
    ;// CONTAINING THE DATA FILES.                                    //




    xor bp, bp                 ;Pass FIND_ENTRY the string index number of 
                               ;the asset directory's name.
    xor dx, dx
    mov ax, 1                  ;We'll increment through root dir's sectors
                               ;by passing UPDATE_SECTOR 1 iteratively. 
FIND_ASSET_DIR:
    call LOAD_SECTOR           ;load sector of the root dir into secBuff
    call FIND_ENTRY            ;is the asset dir entry in this sector?
    test bx, 0ffffh            ;if so, bx will be non zero 
    jnz ASSET_FOUND            ;if entry found, terminate the loop.
    call UPDATE_SECTOR         ;if not found, point to the next sector
    loop FIND_ASSET_DIR        ;Loop till asset dir is found, or all sectors 
                               ;have been searched through.

                               ;handle error if not found
                               ;(to be implemented in the future)
ASSET_FOUND:
    mov di, bx                 ;store the cluster number in di as input for
                               ;LOAD_DATA_SECTOR





    ;// LOAD THE ASSET DIRECTORY, SEARCH FOR THE DATA FILES, AND STORE //
    ;// THEIR CLUSTER NUMBERS.                                         //



    mov bp, 1                  ;bp -> file name str index of 1st file to find                     
    lea si, aClst              ;si -> first cluster number to initialize
    xor dx, dx                 ;dx -> initial sector offset into asset dir  
    mov cx, 4                  ;loop cntr -> number of files to find
INIT_CLST_NUMS:

    call LOAD_DATA_SECTOR      ;load a sector of the asset directory 
    cmp di, 0ffffh             ;If di does not = 0ffffh, sector offset valid.
    jne NOT_END_OF_DIR         ;If sect offset valid, search loaded sector

                               ;If invalid, file not found. Handle error.
                               ;(to be implemented in the future)
NOT_END_OF_DIR:
    call FIND_ENTRY            ;Search sector of the asset dir for file entry
    test bx, 0ffffh            ;If found, bx holds a sector num (non-zero). 
    jnz FILE_FOUND             ;If not found (bx = 0), search next sector.
    inc dx                     ;increment the sector offset
    jmp INIT_CLST_NUMS         ;load the next sector 


FILE_FOUND:
    mov WORD PTR [si], bx      ;store cluster number in it's variable

    inc bp                     ;move on to the next file name to search for
    add si, 2                  ;move on to next cluster number variable
    xor dx, dx                 ;reinitialize dx to sector offset 0 
    loop INIT_CLST_NUMS        ;loop until either all files are found, or one
                               ;of the files is not found. 



    pop si
    pop di
    pop es
    pop bp
    pop dx
    pop cx
    pop bx
    pop ax
    ret

INIT_DISK_VARS ENDP























FIND_ENTRY PROC

;SEARCHES A SINGLE SECTOR OF A DIRECTORY LOADED INTO secBuff FOR A 
;SPECIFIED FILE/SUB DIRECTORY ENTRY

;IF FOUND, RETURNS THE FILE/SUB DIRECTORY'S CLUSTER NUMBER IN BX
;IF NOT FOUND, RETURNS 0 IN BX

;ALTERS BX

;EXPECTS A FILE NAME STRING INDEX NUMBER IN BP
;EXPECTS A SECTOR OF A DIRECTORY (ROOT, OR SUB) TO BE LOADED INTO secBuff
;EXPECTS DS TO BE LOADED WITH varData


    push ax
    push cx
    push es
    push si
    push di




    lea si, fileName             ;si -> file name strings 
    mov ax, 11d                  ;ax -> file name length in bytes/characters
    mul bp                       ;ax -> offset to file name string
    add si, ax                   ;ds:si -> desired file name as source input
                                 ;for LODS
    mov di, ds
    mov es, di
    lea di, secBuff              ;es:di -> first entry in ds:secBuff as input
                                 ;for SCAS


    mov cx, 16d                  ;outer loop cntr -> num entries in a sector
ENTRY_SEARCH:                    
    push cx                      ;store outer loop cntr
    push si                      ;store start of the file name
    push di                      ;store start of the entry


    mov cx, 11d                  ;inner loop cntr -> length of file name
STR_MATCH:                       
    lodsb                        ;al -> character from file name
    scasb                        ;Does it match the char in the entry?
    jne NOT_ENTRY                ;If not, test next entry.
    loop STR_MATCH               ;If loop falls through, the strings match.

    pop di                       ;di -> start of the entry
    mov bx, WORD PTR [di+26]     ;bx -> entry's cluster number

    pop si
    pop cx
    pop di
    pop si
    pop es
    pop cx
    pop ax
    ret

NOT_ENTRY:                       
    pop di                       ;di -> start of the entry
    add di, 32d                  ;di -> start of next entry
    pop si                       ;si -> start of file name
    pop cx                       ;restore the outer loop cntr
    loop ENTRY_SEARCH            ;loop till we've either found a match, or
                                 ;have tested every entry in the sector 
                                 ;without finding a match.

    xor bx, bx                   ;if we're here no match was found. 
                                 ;return 0.




    pop di
    pop si
    pop es
    pop cx
    pop ax
    ret


FIND_ENTRY ENDP



























LOAD_DATA_SECTOR PROC

;OFFSETS TO A SPECIFIED SECTOR OF A FILE/SUB-DIRECTORY BY CONSULTING
;THE FAT, AND FOLLOWING THE CLUSTER CHAIN. THE SECTOR IS THEN LOADED INTO 
;secBuff. IF THE NEXT CLUSTER NUMBER IS 0FFFFH, THE END OF THE FILE HAS 
;BEEN REACHED, AND 0FFFFH IS RETURNED IN DI.        

;ALTERS DI IF END OF FILE IS ENCOUNTERED
;EXPECTS A CLUSTER NUMBER IN DI
;EXPECTS AN ABSOLUTE SECTOR OFFSET IN DX (FROM FIRST SECTOR OF FILE)
;EXPECTS DS TO BE LOADED WITH varData
;EXPECTS clstSize, fatSect, & datSect TO BE INITIALIZED WITH CORRECT VALUES

    push ax
    push bx
    push cx
    push dx
    push bp
    push di


   ;// DIVIDE THE ABSOLUTE SECTOR OFFSET BY clstSize.                      //
   ;// THE QUOTIENT IS THE CLUSTER OFFSET, THE REMAINDER IS THE SECTOR     //
   ;// OFFSET INTO INTO THAT CLUSTER.                                      //

   ;// CX -> CLUSTER OFFSET AS THE CLUSTER CHAIN LOOP COUNTER (IF NOT 0)   //
   ;// BP -> THE SECTOR OFFSET INTO THE CLUSTER. LATER ADDED TO varSect.   //


    mov ax, dx                 ;ax -> absolute sector offset
    xor bh, bh
    mov bl, clstSize           ;bx -> size of a cluster in sectors
    xor dx, dx               
    div bx                     ;ax -> cluster offset 
                               ;dx -> sector offset into that cluster
    mov bp, dx                 ;backup the sector offset into bp 

    test ax, 0ffffh            ;If the cluster offset is 0, di should already
    jz NO_CLST_CHN             ;have desired cluster number. Skip the loop.


    mov cx, ax                 ;loop cntr -> cluster offset




    ;// IF CLUSTER OFFSET > 0, DI NEEDS A NEW CLUSTER NUMBER.              //
    ;// BEGIN THE CLUSTER CHAIN LOOP, WHICH OPERATES AS FOLLOW:            //

    ;// STEP 1) LOAD THE FIRST SECTOR OF THE FAT                           //
    ;// STEP 2) DIVIDE OLD CLUSTER NUMBER BY 256                           //
    ;//         QUOTIENT -> OFFSET TO SECTOR WITH THE NEW CLUSTER NUMBER   //
    ;//         REMAINDER -> OFFSET TO CLUSTER NUMBER IN THAT SECTOR       //      
    ;// STEP 3) ADD THE SECTOR OFFSET (PREVIOUS QUOTIENT) TO varSect, THEN //
    ;//         LOAD THE NEW SECTOR OF THE FAT.                            //
    ;// STEP 4) MULTIPLY THE CLUSTER NUMBER OFFSET (PREVIOUS REMAINDER) BY //
    ;//         2, IT IS NOW THE BYTE OFFSET TO THE CLUSTER NUMBER.        //
    ;// STEP 5) CHECK THE CLUSTER NUMBER, IF IT'S 0FFFFH, THEN THE END OF  //
    ;//         THE FILE OR DIRECTORY HAS BEEN REACHED. IF NOT, THEN IT'S  //
    ;//         A NEW CLUSTER NUMBER IN THE CHAIN, PASS IT TO DI.          //          
    ;// STEP 6) REPEAT STEPS 1-5 <CLUSTER OFFSET> NUMBER OF TIMES.         //         


CLST_CHN:

    mov bx, WORD PTR fatSect+2       
    mov ax, WORD PTR fatSect                          
    mov WORD PTR varSect+2, bx
    mov WORD PTR varSect, ax   ;varSect -> first sector of the FAT 

    mov ax, di                 ;ax -> cluster number
    mov bx, (bytPerSec/2)      ;bx -> 256 since each clst num is 2 bytes 
    xor dx, dx              
    div bx                     ;ax -> offset to FAT sector with clst num 
                               ;dx -> offset to clst num in words 

    push dx                    ;must clear out dx as input for UPDATE_SECTOR  
    xor dx, dx                 ;dx:ax -> offset to FAT sector with clst num
    call UPDATE_SECTOR         ;varSect -> FAT sector that has clst num
    call LOAD_SECTOR           ;load the sector of the FAT into secBuff
    pop dx                     ;restore offset to clst num in words


    lea bx, secBuff            ;bx -> secBuff
    shl dx, 1                  ;dx -> byte offset of the cluster number
    add bx, dx                 ;bx -> desired cluster number 
    cmp WORD PTR [bx], 0ffffh  ;is the cluster number 0ffffh?
    jne NEW_CLST_NUM           ;if not, accept it as the new cluster number
    pop di                     ;if so, return.
    or di, 0ffffh              ;Return 0ffffh in di so that the calling 
    pop bp                     ;procedure knows that either the given  
    pop dx                     ;sector offset is greater than the length of 
    pop cx                     ;file/sub-dir in sectors, or the end of
    pop bx                     ;the file/sub-dir has been reached.
    pop ax 
    ret


NEW_CLST_NUM:            
    mov di, WORD PTR [bx]      ;di -> new cluster number 

    loop CLST_CHN              ;Loop until we've either encountered a cluster 
                               ;number of 0ffffh, or have offset to the 
                               ;desired cluster.

NO_CLST_CHN:



    ;// LOAD THE FIRST SECTOR OF THE DISK DATA AREA INTO varSect           //
    ;// ADD TO varSect THE SECTOR OFFSET INTO THE CLUSTER.                 //
    ;// SUBTRACT 2 FROM THE CLUSTER NUMBER THEN MULTIPLY IT BY clstSize,   //
    ;// PRODUCT IS SECTOR OFFSET (FROM THE SECTOR CURRENTLY IN varSect)    //
    ;// TO THE DESIRED SECTOR OF THE FILE.                                 //


    mov cx, WORD PTR datSect+2       
    mov ax, WORD PTR datSect                          
    mov WORD PTR varSect+2, cx
    mov WORD PTR varSect, ax   ;varSect -> first sector of disk data area
    xor dx, dx            
    mov ax, bp                 ;dx:ax -> sector offset into cluster
    call UPDATE_SECTOR         ;add that to varSect


    mov ax, di                 ;pass the cluster number into ax as input for
                               ;UPDATE_SECTOR  
    dec ax                     ;dec the cluster number twice since the first
    dec ax                     ;two entries in the FAT table do not have 
                               ;corresponding clusters 

    xor dh, dh
    mov dl, clstSize           ;dx -> size of the clusters in sectors
    mul dx                     ;dx:ax -> offset to start of desired cluster  
    call UPDATE_SECTOR         ;varSect -> desired sector of file/directory 
    call LOAD_SECTOR           ;Load the sector into secBuff.



    pop di
    pop bp
    pop dx
    pop cx
    pop bx
    pop ax
    ret

LOAD_DATA_SECTOR ENDP























LOAD_SECTOR PROC

;THIS PROCEDURE LOADS THE SECTOR POINTED TO BY varSect INTO DS:secBuff

;EXPECTS DS TO BE LOADED WITH varData



    push ax
    push bx
    push cx
    push dx
    push es




    ;// USE DIV32 TO DIVIDE varSect INTO SEPARATE C/H/S NUMBERS //


    mov dx, WORD PTR varSect+2 ;dx -> MSW of varSect
    mov ax, WORD PTR varSect   ;ax -> LSW of varSect
    xor bh, bh                  
    mov bl, secPerHd           ;bx -> sectors per head
    call DIV32                 ;bl -> sector number (counting from 0, 0-62)
    inc bl                     ;correct the range of the sector number (1-63)
    mov cl, bl                 ;store that value in cl 
    mov bl, hdPerCyl           ;bx -> heads per cylinder
    call DIV32                 ;bl -> head number, ax -> cylinder number



    ;// CONVERT THE SEPARATE CYLINDER NUMBER AND SECTOR NUMBER INTO THE //
    ;// UNIFIED VALUE IN CX THAT INT 13H, FUNCTION 2 ACCEPTS.           //
    ;// THIS IS DONE USING THE FORMULA:                                 //
    ;// (cylNum shl 8) OR ((cylNum AND 768d)shr 2) OR sectNum           //


    mov dx, ax                 ;need 2 copies of cylNum 

    xor ah, ah
    xchg ah, al                ;ah -> lower 8 bits of cylNum 

    and dx, 768d 
    shr dx, 1 
    shr dx, 1                  ;dx -> upper 2 bits of cylNum

    or ax, dx                  ;ax -> full cylinder number
    or al, cl                  ;OR the sector number into al
    mov cx, ax                 ;cx -> cyl/sectNum (in int 13h format)

    mov dh, bl                 ;dh takes head number
    mov ax, varData
    mov es, ax
    lea bx, secBuff            ;es:bx -> ds:secBuff 
    mov ah, 2                  ;calling int 13 function 2, read in sectors
    mov al, 1                  ;loading a single sector
    mov dl, drv                ;dl -> drive number. 80h=master, 81h=slave 
    int 13h                    ;load the sector into secBuff


    pop es
    pop dx
    pop cx
    pop bx
    pop ax
    ret


LOAD_SECTOR ENDP





























UPDATE_SECTOR PROC

;ADDS A GIVEN NUMBER OF SECTORS TO varSect

;EXPECTS DS TO BE LOADED WITH varData
;EXPECTS VALID VALUES IN secPerHd & hdPerCyl
;EXPECTS THE AMOUNT OF SECTORS TO ADD IN DX:AX



    push bp
    mov bp, ax
    or bp, dx
    test bp, 0ffffh            ;If no bits set, number of sectors to add is 0
    jnz UPD_CHS                ;Proceed if at least 1 bit is set.
    pop bp                     ;Otherwise, return. 
    ret                     
UPD_CHS:
    pop bp


    push ax
    push bx
    push cx
    push dx

    mov cx, WORD PTR varSect+2  
    mov bx, WORD PTR varSect   ;cx:bx -> varSect 
    call ADD32                 ;add varSect to dx:ax
    mov WORD PTR varSect+2, dx     
    mov WORD PTR varSect, ax   ;varSect = dx:ax + varSect

    pop dx
    pop cx
    pop bx
    pop ax
    ret


UPDATE_SECTOR ENDP




















DIV32 PROC

;DIVIDES A 32-BIT VALUE BY A 16-BIT VALUE.

;ALTERS AX
;ALTERS BX
;ALTERS DX

;EXPECTS THE 32-BIT DIVIDEND IN DX:AX
;EXPECTS THE 16-BIT DIVISOR IN BX

;RETURNS THE 32-BIT QUOTIENT IN DX:AX
;RETURNS THE 16-BIT REMAINDER IN BX

    push di
    push si


    mov di, ax ;di -> copy of LSW of given dividend
    mov ax, dx ;ax -> MSW of given dividend
    xor dx, dx ;dx:ax -> 0:MSW  
    div bx     ;ax:dx -> ax=MSW of final quotient, dx=remainder

    mov si, ax ;si -> MSW of final quotient
    mov ax, di ;dx:ax -> dx=previous remainder, ax=LSW of given dividend
    div bx     ;ax:dx -> ax=LSW of final quotient, dx=final remainder  
    mov bx, dx ;bx -> final remainder
    mov dx, si ;dx:ax -> final quotient


    pop si
    pop di
    ret

DIV32 ENDP
























ADD32 PROC

;SUMS TWO GIVEN 32-BIT VALUES.

;ALTERS AX
;ALTERS DX

;EXPECTS THE SOURCE 32-BIT VALUE IN CX:BX
;EXPECTS THE DESTINATION 32-BIT VALUE IN DX:AX


    push bp         ;use bp to see if cx:bx = 0
    mov bp, bx      ;bp -> bx
    or bp, cx       ;If bp now contains no set bits, cx:bx = 0
    test bp, 0ffffh ;If the given value to add is 0, return. 
    jnz ADD_32      ;Otherwise, add cx:bx to dx:ax
    pop bp
    ret

ADD_32:

    pop bp
    add ax, bx      ;LSW of 32-bit sum = ax + bx
    adc dx, cx      ;MSW of 32-bit sum = dx + cx + cf 
    ret


ADD32 ENDP




;----------------------------------------------------------------------------|
 main ENDP                                                                  ;|
                                                                            ;|
 code ENDS                                                                  ;|
                                                                            ;|
 END start                                                                  ;| 
;____________________________________________________________________________|
\$\endgroup\$
  • \$\begingroup\$ Hopefully that's a better title, it describes exactly what the code is intended to do. \$\endgroup\$ – My life is a bug. Dec 31 '17 at 18:46
  • 1
    \$\begingroup\$ Why is the ADD32 proc so complicated? If CX:BX is zero then the cascaded addition add ax, bx adc dx, cx can still be safely executed. It won't harm results. \$\endgroup\$ – Fifoernik Jan 4 '18 at 16:49
  • \$\begingroup\$ You're right. it would be harmless. When I wrote it I was thinking that adding 0 would be a waste of time, so I should return early if DX:AX is 0, however it turned out to be a little more complicated than I originally thought that it would. I don't use it anywhere where speed matters. \$\endgroup\$ – My life is a bug. Jan 5 '18 at 21:04
3
\$\begingroup\$

This code (at the moment) has several issues with robustness that will be corrected before the program is completed and released.

With so many open issues I was at first reluctant to start reviewing this program.
It's certainly a good thing that you know these issues exist, but I would like to advice you to not defer implementing at the very least checking the error status that you obtain from selected BIOS system calls. Disk systems are delicate enough that we should not ignore reported errors!

As per request I've focussed my review on the matter of robustness.

There's a BPB_BytsPerSec.

The fact that your program is "hard wired" to use 512 bytes per sector speaks by itself against robustness. The BIOS Parameter Block has a field that tells you the count of bytes per sector {512, 1024, 2048, 4096}


Getting the disk geometry with AH=08h int 13h.

inc dh                     ;dh -> number of heads (counting from 1) 
mov hdPerCyl, dh           ;initialize hdPerCyl

What this BIOS function returns in CH, DH, and CL are the highest allowed values for Cylinder, Head, and Sector. Cylinder of course also partly in CL !
I don't think there's any reason to believe that the total number of heads could never be 256. If BIOS returned DH=255 (indicating 256 heads) your code would simply fail.

I suggest you define the hdPerCyl variable word-sized and set it up using:

and cl, 3Fh                ;cl -> sector number
mov secPerHd, cl           ;initialize secPerHd
mov cl, 8
shr dx, cl
inc dx
mov hdPerCyl, dx

Loading a sector.

To calculate the CHS values you had to resort to writing a special division procedure DIV32 that can divide a 32-bit number by a 16-bit number yielding a quotient that does not necessarily fit in a 16-bit register. Your solution is correct but could be optimized in quite a few places.

Rather than showing all of these, I'll present my version of the CHS-translation. As you can see it's much shorter and can use the normal division operation without risking any divide exception.
I've also added a much needed retry system because especially disk operations have a tendancy to fail for no reason!

  push ax
  push bx
  push cx
  push dx
  push di
  push es
  mov  di, 5
.Retry:
  mov  al, BYTE PTR secPerHd  ;LE 63
  cbw
  mul  WORD PTR hdPerCyl      ;LE 256 -> DX:AX is sectors per cylinder
  xchg cx, ax                 ;'mov cx, ax'
  mov  dx, WORD PTR varSect+2 ;dx -> MSW of varSect
  mov  ax, WORD PTR varSect   ;ax -> LSW of varSect
  div  cx
  xchg cx, ax                 ;'mov cx, ax' Cylinder number [0,1023]
  xchg ch, cl
  ror  cl, 1
  ror  cl, 1
  xchg ax, dx                 ;'mov ax, dx'
  div  BYTE PTR secPerHd
  mov  dh, al                 ;Head number [0,255]
  or   cl, ah
  inc  cx                     ;Sector number [1,63]
  push ds                     ;'mov ax, varData'
  pop  es                     ;'mov es, ax'
  mov  bx, OFFSET secBuff     ;'lea bx, secBuff'
  mov  dl, drv                ;Drive number
  mov  ax, 0201h              ;BIOS.ReadDiskSector 'mov ah, 2' 'mov al, 1'
  int  13h                    ; -> AX CF
  jnc  .OK
  mov  ah, 00h                ;BIOS.ResetDiskSystem
  int  13h
  dec  di
  jnz  .Retry
  ...                         ;Error handling here...
.OK:
  pop  es
  pop  di
  pop  dx
  pop  cx
  pop  bx
  pop  ax
  ret

The UPDATE_SECTOR proc deserves a better name.

The LOAD_SECTOR proc does exactly what its name suggests. It loads into memory a disk sector.
The UPDATE_SECTOR proc on the other hand doesn't deal with the contents of a sector. It merely changes the value of the variable varSect that holds a sector number.

Better names then would be UPDATE_SECTORNUMBER or UPDATE_VARSECT.

When it comes to actually updating the sector number, I see no benefit in checking if the increase is zero or not.

Given that all it takes to raise the dword variable varSect is

add WORD PTR varSect, ax
adc WORD PTR varSect+2, dx     

there's also no benefit in calling the ADD32 proc (that also can do without the check for a zero increase as commented by Fifoernik).


Determining the number of sectors that the Root occupies.

Microsoft FAT documentation states

For FAT12 and FAT16 volumes, the value of BPB_RootEntCnt should always specify a count that when multiplied by 32 results in an even multiple of BPB_BytsPerSec.

Although this is good advice, even Microsoft uses next prudent calculation:

RootDirSectors = ((BPB_RootEntCnt * 32) + (BPB_BytsPerSec - 1)) / BPB_BytsPerSec

Currently your calculation shifts the BPB_RootEntCnt 5 times to the left immediately followed by shifting it 9 times to the right. Can you see that this comes down to just shifting it 4 times right?

To get code that won't fail on exotic BPB_RootEntCnt values use next calculation:

mov ax, 32
mul WORD PTR [bx+17]       ;BPB_RootEntCnt -> DX:AX is size of root directory in bytes
add ax, bytPerSec-1
adc dx, 0
mov cx, bytPerSec
div cx
xor dx, dx                 ; -> DX:AX is root directory size in sectors

Searching through the directory.

The code expects that certain conditions be met (for the code to execute without errors):
...
It expects to find in the root directory, a sub-directory named ENGINE, within that sub directory it expects to find the four data files mentioned above.

Currently your code cuts too many corners. Even with the assumptions that you're willing to make, you can't seriously search through the directory without specifically looking at the first byte of the 32-byte entry (at offset 0) and at the file attribute byte (at offset 11).

Looking for the ASSET directory:

  ...
ENTRY_SEARCH:                    
  cmp  BYTE PTR [di], 0         ;DS=ES
  je   .EndOfDir
  cmp  BYTE PTR [di], 0E5h
  je   .SkipEntry               ;Deleted file!
  mov  al, BYTE PTR [di+11]
  and  al, 00011000b
  cmp  al, 00010000b
  jnz  .SkipEntry               ;Not a directory!

  push cx                       ;store outer loop cntr
  push si                       ;store start of the file name
  push di                       ;store start of the entry
  mov  cx, 11                   ;inner loop cntr -> length of file name
  repe cmpsb
  pop  di
  pop  si
  pop  cx
  je   .Found
.SkipEntry:
  add  di, 32
  loop ENTRY_SEARCH
.EndOfDir:
  xor  bx, bx                   ;if we're here no match was found. 
  ...                           ;return 0.
  ret
.Found:
  mov  bx, WORD PTR [di+26]     ;bx -> entry's cluster number
  ... 
  ret

Looking for any of the .DAT files:

  ...
ENTRY_SEARCH:                    
  cmp  BYTE PTR [di], 0         ;DS=ES
  je   .EndOfDir
  cmp  BYTE PTR [di], 0E5h
  je   .SkipEntry               ;Deleted file!
  mov  al, BYTE PTR [di+11]
  and  al, 00011000b
  jnz  .SkipEntry               ;Not a file!

  push cx                       ;store outer loop cntr
  push si                       ;store start of the file name
  push di                       ;store start of the entry
  mov  cx, 11                   ;inner loop cntr -> length of file name
  repe cmpsb
  pop  di
  pop  si
  pop  cx
  je   .Found
.SkipEntry:
  add  di, 32
  loop ENTRY_SEARCH
.EndOfDir:
  xor  bx, bx                   ;if we're here no match was found. 
  ...                           ;return 0.
  ret
.Found:
  mov  bx, WORD PTR [di+26]     ;bx -> entry's cluster number
  ... 
  ret
\$\endgroup\$
  • 2
    \$\begingroup\$ All good advice. I did away with ADD32 after Fifoernik left his comment. I knew about the bytes per sector field in the BIOS parameter block, i'm just reluctant to enlarge secBuff, my main data segment only has about 20,000 bytes to spare. DIV32 was a procedure that I wrote a while ago, I reused it here in this code. Your way is better. Basically every web site that I encountered while I was gathering information on the file system mentioned that the root directory size in sectors is always fixed with FAT12/16, so I thought I was making that calculation for no reason. \$\endgroup\$ – My life is a bug. Jan 8 '18 at 1:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.