13
\$\begingroup\$

Presumably everybody here knows that for the legacy BIOS, the tiniest bootable disk image is 512 bytes (the lower bound is due to the 2-byte magic at offset 510), and there is a bootsector demoscene where people try to fit the entire demo into the 510 bytes of usable space.

Now that the BIOS is being superseded by UEFI, there is a question: what will be the equivalent of a "bootsector demo" in the UEFI world?

More formally, here is what I'm curious about: what's the minimum size of a whole-drive image that boots on at least one UEFI?

Rules:

  1. The answer must be a disk image, or must compile to a disk image.
  2. The disk image should boot on at least one UEFI with CSM/Legacy boot disabled.
  3. By "booting" I consider that the CPU starts executing instructions that have been loaded from the disk image. It does not matter what the image actually does.
  4. Exploiting bugs in specific UEFI implementations is OK, unless the bug causes the firmware to attempt a legacy boot.
  5. The winner is the image that is the smallest when encoded in the raw format. If there are multiple answers with the same image size, the one where the most space can be used for x86 code wins.
\$\endgroup\$
4
  • \$\begingroup\$ Welcome to Code Golf! \$\endgroup\$ Dec 25, 2022 at 20:08
  • 3
    \$\begingroup\$ Why x86 code specifically? \$\endgroup\$ Dec 26, 2022 at 1:52
  • \$\begingroup\$ Do you think answers would be testable with qemu or similar? Manually booting is quite annoying. \$\endgroup\$
    – corvus_192
    Dec 26, 2022 at 11:03
  • \$\begingroup\$ > Why x86 code specifically? The exact architecture should not matter, unless the solution actually involves a binary exploit. > testable with qemu That would be great, but once again, this would prevent solutions involving firmware bugs. \$\endgroup\$ Dec 26, 2022 at 23:27

2 Answers 2

11
+100
\$\begingroup\$

1886 1799 bytes (~3.51 sectors), OVMF

-87 bytes by removing . and .. from cluster 2 and reorganizing the sector.

Started off with @набиячлэвэлиь's answer and golfed it down to the point where it's now a single NASM file building the image from scratch. Outputs "Done" and enters an infinite loop.

Hexdump:

00000000: 0000 0000 0000 0000 0000 0000 0201 0100  ................
00000010: 0110 0004 0000 0100 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000150: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000160: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000180: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000190: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000200: 0000 00ff 0f00 0000 0000 0000 0000 0000  ................
00000210: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000220: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000230: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000240: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000250: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000260: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000270: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000280: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000290: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000002f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000300: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000310: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000320: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000330: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000340: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000350: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000360: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000370: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000380: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000390: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000003f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000400: 4546 4920 2020 2020 2020 2010 0000 0000  EFI        .....
00000410: 0000 0000 0000 0000 0000 0200 0000 0000  ................
00000420: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000430: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000440: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000450: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000460: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000470: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000480: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000490: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000004f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000500: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000510: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000520: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000530: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000540: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000550: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000560: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000570: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000580: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000590: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000005f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000600: 4d5a 0000 0000 0000 0000 0000 0000 0000  MZ..............
00000610: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000620: 424f 4f54 2020 2020 2020 2010 0000 0000  BOOT       .....
00000630: 0000 0000 0000 0000 0000 0200 4000 0000  ............@...
00000640: 5045 0000 6486 0100 0000 0000 0000 0000  PE..d...........
00000650: 0000 0000 7000 2e22 0b02 0000 0000 0000  ....p.."........
00000660: 4100 0000 0000 0000 0010 0000 0010 0000  A...............
00000670: 0000 0040 0100 0000 0100 0000 0100 0000  ...@............
00000680: 4200 0000 0000 0000 0000 0000 0000 0000  B...............
00000690: 0040 0000 f000 0000 0000 0000 0a00 0000  .@..............
000006a0: 424f 4f54 5836 3420 4546 4920 0000 0000  BOOTX64 EFI ....
000006b0: 0000 0000 0000 0000 0000 0200 0002 0000  ................
000006c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000006d0: 1a00 0000 0010 0000 1a00 0000 f000 0000  ................
000006e0: 0000 0000 0000 0000 0000 0000 2000 0000  ............ ...
000006f0: 488b 4a40 488d 1505 0000 00ff 5108 ebfe  H.J@H.......Q...
00000700: 4400 6f00 6e00 65                        D.o.n.e

Boot:

