flat assembler
Message board for the users of flat assembler.
![]() |
Author |
|
bzt 29 Mar 2025, 22:40
I got fed up that everybody is struggling with FAT when there's a much better alternative, so some time ago I've created a repository with detailed documentation and code examples on the Minix3 File System. Due to that, recently someone asked me how to boot from MFS (because Minix3 is using the NetBSD's boot loader, which is terrible and not user friendly and they could not find any other example, just FAT boot sectors). Turned out to be a fair question, obviously no existing boot code examples were able to load files from MFS volumes...
So I've decided to add a MFS boot sector example to my repo, written in flat assembler. This code can boot any arbitrary file (a kernel or stage2 loader executable) from a Minix3 File System, it autodetects the format (ELF, PE/COFF, a.out), loads its segments and sets up environment accordingly (either protected mode or long mode). Because of this sophistication, the executable could be compiled from any high-level language (C, Ada, Rust, Go, whatever), but flat assembler works fine as well of course. Code: ; ; https://gitlab.com/bztsrc/minix3fs ; ; Copyright (C) 2025 bzt, MIT license ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to ; deal in the Software without restriction, including without limitation the ; rights to use, copy, modify, merge, publish, distribute, sublicense, and/or ; sell copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in ; all copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ANY ; DEVELOPER OR DISTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, ; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR ; IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ; ; @brief example boot loader for the Minix3 File System ; ; By default, this code is a Master Boot Record, to make it work as a Volume Boot ; Record, just set the partition's starting LBA in "partition_lba" and that's it. ; It boots an arbitrary statically linked executable file from the file system. ; ; Supported executable formats (autodetected in run-time): ; - ELF 32 bit: started in protected mode ; - ELF 64 bit: started in long mode ; - PE/COFF 32 bit: started in protected mode ; - PE/COFF 64 bit: started in long mode ; - a.out 32 bit: started in protected mode ; ; Limitations: ; - no relocations nor dynamic linking, the executable must be statically linked ; - all segment descriptors of the executable must fit in the first block (typically 4K) ; - its segments can be placed in 64K to 640K (conventional RAM) or above 1M (normal RAM) ; - only one indirection level supported (up to 4M with 4K blocks, up to 237M with 32K) ; - for long mode executable, first 3G of RAM is identity mapped (regardless to RAM size). ; ; Memory layout on handover: ; 0h - 400h IVT (must be preserved) ; 400h - 4FFh BDA (must be preserved) ; 4FFh - 500h BIOS boot drive code ; 500h - 51Ch BIOS LBA packet and other bss variables ; 51Ch - 7C00h file's block list (zone numbers), stack from top ; 1000h - 6000h paging tables (only exists if the executable is 64 bit) ; 7C00h - 7E00h 1st sector (boot loader code and data, later segments list) ; 7E00h - 8000h 2nd sector (boot loader code and data) ; 8000h - 10000h temporary disk block buffer (32K) ; 10000h - 9A000h free, to be used by segments (64K to 640K) ; 9A000h - 100000h EBDA and BIOS ROM (must be preserved) ; 100000h - ? free, to be used by segments (above 1M to top of RAM) ; ;********************************************************************* ;* macros * ;********************************************************************* ; uninitialized variables lba_packet equ 0500h block_size equ 0510h entry_point equ 0514h numseg equ 0518h longmode equ 0519h zones equ 051Ch segs equ 7C00h buffer equ 8000h ; Minix3 File System superblock virtual at buffer sb.s_ninodes: dd ? sb.reserved0: dw ? sb.s_imap_blocks: dw ? sb.s_zmap_blocks: dw ? sb.reserved1: db 10 dup ? sb.s_zones: dd ? sb.s_magic: dw ? sb.reserved2: dw ? sb.s_block_size: dw ? sb.s_disk_version:dw ? end virtual ; Minix3 File System inode virtual at 0 inode.i_mode dw ? inode.i_nlinks: dw ? inode.i_uid: dw ? inode.i_gid: dw ? inode.i_size: dd ? inode.i_atime: dd ? inode.i_mtime: dd ? inode.i_ctime: dd ? inode.i_zones: dd 7 dup ? ; direct block numbers inode.i_indirect: dd ? ; indirect inode.i_double: dd ? ; double indirect inode.i_triple: dd ? ; triple indirect end virtual ; ELF format ELF_MAGIC equ 457Fh PT_LOAD equ 1 ; 32 bit virtual at 0 ehdr32.e_ident: db 16 dup ? ; elf header ehdr32.e_type: dw ? ehdr32.e_machine: dw ? ehdr32.e_version: dd ? ehdr32.e_entry: dd ? ehdr32.e_phoff: dd ? ehdr32.e_shoff: dd ? ehdr32.e_flags: dd ? ehdr32.e_ehsize: dw ? ehdr32.e_phentsize: dw ? ehdr32.e_phnum: dw ? ehdr32.e_shentsize: dw ? ehdr32.e_shnum: dw ? ehdr32.e_shstrndx: dw ? end virtual virtual at 0 phdr32.p_type: dd ? ; program header phdr32.p_offset: dd ? phdr32.p_vaddr: dd ? phdr32.p_paddr: dd ? phdr32.p_filesz: dd ? phdr32.p_memsz: dd ? phdr32.p_align: dd ? end virtual ; 64 bit virtual at 0 ehdr64.e_ident: db 16 dup ? ; elf header ehdr64.e_type: dw ? ehdr64.e_machine: dw ? ehdr64.e_version: dd ? ehdr64.e_entry: dq ? ehdr64.e_phoff: dq ? ehdr64.e_shoff: dq ? ehdr64.e_flags: dd ? ehdr64.e_ehsize: dw ? ehdr64.e_phentsize: dw ? ehdr64.e_phnum: dw ? ehdr64.e_shentsize: dw ? ehdr64.e_shnum: dw ? ehdr64.e_shstrndx: dw ? end virtual virtual at 0 phdr64.p_type: dd ? ; program header phdr64.p_flags: dd ? phdr64.p_offset: dq ? phdr64.p_vaddr: dq ? phdr64.p_paddr: dq ? phdr64.p_filesz: dq ? phdr64.p_memsz: dq ? phdr64.p_align: dq ? end virtual ; PE/COFF format MZ_MAGIC equ 5A4Dh PE_MAGIC equ 4550h PE32PLUS_MAGIC equ 020Bh virtual at 0 pe.magic: dd ? ; pe header pe.machine: dw ? pe.sections: dw ? pe.timestamp: dd ? pe.sym_table: dd ? pe.num_sym: dd ? pe.opt_hdr_size: dw ? pe.flags: dw ? pe.file_type: dw ? pe.ld_ver: dw ? pe.text_size: dd ? pe.data_size: dd ? pe.bss_size: dd ? pe.entry_point: dd ? pe.code_base: dd ? pe.data_base: dd ? pe.img_base: dd ? end virtual virtual at 0 pesec.name: dq ? ; section pesec.vsiz: dd ? pesec.vaddr: dd ? pesec.rsiz: dd ? pesec.raddr: dd ? pesec.reloc: dd ? pesec.ln: dd ? pesec.nreloc: dw ? pesec.nln: dw ? pesec.chr: dd ? end virtual ; struct exec (a.out) format AOUT_MAGIC equ 0801h ; NMAGIC, 0410 in big-endian virtual at 0 aout.a_midmag: dd ? ; magic (network byte order) aout.a_text: dd ? ; text segment size aout.a_data: dd ? ; data segment size aout.a_bss: dd ? ; bss segment size aout.a_syms: dd ? ; symbol section size aout.a_entry: dd ? ; entry point aout.a_trsize: dd ? ; text relocation size aout.a_drsize: dd ? ; data relocation size end virtual ; BIOS lba packet virtual at lba_packet lbapacket.size: dw ? lbapacket.count: dw ? lbapacket.addr0: dw ? lbapacket.addr1: dw ? lbapacket.sect0: dd ? lbapacket.sect1: dd ? end virtual ; format-independent file segment descriptor records virtual at 0 seg.offs: dd ? ; byte offset in file seg.addr: dd ? ; memory address to load to seg.size: dd ? ; size in file seg.bss: dd ? ; bss size end virtual ORG 07C00h USE16 ;********************************************************************* ;* 1st sector * ;********************************************************************* loader: jmp short @f ; mandatory jump (magic for some BIOS) nop db "MFS" ;---- set up environment ---- @@: cli cld mov al, 0FFh ; disable PIC out 021h, al out 0A1h, al in al, 70h ; disable NMI or al, 80h out 70h, al xor ax, ax ; set up segment registers and stack mov ss, ax mov ds, ax mov es, ax mov sp, 07C00h jmp 0:@f ;---- read in the remaining part of the boot loader and the MFS superblock ---- @@: mov si, lba_packet mov di, si mov byte [di - 1], dl xor ah, ah mov al, 16 ; size stosw mov al, 2 ; count stosw mov ax, 07E00h ; addr0, load to 0000:7E00h stosw xor ax, ax ; addr1 stosw mov eax, dword [partition_lba] inc eax ; sect0, LBA 1 stosd xor ax, ax stosw ; sect1 stosw push si mov ah, 42h int 13h pop si ;---- parse the MFS superblock ---- ; block size xor eax, eax mov word [numseg], ax mov ax, word [sb.s_block_size] mov dword [block_size], eax shr ax, 9 mov word [lbapacket.count], ax ; get inode table's starting sector mov al, 2 add ax, word [sb.s_imap_blocks] add ax, word [sb.s_zmap_blocks] call blk2sec ; adjust by inode structure's offset mov eax, dword [s_inode_to_boot] dec eax shl eax, 6 ; offset = (s_inode_to_boot - 1) * sizeof(inode_t) mov ebx, eax shr eax, 9 ; eax = offset / sector size add dword [lbapacket.sect0], eax mov byte [lbapacket.addr0 + 1], 80h ; load to 0000:8000h ; load the sector where the inode resides push si push bx mov ah, 42h int 13h xor esi, esi pop si ; was bx ; offset of inode structure inside the sector and si, 511 ;---- parse the MFS inode ---- ; copy direct block numbers from inode to zones[0..6] add si, buffer + inode.i_zones mov di, zones mov cx, 7 repnz movsd ; load and copy indirect zones (if any) lodsd pop si or eax, eax jz @f call blk2sec ; load indirect block push si push di mov ah, 42h int 13h pop di ; copy block numbers to zones[7..7606] mov si, buffer mov cx, 1DB0h ; (7C00h - 51Ch) / 4 - some stack repnz movsd pop si @@: ; load the first block of the file (needed for autodetection) mov eax, dword [zones] call blk2sec mov ah, 42h int 13h ;---- enable protmode ---- mov ax, 2401h ; enable A20 int 15h lgdt [GDT_value] ; prot mode descriptors mov eax, cr0 or al, 1 mov cr0, eax ; prot mode enable flag jmp 16:@f ; reload segments USE32 @@: mov ax, 24 mov ds, ax mov es, ax ;---- parse the executable's header, collect segment descriptor list ---- mov edi, segs mov ebx, buffer mov ax, word [ebx] xor ecx, ecx ; is it in struct exec (a.out) format? isaout: cmp word [ebx + 2], AOUT_MAGIC ; this is in network byte order, so big endian jne ispe inc ch ; we have 1 segment mov eax, 32 ; offs = sizeof(struct exec) stosd mov eax, dword [ebx + aout.a_entry] mov dword [entry_point], eax ; addr (first byte of text segment) stosd mov eax, dword [ebx + aout.a_text] ; size (bytes to load) add eax, dword [ebx + aout.a_data] stosd mov eax, dword [ebx + aout.a_bss] ; bss (bytes to zero out) stosd jmp loadsegs ; is it in PE/COFF format? ispe: cmp ax, MZ_MAGIC jne iself add ebx, dword [ebx + 0x3c] cmp word [ebx + pe.magic], PE_MAGIC jne die mov ebp, dword [ebx + pe.img_base] cmp word [ebx + pe.file_type], PE32PLUS_MAGIC jne @f inc byte [longmode] mov ebp, dword [ebx + pe.data_base] ; there's no data base, the two is one 64 bit base @@: mov eax, dword [ebx + pe.entry_point] add eax, ebp mov dword [entry_point], eax mov cl, byte [ebx + pe.sections] ; number of sections add bx, word [ebx + pe.opt_hdr_size] add bx, 24 ; ebx now points to sections .next: cmp di, @b - 16 ; failsafe bound check jae loadsegs mov eax, dword [ebx + pesec.raddr] stosd ; offs (in file offset) mov eax, dword [ebx + pesec.vaddr] add eax, ebp stosd ; addr (to memory) ; unfortunately rsiz can be bigger than vsiz, which would overflow... mov eax, dword [ebx + pesec.rsiz] mov edx, dword [ebx + pesec.vsiz] cmp eax, edx ; rsiz < vsiz? jb @f mov eax, edx ; size = vsiz xor edx, edx ; bss = 0 jmp .stor @@: sub edx, eax ; size = rsiz, bss = vsiz - rsiz .stor: stosd ; size (size in file) mov eax, edx stosd ; bss (bytes to zero out) inc ch add ebx, 40 ; go to next section dec cl jnz .next jmp loadsegs ; this code never reached, instead execution continues on 2nd sector ;---- die function ---- ; prints an error string and halts processor die: mov esi, errstr mov edi, 0B8000h mov ah, 04fh @@: lodsb or al, al jz @f stosw jmp @b @@: hlt ;---- convert block to sectors ---- ; eax: block number USE16 blk2sec: movzx ebx, word [lbapacket.count] mul ebx add eax, dword [partition_lba] mov dword [lbapacket.sect0], eax mov dl, byte [lba_packet - 1] ret USE32 errstr: db "ERROR", 0 GDT_value: dw GDT_value.end-GDT_value ; value / null descriptor dd GDT_value dw 0 dd 0000FFFFh,00009800h ; 8 - legacy real cs dd 0000FFFFh,00CF9A00h ; 16 - prot mode cs dd 0000FFFFh,008F9200h ; 24 - prot mode ds dd 0000FFFFh,00AF9A00h ; 32 - long mode cs dd 0000FFFFh,00CF9200h ; 40 - long mode ds .end: db 01FAh-($-$$) dup 0 ;---- in case the Minix3 File System is on a partition ---- partition_lba: dd 0 db 55h, 0AAh ; mandatory magic bytes ;********************************************************************* ;* 2nd sector * ;********************************************************************* ; is it in ELF format? iself: cmp ax, ELF_MAGIC jne die mov eax, dword [ebx + ehdr32.e_entry] mov dword [entry_point], eax cmp byte [ebx + ehdr32.e_ident + 4], 2 ; ELFCLASS64? je iself64 ; 32 bit mov cl, byte [ebx + ehdr32.e_phnum] mov dx, word [ebx + ehdr32.e_phentsize] add bx, word [ebx + ehdr32.e_phoff] .next32: cmp edi, die - 16 ; failsafe bound check jae loadsegs cmp byte [ebx + phdr32.p_type], PT_LOAD jne @f mov eax, dword [ebx + phdr32.p_offset] stosd ; offs (in file offset) mov eax, dword [ebx + phdr32.p_vaddr] stosd ; addr (to memory) mov eax, dword [ebx + phdr32.p_filesz] stosd ; size (size in file) mov eax, dword [ebx + phdr32.p_memsz] sub eax, dword [ebx + phdr32.p_filesz] stosd ; bss inc ch @@: add bx, dx ; go to next program header dec cl jnz .next32 jmp loadsegs ; 64 bit iself64: inc byte [longmode] mov cl, byte [ebx + ehdr64.e_phnum] mov dx, word [ebx + ehdr64.e_phentsize] add bx, word [ebx + ehdr64.e_phoff] .next64: cmp edi, die - 16 ; failsafe bound check jae loadsegs cmp byte [ebx + phdr64.p_type], PT_LOAD jne @f mov eax, dword [ebx + phdr64.p_offset] stosd ; offs (in file offset) mov eax, dword [ebx + phdr64.p_vaddr] stosd ; addr (to memory) mov eax, dword [ebx + phdr64.p_filesz] stosd ; size (size in file) mov eax, dword [ebx + phdr64.p_memsz] sub eax, dword [ebx + phdr64.p_filesz] stosd ; bss (bytes to zero out) inc ch @@: add bx, dx ; go to next program header dec cl jnz .next64 ;---- load the executable's segments ---- ; numseg: number of segments ; segs: segment descriptor records, each 16 bytes, offs + addr + size + bss loadsegs: or ch, ch jz die mov byte [numseg], ch mov esi, segs ; there are three possible scenarios here for every segment: ; 1. first iteration, maybe edx != 0, ecx <= block size (maybe ecx = filesz) ; 2. middle iteration, edx = 0, ecx = block size (maybe called multiple times) ; 3. last iteration, edx = 0, ecx < block size (if filesz % block size != 0) ; (+1 special case, when filesz < block size also taken care of in step 1) .nextseg: xor edx, edx mov eax, dword [esi + seg.offs] mov ecx, dword [block_size] ; file offset to file block number (eax) and block offset (edx) div ecx sub ecx, edx ; check size (ecx = block size - block offset) is not bigger than segment filesz mov ebx, dword [esi + seg.size] cmp ecx, ebx jbe @f mov ecx, ebx ; convert file block number to volume block number @@: shl eax, 2 add eax, zones mov eax, dword [eax] ; eax = zones[eax] mov edi, dword [esi + seg.addr] ; load the next block from the segment ; eax: volume block number ; edx: offset within the block ; edi: destination buffer ; ecx: number of bytes to load or ecx, ecx jz .nofilesz push ecx push esi push edx push edi push ecx movzx ebx, word [lbapacket.count] mul ebx add eax, dword [partition_lba] mov dword [lbapacket.sect0], eax jmp 8:@f USE16 @@: mov eax, CR0 and al, 0FEh ; switching back to real mode mov CR0, eax jmp 0:@f @@: xor ax, ax mov ds, ax mov es, ax mov ss, ax mov si, lba_packet ; call BIOS mov dl, byte [si - 1] mov ah, 42h int 13h mov eax, cr0 ; enable protected mode or al, 1 mov cr0, eax jmp 16:@f USE32 @@: mov ax, 24 mov ds, ax mov es, ax mov ss, ax pop ecx pop edi pop esi ; copy from temporary buffer to final position add esi, buffer repnz movsb pop esi pop ecx ; adjust offsets (ecx is no bigger than block size) add dword [esi + seg.offs], ecx add dword [esi + seg.addr], ecx sub dword [esi + seg.size], ecx ; repeat if there's more blocks left to load in the segment cmp dword [esi + seg.size], 0 jnz .nextseg ; load next block of segment ; finished with all blocks, edi now points right after the loaded data .nofilesz: mov ecx, dword [esi + seg.bss] or ecx, ecx ; clear bss area jz @f xor al, al repnz stosb @@: add esi, 16 ; go to first block of next segment dec byte [numseg] jnz .nextseg ;---- pass control to executable's entry point ---- cmp byte [longmode], 0 jnz @f jmp dword [entry_point] @@: ;---- enable longmode ---- xor eax, eax mov ah, 010h mov cr3, eax ; we only map 3G here mov edx, eax ; PML4 mov ebx, eax xor eax, eax mov dword [ebx], 02003h ; pointer to 2M PDPE mov dword [ebx + 4], eax add ebx, edx ; 2M PDPE mov dword [ebx], 03003h mov dword [ebx + 4], eax mov dword [ebx + 8], 04003h mov dword [ebx + 12], eax mov dword [ebx + 16], 05003h mov dword [ebx + 20], eax add ebx, edx ; 2M PDE mov cx, 3 * 512 mov al, 83h @@: mov dword [ebx], eax mov dword [ebx + 4], 0 add ebx, 8 add eax, 2*1024*1024 dec cx jnz @b xor eax, eax mov al, 0E0h ; set PAE, MCE, PGE; clear everything else mov cr4, eax mov ecx, 0C0000080h ; EFER MSR rdmsr bts eax, 8 ; enable long mode page tables wrmsr mov eax, cr0 xor cl, cl or eax, ecx btc eax, 16 ; clear WP mov cr0, eax ; enable paging with cache disabled (set PE, CD) lgdt [GDT_value] ; read 80 bit address (16+64) jmp 32:@f USE64 @@: xor rax, rax ; load long mode segments mov al, 40 mov ds, ax mov es, ax mov ss, ax mov eax, dword [entry_point] jmp rax db 1020-($-$$) dup 0 ;---- file to be booted, set by the mfsboot installer tool ---- s_inode_to_boot: dd 0 ;---- this is followed directly by the Minix3 File System superblock on disk ---- ;********************************************************************* ;* the power of fasm macros: generate C literals from the binary * ;********************************************************************* display "/* Generated by fasm from boot.asm */", 10 first = 1 repeat $-loader load b byte from (loader+%-1) if first first = 0 else display ',' end if display '0', 'x' repeat 2 d = '0' + b shr (8-%*4) and 0Fh if d > '9' d = d + 'A'-'9'-1 end if display d end repeat end repeat display 10 Hint: there's also a dependency-free, multiplatform command line tool mfsboot in the repo, which installs this boot sector into a Minix3 File System's superblock. There's no more component to this loader, it only consist of this boot sector. Cheers, bzt |
|||
![]() |
|
< Last Thread | Next Thread > |
Forum Rules:
|
Copyright © 1999-2025, Tomasz Grysztar. Also on GitHub, YouTube.
Website powered by rwasa.