BASEADDR     equ 0h   ;Change this to 0h or 7C00h for global start
BOOT_ADDR    equ 7C00h
BASEADDR_SEG equ BOOT_ADDR>>4
org BASEADDR

%define resbytes 510-(unused-_00h_jmp)

_00h_jmp: db 0EBh                             ;jmps 003E
          db 03Ch                             ;2  (2)
_02h_nop: nop                                 ;1  (3)

_03h_OEMid               db    "OEMIdent"      ;8  (11)
_0Bh_bytesPerSect        dw         0200h      ;2  (13)
_0Dh_sectsPerClust       db          001h      ;1  (14)
_0Eh_reservedSects       dw         0001h      ;2  (16)
_10h_numOfFATs           db          002h      ;1  (17)
_11h_numRootDirEntries   dw         00E0h      ;2  (19)
_13h_numSectors          dw         0B40h      ;2  (21)
_15h_mediaType           db          0F0h      ;1  (22)
_16h_numFATsectors       dw         0009h      ;2  (24)
_18h_sectorsPerTrack     dw         0012h      ;2  (26)
_1Ah_numHeads            dw         0002h      ;2  (28)
_1Ch_numHiddenSects      dd     00000000h      ;4  (32)
_20h_numSectorsHuge      dd     00000000h      ;4  (36)
_24h_driveNumber         db           00h      ;1  (37)
_25h_reserved            db           00h      ;1  (38)
_26h_signature           db           29h      ;1  (39)
_27h_volumeID            db        "????"      ;4  (43)
_28h_volumeLabel         db "VolumeLabel"      ;11 (54)
_36h_FSType              db    "FAT12   "      ;8  (62)




;INIT: 448 free bytes
;INIT: 448 free bytes
;INIT: 448 free bytes

;1. Code to configure CS:IP here:
;;

;>   cs = 0
;>>  dl = drive we were booted from
;INIT: Normalize the Boot Code Segment:Offset address
;INIT: Normalize the Boot Code Segment:Offset address
;INIT: Normalize the Boot Code Segment:Offset address

  ;Force CS to be 7C00h and start program at start_address:
  ;;
   jmp BASEADDR_SEG:start    ;jmp 7C0h:start_address

   start:
   xor eax,eax   ;Get clean divisions by making sure 32 bits of EAX are 0
   cli           ;Make safe transition to Protected Mode
   push cs       ;Get value of Code Segment
   pop ds        ;Make DS == CS



  ;Save drive number we came from. Since
  ;we won't use again the first byte of code from
  ;the boot, the byte of *jmp 03Eh* instruction,
  ;we will use that byte as the variable space for
  ;the disk number. We must recognize that this is
  ;pretty clever.
  ;;
   mov [_00h_jmp],dl


;END:  Normalize the Boot Code Segment:Offset address
;END:  Normalize the Boot Code Segment:Offset address
;END:  Normalize the Boot Code Segment:Offset address


;2. Code to load GDT and enable Protected Mode here:
;;

;INIT: Enable protected mode to load GDT and keep it enabled
;INIT: Enable protected mode to load GDT and keep it enabled
;INIT: Enable protected mode to load GDT and keep it enabled

   lgdt[GDT]     ;Load GDT. This is how we would
                 ;exactly access the GDT in
                 ;Real Mode

   mov ecx, cr0  ;Switch to Protected Mode...
   inc cx        ;Set PE bit
   mov cr0, ecx  ;{5} Here we activate Protected Mode


;END:  Enable protected mode to load GDT and keep it enabled
;END:  Enable protected mode to load GDT and keep it enabled
;END:  Enable protected mode to load GDT and keep it enabled

;3. Load registers with GDT selectors, disable Protected Mode and
;   set up stack and Un/Real Mode segment registers:
;;

;;INIT: Configuration of Unreal segments and also stack
;;INIT: Configuration of Unreal segments and also stack
;;INIT: Configuration of Unreal segments and also stack
  ;Registers that are modified in this INIT-END portion:
   ;AX (EAX) 0
   ;DS 0
   ;ES 0x800
   ;SS 0
   ;SP 0x800

     ;Registers with values to reuse in the next INIT-END portion:
      ;Apparently ALL of the previous ones.