$ qemu-system-x86_64 -bios OVMF.fd -nographic -drive file=disk.bin,format=raw
BdsDxe: failed to load Boot0001 "UEFI QEMU DVD-ROM QM00003 " from    PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Master,0x0): Not Found
BdsDxe: loading Boot0002 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
BdsDxe: starting Boot0002 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
Done

There's way too much to explain in a single answer, so here's a .zip of everything in my working directory, and here's the final .asm:

[bits 64]

; Creates a non-partitioned disk image with no MBR/GPT, just FAT12.
; (UEFI specs req. implementations to support FAT12, FAT16, and FAT32 ESP's,
; although there might be smaller partition formats for certain 
; implementations).
; FAT12 appears to be optimal.

; Resources:
; https://wiki.osdev.org/FAT
; https://wiki.osdev.org/UEFI
; https://wiki.archlinux.org/title/EFI_system_partition
; https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system
; https://uefi.org/sites/default/files/resources/UEFI_Spec_2_10_Aug29.pdf
; https://archive.vn/w01DO#selection-265.0-265.44
; https://wiki.osdev.org/PE
; https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
; https://johv.dk/blog/bare-metal-assembly-tutorial
; https://github.com/tianocore/edk2
; https://retrage.github.io/2019/12/05/debugging-ovmf-en.html
; https://edk2-docs.gitbook.io/edk-ii-build-specification/
; https://uefi.org/sites/default/files/resources/PI_Spec_1_6.pdf

; As long as OVMF boots this, we're good.

; Layout:
;   - Reserved sectors (1 boot sector, everything else opt.)
;   - FAT region (1 FAT * 1 sector per fat = 1 sector)
;   - Root directory region (1 sector)
;   - Data region (1 sector)


; Now, in a golfing view: whats the absolute smallest this image can be?
; - The minimum SectorSize recognized by OVMF is 512 bytes.
; - I have tried merging the FAT into the boot sector, and certain forensics
; tools can read the image just fine, but OVMF can't as it requires that
; ReservedSectors != 0.
; - OVMF requires a full, dedicated directory for the root directory, and will
; only read it right where it should be, after the FAT. This puts the total to
; 3 sectors so far.
; - It does not look like directories can point to cluster 0, meaning we need
; at least 1 sector for data-- now 4 sectors.


; A lot of the boot sector is intended for old BIOS bootloader code, but
; because we're using UEFI, we can fill most of it with zeros.
boot_sector:
    times 11 db 0x00 ; Ignored by OVMF
    
    ; DOS 2.0 BPB
    ; OVMF requires a sector size within the range [512, 4096].
    ; See edk2/FatPkg/EnhancedFatDxe/Fat.h:63 for more info.
    dw 512          ; Bytes per sector (512, optimal value)
    
    ; Need at least one sector per cluster (duh)
    ; Volume->FirstClusterLba is fixed to be directly after the root directory
    ; sector(s), so there's no weird bug that you could get by setting this to
    ; zero or -1. See edk2/FatPkg/EnhancedFatDxe/Init.c:316 for more info.
    db 1            ; Sectors per cluster (1, optimal value)
    
    ; Can't merge sectors as if ReservedSectors == 0, OVMF returns
    ; EFI_UNSUPPORTED. This is due to a check in the code that assumes this is
    ; not a FAT volume if ReservedSectors is 0.
    ; See edk2/FatPkg/EnhancedFatDxe/Init.c:250 for more info.
    dw 1            ; Reserved sectors (1, optimal value)

    ; Just like ReservedSectors, OVMF returns EFI_UNSUPPORTED if NumFats is
    ; zero. See edk2/FatPkg/EnhancedFatDxe/Init.c:250 for more info.
    db 1            ; # of FATs (1, optimal value)

    ; OVMF returns EFI_UNSUPPORTED if RootEntries is zero and the volume is
    ; not FAT32. See edk2/FatPkg/EnhancedFatDxe/Init.c:285 for more info.
    ; Furthermore, OVMF assumes that the root directory entries will take up a
    ; fixed number of sectors, so any value from 1 to 16 inclusive will equal
    ; a single full sector. See edk2/FatPkg/EnhancedFatDxe/Init.c:312 for more
    ; info.
    ; 16 entries * 32 bytes per entry = 512 bytes, 1 sector.
    dw 16           ; # of root directory entries (16, optimal value)
    
    ; If Sectors and LargeSectors (which we are not using) are both zero, OVMF
    ; returns EFI_UNSUPPORTED. See edk2/FatPkg/EnhancedFatDxe/Init.c:250 for
    ; more info.
    ; OVMF will, understandably, fail to properly read the volume if this is
    ; not just the actual value that it should be.
    dw 4            ; Total logical sectors (4)
    
    db 0            ; Ignored by OVMF

    ; If SectorsPerFat is zero, OVMF assumes that this volume is FAT32. FAT32
    ; requires a whole lot more sectors of random information, so we can't use
    ; it here.
    ; See edk2/FatPkg/EnhancedFatDxe/Init.c:239 for more info.
    dw 1            ; Sectors per FAT (1, optimal value)
    
    times 512 - ($ - boot_sector) db 0 ; Boot code + BIOS signature

