| ; -*- fundamental -*- (asm-mode sucks) |
| ; **************************************************************************** |
| ; |
| ; extlinux.asm |
| ; |
| ; A program to boot Linux kernels off an ext2/ext3 filesystem. |
| ; |
| ; Copyright 1994-2008 H. Peter Anvin - All Rights Reserved |
| ; |
| ; This program is free software; you can redistribute it and/or modify |
| ; it under the terms of the GNU General Public License as published by |
| ; the Free Software Foundation, Inc., 53 Temple Place Ste 330, |
| ; Boston MA 02111-1307, USA; either version 2 of the License, or |
| ; (at your option) any later version; incorporated herein by reference. |
| ; |
| ; **************************************************************************** |
| |
| %define IS_EXTLINUX 1 |
| %include "head.inc" |
| %include "ext2_fs.inc" |
| |
| ; |
| ; Some semi-configurable constants... change on your own risk. |
| ; |
| my_id equ extlinux_id |
| ; NASM 0.98.38 croaks if these are equ's rather than macros... |
| FILENAME_MAX_LG2 equ 8 ; log2(Max filename size Including final null) |
| FILENAME_MAX equ (1 << FILENAME_MAX_LG2) ; Max mangled filename size |
| NULLFILE equ 0 ; Null character == empty filename |
| NULLOFFSET equ 0 ; Position in which to look |
| retry_count equ 16 ; How patient are we with the disk? |
| %assign HIGHMEM_SLOP 0 ; Avoid this much memory near the top |
| LDLINUX_MAGIC equ 0x3eb202fe ; A random number to identify ourselves with |
| |
| MAX_OPEN_LG2 equ 6 ; log2(Max number of open files) |
| MAX_OPEN equ (1 << MAX_OPEN_LG2) |
| |
| SECTOR_SHIFT equ 9 |
| SECTOR_SIZE equ (1 << SECTOR_SHIFT) |
| |
| MAX_SYMLINKS equ 64 ; Maximum number of symlinks per lookup |
| SYMLINK_SECTORS equ 2 ; Max number of sectors in a symlink |
| ; (should be >= FILENAME_MAX) |
| |
| ; |
| ; This is what we need to do when idle |
| ; |
| %macro RESET_IDLE 0 |
| ; Nothing |
| %endmacro |
| %macro DO_IDLE 0 |
| ; Nothing |
| %endmacro |
| |
| ; |
| ; The following structure is used for "virtual kernels"; i.e. LILO-style |
| ; option labels. The options we permit here are `kernel' and `append |
| ; Since there is no room in the bottom 64K for all of these, we |
| ; stick them in high memory and copy them down before we need them. |
| ; |
| struc vkernel |
| vk_vname: resb FILENAME_MAX ; Virtual name **MUST BE FIRST!** |
| vk_rname: resb FILENAME_MAX ; Real name |
| vk_appendlen: resw 1 |
| vk_type: resb 1 ; Type of file |
| alignb 4 |
| vk_append: resb max_cmd_len+1 ; Command line |
| alignb 4 |
| vk_end: equ $ ; Should be <= vk_size |
| endstruc |
| |
| ; |
| ; Segment assignments in the bottom 640K |
| ; Stick to the low 512K in case we're using something like M-systems flash |
| ; which load a driver into low RAM (evil!!) |
| ; |
| ; 0000h - main code/data segment (and BIOS segment) |
| ; |
| real_mode_seg equ 3000h |
| cache_seg equ 2000h ; 64K area for metadata cache |
| xfer_buf_seg equ 1000h ; Bounce buffer for I/O to high mem |
| comboot_seg equ real_mode_seg ; COMBOOT image loading zone |
| |
| ; |
| ; File structure. This holds the information for each currently open file. |
| ; |
| struc open_file_t |
| file_left resd 1 ; Number of sectors left (0 = free) |
| file_sector resd 1 ; Next linear sector to read |
| file_in_sec resd 1 ; Sector where inode lives |
| file_in_off resw 1 |
| file_mode resw 1 |
| endstruc |
| |
| %ifndef DEPEND |
| %if (open_file_t_size & (open_file_t_size-1)) |
| %error "open_file_t is not a power of 2" |
| %endif |
| %endif |
| |
| ; --------------------------------------------------------------------------- |
| ; BEGIN CODE |
| ; --------------------------------------------------------------------------- |
| |
| ; |
| ; Memory below this point is reserved for the BIOS and the MBR |
| ; |
| section .earlybss |
| trackbufsize equ 8192 |
| trackbuf resb trackbufsize ; Track buffer goes here |
| ; ends at 2800h |
| |
| section .bss |
| SuperBlock resb 1024 ; ext2 superblock |
| SuperInfo resq 16 ; DOS superblock expanded |
| ClustSize resd 1 ; Bytes/cluster ("block") |
| SecPerClust resd 1 ; Sectors/cluster |
| ClustMask resd 1 ; Sectors/cluster - 1 |
| PtrsPerBlock1 resd 1 ; Pointers/cluster |
| PtrsPerBlock2 resd 1 ; (Pointers/cluster)^2 |
| DriveNumber resb 1 ; BIOS drive number |
| ClustShift resb 1 ; Shift count for sectors/cluster |
| ClustByteShift resb 1 ; Shift count for bytes/cluster |
| |
| alignb open_file_t_size |
| Files resb MAX_OPEN*open_file_t_size |
| |
| section .text |
| ; |
| ; Some of the things that have to be saved very early are saved |
| ; "close" to the initial stack pointer offset, in order to |
| ; reduce the code size... |
| ; |
| StackBuf equ $-44-32 ; Start the stack here (grow down - 4K) |
| PartInfo equ StackBuf ; Saved partition table entry |
| FloppyTable equ PartInfo+16 ; Floppy info table (must follow PartInfo) |
| OrigFDCTabPtr equ StackBuf-8 ; The 2nd high dword on the stack |
| OrigESDI equ StackBuf-4 ; The high dword on the stack |
| |
| ; |
| ; Primary entry point. Tempting as though it may be, we can't put the |
| ; initial "cli" here; the jmp opcode in the first byte is part of the |
| ; "magic number" (using the term very loosely) for the DOS superblock. |
| ; |
| bootsec equ $ |
| jmp short start ; 2 bytes |
| nop ; 1 byte |
| ; |
| ; "Superblock" follows -- it's in the boot sector, so it's already |
| ; loaded and ready for us |
| ; |
| bsOemName db 'EXTLINUX' ; The SYS command sets this, so... |
| ; |
| ; These are the fields we actually care about. We end up expanding them |
| ; all to dword size early in the code, so generate labels for both |
| ; the expanded and unexpanded versions. |
| ; |
| %macro superb 1 |
| bx %+ %1 equ SuperInfo+($-superblock)*8+4 |
| bs %+ %1 equ $ |
| zb 1 |
| %endmacro |
| %macro superw 1 |
| bx %+ %1 equ SuperInfo+($-superblock)*8 |
| bs %+ %1 equ $ |
| zw 1 |
| %endmacro |
| %macro superd 1 |
| bx %+ %1 equ $ ; no expansion for dwords |
| bs %+ %1 equ $ |
| zd 1 |
| %endmacro |
| superblock equ $ |
| superw BytesPerSec |
| superb SecPerClust |
| superw ResSectors |
| superb FATs |
| superw RootDirEnts |
| superw Sectors |
| superb Media |
| superw FATsecs |
| superw SecPerTrack |
| superw Heads |
| superinfo_size equ ($-superblock)-1 ; How much to expand |
| superd Hidden |
| superd HugeSectors |
| ; |
| ; This is as far as FAT12/16 and FAT32 are consistent |
| ; |
| zb 54 ; FAT12/16 need 26 more bytes, |
| ; FAT32 need 54 more bytes |
| superblock_len equ $-superblock |
| |
| ; |
| ; Note we don't check the constraints above now; we did that at install |
| ; time (we hope!) |
| ; |
| start: |
| cli ; No interrupts yet, please |
| cld ; Copy upwards |
| ; |
| ; Set up the stack |
| ; |
| xor ax,ax |
| mov ss,ax |
| mov sp,StackBuf ; Just below BSS |
| push es ; Save initial ES:DI -> $PnP pointer |
| push di |
| mov es,ax |
| ; |
| ; DS:SI may contain a partition table entry. Preserve it for us. |
| ; |
| mov cx,8 ; Save partition info |
| mov di,PartInfo |
| rep movsw |
| |
| mov ds,ax ; Now we can initialize DS... |
| |
| ; |
| ; Now sautee the BIOS floppy info block to that it will support decent- |
| ; size transfers; the floppy block is 11 bytes and is stored in the |
| ; INT 1Eh vector (brilliant waste of resources, eh?) |
| ; |
| ; Of course, if BIOSes had been properly programmed, we wouldn't have |
| ; had to waste precious space with this code. |
| ; |
| mov bx,fdctab |
| lfs si,[bx] ; FS:SI -> original fdctab |
| push fs ; Save on stack in case we need to bail |
| push si |
| |
| ; Save the old fdctab even if hard disk so the stack layout |
| ; is the same. The instructions above do not change the flags |
| mov [DriveNumber],dl ; Save drive number in DL |
| and dl,dl ; If floppy disk (00-7F), assume no |
| ; partition table |
| js harddisk |
| |
| floppy: |
| mov cl,6 ; 12 bytes (CX == 0) |
| ; es:di -> FloppyTable already |
| ; This should be safe to do now, interrupts are off... |
| mov [bx],di ; FloppyTable |
| mov [bx+2],ax ; Segment 0 |
| fs rep movsw ; Faster to move words |
| mov cl,[bsSecPerTrack] ; Patch the sector count |
| mov [di-8],cl |
| ; AX == 0 here |
| int 13h ; Some BIOSes need this |
| |
| jmp short not_harddisk |
| ; |
| ; The drive number and possibly partition information was passed to us |
| ; by the BIOS or previous boot loader (MBR). Current "best practice" is to |
| ; trust that rather than what the superblock contains. |
| ; |
| ; Would it be better to zero out bsHidden if we don't have a partition table? |
| ; |
| ; Note: di points to beyond the end of PartInfo |
| ; |
| harddisk: |
| test byte [di-16],7Fh ; Sanity check: "active flag" should |
| jnz no_partition ; be 00 or 80 |
| mov eax,[di-8] ; Partition offset (dword) |
| mov [bsHidden],eax |
| no_partition: |
| ; |
| ; Get disk drive parameters (don't trust the superblock.) Don't do this for |
| ; floppy drives -- INT 13:08 on floppy drives will (may?) return info about |
| ; what the *drive* supports, not about the *media*. Fortunately floppy disks |
| ; tend to have a fixed, well-defined geometry which is stored in the superblock. |
| ; |
| ; DL == drive # still |
| mov ah,08h |
| int 13h |
| jc no_driveparm |
| and ah,ah |
| jnz no_driveparm |
| shr dx,8 |
| inc dx ; Contains # of heads - 1 |
| mov [bsHeads],dx |
| and cx,3fh |
| mov [bsSecPerTrack],cx |
| no_driveparm: |
| not_harddisk: |
| ; |
| ; Ready to enable interrupts, captain |
| ; |
| sti |
| |
| ; |
| ; Do we have EBIOS (EDD)? |
| ; |
| eddcheck: |
| mov bx,55AAh |
| mov ah,41h ; EDD existence query |
| mov dl,[DriveNumber] |
| int 13h |
| jc .noedd |
| cmp bx,0AA55h |
| jne .noedd |
| test cl,1 ; Extended disk access functionality set |
| jz .noedd |
| ; |
| ; We have EDD support... |
| ; |
| mov byte [getlinsec.jmp+1],(getlinsec_ebios-(getlinsec.jmp+2)) |
| .noedd: |
| |
| ; |
| ; Load the first sector of LDLINUX.SYS; this used to be all proper |
| ; with parsing the superblock and root directory; it doesn't fit |
| ; together with EBIOS support, unfortunately. |
| ; |
| mov eax,[FirstSector] ; Sector start |
| mov bx,ldlinux_sys ; Where to load it |
| call getonesec |
| |
| ; Some modicum of integrity checking |
| cmp dword [ldlinux_magic+4],LDLINUX_MAGIC^HEXDATE |
| jne kaboom |
| |
| ; Go for it... |
| jmp ldlinux_ent |
| |
| ; |
| ; getonesec: get one disk sector |
| ; |
| getonesec: |
| mov bp,1 ; One sector |
| ; Fall through |
| |
| ; |
| ; getlinsec: load a sequence of BP floppy sector given by the linear sector |
| ; number in EAX into the buffer at ES:BX. We try to optimize |
| ; by loading up to a whole track at a time, but the user |
| ; is responsible for not crossing a 64K boundary. |
| ; (Yes, BP is weird for a count, but it was available...) |
| ; |
| ; On return, BX points to the first byte after the transferred |
| ; block. |
| ; |
| ; This routine assumes CS == DS, and trashes most registers. |
| ; |
| ; Stylistic note: use "xchg" instead of "mov" when the source is a register |
| ; that is dead from that point; this saves space. However, please keep |
| ; the order to dst,src to keep things sane. |
| ; |
| getlinsec: |
| add eax,[bsHidden] ; Add partition offset |
| xor edx,edx ; Zero-extend LBA (eventually allow 64 bits) |
| |
| .jmp: jmp strict short getlinsec_cbios |
| |
| ; |
| ; getlinsec_ebios: |
| ; |
| ; getlinsec implementation for EBIOS (EDD) |
| ; |
| getlinsec_ebios: |
| .loop: |
| push bp ; Sectors left |
| .retry2: |
| call maxtrans ; Enforce maximum transfer size |
| movzx edi,bp ; Sectors we are about to read |
| mov cx,retry_count |
| .retry: |
| |
| ; Form DAPA on stack |
| push edx |
| push eax |
| push es |
| push bx |
| push di |
| push word 16 |
| mov si,sp |
| pushad |
| mov dl,[DriveNumber] |
| push ds |
| push ss |
| pop ds ; DS <- SS |
| mov ah,42h ; Extended Read |
| int 13h |
| pop ds |
| popad |
| lea sp,[si+16] ; Remove DAPA |
| jc .error |
| pop bp |
| add eax,edi ; Advance sector pointer |
| sub bp,di ; Sectors left |
| shl di,SECTOR_SHIFT ; 512-byte sectors |
| add bx,di ; Advance buffer pointer |
| and bp,bp |
| jnz .loop |
| |
| ret |
| |
| .error: |
| ; Some systems seem to get "stuck" in an error state when |
| ; using EBIOS. Doesn't happen when using CBIOS, which is |
| ; good, since some other systems get timeout failures |
| ; waiting for the floppy disk to spin up. |
| |
| pushad ; Try resetting the device |
| xor ax,ax |
| mov dl,[DriveNumber] |
| int 13h |
| popad |
| loop .retry ; CX-- and jump if not zero |
| |
| ;shr word [MaxTransfer],1 ; Reduce the transfer size |
| ;jnz .retry2 |
| |
| ; Total failure. Try falling back to CBIOS. |
| mov byte [getlinsec.jmp+1],(getlinsec_cbios-(getlinsec.jmp+2)) |
| ;mov byte [MaxTransfer],63 ; Max possibe CBIOS transfer |
| |
| pop bp |
| ; ... fall through ... |
| |
| ; |
| ; getlinsec_cbios: |
| ; |
| ; getlinsec implementation for legacy CBIOS |
| ; |
| getlinsec_cbios: |
| .loop: |
| push edx |
| push eax |
| push bp |
| push bx |
| |
| movzx esi,word [bsSecPerTrack] |
| movzx edi,word [bsHeads] |
| ; |
| ; Dividing by sectors to get (track,sector): we may have |
| ; up to 2^18 tracks, so we need to use 32-bit arithmetric. |
| ; |
| div esi |
| xor cx,cx |
| xchg cx,dx ; CX <- sector index (0-based) |
| ; EDX <- 0 |
| ; eax = track # |
| div edi ; Convert track to head/cyl |
| |
| ; We should test this, but it doesn't fit... |
| ; cmp eax,1023 |
| ; ja .error |
| |
| ; |
| ; Now we have AX = cyl, DX = head, CX = sector (0-based), |
| ; BP = sectors to transfer, SI = bsSecPerTrack, |
| ; ES:BX = data target |
| ; |
| |
| call maxtrans ; Enforce maximum transfer size |
| |
| ; Must not cross track boundaries, so BP <= SI-CX |
| sub si,cx |
| cmp bp,si |
| jna .bp_ok |
| mov bp,si |
| .bp_ok: |
| |
| shl ah,6 ; Because IBM was STOOPID |
| ; and thought 8 bits were enough |
| ; then thought 10 bits were enough... |
| inc cx ; Sector numbers are 1-based, sigh |
| or cl,ah |
| mov ch,al |
| mov dh,dl |
| mov dl,[DriveNumber] |
| xchg ax,bp ; Sector to transfer count |
| mov ah,02h ; Read sectors |
| mov bp,retry_count |
| .retry: |
| pushad |
| int 13h |
| popad |
| jc .error |
| .resume: |
| movzx ecx,al ; ECX <- sectors transferred |
| shl ax,SECTOR_SHIFT ; Convert sectors in AL to bytes in AX |
| pop bx |
| add bx,ax |
| pop bp |
| pop eax |
| pop edx |
| add eax,ecx |
| sub bp,cx |
| jnz .loop |
| ret |
| |
| .error: |
| dec bp |
| jnz .retry |
| |
| xchg ax,bp ; Sectors transferred <- 0 |
| shr word [MaxTransfer],1 |
| jnz .resume |
| ; Fall through to disk_error |
| |
| ; |
| ; kaboom: write a message and bail out. |
| ; |
| disk_error: |
| kaboom: |
| xor si,si |
| mov ss,si |
| mov sp,StackBuf-4 ; Reset stack |
| mov ds,si ; Reset data segment |
| pop dword [fdctab] ; Restore FDC table |
| .patch: ; When we have full code, intercept here |
| mov si,bailmsg |
| |
| ; Write error message, this assumes screen page 0 |
| .loop: lodsb |
| and al,al |
| jz .done |
| mov ah,0Eh ; Write to screen as TTY |
| mov bx,0007h ; Attribute |
| int 10h |
| jmp short .loop |
| .done: |
| cbw ; AH <- 0 |
| .again: int 16h ; Wait for keypress |
| ; NB: replaced by int 18h if |
| ; chosen at install time.. |
| int 19h ; And try once more to boot... |
| .norge: jmp short .norge ; If int 19h returned; this is the end |
| |
| ; |
| ; Truncate BP to MaxTransfer |
| ; |
| maxtrans: |
| cmp bp,[MaxTransfer] |
| jna .ok |
| mov bp,[MaxTransfer] |
| .ok: ret |
| |
| ; |
| ; Error message on failure |
| ; |
| bailmsg: db 'Boot error', 0Dh, 0Ah, 0 |
| |
| ; This fails if the boot sector overflows |
| zb 1F8h-($-$$) |
| |
| FirstSector dd 0xDEADBEEF ; Location of sector 1 |
| MaxTransfer dw 0x007F ; Max transfer size |
| |
| ; This field will be filled in 0xAA55 by the installer, but we abuse it |
| ; to house a pointer to the INT 16h instruction at |
| ; kaboom.again, which gets patched to INT 18h in RAID mode. |
| bootsignature dw kaboom.again-bootsec |
| |
| ; |
| ; =========================================================================== |
| ; End of boot sector |
| ; =========================================================================== |
| ; Start of LDLINUX.SYS |
| ; =========================================================================== |
| |
| ldlinux_sys: |
| |
| syslinux_banner db 0Dh, 0Ah |
| db 'EXTLINUX ' |
| db version_str, ' ', date, ' ', 0 |
| db 0Dh, 0Ah, 1Ah ; EOF if we "type" this in DOS |
| |
| align 8, db 0 |
| ldlinux_magic dd LDLINUX_MAGIC |
| dd LDLINUX_MAGIC^HEXDATE |
| |
| ; |
| ; This area is patched by the installer. It is found by looking for |
| ; LDLINUX_MAGIC, plus 8 bytes. |
| ; |
| patch_area: |
| LDLDwords dw 0 ; Total dwords starting at ldlinux_sys, |
| ; not including ADVs |
| LDLSectors dw 0 ; Number of sectors, not including |
| ; bootsec & this sec, but including the two ADVs |
| CheckSum dd 0 ; Checksum starting at ldlinux_sys |
| ; value = LDLINUX_MAGIC - [sum of dwords] |
| CurrentDir dd 2 ; "Current" directory inode number |
| |
| ; Space for up to 64 sectors, the theoretical maximum |
| SectorPtrs times 64 dd 0 |
| |
| ldlinux_ent: |
| ; |
| ; Note that some BIOSes are buggy and run the boot sector at 07C0:0000 |
| ; instead of 0000:7C00 and the like. We don't want to add anything |
| ; more to the boot sector, so it is written to not assume a fixed |
| ; value in CS, but we don't want to deal with that anymore from now |
| ; on. |
| ; |
| jmp 0:.next |
| .next: |
| |
| ; |
| ; Tell the user we got this far |
| ; |
| mov si,syslinux_banner |
| call writestr |
| |
| ; |
| ; Tell the user if we're using EBIOS or CBIOS |
| ; |
| print_bios: |
| mov si,cbios_name |
| cmp byte [getlinsec.jmp+1],(getlinsec_ebios-(getlinsec.jmp+2)) |
| jne .cbios |
| mov si,ebios_name |
| .cbios: |
| mov [BIOSName],si |
| call writestr |
| |
| section .bss |
| %define HAVE_BIOSNAME 1 |
| BIOSName resw 1 |
| |
| section .text |
| ; |
| ; Now we read the rest of LDLINUX.SYS. Don't bother loading the first |
| ; sector again, though. |
| ; |
| load_rest: |
| mov si,SectorPtrs |
| mov bx,7C00h+2*SECTOR_SIZE ; Where we start loading |
| mov cx,[LDLSectors] |
| |
| .get_chunk: |
| jcxz .done |
| xor bp,bp |
| lodsd ; First sector of this chunk |
| |
| mov edx,eax |
| |
| .make_chunk: |
| inc bp |
| dec cx |
| jz .chunk_ready |
| inc edx ; Next linear sector |
| cmp [si],edx ; Does it match |
| jnz .chunk_ready ; If not, this is it |
| add si,4 ; If so, add sector to chunk |
| jmp short .make_chunk |
| |
| .chunk_ready: |
| call getlinsecsr |
| shl bp,SECTOR_SHIFT |
| add bx,bp |
| jmp .get_chunk |
| |
| .done: |
| |
| ; |
| ; All loaded up, verify that we got what we needed. |
| ; Note: the checksum field is embedded in the checksum region, so |
| ; by the time we get to the end it should all cancel out. |
| ; |
| verify_checksum: |
| mov si,ldlinux_sys |
| mov cx,[LDLDwords] |
| mov edx,-LDLINUX_MAGIC |
| .checksum: |
| lodsd |
| add edx,eax |
| loop .checksum |
| |
| and edx,edx ; Should be zero |
| jz all_read ; We're cool, go for it! |
| |
| ; |
| ; Uh-oh, something went bad... |
| ; |
| mov si,checksumerr_msg |
| call writestr |
| jmp kaboom |
| |
| ; |
| ; ----------------------------------------------------------------------------- |
| ; Subroutines that have to be in the first sector |
| ; ----------------------------------------------------------------------------- |
| |
| ; |
| ; |
| ; writestr: write a null-terminated string to the console |
| ; This assumes we're on page 0. This is only used for early |
| ; messages, so it should be OK. |
| ; |
| writestr: |
| .loop: lodsb |
| and al,al |
| jz .return |
| mov ah,0Eh ; Write to screen as TTY |
| mov bx,0007h ; Attribute |
| int 10h |
| jmp short .loop |
| .return: ret |
| |
| |
| ; getlinsecsr: save registers, call getlinsec, restore registers |
| ; |
| getlinsecsr: pushad |
| call getlinsec |
| popad |
| ret |
| |
| ; |
| ; Checksum error message |
| ; |
| checksumerr_msg db ' Load error - ', 0 ; Boot failed appended |
| |
| ; |
| ; BIOS type string |
| ; |
| cbios_name db 'CBIOS', 0 |
| ebios_name db 'EBIOS', 0 |
| |
| ; |
| ; Debug routine |
| ; |
| %ifdef debug |
| safedumpregs: |
| cmp word [Debug_Magic],0D00Dh |
| jnz nc_return |
| jmp dumpregs |
| %endif |
| |
| rl_checkpt equ $ ; Must be <= 8000h |
| |
| rl_checkpt_off equ ($-$$) |
| %ifndef DEPEND |
| %if rl_checkpt_off > 400h |
| %error "Sector 1 overflow" |
| %endif |
| %endif |
| |
| ; ---------------------------------------------------------------------------- |
| ; End of code and data that have to be in the first sector |
| ; ---------------------------------------------------------------------------- |
| |
| all_read: |
| ; |
| ; Let the user (and programmer!) know we got this far. This used to be |
| ; in Sector 1, but makes a lot more sense here. |
| ; |
| mov si,copyright_str |
| call writestr |
| |
| ; |
| ; Insane hack to expand the DOS superblock to dwords |
| ; |
| expand_super: |
| xor eax,eax |
| mov si,superblock |
| mov di,SuperInfo |
| mov cx,superinfo_size |
| .loop: |
| lodsw |
| dec si |
| stosd ; Store expanded word |
| xor ah,ah |
| stosd ; Store expanded byte |
| loop .loop |
| |
| ; |
| ; Load the real (ext2) superblock; 1024 bytes long at offset 1024 |
| ; |
| mov bx,SuperBlock |
| mov eax,1024 >> SECTOR_SHIFT |
| mov bp,ax |
| call getlinsec |
| |
| ; |
| ; Compute some values... |
| ; |
| xor edx,edx |
| inc edx |
| |
| ; s_log_block_size = log2(blocksize) - 10 |
| mov cl,[SuperBlock+s_log_block_size] |
| add cl,10 |
| mov [ClustByteShift],cl |
| mov eax,edx |
| shl eax,cl |
| mov [ClustSize],eax |
| |
| sub cl,SECTOR_SHIFT |
| mov [ClustShift],cl |
| shr eax,SECTOR_SHIFT |
| mov [SecPerClust],eax |
| dec eax |
| mov [ClustMask],eax |
| |
| add cl,SECTOR_SHIFT-2 ; 4 bytes/pointer |
| shl edx,cl |
| mov [PtrsPerBlock1],edx |
| shl edx,cl |
| mov [PtrsPerBlock2],edx |
| |
| ; |
| ; Common initialization code |
| ; |
| %include "init.inc" |
| %include "cpuinit.inc" |
| |
| ; |
| ; Initialize the metadata cache |
| ; |
| call initcache |
| |
| ; |
| ; Now, everything is "up and running"... patch kaboom for more |
| ; verbosity and using the full screen system |
| ; |
| ; E9 = JMP NEAR |
| mov dword [kaboom.patch],0e9h+((kaboom2-(kaboom.patch+3)) << 8) |
| |
| ; |
| ; Now we're all set to start with our *real* business. First load the |
| ; configuration file (if any) and parse it. |
| ; |
| ; In previous versions I avoided using 32-bit registers because of a |
| ; rumour some BIOSes clobbered the upper half of 32-bit registers at |
| ; random. I figure, though, that if there are any of those still left |
| ; they probably won't be trying to install Linux on them... |
| ; |
| ; The code is still ripe with 16-bitisms, though. Not worth the hassle |
| ; to take'm out. In fact, we may want to put them back if we're going |
| ; to boot ELKS at some point. |
| ; |
| |
| ; |
| ; Load configuration file |
| ; |
| load_config: |
| mov si,config_name ; Save config file name |
| mov di,ConfigName |
| call strcpy |
| |
| mov di,ConfigName |
| call open |
| jz no_config_file |
| |
| ; |
| ; Now we have the config file open. Parse the config file and |
| ; run the user interface. |
| ; |
| %include "ui.inc" |
| |
| ; |
| ; getlinsec_ext: same as getlinsec, except load any sector from the zero |
| ; block as all zeros; use to load any data derived |
| ; from an ext2 block pointer, i.e. anything *except the |
| ; superblock.* |
| ; |
| getonesec_ext: |
| mov bp,1 |
| |
| getlinsec_ext: |
| cmp eax,[SecPerClust] |
| jae getlinsec ; Nothing fancy |
| |
| ; If we get here, at least part of what we want is in the |
| ; zero block. Zero one sector at a time and loop. |
| push eax |
| push cx |
| xchg di,bx |
| xor eax,eax |
| mov cx,SECTOR_SIZE >> 2 |
| rep stosd |
| xchg di,bx |
| pop cx |
| pop eax |
| inc eax |
| dec bp |
| jnz getlinsec_ext |
| ret |
| |
| ; |
| ; allocate_file: Allocate a file structure |
| ; |
| ; If successful: |
| ; ZF set |
| ; BX = file pointer |
| ; In unsuccessful: |
| ; ZF clear |
| ; |
| allocate_file: |
| TRACER 'a' |
| push cx |
| mov bx,Files |
| mov cx,MAX_OPEN |
| .check: cmp dword [bx], byte 0 |
| je .found |
| add bx,open_file_t_size ; ZF = 0 |
| loop .check |
| ; ZF = 0 if we fell out of the loop |
| .found: pop cx |
| ret |
| ; |
| ; open_inode: |
| ; Open a file indicated by an inode number in EAX |
| ; |
| ; NOTE: This file considers finding a zero-length file an |
| ; error. This is so we don't have to deal with that special |
| ; case elsewhere in the program (most loops have the test |
| ; at the end). |
| ; |
| ; If successful: |
| ; ZF clear |
| ; SI = file pointer |
| ; DX:AX = EAX = file length in bytes |
| ; ThisInode = the first 128 bytes of the inode |
| ; If unsuccessful |
| ; ZF set |
| ; |
| ; Assumes CS == DS == ES. |
| ; |
| open_inode.allocate_failure: |
| xor eax,eax |
| pop bx |
| pop di |
| ret |
| |
| open_inode: |
| push di |
| push bx |
| call allocate_file |
| jnz .allocate_failure |
| |
| push cx |
| push gs |
| ; First, get the appropriate inode group and index |
| dec eax ; There is no inode 0 |
| xor edx,edx |
| mov [bx+file_sector],edx |
| div dword [SuperBlock+s_inodes_per_group] |
| ; EAX = inode group; EDX = inode within group |
| push edx |
| |
| ; Now, we need the block group descriptor. |
| ; To get that, we first need the relevant descriptor block. |
| |
| shl eax, ext2_group_desc_lg2size ; Get byte offset in desc table |
| xor edx,edx |
| div dword [ClustSize] |
| ; eax = block #, edx = offset in block |
| add eax,dword [SuperBlock+s_first_data_block] |
| inc eax ; s_first_data_block+1 |
| mov cl,[ClustShift] |
| shl eax,cl |
| push edx |
| shr edx,SECTOR_SHIFT |
| add eax,edx |
| pop edx |
| and dx,SECTOR_SIZE-1 |
| call getcachesector ; Get the group descriptor |
| add si,dx |
| mov esi,[gs:si+bg_inode_table] ; Get inode table block # |
| pop eax ; Get inode within group |
| movzx edx, word [SuperBlock+s_inode_size] |
| mul edx |
| ; edx:eax = byte offset in inode table |
| div dword [ClustSize] |
| ; eax = block # versus inode table, edx = offset in block |
| add eax,esi |
| shl eax,cl ; Turn into sector |
| push dx |
| shr edx,SECTOR_SHIFT |
| add eax,edx |
| mov [bx+file_in_sec],eax |
| pop dx |
| and dx,SECTOR_SIZE-1 |
| mov [bx+file_in_off],dx |
| |
| call getcachesector |
| add si,dx |
| mov cx,EXT2_GOOD_OLD_INODE_SIZE >> 2 |
| mov di,ThisInode |
| gs rep movsd |
| |
| mov ax,[ThisInode+i_mode] |
| mov [bx+file_mode],ax |
| mov eax,[ThisInode+i_size] |
| push eax |
| add eax,SECTOR_SIZE-1 |
| shr eax,SECTOR_SHIFT |
| mov [bx+file_left],eax |
| pop eax |
| mov si,bx |
| mov edx,eax |
| shr edx,16 ; 16-bitism, sigh |
| and eax,eax ; ZF clear unless zero-length file |
| pop gs |
| pop cx |
| pop bx |
| pop di |
| ret |
| |
| section .bss |
| alignb 4 |
| ThisInode resb EXT2_GOOD_OLD_INODE_SIZE ; The most recently opened inode |
| |
| section .text |
| ; |
| ; close_file: |
| ; Deallocates a file structure (pointer in SI) |
| ; Assumes CS == DS. |
| ; |
| close_file: |
| and si,si |
| jz .closed |
| mov dword [si],0 ; First dword == file_left |
| .closed: ret |
| |
| ; |
| ; searchdir: |
| ; Search the root directory for a pre-mangled filename in DS:DI. |
| ; |
| ; NOTE: This file considers finding a zero-length file an |
| ; error. This is so we don't have to deal with that special |
| ; case elsewhere in the program (most loops have the test |
| ; at the end). |
| ; |
| ; If successful: |
| ; ZF clear |
| ; SI = file pointer |
| ; DX:AX = EAX = file length in bytes |
| ; If unsuccessful |
| ; ZF set |
| ; |
| ; Assumes CS == DS == ES; *** IS THIS CORRECT ***? |
| ; |
| searchdir: |
| push bx |
| push cx |
| push bp |
| mov byte [SymlinkCtr],MAX_SYMLINKS |
| |
| mov eax,[CurrentDir] |
| .begin_path: |
| .leadingslash: |
| cmp byte [di],'/' ; Absolute filename? |
| jne .gotdir |
| mov eax,EXT2_ROOT_INO |
| inc di ; Skip slash |
| jmp .leadingslash |
| .gotdir: |
| |
| ; At this point, EAX contains the directory inode, |
| ; and DS:DI contains a pathname tail. |
| .open: |
| push eax ; Save directory inode |
| |
| call open_inode |
| jz .missing ; If error, done |
| |
| mov cx,[si+file_mode] |
| shr cx,S_IFSHIFT ; Get file type |
| |
| cmp cx,T_IFDIR |
| je .directory |
| |
| add sp,4 ; Drop directory inode |
| |
| cmp cx,T_IFREG |
| je .file |
| cmp cx,T_IFLNK |
| je .symlink |
| |
| ; Otherwise, something bad... |
| .err: |
| call close_file |
| .err_noclose: |
| xor eax,eax |
| xor si,si |
| cwd ; DX <- 0 |
| |
| .done: |
| and eax,eax ; Set/clear ZF |
| pop bp |
| pop cx |
| pop bx |
| ret |
| |
| .missing: |
| add sp,4 ; Drop directory inode |
| jmp .done |
| |
| ; |
| ; It's a file. |
| ; |
| .file: |
| cmp byte [di],0 ; End of path? |
| je .done ; If so, done |
| jmp .err ; Otherwise, error |
| |
| ; |
| ; It's a directory. |
| ; |
| .directory: |
| pop dword [ThisDir] ; Remember what directory we're searching |
| |
| cmp byte [di],0 ; More path? |
| je .err ; If not, bad |
| |
| .skipslash: ; Skip redundant slashes |
| cmp byte [di],'/' |
| jne .readdir |
| inc di |
| jmp .skipslash |
| |
| .readdir: |
| mov bx,trackbuf |
| push bx |
| mov cx,[SecPerClust] |
| call getfssec |
| pop bx |
| pushf ; Save EOF flag |
| push si ; Save filesystem pointer |
| .getent: |
| cmp dword [bx+d_inode],0 |
| je .endblock |
| |
| push di |
| movzx cx,byte [bx+d_name_len] |
| lea si,[bx+d_name] |
| repe cmpsb |
| je .maybe |
| .nope: |
| pop di |
| |
| add bx,[bx+d_rec_len] |
| jmp .getent |
| |
| .endblock: |
| pop si |
| popf |
| jnc .readdir ; There is more |
| jmp .err ; Otherwise badness... |
| |
| .maybe: |
| mov eax,[bx+d_inode] |
| |
| ; Does this match the end of the requested filename? |
| cmp byte [di],0 |
| je .finish |
| cmp byte [di],'/' |
| jne .nope |
| |
| ; We found something; now we need to open the file |
| .finish: |
| pop bx ; Adjust stack (di) |
| pop si |
| call close_file ; Close directory |
| pop bx ; Adjust stack (flags) |
| jmp .open |
| |
| ; |
| ; It's a symlink. We have to determine if it's a fast symlink |
| ; (data stored in the inode) or not (data stored as a regular |
| ; file.) Either which way, we start from the directory |
| ; which we just visited if relative, or from the root directory |
| ; if absolute, and append any remaining part of the path. |
| ; |
| .symlink: |
| dec byte [SymlinkCtr] |
| jz .err ; Too many symlink references |
| |
| cmp eax,SYMLINK_SECTORS*SECTOR_SIZE |
| jae .err ; Symlink too long |
| |
| ; Computation for fast symlink, as defined by ext2/3 spec |
| xor ecx,ecx |
| cmp [ThisInode+i_file_acl],ecx |
| setne cl ; ECX <- i_file_acl ? 1 : 0 |
| cmp [ThisInode+i_blocks],ecx |
| jne .slow_symlink |
| |
| ; It's a fast symlink |
| .fast_symlink: |
| call close_file ; We've got all we need |
| mov si,ThisInode+i_block |
| |
| push di |
| mov di,SymlinkTmpBuf |
| mov ecx,eax |
| rep movsb |
| pop si |
| |
| .symlink_finish: |
| cmp byte [si],0 |
| je .no_slash |
| mov al,'/' |
| stosb |
| .no_slash: |
| mov bp,SymlinkTmpBufEnd |
| call strecpy |
| jc .err_noclose ; Buffer overflow |
| |
| ; Now copy it to the "real" buffer; we need to have |
| ; two buffers so we avoid overwriting the tail on the |
| ; next copy |
| mov si,SymlinkTmpBuf |
| mov di,SymlinkBuf |
| push di |
| call strcpy |
| pop di |
| mov eax,[ThisDir] ; Resume searching previous directory |
| jmp .begin_path |
| |
| .slow_symlink: |
| mov bx,SymlinkTmpBuf |
| mov cx,SYMLINK_SECTORS |
| call getfssec |
| ; The EOF closed the file |
| |
| mov si,di ; SI = filename tail |
| mov di,SymlinkTmpBuf |
| add di,ax ; AX = file length |
| jmp .symlink_finish |
| |
| |
| section .bss |
| alignb 4 |
| SymlinkBuf resb SYMLINK_SECTORS*SECTOR_SIZE+64 |
| SymlinkTmpBuf equ trackbuf |
| SymlinkTmpBufEnd equ trackbuf+SYMLINK_SECTORS*SECTOR_SIZE+64 |
| ThisDir resd 1 |
| SymlinkCtr resb 1 |
| |
| section .text |
| ; |
| ; mangle_name: Mangle a filename pointed to by DS:SI into a buffer pointed |
| ; to by ES:DI; ends on encountering any whitespace. |
| ; DI is preserved. |
| ; |
| ; This verifies that a filename is < FILENAME_MAX characters, |
| ; doesn't contain whitespace, zero-pads the output buffer, |
| ; and removes redundant slashes, |
| ; so "repe cmpsb" can do a compare, and the |
| ; path-searching routine gets a bit of an easier job. |
| ; |
| ; FIX: we may want to support \-escapes here (and this would |
| ; be the place.) |
| ; |
| mangle_name: |
| push di |
| push bx |
| xor ax,ax |
| mov cx,FILENAME_MAX-1 |
| mov bx,di |
| |
| .mn_loop: |
| lodsb |
| cmp al,' ' ; If control or space, end |
| jna .mn_end |
| cmp al,ah ; Repeated slash? |
| je .mn_skip |
| xor ah,ah |
| cmp al,'/' |
| jne .mn_ok |
| mov ah,al |
| .mn_ok stosb |
| .mn_skip: loop .mn_loop |
| .mn_end: |
| cmp bx,di ; At the beginning of the buffer? |
| jbe .mn_zero |
| cmp byte [di-1],'/' ; Terminal slash? |
| jne .mn_zero |
| .mn_kill: dec di ; If so, remove it |
| inc cx |
| jmp short .mn_end |
| .mn_zero: |
| inc cx ; At least one null byte |
| xor ax,ax ; Zero-fill name |
| rep stosb |
| pop bx |
| pop di |
| ret ; Done |
| |
| ; |
| ; unmangle_name: Does the opposite of mangle_name; converts a DOS-mangled |
| ; filename to the conventional representation. This is needed |
| ; for the BOOT_IMAGE= parameter for the kernel. |
| ; |
| ; DS:SI -> input mangled file name |
| ; ES:DI -> output buffer |
| ; |
| ; On return, DI points to the first byte after the output name, |
| ; which is set to a null byte. |
| ; |
| unmangle_name: call strcpy |
| dec di ; Point to final null byte |
| ret |
| |
| ; |
| ; |
| ; kaboom2: once everything is loaded, replace the part of kaboom |
| ; starting with "kaboom.patch" with this part |
| |
| kaboom2: |
| mov si,err_bootfailed |
| call cwritestr |
| cmp byte [kaboom.again+1],18h ; INT 18h version? |
| je .int18 |
| call getchar |
| call vgaclearmode |
| int 19h ; And try once more to boot... |
| .norge: jmp short .norge ; If int 19h returned; this is the end |
| .int18: |
| call vgaclearmode |
| int 18h |
| .noreg: jmp short .noreg ; Nynorsk |
| |
| |
| ; |
| ; linsector: Convert a linear sector index in a file to a linear sector number |
| ; EAX -> linear sector number |
| ; DS:SI -> open_file_t |
| ; |
| ; Returns next sector number in EAX; CF on EOF (not an error!) |
| ; |
| linsector: |
| push gs |
| push ebx |
| push esi |
| push edi |
| push ecx |
| push edx |
| push ebp |
| |
| push eax ; Save sector index |
| mov cl,[ClustShift] |
| shr eax,cl ; Convert to block number |
| push eax |
| mov eax,[si+file_in_sec] |
| mov bx,si |
| call getcachesector ; Get inode |
| add si,[bx+file_in_off] ; Get *our* inode |
| pop eax |
| lea ebx,[i_block+4*eax] |
| cmp eax,EXT2_NDIR_BLOCKS |
| jb .direct |
| mov ebx,i_block+4*EXT2_IND_BLOCK |
| sub eax,EXT2_NDIR_BLOCKS |
| mov ebp,[PtrsPerBlock1] |
| cmp eax,ebp |
| jb .ind1 |
| mov ebx,i_block+4*EXT2_DIND_BLOCK |
| sub eax,ebp |
| mov ebp,[PtrsPerBlock2] |
| cmp eax,ebp |
| jb .ind2 |
| mov ebx,i_block+4*EXT2_TIND_BLOCK |
| sub eax,ebp |
| |
| .ind3: |
| ; Triple indirect; eax contains the block no |
| ; with respect to the start of the tind area; |
| ; ebx contains the pointer to the tind block. |
| xor edx,edx |
| div dword [PtrsPerBlock2] |
| ; EAX = which dind block, EDX = pointer within dind block |
| push ax |
| shr eax,SECTOR_SHIFT-2 |
| mov ebp,[gs:si+bx] |
| shl ebp,cl |
| add eax,ebp |
| call getcachesector |
| pop bx |
| and bx,(SECTOR_SIZE >> 2)-1 |
| shl bx,2 |
| mov eax,edx ; The ind2 code wants the remainder... |
| |
| .ind2: |
| ; Double indirect; eax contains the block no |
| ; with respect to the start of the dind area; |
| ; ebx contains the pointer to the dind block. |
| xor edx,edx |
| div dword [PtrsPerBlock1] |
| ; EAX = which ind block, EDX = pointer within ind block |
| push ax |
| shr eax,SECTOR_SHIFT-2 |
| mov ebp,[gs:si+bx] |
| shl ebp,cl |
| add eax,ebp |
| call getcachesector |
| pop bx |
| and bx,(SECTOR_SIZE >> 2)-1 |
| shl bx,2 |
| mov eax,edx ; The int1 code wants the remainder... |
| |
| .ind1: |
| ; Single indirect; eax contains the block no |
| ; with respect to the start of the ind area; |
| ; ebx contains the pointer to the ind block. |
| push ax |
| shr eax,SECTOR_SHIFT-2 |
| mov ebp,[gs:si+bx] |
| shl ebp,cl |
| add eax,ebp |
| call getcachesector |
| pop bx |
| and bx,(SECTOR_SIZE >> 2)-1 |
| shl bx,2 |
| |
| .direct: |
| mov ebx,[gs:bx+si] ; Get the pointer |
| |
| pop eax ; Get the sector index again |
| shl ebx,cl ; Convert block number to sector |
| and eax,[ClustMask] ; Add offset within block |
| add eax,ebx |
| |
| pop ebp |
| pop edx |
| pop ecx |
| pop edi |
| pop esi |
| pop ebx |
| pop gs |
| ret |
| |
| ; |
| ; getfssec: Get multiple sectors from a file |
| ; |
| ; Same as above, except SI is a pointer to a open_file_t |
| ; |
| ; ES:BX -> Buffer |
| ; DS:SI -> Pointer to open_file_t |
| ; CX -> Sector count (0FFFFh = until end of file) |
| ; Must not exceed the ES segment |
| ; Returns CF=1 on EOF (not necessarily error) |
| ; All arguments are advanced to reflect data read. |
| ; |
| getfssec: |
| push ebp |
| push eax |
| push edx |
| push edi |
| |
| movzx ecx,cx |
| cmp ecx,[si] ; Number of sectors left |
| jbe .lenok |
| mov cx,[si] |
| .lenok: |
| .getfragment: |
| mov eax,[si+file_sector] ; Current start index |
| mov edi,eax |
| call linsector |
| push eax ; Fragment start sector |
| mov edx,eax |
| xor ebp,ebp ; Fragment sector count |
| .getseccnt: |
| inc bp |
| dec cx |
| jz .do_read |
| xor eax,eax |
| mov ax,es |
| shl ax,4 |
| add ax,bx ; Now DI = how far into 64K block we are |
| not ax ; Bytes left in 64K block |
| inc eax |
| shr eax,SECTOR_SHIFT ; Sectors left in 64K block |
| cmp bp,ax |
| jnb .do_read ; Unless there is at least 1 more sector room... |
| inc edi ; Sector index |
| inc edx ; Linearly next sector |
| mov eax,edi |
| call linsector |
| ; jc .do_read |
| cmp edx,eax |
| je .getseccnt |
| .do_read: |
| pop eax ; Linear start sector |
| pushad |
| call getlinsec_ext |
| popad |
| push bp |
| shl bp,9 |
| add bx,bp ; Adjust buffer pointer |
| pop bp |
| add [si+file_sector],ebp ; Next sector index |
| sub [si],ebp ; Sectors consumed |
| jcxz .done |
| jnz .getfragment |
| ; Fall through |
| .done: |
| cmp dword [si],1 ; Did we run out of file? |
| ; CF set if [SI] < 1, i.e. == 0 |
| pop edi |
| pop edx |
| pop eax |
| pop ebp |
| ret |
| |
| ; ----------------------------------------------------------------------------- |
| ; Common modules |
| ; ----------------------------------------------------------------------------- |
| |
| %include "getc.inc" ; getc et al |
| %include "conio.inc" ; Console I/O |
| %include "plaincon.inc" ; writechr |
| %include "writestr.inc" ; String output |
| %include "configinit.inc" ; Initialize configuration |
| %include "parseconfig.inc" ; High-level config file handling |
| %include "parsecmd.inc" ; Low-level config file handling |
| %include "bcopy32.inc" ; 32-bit bcopy |
| %include "loadhigh.inc" ; Load a file into high memory |
| %include "font.inc" ; VGA font stuff |
| %include "graphics.inc" ; VGA graphics |
| %include "highmem.inc" ; High memory sizing |
| %include "strcpy.inc" ; strcpy() |
| %include "strecpy.inc" ; strcpy with end pointer check |
| %include "cache.inc" ; Metadata disk cache |
| %include "adv.inc" ; Auxillary Data Vector |
| |
| ; ----------------------------------------------------------------------------- |
| ; Begin data section |
| ; ----------------------------------------------------------------------------- |
| |
| section .data |
| copyright_str db ' Copyright (C) 1994-', year, ' H. Peter Anvin' |
| db CR, LF, 0 |
| err_bootfailed db CR, LF, 'Boot failed: please change disks and press ' |
| db 'a key to continue.', CR, LF, 0 |
| config_name db 'extlinux.conf',0 ; Unmangled form |
| |
| ; |
| ; Command line options we'd like to take a look at |
| ; |
| ; mem= and vga= are handled as normal 32-bit integer values |
| initrd_cmd db 'initrd=' |
| initrd_cmd_len equ 7 |
| |
| ; |
| ; Config file keyword table |
| ; |
| %include "keywords.inc" |
| |
| ; |
| ; Extensions to search for (in *forward* order). |
| ; |
| align 4, db 0 |
| exten_table: db '.cbt' ; COMBOOT (specific) |
| db '.img' ; Disk image |
| db '.bs', 0 ; Boot sector |
| db '.com' ; COMBOOT (same as DOS) |
| db '.c32' ; COM32 |
| exten_table_end: |
| dd 0, 0 ; Need 8 null bytes here |
| |
| ; |
| ; Misc initialized (data) variables |
| ; |
| %ifdef debug ; This code for debugging only |
| debug_magic dw 0D00Dh ; Debug code sentinel |
| %endif |
| |
| alignb 4, db 0 |
| BufSafe dw trackbufsize/SECTOR_SIZE ; Clusters we can load into trackbuf |
| BufSafeBytes dw trackbufsize ; = how many bytes? |
| %ifndef DEPEND |
| %if ( trackbufsize % SECTOR_SIZE ) != 0 |
| %error trackbufsize must be a multiple of SECTOR_SIZE |
| %endif |
| %endif |