;>   ah = 0
;>   dl = drive we were booted from
  mov ax,SELDat32 ;Selector for 4GB data seg. What we do here
                  ;is to take the address of the second selector
                  ;of the GDT.
                  ;We are using the 8-bit register AL to hold the
                  ;16-bit data selector number because AH is
                  ;already set to 0, and with this we save 1 byte
                  ;or so of boot space.

  mov ds,ax       ;{2} Extend limit for DS. Typically
                  ;would get the value 0010h. (16)

  mov es,ax       ;Extend limit for ES. ES would also
                  ;be set to 16.

  dec cx          ;Switch back into UnReal Mode. From the start, ECX
                  ;contained the value of CR0 with PE
                  ;bit set. Here we disable the PE bit again,
                  ;and we put it into CR0 in the next
                  ;instruction.

;Disable Protected Mode but keeping extended limits
;for segments so we can get into Unreal Mode
;;
  mov cr0,ecx


;Here we are in UnReal Mode:
;Here we are in UnReal Mode:
;Here we are in UnReal Mode:
;;

;Set end of stack to 700h.
;From here we have 200h (512) free stack bytes.
;The first PUSH will result at 6FCh and if we POP again
;we will return to the empty limit of 700h:
;;
  xor ax,ax
  mov ss,ax            ;Segment 0.
  mov sp,0x700         ;{1B}. Configure the end of the stack at 0x700
                       ;the very end of the free memory area
                       ;from 500h-700h


;Now that we are back in Unreal Mode,
;we can set the main data segment register, DS,
;to its intended value throughout the program,
;since we loaded a GDT selector value here, but now
;we need a Real-Mode-styled memory segment value again:
;;
  push cs
  pop ds

;;END:  Configuration of Unreal segments and also stack
;;END:  Configuration of Unreal segments and also stack
;;END:  Configuration of Unreal segments and also stack


;4. Enable A20 Line here:
;;

;;INIT: Enable A20
;;INIT: Enable A20
;;INIT: Enable A20
  ;Registers modified in this INIT-END portion:
   ;AX (EAX)
    ;Registers with values to reuse in the next INIT--END:
     ;AH (cleared to 0)

;To see if the A20 line is already enabled,
;look at address (7C00h+510) from 2 different
;addresses, one in the first Megabyte and the second
;in the second Megabyte.
;
;Compare the 16-bit word from 0000:7DFEh (7DFEh physical)
;and from FFFFh:7E0Eh (107DFEh physical).
;
;They must be different. If they aren't, then the A20 line
;is disabled and we will have to enable it:
;;
mov ax,[510]
push word 0xFFFF
pop es
cmp word[es:7E0Eh],ax
jne short .A20alreadyEnabled

.5:
  in al,0x64 ;Enable A20 {4A}. Port 0x64 is the
             ;KBC port at the motherboard.

  test al,2  ;See if bit 2 at this port is set to 1, which means
             ;that the KBC is not ready.

  jnz .5     ;Repeat until bit 2 of port 0x64 is 0, which means
             ;that the KBC is ready for commands.

  mov al,0xD1 ;This command is to write the status byte.
              ;This is the so-called *WRITE OUTPUT PORT*.

  out 0x64,al ;Here we send it and it gets executed.

.6:
  in al,0x64    ;Read the byte at port 0x64.
  and ax,byte 2 ;See if this bit is 0.
                ;NOTE: This will leave AL to 0 at once, which
                ;      will be used in the next
                ;      INIT-END block.

  jnz short .6  ;Repeat until the KBC is ready
                ;(bit 2 to 0)

  mov al,0xDF   ;Set the configuration bits to send.

  out 0x60,al   ;Send this parameter to the data port
                ;of the KBC. At this point is where the
                ;A20 line is enabled.

.A20alreadyEnabled:

;;END:  Enable A20
;;END:  Enable A20
;;END:  Enable A20


;5. Reenable interrupts here:
;;

sti  ;Reenable interrupts for normal BIOS functioning



;6. Configure program disk parameter information here:
;;