; Sector 2: The File Allocation Table
; Since we're using FAT12, things get a little difficult in NASM.
; Two entries are 24 bits = 3 bytes, but each entry is stored a little 
; awkwardly. See Wikipedia for more info.
; 0b111111111111000000000000
; Split
; 0b11111111 0b11110000 0b00000000
; Reverse byte order (little endian)
; 0b00000000 0b11110000 0b11111111

; Table index 0 is the FAT ID (equal to BPB_Media with high bits 1), and index
; 1 is the end-of-chain indicator (0xFFF)
; However, setting them both to zero results in no difference to OVMF.
file_allocation_table:
    ; FAT ID (0), EOC (0), Cluster 2 (EOC)
    db 0x00, 0x00, 0x00, 0xff, 0x0f, 0x00

    ; Fill rest of table with zero bytes
    times 512 - ($ - file_allocation_table) db 0

; Sector 3: Root directory region
; Each entry is 32 or 64 bytes, an 8.3 format chunk followed by an optional
; long file name chunk. Here we will just use 8.3.
; There is only one file in the root directory-- EFI.
; (/EFI/BOOT/BOOTX64.EFI)
root_dir_region:
    db "EFI        "    ; 8.3 file name (11 chars, space padded)
    db 0x10             ; Attributes (DIRECTORY)
    times 14 db 0       ; Ignored to boot by OVMF
    dw 2                ; Low 16 bits first cluster number (cluster 2)
    times 4 db 0        ; Ignored to boot by OVMF

    ; Fill rest of region with zero bytes
    times 512 - ($ - root_dir_region) db 0

; Since indices 0 and 1 of the FAT are reserved, the first cluster is really
; cluster 2. Using some tricks, we can store all of the data we need in this
; single cluster.
; This cluster is:
;   - The contents of /EFI/ (a listing of directories and files, 
;       including BOOT)
;   - The contents of /EFI/BOOT/ (a listing of directories and files,
;       including BOOTX64.EFI)
;   - The contents of BOOTX64.EFI (the file to be booted)
cluster_2:
    ; Once OVMF reads an entry with a zeroed first byte, it stops reading.
    ; Due to how much of a pain OVMF is, the best way to golf this is to
    ; squeeze in the entries where they fit in the EFI and hope for the best.    
    
    ; MZ header
    ; Looks like it's required, even though parts of edk2 are able to handle PE
    ; executables that don't have an MZ header.
    ; EDK II Build Spec. doesn't say much other than "All EFI images must be
    ; formatted PE32/PE32+/COFF." The UEFI specs seem to require the MS-DOS
    ; stub.
    ; There's also extensive support for Terse Executable images (see UEFI 
    ; PI specs section 15), but it appears that won't work here either.
mz_hdr:
    db "MZ"
    times 30 db 0

    ; ./BOOT/, but also MZ header part 2.
    ; Here a bit of a hack is employed to save a sector:
    ; If we say that BOOT occupies cluster 2, we can put BOOTX64.EFI right
    ; in this same sector as it refers back to this.
    db "BOOT       "    ; 8.3 file name (11 chars, space padded)
    db 0x10             ; Attributes (DIRECTORY)
    times 14 db 0       ; Ignored to boot by OVMF
    dw 2                ; Low 16 bits of first cluster number (cluster 2)
    
    ; edk2/BaseTools/Source/C/Common/BasePeCoff.c for some details on how 
    ; e_lfanew is read
    dd pe_hdr - cluster_2   ; e_lfanew   

    ; PE header
pe_hdr:
    dd "PE"         ; mMagic (0x00004550)
    dw 0x8664       ; mMachine (IMAGE_FILE_MACHINE_AMD64)
    dw 1            ; mNumberOfSections (1)
    dd 0            ; mTimeDateStamp (unused)
    dd 0            ; mPointerToSymbolTable (unused)
    dd 0            ; mNumberOfSymbols (unused)
    dw popt_hdr_len ; mSizeOfOptionalHeader (required to be correct value)
    dw 0x222e       ; mCharacteristics (IMAGE_...)
    
    ; PE optional (required by OVMF) header
    ; Setting popt_hdr_len to zero results in RETURN_UNSUPPORTED, so there
    ; doesn't seem to be any way that the code section header can be merged
    ; with the optional header.
