1/* 2 * PROJECT: FreeLoader 3 * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) 4 * PURPOSE: FAT12 file system boot sector for NEC PC-98 series 5 * NOTES: The source code in this file is based on the Brian Palmer's work 6 * (boot/freeldr/bootsect/fat.S) 7 * COPYRIGHT: Copyright 2019 Dmitry Borisov (di.sean@protonmail.com) 8 */ 9 10#include <asm.inc> 11#include <freeldr/include/arch/pc/x86common.h> 12 13#define BP_REL(x) [bp + x - offset start] 14#define VGA_WIDTH 80 15#define VGA_HEIGHT 25 16 17/* 18 * See https://www.webtech.co.jp/company/doc/undocumented_mem/memsys.txt 19 * At segment 0000h 20 */ 21#define BOOT_DAUA HEX(0584) 22 23DataAreaStartHigh = 2 24DataAreaStartLow = 4 25BiosCHSDriveSizeHigh = 6 26BiosCHSDriveSizeLow = 8 27BiosCHSDriveSize = 8 28ReadSectorsOffset = 10 29ReadClusterOffset = 12 30PutCharsOffset = 14 31BootSectorStackTop = HEX(7C00) - 16 32 33if 0 34.macro DEBUG_STOP 35 hlt 36 jmp short $ - 1 37.endm 38 39.macro DEBUG_STEP 40 push ax 41 xor ah, ah 42 int HEX(18) // Wait for a keypress 43 pop ax 44.endm 45endif 46 47// org 7C00h 48 49.code16 50 51start: 52 jmp main 53 nop 54 55// After running fatten tool it overwrites (BPB & EBPB) 56OEMName: 57 .ascii "FrLdr1.0" 58BytesPerSector: 59 .word 512 60SectsPerCluster: 61 .byte 1 62ReservedSectors: 63 .word 1 64NumberOfFats: 65 .byte 2 66MaxRootEntries: 67 .word 224 68TotalSectors: 69 .word 2880 70MediaDescriptor: 71 .byte HEX(0f0) 72SectorsPerFat: 73 .word 9 74SectorsPerTrack: 75 .word 18 76NumberOfHeads: 77 .word 2 78HiddenSectors: 79 .long 0 80TotalSectorsBig: 81 .long 0 82BootDrive: 83 .byte HEX(0ff) 84Reserved: 85 .byte 0 86ExtendSig: 87 .byte HEX(29) 88SerialNumber: 89 .long 00000000 90VolumeLabel: 91 .ascii "NO NAME " 92FileSystem: 93 .ascii "FAT12 " 94 95/* 96 * Real-mode entry point 97 */ 98main: 99 xor ax, ax // Setup segment registers: 100 mov ds, ax // Make DS correct 101 mov es, ax // Make ES correct 102 mov ss, ax // Make SS correct 103 mov bp, HEX(7C00) 104 mov sp, BootSectorStackTop // Stack grows downwards from BootSectorStackTop 105 106if 0 // It would have been nice to have had this check... 107 mov ax, HEX(1000) // Detecting hardware 108 109 /* 110 * INSTALLATION CHECK interrupt 111 * See http://www.ctyme.com/intr/rb-2293.htm 112 */ 113 int HEX(1A) 114 cmp ax, HEX(1000) 115 je HardwareError // An IBM-compatible PC 116endif 117 118 /* 119 * IBM PC here: NEC PC-98 here: 120 * ESI = 000E:0000 ESI = 0000:0000 121 * EDI = 0000:070C EDI = 0000:0000 122 * EBP = 0000:7C00 EBP = 0000:7C00 123 * ESP = 0000:7BF0 ESP = 0000:7BF0 124 * CS:IP = 0000:7C4E CS:IP = 1FE0:004E 125 */ 126 mov ax, word ptr ds:[BOOT_DAUA] // Get the Device Address/Unit Address (DA/UA) 127 mov si, ax 128 129 push si 130 push cs 131 pop ds 132 xor si, si 133 mov ax, HEX(0000) 134 mov es, ax 135 mov di, HEX(7C00) 136 mov cx, 512 137 rep movsw // Move our 512 bytes boot sector to the [0000:7C00] 138 pop si 139 140 ljmp16 0, relocated // Jump into relocated code 141 142relocated: 143 xor ax, ax // Clean-up segments 144 mov es, ax 145 mov ds, ax 146 147 test byte ptr ds:[HEX(501)], HEX(08) // High-resolution mode check 148 jz VideoTestNormalMode 149 mov ax, HEX(0E000) 150 jmp short VideoTestDone 151VideoTestNormalMode: 152 mov ax, HEX(0A000) 153VideoTestDone: 154 mov word ptr BP_REL(VramSegment), ax 155 156 mov ax, si 157 mov byte ptr BP_REL(BootDrive), al // Save the boot drive 158 159 /* 160 * Now we must find our way to the first sector of the root directory 161 * 162 * LBA = NumberOfFats * SectorsPerFat + HiddenSectors + ReservedSectors 163 */ 164 xor ax, ax 165 xor cx, cx 166 mov al, byte ptr BP_REL(NumberOfFats) // Number of fats 167 mul word ptr BP_REL(SectorsPerFat) // Times sectors per fat 168 add ax, word ptr BP_REL(HiddenSectors) 169 adc dx, word ptr BP_REL(HiddenSectors + 2) // Add the number of hidden sectors 170 add ax, word ptr BP_REL(ReservedSectors) // Add the number of reserved sectors 171 adc dx, cx // Add carry bit 172 mov word ptr [bp - DataAreaStartLow], ax // Save the starting sector of the root directory 173 mov word ptr [bp - DataAreaStartHigh], dx // Save it in the first 4 bytes before the boot sector 174 mov si, word ptr BP_REL(MaxRootEntries) // Get number of root dir entries in SI 175 pusha // Save 32-bit logical start sector of root dir 176 // DX:AX now has the number of the starting sector of the root directory 177 178 /* 179 * Now calculate the size of the root directory 180 * 181 * Root directory sectors = (MaxRootEntries * 32 + BytesPerSector - 1) / BytesPerSector 182 */ 183 xor dx, dx 184 mov ax, 32 // Size of dir entry 185 mul si // Times the number of entries 186 mov bx, word ptr BP_REL(BytesPerSector) 187 add ax, bx 188 dec ax 189 div bx // Divided by the size of a sector 190 // AX now has the number of root directory sectors 191 192 add word ptr [bp - DataAreaStartLow], ax // Add the number of sectors of the root directory to our other value 193 adc word ptr [bp - DataAreaStartHigh], cx // Now the first 4 bytes before the boot sector contain the starting sector of the data area 194 popa 195 196/* 197 * Reads root directory into [0000:7E00] and finds 'FREELDR SYS' 198 * 199 * Call with: 200 * 201 * DX:AX - LBA of the starting sector of the root directory 202 */ 203LoadRootDirSector: 204 mov bx, HEX(7E0) // We will load the root directory sector 205 mov es, bx // Right after the boot sector in memory 206 xor bx, bx // We will load it to [0000:7E00] 207 xor cx, cx // Zero out CX 208 inc cx // Now increment it to 1, we are reading one sector 209 xor di, di // Zero out di 210 push es // Save ES because it will get incremented by 20h 211 call ReadSectors // Read the first sector of the root directory 212 pop es // Restore ES (ES:DI = 7E0:0000) 213 214SearchRootDirSector: 215 cmp byte ptr es:[di], ch // If the first byte of the directory entry is zero then we have 216 jz PrintFileNotFound // reached the end of the directory and FREELDR.SYS is not here so reboot 217 pusha // Save all registers 218 mov cl, 11 // Put 11 in cl (length of filename in directory entry) 219 mov si, offset filename // Put offset of filename string in DS:SI 220 repe cmpsb // Compare this directory entry against 'FREELDR SYS' 221 popa // Restore all the registers 222 jz FoundFreeLoader // If we found it then jump 223 dec si // SI holds MaxRootEntries, subtract one 224 jz PrintFileNotFound // If we are out of root dir entries then reboot 225 add di, 32 // Increment DI by the size of a directory entry 226 cmp di, HEX(0200) // Compare DI to 512 (DI has offset to next dir entry, make sure we haven't gone over one sector) 227 jc SearchRootDirSector // If DI is less than 512 loop again 228 jmp short LoadRootDirSector // Didn't find FREELDR.SYS in this directory sector, try again 229 230FoundFreeLoader: 231 /* 232 * We found freeldr.sys on the disk 233 * so we need to load the first 512 bytes of it to [0000:F800] 234 * ES:DI has dir entry (ES:DI == 07E0:XXXX) 235 */ 236 mov ax, word ptr es:[di + HEX(1A)] // Get start cluster 237 push ax // Save start cluster 238 push FREELDR_BASE / 16 // Put load segment on the stack and load it 239 pop es // Into ES so that we load the cluster at [0000:F800] 240 call ReadCluster // Read the cluster 241 pop ax // Restore start cluster of FreeLoader 242 243 /* 244 * Save the addresses of needed functions so 245 * the helper code will know where to call them 246 */ 247 mov word ptr [bp - ReadSectorsOffset], offset ReadSectors // Save the address of ReadSectors 248 mov word ptr [bp - ReadClusterOffset], offset ReadCluster // Save the address of ReadCluster 249 mov word ptr [bp - PutCharsOffset], offset PrintString // Save the address of PrintString 250 251 /* 252 * Now AX has start cluster of FreeLoader and we 253 * have loaded the helper code in the first 512 bytes 254 * of FreeLoader to 0000:F800. Now transfer control 255 * to the helper code. Skip the first three bytes 256 * because they contain a jump instruction to skip 257 * over the helper code in the FreeLoader image 258 */ 259 ljmp16 0, FREELDR_BASE + 3 260 261/* 262 * Reads cluster number in AX into [ES:BX] 263 * 264 * Call with: 265 * 266 * AX - cluster number 267 * ES:BX - buffer to read data into 268 */ 269ReadCluster: 270 /* 271 * StartSector = ((Cluster - 2) * SectorsPerCluster) + ReservedSectors + HiddenSectors 272 */ 273 dec ax // Adjust start cluster by 2 274 dec ax // Because the data area starts on cluster 2 275 xor ch, ch 276 mov cl, byte ptr BP_REL(SectsPerCluster) 277 mul cx // Times sectors per cluster 278 add ax, [bp - DataAreaStartLow] // Add start of data area 279 adc dx, [bp - DataAreaStartHigh] // Now we have DX:AX with the logical start sector of FREELDR.SYS 280 xor bx, bx // We will load it to [ES:0000], ES loaded before function call 281 282/* 283 * Reads logical sectors into [ES:BX] 284 * 285 * Call with: 286 * 287 * DX:AX - logical sector number to read (LBA value) 288 * ES:BX - buffer to read data into 289 * CX - number of sectors to read 290 */ 291ReadSectors: 292 293 .ReadSectorsLoop: 294 pusha 295 296 /* 297 * Converting LBA (Linear Block Address) into a format CHS (Cylinder:Head:Sector) 298 * 299 * C = (LBA / SPT) / HPC 300 * H = (LBA / SPT) % HPC 301 * S = (LBA % SPT) + 1 302 */ 303 xchg ax, cx 304 xchg ax, dx 305 xor dx, dx 306 div word ptr BP_REL(SectorsPerTrack) 307 xchg ax, cx 308 div word ptr BP_REL(SectorsPerTrack) // Divide logical by SectorsPerTrack 309 inc dx // Sectors numbering starts at 1 not 0 310 xchg cx, dx 311 div word ptr BP_REL(NumberOfHeads) // Number of heads 312 313 mov dh, dl // DH - head number (0-1) 314 mov dl, cl // DL - sector number (1-26) 315 mov cl, al // CL - cylinder number (0-76) 316 317 // TODO: This should be calculated using the equation: BytesPerSector = (CH + 1) * 128 318 mov ch, 2 // CH - sector size (0-4): 0 (128), 1 (256), 2 (512), 3 (1024), 4 (2048) 319 320 mov al, byte ptr BP_REL(BootDrive) // AL - DA/UA 321 push bp 322 push bx 323 mov bx, word ptr BP_REL(BytesPerSector) // BX - bytes to read 324 pop bp // ES:BP - buffer to read data into 325 mov ah, HEX(56) // AH - read sectors from a floppy disk with SEEK, and use double-density format (MFM) 326 327 /* 328 * Disk BIOS interrupt 329 * See http://radioc.web.fc2.com/column/pc98bas/bios/int1b_06.htm 330 */ 331 int HEX(1b) 332 333 pop bp 334 jc PrintDiskError // CF set on failure 335 336 popa 337 338 inc ax // Increment sector to read 339 jnz .NoCarryCHS 340 inc dx 341 342 .NoCarryCHS: 343 push bx 344 mov bx, es 345 add bx, HEX(20) // Add size of dir entry to the buffer address for the next sector 346 mov es, bx 347 pop bx 348 loop .ReadSectorsLoop // Increment read buffer for next sector, read next sector 349 350 ret 351 352/* 353 * Prints a character 354 * 355 * Call with: 356 * 357 * AL - ASCII code 358 */ 359PutChar: 360 push di 361 push es 362 363 push word ptr BP_REL(VramSegment) 364 pop es 365 mov di, word ptr BP_REL(VramOffset) // ES:DI = VramSegment:VramOffset 366 .PutCharWrite: 367 xor ah, ah 368 stosw // Write ASCII directly to the VRAM 369 370 mov word ptr BP_REL(VramOffset), di 371 pop es 372 pop di 373 374 ret 375 376/* 377 * Prints a null-terminated string 378 * 379 * Call with: 380 * 381 * DS:SI - pointer to a string 382 */ 383PrintString: 384 xor ah, ah 385 lodsb // Get a single char from a ptr 386 387 or al, al 388 jz short .PrintEnd // Found NULL 389 390 cmp al, HEX(0D) 391 jz short .PrintStringHandleCR // Found CR 392 393 call PutChar 394 jmp short PrintString 395 396 .PrintStringHandleCR: 397 mov ax, word ptr BP_REL(VramOffset) 398 mov dl, VGA_WIDTH * 2 399 div dl 400 inc ax 401 mul dl 402 mov word ptr BP_REL(VramOffset), ax 403 inc si // Skip the next LF character 404 jmp short PrintString 405 406.PrintEnd: 407 ret 408 409if 0 410/* 411 * Displays a hardware error message and reboots 412 */ 413HardwareError: 414 mov si, offset msgHardwareError 415 416 .PrintStringVGA: 417 lodsb // Get a single char from a ptr 418 419 or al, al 420 jz short .HardwareErrorDone // Found NULL 421 422 mov ah, HEX(0E) // Teletype output 423 mov bx, 7 // BH - video page number, BL - foreground color 424 int HEX(10) // Display a character via TTY mode 425 jmp short .PrintStringVGA 426 427.HardwareErrorDone: 428 xor ax, ax 429 int HEX(16) // Wait for a keypress 430 int HEX(19) // Reboot 431endif 432 433/* 434 * Displays a disk error message and reboots 435 */ 436PrintDiskError: 437 mov si, offset msgDiskError // Disk error message 438 call PrintString // Display it 439 440 jmp short Reboot 441 442/* 443 * Displays a file not found error message and reboots 444 */ 445PrintFileNotFound: 446 mov si, offset msgNotFoundError // FreeLdr not found message 447 call PrintString // Display it 448 449 jmp short Reboot 450 451/* 452 * Reboots the computer after keypress 453 */ 454Reboot: 455 mov si, offset msgAnyKey // Press any key message 456 call PrintString // Display it 457 458 xor ax, ax 459 int HEX(18) // Wait for a keypress 460 461 /* 462 * Activate the CPU reset line 463 * See https://people.freebsd.org/~kato/pc98-arch.html#cpureset 464 * and http://www.webtech.co.jp/company/doc/undocumented_mem/io_cpu.txt 465 */ 466 xor ax, ax 467 out HEX(0F0), al 468 469 hlt 470Halt: 471 jmp short Halt // Spin 472 473VramSegment: 474 .word 0 475VramOffset: 476 .word 0 477msgDiskError: 478 .ascii "ERR", CR, LF, NUL 479msgNotFoundError: 480 .ascii "NFE", CR, LF, NUL 481msgAnyKey: 482 .ascii "Press any key", NUL 483filename: 484 .ascii "FREELDR SYS" 485 486if 0 // So totally out of space here... 487msgHardwareError: 488 .ascii "It's not PC-98", NUL 489endif 490 491 .org 509 // Pad to 509 bytes 492 493BootPartition: 494 .byte 0 495 496BootSignature: 497 .word HEX(0AA55) // BootSector signature 498 499.endcode16 500 501END 502