;;INIT: Configure disk information (calculate geometry)
;;INIT: Configure disk information (calculate geometry)
;;INIT: Configure disk information (calculate geometry)
  ;>   eax = 00000000

  ;Here we are in UnReal Mode since the previous INIT:END:
  ;Here we are in UnReal Mode since the previous INIT:END:
  ;Here we are in UnReal Mode since the previous INIT:END:
  ;;
   xor ax,ax
   mov al,[_10h_numOfFATs]      ;Number of FATs
   mul byte[_16h_numFATsectors] ;Times size of FAT (in sectors)
   add ax,[_0Eh_reservedSects]  ;Plus Sectors before first FAT - currently just the boot sector
        ;(_10h_numOfFATs*_16h_numFATsectors)+_0Eh_reservedSects
           ;EAX = LBA of Root Directory == normally 19
   mov [_RootDirSect],ax
   mov [_ClustAreaSect],ax


   movzx edx,word[_11h_numRootDirEntries]

   shr dx,4   ;16 directory entries per sector.
              ;Here we are getting the number of sectors instead
              ;by dividing the total number of directory entries
              ;across the root directory sectors.

mov [_RootDirSectCount],dx
add [_ClustAreaSect],dx

;;END:  Configure disk information (calculate geometry)
;;END:  Configure disk information (calculate geometry)
;;END:  Configure disk information (calculate geometry)


;7. Search 8.3 file name in root directory:
;;

;;INIT: Search file name in the root directory (read root directory sectors)
;;INIT: Search file name in the root directory (read root directory sectors)
;;INIT: Search file name in the root directory (read root directory sectors)

  ;>  EAX  = LBA of root directory
  ;>  EDI  = length of root directory in sectors
  ;> [SP]  = length of root directory in entries
  ;>  ESI  = 00000000

;Read current root dir sector at 0x7E0:
;;

.RootDirSectLoop:
  push word 0x7E0
  pop es

  xor di,di
  inc di

  ; Input:
  ;       EAX = LBA
  ;        DI = sector count
  ;        ES = segment
  ; Output:
  ;       EBX high half cleared
  ;       DL = drive # number
  ;       EDX high half cleared
  ;       ESI = 0
  ; Clobbered:
  ;       BX, CX, DH, BP
  ;;
   call read_sectors   ;CALL to AHEAD address


  ;Here we are still in UnReal Mode since the past 2 INIT--END blocks:
  ;Here we are still in UnReal Mode since the past 2 INIT--END blocks:
  ;Here we are still in UnReal Mode since the past 2 INIT--END blocks:
  ;;
   mov bx,16   ;16 Dir Entries Per Root Dir Sector, 32 bytes each
   

   xor edi,edi            ;Point at directory buffer {1C}
   xor esi,esi

  .20:
   test byte[es:di+11],0x18  ;See if entry is a file and not
   jnz short .skipDirent     ;a volume ID. If not, skip it.


   mov si,kernelFile    ;Name of file we want. We take the 16-bit
                        ;address of the string.
   xor ecx,ecx
   mov cl,11            ;Number of bytes to read (length of the string
                        ;"BOOTKERNBIN")
   push bx
   mov bx,di  ;Go to current Root Directory Sector Offset
   add bx,26  ;Go to its FAT Cluster Number
   mov bx,[es:bx]  ;Read raw Cluster Number
   mov [_CurrFileClust],bx  ;Store it for file read
   pop bx
   push edi
   a32 repe cmpsb       ;Found the file? Here is where we use
                        ;the 11 in CL
   pop edi



   je short found       ;Yes? This is the key to start loading
                        ;     our program into memory.
   .skipDirent:
   add di,32
   dec bx               ;Loop through all entries

   jnz short .20        ;According to the result of a bit in FLAGS, produced by
                        ;*dec bx*. Concretely the bit that indicates that the
                        ;operation has set the value of the operand down to 0.

inc ax  ;Go to the next Root Directory Sector LBA
dec word[_RootDirSectCount]
jnz short .RootDirSectLoop


  ;Couldn't find the file in directory, so
  ;reboot the machine to retry loading a system:
  ;;
   boot_error:
   int 19h


   found:
;NOTE: MAKE SURE THAT THE KERNEL FINDING CODE FOR THE
;ROOT DIRECTORY ALWAYS WORKS (COPY LOTS OF FILES, DELETE THEM
;AND SEE IF IT STILL IS FOUND). ... OK

;;END:  Search file name in the root directory (read root directory sectors)
;;END:  Search file name in the root directory (read root directory sectors)
;;END:  Search file name in the root directory (read root directory sectors)


;8. Read file contents:
;;

;;INIT: Final processes and pass control to the kernel
;;INIT: Final processes and pass control to the kernel
;;INIT: Final processes and pass control to the kernel

.FileReadLoop:


  ;>>    ECX = 0000????
  ;>    [SP] = Next cluster of file
  ;>     ESI = 0000????
  ;>     EDX = 0000????
;>  ES:EDI = Destination address
  ;>     EBP = LBA of cluster 2
  ;>      DS = 0
  ;;
   xor ax,ax
   mov ax,[_CurrFileClust]  ;Get the most recent cluster number
   cmp ax,0xFF8        ;Valid cluster?
   jae short eof       ;No:  assume end of file
                       ;Yes: (c-bit set)



  ;Read file sector here
  ;;
   movzx edi,byte[_0Dh_sectsPerClust]  ;File data sectors to read
   push es
   push word[_FileBuffSegment]
   pop es


;Convert cluster number (which starts from 2) into LBA sector.
;The cluster area normally starts at the 34th sector (sector 33
;counting from 0). Cluster numbers start from 2, but to get their LBA address
;we must substract 2 again from the start cluster number, as set
;by the FAT algorithm. It looks like the minimum usable sector we can get
;for clusters as LBA is 33.
;
;It seems as if every major area was seen as a cluster within FAT, so
;the boot sector doesn't have a cluster number assigned,
;the area of FATs had special cluster number 0, and
;the root directory area had special cluster number 1,
;so normal cluster numbers start at 2, but we need to substract that 2
;to then, now obviously, get the correct LBA number.
;;
  add ax,[_ClustAreaSect]  ;Get first sector of cluster area
  dec ax
  dec ax  ;Remove the artifact of starting cluster numbers from 2


  ; Input:
  ;       EAX = LBA
  ;        DI = sector count
  ;        ES = segment
  ; Output:
  ;       EBX high half cleared
  ;       DL = drive # number
  ;       EDX high half cleared
  ;       ESI = 0
  ; Clobbered:
  ;       BX, CX, DH, BP
  ;;
   call read_sectors             ;CALL to AHEAD address



   mov cx,di  ;Copy sectors per cluster
   dec cx  ;Turn it into 0-based count for shift multiply
   mov di,[_0Bh_bytesPerSect]  ;Get bytes per sector
   shl di,cl  ;Get total bytes in cluster
   shr di,4   ;Convert to Real Mode segment
   add [_FileBuffSegment],di   ;Advance the base copy segment
   pop es

;Start getting next cluster:
   mov ax,[_CurrFileClust]
    push ax  ;Save original cluster
  mov si,ax
   shr si,1    ;Divide SI by 2. Now we have 0.5 of its cluster value
   add si,ax   ;Add original cluster value, now we have 1.5 of its value

  push si  ;Save multiplied value

;Read 2 FAT sectors:
     mov ax,si  ;Get multiplied cluster for actual sector offset
    shr ax,9  ;Get the sector number, shift divide by 512
   add ax,[_0Eh_reservedSects]  ;Get to first actual FAT sector in LBA
    xor di,di
    inc di
    inc di   ;Specify 2 sectors to read, in the correct FAT sector, load at
             ;segment 0x7E0

  ; Input:
  ;       EAX = LBA
  ;        DI = sector count
  ;        ES = segment
  ; Output:
  ;       EBX high half cleared
  ;       DL = drive # number
  ;       EDX high half cleared
  ;       ESI = 0
  ; Clobbered:
  ;       BX, CX, DH, BP
  ;;
   call read_sectors             ;CALL to AHEAD address