pe_opt_hdr:
    dw 0x20B        ; mMagic (PE32+ 64 bit)
    db 0            ; mMajorLinkerVersion (unused)
    db 0            ; mMinorLinkerVersion (unused)
    dd 0            ; mSizeOfCode (unused)
    db "A", 0, 0, 0 ; mSizeOfInitializedData (used to keep FAT entries going)
    dd 0            ; mSizeOfUninitializedData (unused)
    dd 0x1000       ; mAddressOfEntryPoint (0x1000)
    dd 0x1000       ; mBaseOfCode (0x1000)
    dq 0x140000000  ; mImageBase (OVMF ignores this, but with PIC who cares)
    dd 0x1          ; mSectionAlignment (1 byte)
    dd 0x1          ; mFileAlignment (1 byte)
    db "B", 0       ; mMajorOperatingSystemVersion (used to keep FAT going)
    dw 0            ; mMinorOperatingSystemVersion (unused)
    dw 0            ; mMajorImageVersion (unused)
    dw 0            ; mMinorImageVersion (unused)
    dw 0            ; mMajorSubsystemVersion (unused)
    dw 0            ; mMinorSubsystemVersion (unused)
    dd 0            ; mWin32VersionValue (unused)
    dd 0x4000       ; mSizeOfImage (must be weirdly large)
    dd all_hdrs_len ; mSizeOfHeaders
    dd 0            ; mCheckSum (unused)
    dw 0xa          ; mSubsystem (EFI)
    dw 0            ; mDllCharacteristics (unused)
    db "BOOTX64 "               ; mSizeOfStackReserve (entry BOOTX64.EFI)
    db "EFI", 0x20, 0, 0, 0, 0  ; mSizeOfStackCommit (BOOTX64 name + attr)
    db 0, 0, 0, 0, 0, 0, 0, 0   ; mSizeOfHeapReserve (BOOTX64 unused)
    db 0, 0, 2, 0, 0, 2, 0, 0   ; mSizeOfHeapCommit (BOOTX64 unused, clst, sze)
    dd 0            ; mLoaderFlags (unused)
    dd 0            ; mNumberOfRvaAndSizes (unused)

popt_hdr_len: equ $ - pe_opt_hdr

; Code section (only section)
code_sec_hdr:
    times 8 db 0    ; mName (unused)
    dd code_sec_len ; mVirtualSize (small as possible)
    dd 0x1000       ; mVirtualAddress (0x1000)
    dd code_sec_len ; mSizeOfRawData (small as possible)
    dd code_sec-cluster_2   ; mPointerToRawData
    dd 0            ; mPointerToRelocations (unused)
    dd 0            ; mPointerToLinenumbers (unused)
    dw 0            ; mNumberOfRelocations (unused)
    dw 0            ; mNumberOfLinenumbers (unused)
    dd 0x20         ; mCharacteristics (rx)

all_hdrs_len: equ $ - mz_hdr

; OVMF REFUSES to load sections with mPointerToRawData before mSizeOfHeaders,
; and falsifying mSizeOfHeaders results in the EFI file just not loading.
; Overall the PE loader seems pretty robust; see
; edk2/MdePkg/Library/BasePeCoff.c for more info.

code_sec:
    ; Output "Done" and loop forever.
    ; In RDX: EFI_SYSTEM_TABLE*

    ; Get EFI_SYSTEM_TABLE->ConOut (EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL*)
    mov rcx, [rdx+64]

    ; Get EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL->OutputString()
    ; <inlined into call directive>

    ; OutputString(rcx this, rdx string)
    ; `this` is already in RCX
    lea rdx, [rel str]
    
    ; 32 bytes of shadow space is supposed to be allocated, but OVMF runs
    ; without it.
    call [rcx+8] ; Call print

    ; jmp $
    db 0xeb, 0xfe

    ; "Done" + NUL in UTF-16LE
    ; echo -ne "Done\0" | iconv -f UTF-8 -t UTF-16LE |
    ;   hexdump -e ' 1/1 "0x%02x, " ' -v
    ; The last three bytes are 0x00, and since we are at the end of file we can
    ; let OVMF fill in the rest.
    str: db 0x44, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x65

code_sec_len: equ ($ - code_sec) + 3

    ; OVMF fills the rest of the sector with zero bytes, meaning the smaller
    ; the .efi file is, the smaller the image is.

