/* * PROJECT: FreeLoader * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) * PURPOSE: EXTFS volume boot sector * COPYRIGHT: Copyright 2002-2003 Brian Palmer * Copyright 2024-2025 Daniel Victor */ #include #include // Boot sector constants #define BOOTSECTOR_BASE_ADDRESS HEX(7C00) #define EXTLDR_BOOTSECTOR_SIZE 1024 #define EXT_POINTER_SIZE 4 #define EXT_EXTENT_SIZE 12 // Maximum extent values #define EXT_EXTENT_MAX_LEVEL 5 #define EXT_EXTENT_MAX_LENGTH 32768 // Group descriptor offsets #define EXT_GROUP_DESC_INODE_TABLE_OFFSET 8 // Extent offsets #define EXT_EXTENT_HEADER_ENTRIES_OFFSET 2 #define EXT_EXTENT_HEADER_DEPTH_OFFSET 6 #define EXT_EXTENT_INDEX_LEAF_OFFSET 4 #define EXT_EXTENT_LENGTH_OFFSET 4 #define EXT_EXTENT_START_OFFSET 8 // Inode offsets #define EXT_INODE_SIZE_OFFSET 4 #define EXT_INODE_FLAGS_OFFSET 32 #define EXT_INODE_BLOCK_POINTER_OFFSET 40 // Directory entry offsets #define EXT_DIRECTORY_ENTRY_SIZE_OFFSET 4 #define EXT_DIRECTORY_ENTRY_NAME_LENGTH_OFFSET 6 #define EXT_DIRECTORY_ENTRY_NAME_OFFSET 8 // Inode flags #define EXT_INODE_FLAG_EXTENTS HEX(80000) // Inode blocks constants #define EXT_INODE_BLOCKS 12 #define EXT_INODE_INDIRECT_BLOCKS 3 // Root Inode #define EXT_ROOT_INODE 2 // Inode address #define EXT_INODE_ADDRESS HEX(9000) // Data block addresses #define EXT_BLOCK_ADDRESS HEX(1000) #define EXT_BLOCK2_ADDRESS HEX(2000) #define EXT_BLOCK3_ADDRESS HEX(3000) #define EXT_BLOCK4_ADDRESS HEX(4000) #define EXT_BLOCK5_ADDRESS HEX(5000) #define EXT_BLOCK6_ADDRESS HEX(6000) #define EXT_BLOCK7_ADDRESS HEX(A000) // Inode types #define EXT_INODE_TYPE_MASK HEX(F000) #define EXT_INODE_TYPE_REGULAR HEX(8000) // File size limit #define EXT_INODE_DATA_SIZE_LIMIT HEX(F000) // Offset of functions addresses that will be used by the extldr.sys 3rd-stage bootsector #define ExtReadBlockOffset 2 #define ExtReadInodeOffset 4 #define DisplayItAndRebootOffset 6 #define PutCharsOffset 8 // Boot sector stack constants #define BOOTSECTOR_STACK_TEMP_VARIABLES 2 #define BOOTSECTOR_STACK_TEMP_VARIABLES_SIZE (4 * BOOTSECTOR_STACK_TEMP_VARIABLES) #define BOOTSECTOR_STACK_OFFSET (8 + BOOTSECTOR_STACK_TEMP_VARIABLES_SIZE) #define BOOTSECTOR_STACK_BASE (BOOTSECTOR_BASE_ADDRESS - BOOTSECTOR_STACK_OFFSET) #define BP_REL(x) ss:[bp + (x - BOOTSECTOR_BASE_ADDRESS)] // Temporary variables #define ExtFileSizeState ((BOOTSECTOR_STACK_BASE + BOOTSECTOR_STACK_TEMP_VARIABLES_SIZE) - 4) #define LBASectorsRead (ExtFileSizeState - 4) .code16 #ifndef INCLUDED_ASM start: jmp short main nop // Fields that will be changed by the installer BootDrive: .byte HEX(FF) ExtVolumeStartSector: .long 263088 // Start sector of the ext2 volume ExtBlockSize: .long 2 // Block size in sectors ExtBlockSizeInBytes: .long 1024 // Block size in bytes ExtPointersPerBlock: .long 256 // Number of block pointers that can be contained in one block ExtGroupDescSize: .long 32 // Size of Group Descriptor ExtFirstDataBlock: .long 2 // First data block (2 for 1024-byte blocks, 1 for bigger sizes) ExtInodeSize: .long 128 // Size of Inode ExtInodesPerGroup: .long 2048 // Number of inodes per group // File variables ExtFileSize: .long 0 // File size in bytes ExtFileAddress: .long FREELDR_BASE // File address ExtFileAddressOld: .long FREELDR_BASE // Old file address // Inode variables ExtReadInodeGroup: .long 0 ExtReadInodeIndex: .long 0 ExtReadInodeGroupBlock: .long 0 ExtReadInodeIndexBlock: .long 0 ExtReadInodeGroupOffset: .word 0 ExtReadInodeIndexOffset: .word 0 main: xor ax, ax // Setup segment registers mov ds, ax // Make DS correct mov es, ax // Make ES correct mov ss, ax // Make SS correct mov bp, BOOTSECTOR_BASE_ADDRESS mov sp, bp // Setup a stack sub sp, BOOTSECTOR_STACK_OFFSET // Save the function addresses so the helper code knows where to call them mov word ptr ss:[bp-ExtReadBlockOffset], offset ExtReadBlock mov word ptr ss:[bp-ExtReadInodeOffset], offset ExtReadInode mov word ptr ss:[bp-DisplayItAndRebootOffset], offset DisplayItAndReboot mov word ptr ss:[bp-PutCharsOffset], offset PutChars mov si, offset BootDrive cmp byte ptr [si], HEX(0ff) // If they have specified a boot drive then use it jne CheckInt13hExtensions mov byte ptr [si], dl // Save the boot drive // Now check if this computer supports extended reads. This boot sector will not work without it. CheckInt13hExtensions: mov ah, HEX(41) // AH = 41h mov bx, HEX(55aa) // BX = 55AAh int HEX(13) // IBM/MS INT 13 Extensions - INSTALLATION CHECK jc PrintDiskError // CF set on error (extensions not supported) cmp bx, HEX(aa55) // BX = AA55h if installed jne PrintDiskError test cl, 1 // si = API subset support bitmap jz PrintDiskError // Bit 0, extended disk access functions (AH=42h-44h,47h,48h) supported LoadExtraBootCode: // First we have to load our extra boot code at // sector 1 into memory at [0000:7e00h] xor eax, eax inc eax mov cx, 1 xor bx, bx mov es, bx // Read sector to [0000:7e00h] mov bx, HEX(7e00) call ReadSectors jmp LoadRootDirectory // Reads logical sectors into ES:[BX] // EAX has logical sector number to read // CX has number of sectors to read ReadSectors: push es add eax, dword ptr BP_REL(ExtVolumeStartSector) // Add the start of the volume // If at all possible we want to use LBA routines because // they are optimized to read more than 1 sector per read ReadSectorsLBA: pushad // Save logical sector number & sector count cmp cx, 64 // Since the LBA calls only support 0x7F sectors at a time we will limit ourselves to 64 jbe ReadSectorsSetupDiskAddressPacket // If we are reading less than 65 sectors then just do the read mov cx, 64 // Otherwise read only 64 sectors on this loop iteration ReadSectorsSetupDiskAddressPacket: movzx ecx, cx mov dword ptr BP_REL(LBASectorsRead), ecx data32 push 0 push eax // Put 64-bit logical block address on stack push es // Put transfer segment on stack push bx // Put transfer offset on stack push cx // Set transfer count push 16 // Set size of packet to 10h mov si, sp // Setup disk address packet on stack mov dl, byte ptr BP_REL(BootDrive) // Drive number mov ah, HEX(42) // Int 13h, AH = 42h - Extended Read int HEX(13) // Call BIOS jc PrintDiskError // If the read failed then abort add sp, 16 // Remove disk address packet from stack popad // Restore sector count & logical sector number push bx mov ebx, dword ptr BP_REL(LBASectorsRead) add eax, ebx // Increment sector to read shl ebx, 5 mov dx, es add dx, bx // Setup read buffer for next sector mov es, dx pop bx sub cx, word ptr BP_REL(LBASectorsRead) jnz ReadSectorsLBA // Read next sector pop es ret // Displays a disk error message // And reboots PrintDiskError: mov si, offset msgDiskError // Bad boot disk message call PutChars // Display it Reboot: mov si, offset msgAnyKey // Press any key message call PutChars // Display it xor ax, ax int HEX(16) // Wait for a keypress int HEX(19) // Reboot .PutCharsLoop: mov ah, HEX(0E) mov bx, 7 int HEX(10) PutChars: lodsb or al, al jnz .PutCharsLoop ret SwapESWithDS: // Swap ES and DS push es push ds pop es pop ds ret ExtReadGroupDescriptor: mov eax, dword ptr BP_REL(ExtReadInodeGroupBlock) // Get Inode group block add eax, dword ptr BP_REL(ExtFirstDataBlock) // Add the Group Descriptor offset call ExtSetInodeSegment ExtReadBlock: xor edx, edx mov ecx, dword ptr BP_REL(ExtBlockSize) mul ecx jmp ReadSectors // EAX ExtCalculateBlock: xor edx, edx // Clear EDX before division div dword ptr BP_REL(ExtBlockSizeInBytes) // Inode /= ExtBlockSizeInBytes mov dword ptr ds:[bp + si], eax // Store the Inode block ret // SI, DI ExtCalculateOffset: add bx, bp // Sum BX with BP for absolute address xor edx, edx // Clear EDX before multiplication mov eax, dword ptr ds:[bp + si] // Get the Inode block mul dword ptr BP_REL(ExtBlockSizeInBytes) // Inode *= ExtBlockSizeInBytes mov ecx, dword ptr ds:[bx] // Get the Inode sub ecx, eax // Subtract the original Inode with rounded down Inode mov word ptr ds:[bp + di], cx // Store the rounded down Inode ret ExtSetOldFileSegment: mov ebx, dword ptr BP_REL(ExtFileAddressOld) // Get the EXT old file address jmp .ExtSegSkip ExtSetFileSegment: mov ebx, dword ptr BP_REL(ExtFileAddress) // Get the EXT file address .ExtSegSkip: shr ebx, 4 // Shift four bits to the right to get segment jmp .ExtSkip ExtSetInodeSegment: mov bx, EXT_INODE_ADDRESS / 16 // Get the EXT inode address .ExtSkip: mov es, bx // Set ES xor bx, bx // Clear BX ret // Read the Inode in EAX register ExtReadInode: xor edx, edx // Clear EDX before division dec eax // Inode-- div dword ptr BP_REL(ExtInodesPerGroup) // Inode /= ExtInodesPerGroup mov dword ptr BP_REL(ExtReadInodeGroup), eax // Store the Inode group mov dword ptr BP_REL(ExtReadInodeIndex), edx // Store the Inode index xor edx, edx // Clear EDX before multiplication mul dword ptr BP_REL(ExtGroupDescSize) // Inode group *= ExtGroupDescSize mov dword ptr BP_REL(ExtReadInodeGroup), eax // Store the precalculated Inode group xor edx, edx // Clear EDX before multiplication mov eax, dword ptr BP_REL(ExtReadInodeIndex) // Get the read Inode index mul dword ptr BP_REL(ExtInodeSize) // Inode group *= ExtInodeSize mov dword ptr BP_REL(ExtReadInodeIndex), eax // Store the Inode index // Calculate the Inode index block mov si, offset ExtReadInodeIndexBlock - start call ExtCalculateBlock // Calculate the Inode group block mov eax, dword ptr BP_REL(ExtReadInodeGroup) mov si, offset ExtReadInodeGroupBlock - start call ExtCalculateBlock // Calculate the Inode group offset mov bx, offset ExtReadInodeGroup - start mov si, offset ExtReadInodeGroupBlock - start mov di, offset ExtReadInodeGroupOffset - start call ExtCalculateOffset // Calculate the Inode index offset mov bx, offset ExtReadInodeIndex - start mov si, offset ExtReadInodeIndexBlock - start mov di, offset ExtReadInodeIndexOffset - start call ExtCalculateOffset // Read group descriptor call ExtReadGroupDescriptor // Set the offset address mov si, word ptr BP_REL(ExtReadInodeGroupOffset) // Get InodeTable field from the ExtGroupDescriptor structure mov eax, dword ptr es:[si + EXT_GROUP_DESC_INODE_TABLE_OFFSET] // Sum EAX with Inode index block add eax, dword ptr BP_REL(ExtReadInodeIndexBlock) jmp ExtReadBlock msgDiskError: .ascii "Disk error", CR, LF, NUL msgAnyKey: .ascii "Press any key", CR, LF, NUL .org 509 BootPartition: .byte 0 .word HEX(AA55) // BootSector signature // End of bootsector // // Now starts the extra boot code that we will store // at sector 1 on a EXT volume LoadRootDirectory: mov al, EXT_ROOT_INODE // Put the root directory inode number in AL movzx eax, al // Convert AL to EAX call ExtReadInode // Read the inode call BasicReadFile // Load the directory entries using basic function call SearchFile // Find the extended loader and run it jmp ExtLdrPrintFileNotFound // If the extended loader wasn't found, display an error ExtInodeDetectExtentsFlag: mov eax, es:[si + EXT_INODE_FLAGS_OFFSET] test eax, EXT_INODE_FLAG_EXTENTS ret ExtUpdateFileSize: mov eax, dword ptr BP_REL(ExtBlockSizeInBytes) ExtAdjustFileSize: // Update the file size sub dword ptr BP_REL(ExtFileSizeState), eax add dword ptr BP_REL(ExtFileAddress), eax ret ExtReadFileDone: push eax mov eax, dword ptr BP_REL(ExtFileSizeState) cmp eax, dword ptr BP_REL(ExtBlockSizeInBytes) pop eax ret ExtFileReadBlocks: push es .FRLoop: // Check if there is no more blocks to read call ExtReadFileDone jb .FRDone // If the block count is zero then do nothing test bx, bx jz .FRDone // Read the block pushad call ExtSetFileSegment call ExtReadBlock popad // Update the file size call ExtUpdateFileSize // Go to the next block and decrement the block count inc eax dec bx // Loop until all blocks are read jmp .FRLoop .FRDone: pop es ret BasicReadFileExtents: // Add block pointer offset add si, EXT_INODE_BLOCK_POINTER_OFFSET .DepthExtentsLoop: // Load extent header depth mov dx, word ptr es:[si + EXT_EXTENT_HEADER_DEPTH_OFFSET] // Check if depth is zero test dx, dx jz .DepthExtentsDone // Go to next extent add si, EXT_EXTENT_SIZE // Push all registers pushad // Read the extent block mov eax, dword ptr es:[si + EXT_EXTENT_INDEX_LEAF_OFFSET] call ExtSetInodeSegment call ExtReadBlock // Pop all registers popad // Reset SI xor si, si jmp .DepthExtentsLoop .DepthExtentsDone: // Load extent header entries mov cx, word ptr es:[si + EXT_EXTENT_HEADER_ENTRIES_OFFSET] .FinalExtentsLoop: // Check if there is no more blocks to read call ExtReadFileDone jb .FinalExtentsDone // Go to next extent add si, EXT_EXTENT_SIZE // Load extent length mov bx, word ptr es:[si + EXT_EXTENT_LENGTH_OFFSET] and ebx, HEX(FFFF) // Check if extent is sparse cmp bx, EXT_EXTENT_MAX_LENGTH jbe .NotSparse // Adjust sparse extent length sub bx, EXT_EXTENT_MAX_LENGTH // Adjust extent length to byte count // by multiplying extent length to block size xor edx, edx mov eax, dword ptr BP_REL(ExtBlockSizeInBytes) mul ebx // Adjust file size for sparse extent call ExtAdjustFileSize jmp .FinalExtentsSkip .NotSparse: // Read blocks from extent start mov eax, dword ptr es:[si + EXT_EXTENT_START_OFFSET] call ExtFileReadBlocks .FinalExtentsSkip: // Loop to process next extent loop .FinalExtentsLoop .FinalExtentsDone: ret BasicReadFile: push es pushad call ExtSetInodeSegment // Set the correct Inode offset mov si, word ptr BP_REL(ExtReadInodeIndexOffset) // Set the old file address mov eax, dword ptr BP_REL(ExtFileAddress) mov dword ptr BP_REL(ExtFileAddressOld), eax // Set the file size limit mov eax, EXT_INODE_DATA_SIZE_LIMIT // Load file size from Inode mov ebx, dword ptr es:[si + EXT_INODE_SIZE_OFFSET] // Compare and limit file size cmp ebx, eax jbe .BelowOrEqualSize mov ebx, eax .BelowOrEqualSize: // Store the file size in the ExtFileSize variable mov dword ptr BP_REL(ExtFileSize), ebx // Set rounded up file size add ebx, dword ptr BP_REL(ExtBlockSizeInBytes) dec ebx mov dword ptr BP_REL(ExtFileSizeState), ebx // Don't use the extents method if theres no extents flag call ExtInodeDetectExtentsFlag jz .NoExtents // If this Inode use Extents mapping then use the extents method and skip the entire classic method call BasicReadFileExtents jmp .LDone .NoExtents: // Set up for reading direct block addresses xor ecx, ecx mov cl, EXT_INODE_BLOCKS add si, EXT_INODE_BLOCK_POINTER_OFFSET .LLoop: call ExtSetInodeSegment call ExtReadFileDone jb .LDone // Get the block address mov eax, dword ptr es:[si] // If the block address is zero, skip the block test eax, eax jz .LSkipBlock // Set the file segment call ExtSetFileSegment // Read the block call ExtReadBlock .LSkipBlock: call ExtUpdateFileSize // Increment block add si, EXT_POINTER_SIZE // Loop until all blocks are loaded loop .LLoop .LDone: popad pop es ret SearchFile: call ExtSetOldFileSegment call SwapESWithDS xor si, si mov dx, word ptr BP_REL(ExtFileSize) .FLoop: mov eax, dword ptr ds:[si] // Load directory Inode cmp si, dx // End of buffer reached? jae .Done // Abort the search if yes // Save SI push si test eax, eax // Check if Inode is zero jz .Skip // Skip this entry if yes mov di, offset ExtLdrFileName // Load target filename address mov cx, offset ExtLdrFileNameEnd - ExtLdrFileName // Length of filename to compare cmp byte ptr ds:[si + EXT_DIRECTORY_ENTRY_NAME_LENGTH_OFFSET], cl // Compare if both names have the same length jnz .Skip // Skip this entry if not add si, EXT_DIRECTORY_ENTRY_NAME_OFFSET // Move to filename in entry repe cmpsb // Compare filenames pop si // Restore SI jz LoadExtLdr // Found matching file push si // Save SI .Skip: // Restore SI pop si // Move to next directory entry and continue looping add si, word ptr ds:[si + EXT_DIRECTORY_ENTRY_SIZE_OFFSET] jmp .FLoop .Done: ret LoadExtLdr: // Swap ES and DS call SwapESWithDS push si // Save SI mov si, offset msgLoadingExtLdr // Point SI to a loading message call PutChars // Show the message pop si // Restore SI mov eax, dword ptr es:[si] // Load directory Inode call ExtReadInode // Read the inode mov si, word ptr BP_REL(ExtReadInodeIndexOffset) // Set the correct offset // Get Inode type mov ax, word ptr es:[si] and ax, EXT_INODE_TYPE_MASK cmp ax, EXT_INODE_TYPE_REGULAR // Check if regular file jnz ExtLdrPrintRegFileError // If not, handle error call BasicReadFile // Load the file using basic function call ExtSetOldFileSegment // Set old file segment call SwapESWithDS // Swap ES with DS before copy // Copy the loaded file to 1KB ahead of this bootsector xor si, si mov di, offset ExtLdrEntryPoint mov cx, EXTLDR_BOOTSECTOR_SIZE rep movsb ljmp16 0, ExtLdrEntryPoint ExtLdrPrintFileNotFound: // Make DS correct, display it and reboot call SwapESWithDS mov si, offset msgExtLdr jmp DisplayItAndReboot ExtLdrPrintRegFileError: mov si, offset msgExtLdrNotRegularFile // ExtLdr not found message DisplayItAndReboot: call PutChars // Display it jmp Reboot ExtLdrFileName: .ascii "extldr.sys" ExtLdrFileNameEnd: msgExtLdr: .ascii "extldr.sys not found", CR, LF, NUL msgExtLdrNotRegularFile: .ascii "extldr.sys is not a regular file", CR, LF, NUL msgLoadingExtLdr: .ascii "Loading ExtLoader...", CR, LF, NUL .org 1022 .word HEX(AA55) // BootSector signature ExtLdrEntryPoint: // ExtLdr is loaded here .endcode16 END #else #define start BOOTSECTOR_BASE_ADDRESS #define BootDrive (start + 3) #define ExtVolumeStartSector (BootDrive + 1) #define ExtBlockSize (ExtVolumeStartSector + 4) #define ExtBlockSizeInBytes (ExtBlockSize + 4) #define ExtPointersPerBlock (ExtBlockSizeInBytes + 4) #define ExtGroupDescSize (ExtPointersPerBlock + 4) #define ExtFirstDataBlock (ExtGroupDescSize + 4) #define ExtInodeSize (ExtFirstDataBlock + 4) #define ExtInodesPerGroup (ExtInodeSize + 4) #define ExtFileSize (ExtInodesPerGroup + 4) #define ExtFileAddress (ExtFileSize + 4) #define ExtFileAddressOld (ExtFileAddress + 4) #define ExtReadInodeGroup (ExtFileAddressOld + 4) #define ExtReadInodeIndex (ExtReadInodeGroup + 4) #define ExtReadInodeGroupBlock (ExtReadInodeIndex + 4) #define ExtReadInodeIndexBlock (ExtReadInodeGroupBlock + 4) #define ExtReadInodeGroupOffset (ExtReadInodeIndexBlock + 4) #define ExtReadInodeIndexOffset (ExtReadInodeGroupOffset + 2) #define BootPartition (BootDrive + 506) #define ExtReadBlock word ptr ss:[bp-ExtReadBlockOffset] #define ExtReadInode word ptr ss:[bp-ExtReadInodeOffset] #define DisplayItAndReboot word ptr ss:[bp-DisplayItAndRebootOffset] #define PutChars word ptr ss:[bp-PutCharsOffset] #endif