;Get 2 raw cluster bytes:
  pop si  ;Get multiplied cluster
  pop ax  ;Get original cluster
  and si,111111111b ;Limit SI buffer offset to first 512 bytes
   mov si,[es:si]  ;Access 1024-byte buffer with limitation above
   test ax,1      ;See if original cluster number is even or odd
   jz .evenClustNum
    shr si,4       ;If odd, just keep the higher 4 bits (discard first 4 bits)
    jmp short .DoneAdjustClustNum
   .evenClustNum:
    and si,0x0FFF  ;If even, just keep the lower/first 12 bits
   .DoneAdjustClustNum:


;Save the new cleaned-up cluster value:
;;
  mov [_CurrFileClust],si

  jmp short .FileReadLoop
  eof:


;;END:  Final processes and pass control to the kernel
;;END:  Final processes and pass control to the kernel
;;END:  Final processes and pass control to the kernel



;9. Jump to the 16-bit Real Mode bootup image
;   (it's intended to jump to 70h:0000h or 700h physical):
;;

;Now jump to the kernel image we loaded into
;address 0x500, 0x600 or 0x700 physical (just like DOS):
;;
  jmp _kern16seg:0






;10. read_sectors BIOS-based function:
;;

;;INIT: read_sectors
;;INIT: read_sectors
;;INIT: read_sectors
  ; Input:
  ;       EAX = LBA
  ;        DI = sector count
  ;        ES = segment
  ; Output:
  ;       EBX high half cleared
  ;       DL = drive # number
  ;       EDX high half cleared
  ;       ESI = 0
  ; Clobbered:
  ;       BX, CX, DH, BP
  ;;
  read_sectors:
   ;Save EAX, ES and DI:
   ;;
    push eax
    push es
    push di
   
  .sectorLoop:
   push  eax        ;LBA sector value

  ;Sign-extend EAX into EDX:EAX,
  ;so here we intend to get EDX=0:
  ;;
   cdq

  ;Perform EDX:EAX / EBX
  ;
  ;Perform LBA/Sectors Per Track. It will give us
  ;the track number in EAX and
  ;the zero-based sector value in EDX:
  ;;
   movzx ebx, byte[_18h_sectorsPerTrack]
   div   ebx        ;EAX=track; EDX=sector-1

  ;Get a copy of the sector value in CX.
  ;Now substract it from the sectors per track
  ;to know how many sectors to read from a track:
  ;;
   mov   cx, dx     ;CL=sector-1; CH=0
   sub   bl, dl     ;BX=max transfer before end of track

  ;See if the number of requested sectors by the user
  ;are more than the remaining sectors in the track.
  ;If so, go, jump and read the maximum number of sectors.
  ;If not, just read the remaining number of pending sectors
  ;requested by the user:
  ;;
   cmp   di, bx             ;Do we want more than that?
   ja short .sectorOverflow ;Yes, do this much now
   mov   bx, di             ;No, do it all now

  .sectorOverflow:
  ;Save a copy of the calculated remaining
  ;sectors-in-a-track count:
  ;;
   mov  esi, ebx    ;Save count for this transfer

  ;Convert the zero-based sector count for the CHS S value:
  ;;
   inc  cx          ;CL=sector number

  ;Here EDX is just 0 to perform a clean 32-bit division.
  ;
  ;Perform EDX:EAX divided by EBX
  ;
  ;Track_Number / Number_of_Heads. Now we will get
  ;the Cylinder number in EAX and
  ;the Head number in EDX:
  ;;
   xor  dx,  dx
   mov  bl,  [_1Ah_numHeads]
   div  ebx         ;EAX=cylinder; EDX=head



   mov  dh,  dl           ;*DH=Head CHS H value
   mov  dl,  [_00h_jmp]   ;*DL=Drive number stored at the start


  ;Get a packed value of Cylinder and Sector:
  ;
  ;Sector   S is in bits 0-5 of CL and
  ;Cylinder C is in bits 0-7 of CH and bits 6-7 of CL
  ;;
   xchg ch,  al           ;*CH=cylinder number [0:7]; AL=0
   shr  ax,  2            ;AL[6:7]=High two bits of cylinder
   or   cl,  al           ;*CL=cylinder [8:9] and sector [0:5]



   mov  ax,  si           ;*AL=Remaining sectors-in-a-track count
   mov  ah,  2            ;*Service: Read
   xor  bx,  bx           ;*ES:BX -- Destination buffer pointer

  ;Here we save AX for being able to retry since
  ;this BIOS service destroys the original value
  ;if AX we pass it.
  ;
  ;Parameters:
  ;     AH = 2 -- Read Sectors
  ;     AL = non-zero Sector Count
  ;     CH = Cylinder      bits 0-7 into 0-7
  ;     CL = Cylinder      bits 8-9 into 6-7
  ;          Sector Number bits 0-5
  ;
  ;     DH = Head Number
  ;     DL = Drive Number (bit 7 set for hard disks)
  ;;
   mov bp,3
   .retry:
    push ax
     int 13h
    pop ax
    jnc .OK_noerror
    .retryRecalibrate:
    push ax
     xor ax,ax
     int 13h
    pop ax
    jc .retryRecalibrate
   dec bp
   jnz short .retry

   ;If we are here, there was a boot error,
   ;so try booting again:
   ;;
    mov ax,0xE07 ;{3}
    int 10h      ;Video service. Here we will "ring" the bell.
    int 19h      ;Since this is an error we found, we reboot.


  .OK_noerror:
   pop  eax
   add  eax, esi          ;Advance LBA

  ;Here we convert the value of SI, which contained the
  ;remaining sectors-in-a-track value, to a 16-bit segment
  ;value, converting it from sector count to byte count
  ;(with the implicit 4-bit right shift for the segment value).
  ;
  ;This will allow us to advance as many 512-byte memory chunks
  ;as we just read:
  ;;
   push si
   shl  si, 5
   mov  bx, es
   add  bx, si            ;Advance segment
   mov  es, bx
   pop  si

  ;Substract the remaining sectors-in-a-track count (in SI)
  ;from the number of sectors requested by the user,
  ;If such number of user-requested sectors (in DI) is still
  ;greater than the remaining sectors-in-a-track,
  ;;
   sub  di, si
   ja   .sectorLoop

   ;Restore EAX, ES and DI:
   ;;
    pop  di
    pop  es
    pop  eax
  ret
;;END:  read_sectors
;;END:  read_sectors
;;END:  read_sectors




;11. 8.3 bootup file name:
;;
kernelFile db "BOOTKERNBIN"

;12. GDT table with 3 selectors (null, code, data) here:
;;

;INIT: GDT
;INIT: GDT
;INIT: GDT
    GDT:
   ;WARNING: This selector, besides being the pointer to the GDT,
   ;         is the null selector.
   ;;
    SELNull equ 0
         GDT_size:
          dw GDTsize
         GDT_actualptr:
          dd 7C00h+GDT
          dw 0x0000

   SELCod32 equ 8
     dw 0FFFFh        ; bits 0-15 length
     dw 00000h        ; bits 0-15 base address
     db 0             ; bits 16-23 base address
     db 10011010b     ; bits P, DPL, DT and type
     db 11001111b     ; bits G, D and bits 16-19 length
     db 0             ; bits 24-31 base address

   SELDat32 equ 16    ; This is the "flat data selector"
     dw 0FFFFh        ; bits 0-15 length
     dw 00000h        ; bits 0-15 base address
     db 0             ; bits 16-23 base address
     db 10010010b     ; bits P, DPL, DT and type
     db 11001111b     ; bits G, D and bits 16-19 length
     db 0             ; bits 24-31 base address
   GDT_end:

   GDTsize equ (GDT_end-GDT)-1
;END:  GDT
;END:  GDT
;END:  GDT



;13. Program variables here:
;;

;All of the variables below are 16-bit.
;
;They are outside of the boot code to make
;more room and only _FileBuffSegment is better off
;if we define and initialize it:
;;
_kern16seg         equ 0x70
_FileBuffSegment   dw _kern16seg

_RootDirSect       equ 8200h+0
_RootDirSectCount  equ 8200h+2
_CurrFileClust     equ 8200h+4
_ClustAreaSect     equ 8200h+8



  unused: times resbytes db 0x55
;END:  448 free bytes
;END:  448 free bytes
;END:  448 free bytes



_1F_55AA_signature      dw 0xAA55        ;2    (512)