At this point, I think this is pretty close to the lower bound for OVMF without using any major bugs or exploits.

\$\endgroup\$
1
  • \$\begingroup\$ delicious. i was almost sure <=6 blocks + data was possible if you hand-roll the FAT and you've blown that out the water; having the executable and BOOT be the same is a stroke of genius \$\endgroup\$ Jan 2 at 21:03
8
\$\begingroup\$

16897 bytes (33+1 512-byte blocks), OVMF

Reproduce with

base64 -d <<\EOF | lz4 -d > a
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
EOF

Boots as expected:

$ qemu-system-x86_64 -enable-kvm -bios /usr/share/ovmf/OVMF.fd -nographic -drive file=a,format=raw
BdsDxe: failed to load Boot0001 "UEFI QEMU DVD-ROM QM00003 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Master,0x0): Not Found
BdsDxe: loading Boot0002 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
BdsDxe: starting Boot0002 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
Hello, tractor.
^AxQEMU: Terminated

Obtained with

$ for i in $(seq 100); do truncate -s $((512 * i)) a && mkfs.fat -af1 -F12 -h0 --mbr=no -r16 -S 512 -R 1 -b 0 --invariant a && break; done 2>&1 | sed 'N;s/\n/\t/' | uniq -c
      6 mkfs.fat: Attempting to create a too small or a too large filesystem    mkfs.fat 4.2 (2021-01-31)
     61 mkfs.fat: Too few blocks for viable filesystem  mkfs.fat 4.2 (2021-01-31)
      1 mkfs.fat 4.2 (2021-01-31)
$ ls -l a
-rw-r--r-- 1 nabijaczleweli users 34816 Dec 30 02:35 a
# mount -o noatime a a.d
# mkdir -p a.d/EFI/BOOT/
# cp uefi-simple/main.efi a.d/EFI/BOOT/BOOTX64.EFI
# umount a.d
$ truncate -s 16897 a

Why 16897? The first all-zero block is 31, so I iterated on for f in $(seq 31 68); do dd count=$f <a.bkp >b; st -e qemu-system-x86_64 -enable-kvm -bios /usr/share/ovmf/OVMF.fd b -nographic; done, and the first one that booted to the image was 34(*512=17408). Then I tried to trim it back even more, and having just the first byte of block 34 is sufficient, so 17408-511=16897.
I'm assuming this is due to QEMU rounding the block count up with standard semantics, and OVMF needing to "see" the entire \EFI\BOOT\BOOTX64.EFI.

Seeing as these are disk images, byte counts make little sense. Judge this as you wish.

The only limiting condition here is the bootable file which has huge swaths of empty space:

$ wc -c main.efi
6038 main.efi
$ size main.efi
   text    data     bss     dec     hex filename
    211      20       0     231      e7 main.efi

.text EfiMain is a whole of 56 bytes. (Yes, it's possible to produce a 5490-byte executable by getting rid of the print, as well as 4096/3584-byte executables by strip --strip-all resp., the minimum size is the same. I maintain that making a pathologically smaller PE would probably work to shrink this method, but this is not my area of expertise, so who knows.)

main.efi was obtained by building https://github.com/utshina/uefi-simple/blob/e02a105b2d63cf8a116fa64600b6755d625ced28/main.c:

#define IN
#define EFIAPI
#define EFI_SUCCESS 0

typedef unsigned short CHAR16;
typedef unsigned long long EFI_STATUS;
typedef void *EFI_HANDLE;

struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_STRING) (
    IN struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *This,
    IN CHAR16                                   *String
    );

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
    void             *a;
    EFI_TEXT_STRING  OutputString;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef struct {
    char                             a[52];
    EFI_HANDLE                       ConsoleOutHandle;
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
} EFI_SYSTEM_TABLE;

EFI_STATUS
EFIAPI
EfiMain (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
    )
{
    SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello, tractor.\n");
    while(1);
    return EFI_SUCCESS;
}

with clang trunk as cc --target=x86_64-w64-mingw32 -shared -nostdlib -mno-red-zone -fno-stack-protector -Wall -e EfiMain main.c -o main.dll; objcopy --target=efi-app-x86_64 main.dll main.efi, likewise adapted from instructions in the linked.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Magnificent! Well done. \$\endgroup\$
    – Simd
    Dec 30, 2022 at 5:34
  • \$\begingroup\$ This looks as a good baseline, i.e. a straight-forward solution that does not really use any space-saving tricks. \$\endgroup\$ Dec 30, 2022 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.