BGGP23

eight kilos and no swap

a screenshot of the result

a screenshot of the result

While I think that golf should be abolished, I think binary golf is pretty neat. So I participated in this year’s Binary Golf Grand Prix. And I didn’t win.

But at least you’ll learn how to write a small UEFI application in Rust.

what are we going to do?

This year’s BGGP had the following objective:

A valid submission will:

  • Produce exactly 1 copy of itself.

  • Name the copy “4”.

  • Not execute the copied file.

  • Print, return, or display the number 4.

A program doing this could be implemented in many different file formats1, I chose “UEFI app” because I know the stack quite well by now, I think.

make it work

The first thing we’ll need to do is to create an application that prints 4:

rustup target add x86_64-unknown-uefi
cargo new --bin four
cargo add uefi
cargo add uefi-services

Put the following in .cargo/config.toml:

[build]
target = "x86_64-unknown-uefi"

and the following in src/main.rs

#![no_std]
#![no_main]

use uefi::prelude::*;
use uefi_services::println;

#[entry]
fn efi_main(image: Handle, mut systab: SystemTable<Boot>) -> Status {
    uefi_services::init(&mut systab).unwrap();
    println!("4");
    Status::SUCCESS
}

That was easy, right? Let’s see whether this actually works:

cargo build
cargo install uefi-run
uefi-run target/x86_64-unknown-uefi/debug/four.efi

Cool, right?

Now let’s copy the file:

cargo add uefi --features alloc
#![no_std]
#![no_main]

use uefi::{
    prelude::*,
    proto::{
        device_path::text::{AllowShortcuts, DisplayOnly},
        loaded_image::LoadedImage,
    },
};
use uefi_services::println;

#[entry]
fn efi_main(image: Handle, mut systab: SystemTable<Boot>) -> Status {
    uefi_services::init(&mut systab).unwrap();
    let mut fs = systab.boot_services().get_image_file_system(image).unwrap();
    let loaded_image = systab
        .boot_services()
        .open_protocol_exclusive::<LoadedImage>(image)
        .unwrap();
    let src_path = loaded_image
        .file_path()
        .unwrap()
        .to_string(
            systab.boot_services(),
            DisplayOnly(false),
            AllowShortcuts(false),
        )
        .unwrap()
        .unwrap();
    fs.copy(&*src_path, cstr16!("4")).unwrap();
    println!("4");
    Status::SUCCESS
}

So, this works. This is the MVP. And it is 50kB.

making it smaller

The easiest thing to do is to simply ask the Rust compiler to produce small binaries. Put the following into .config/cargo.toml:

[build]
target = "x86_64-unknown-uefi"

# see https://github.com/johnthagen/min-sized-rust
[profile.release]
opt-level = "z"  # Optimize for size.
lto = true
codegen-units = 1
panic = "abort"

Now we’re down to 40kB (and we have worse error handling).

Why is this not getting smaller? Mostly because of the standard library. It comes prebuilt, it is not affected by our build-configuration. We could rebuild it, but we can’t.

who needs features anyway?

Let’s take a look at all dependencies and the features we’re using:

$ cargo tree --edges features
four v0.1.0
├── uefi feature "alloc"   └── uefi v0.24.0
│       ├── log v0.4.19
│       ├── ptr_meta v0.2.0
│          └── ptr_meta_derive feature "default"              └── ptr_meta_derive v0.2.0 (proc-macro)                  ├── proc-macro2 feature "default"                     ├── proc-macro2 v1.0.60
│                        └── unicode-ident feature "default"                            └── unicode-ident v1.0.9
│                     └── proc-macro2 feature "proc-macro"                         └── proc-macro2 v1.0.60 (*)                  ├── quote feature "default"                     ├── quote v1.0.28
│                        └── proc-macro2 v1.0.60 (*)                     └── quote feature "proc-macro"                         ├── quote v1.0.28 (*)                         └── proc-macro2 feature "proc-macro" (*)                  ├── syn feature "default"                     ├── syn v1.0.109
│                        ├── proc-macro2 v1.0.60 (*)                        ├── quote v1.0.28 (*)                        └── unicode-ident feature "default" (*)                     ├── syn feature "clone-impls"                        └── syn v1.0.109 (*)                     ├── syn feature "derive"                        └── syn v1.0.109 (*)                     ├── syn feature "parsing"                        └── syn v1.0.109 (*)                     ├── syn feature "printing"                        ├── syn v1.0.109 (*)                        └── syn feature "quote"                            └── syn v1.0.109 (*)                     └── syn feature "proc-macro"                         ├── syn v1.0.109 (*)                         ├── proc-macro2 feature "proc-macro" (*)                         ├── quote feature "proc-macro" (*)                         └── syn feature "quote" (*)                  └── syn feature "full"                      └── syn v1.0.109 (*)       ├── bitflags feature "default"          └── bitflags v2.3.2
│       ├── ucs2 feature "default"          └── ucs2 v0.3.2
│              └── bit_field feature "default"                  └── bit_field v0.10.2
│       ├── uefi-macros feature "default"          └── uefi-macros v0.12.0 (proc-macro)              ├── proc-macro2 feature "default" (*)              ├── quote feature "default" (*)              ├── syn feature "default"                 ├── syn v2.0.20
│                    ├── proc-macro2 v1.0.60 (*)                    ├── quote v1.0.28 (*)                    └── unicode-ident feature "default" (*)                 ├── syn feature "clone-impls"                    └── syn v2.0.20 (*)                 ├── syn feature "derive"                    └── syn v2.0.20 (*)                 ├── syn feature "parsing"                    └── syn v2.0.20 (*)                 ├── syn feature "printing"                    ├── syn v2.0.20 (*)                    └── syn feature "quote"                        └── syn v2.0.20 (*)                 └── syn feature "proc-macro"                     ├── syn v2.0.20 (*)                     ├── proc-macro2 feature "proc-macro" (*)                     ├── quote feature "proc-macro" (*)                     └── syn feature "quote" (*)              └── syn feature "full"                  └── syn v2.0.20 (*)       ├── uefi-raw feature "default"          └── uefi-raw v0.3.0
│              ├── ptr_meta v0.2.0 (*)              ├── bitflags feature "default" (*)              └── uguid feature "default"                  └── uguid v2.0.0
│       └── uguid feature "default" (*)
├── uefi feature "default"   ├── uefi v0.24.0 (*)   └── uefi feature "panic-on-logger-errors"       └── uefi v0.24.0 (*)
└── uefi-services feature "default"
    ├── uefi-services v0.21.0
       ├── log v0.4.19
       ├── uefi feature "default" (*)
       ├── uefi feature "global_allocator"
          └── uefi v0.24.0 (*)
       └── cfg-if feature "default"
           └── cfg-if v1.0.0
    ├── uefi-services feature "logger"
       ├── uefi-services v0.21.0 (*)
       └── uefi feature "logger"
           └── uefi v0.24.0 (*)
    └── uefi-services feature "panic_handler"
        └── uefi-services v0.21.0 (*)

Do we actually need all of them?

No. We can get rid of the alloc feature and of uefi-services if we assume that everything is in the top-level directory, don’t use println! and write a panic handler:

cargo remove uefi-services
cargo remove uefi
cargo add uefi --no-default-features
#![no_std]
#![no_main]
#![feature(panic_info_message)]

use core::{mem::MaybeUninit, panic::PanicInfo};

use uefi::{
    prelude::*,
    proto::{
        device_path::media::FilePath,
        loaded_image::LoadedImage,
        media::{
            file::{File, FileAttribute, FileMode},
            fs::SimpleFileSystem,
        },
    },
};

#[entry]
fn efi_main(image: Handle, mut systab: SystemTable<Boot>) -> Status {
    copy(image, &systab);
    systab.stdout().output_string(cstr16!("4\n")).unwrap();
    Status::SUCCESS
}

fn copy(image: Handle, systab: &SystemTable<Boot>) {
    let loaded_image = systab
        .boot_services()
        .open_protocol_exclusive::<LoadedImage>(image)
        .unwrap();
    let mut fs = systab
        .boot_services()
        .open_protocol_exclusive::<SimpleFileSystem>(loaded_image.device())
        .unwrap();
    let src_path = loaded_image.file_path().unwrap();
    let mut dir = fs.open_volume().unwrap();
    let first_path_entry: &FilePath = src_path.node_iter().next().unwrap().try_into().unwrap();
    let mut file_name_buffer: [MaybeUninit<u16>; 256] = [MaybeUninit::uninit(); 256];
    let mut src_file = dir
        .open(
            first_path_entry
                .path_name()
                .to_cstr16(&mut file_name_buffer)
                .unwrap(),
            FileMode::Read,
            FileAttribute::empty(),
        )
        .unwrap()
        .into_regular_file()
        .unwrap();
    let mut dst_file = dir
        .open(
            cstr16!("4"),
            FileMode::CreateReadWrite,
            FileAttribute::empty(),
        )
        .unwrap()
        .into_regular_file()
        .unwrap();
    let mut data_buffer: [u8; 4] = [0; 4];
    loop {
        let read_count = src_file.read(&mut data_buffer).unwrap();
        if read_count == 0 {
            break;
        }
        dst_file.write(&data_buffer).unwrap();
    }
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Sure, this is worse code, but it has less dependencies:

$ cargo tree --edges features
four v0.1.0
└── uefi v0.24.0
    ├── log v0.4.19
    ├── ptr_meta v0.2.0
       └── ptr_meta_derive feature "default"
           └── ptr_meta_derive v0.2.0 (proc-macro)
               ├── proc-macro2 feature "default"
                  ├── proc-macro2 v1.0.60
                     └── unicode-ident feature "default"
                         └── unicode-ident v1.0.9
                  └── proc-macro2 feature "proc-macro"
                      └── proc-macro2 v1.0.60 (*)
               ├── quote feature "default"
                  ├── quote v1.0.28
                     └── proc-macro2 v1.0.60 (*)
                  └── quote feature "proc-macro"
                      ├── quote v1.0.28 (*)
                      └── proc-macro2 feature "proc-macro" (*)
               ├── syn feature "default"
                  ├── syn v1.0.109
                     ├── proc-macro2 v1.0.60 (*)
                     ├── quote v1.0.28 (*)
                     └── unicode-ident feature "default" (*)
                  ├── syn feature "clone-impls"
                     └── syn v1.0.109 (*)
                  ├── syn feature "derive"
                     └── syn v1.0.109 (*)
                  ├── syn feature "parsing"
                     └── syn v1.0.109 (*)
                  ├── syn feature "printing"
                     ├── syn v1.0.109 (*)
                     └── syn feature "quote"
                         └── syn v1.0.109 (*)
                  └── syn feature "proc-macro"
                      ├── syn v1.0.109 (*)
                      ├── proc-macro2 feature "proc-macro" (*)
                      ├── quote feature "proc-macro" (*)
                      └── syn feature "quote" (*)
               └── syn feature "full"
                   └── syn v1.0.109 (*)
    ├── bitflags feature "default"
       └── bitflags v2.3.2
    ├── ucs2 feature "default"
       └── ucs2 v0.3.2
           └── bit_field feature "default"
               └── bit_field v0.10.2
    ├── uefi-macros feature "default"
       └── uefi-macros v0.12.0 (proc-macro)
           ├── proc-macro2 feature "default" (*)
           ├── quote feature "default" (*)
           ├── syn feature "default"
              ├── syn v2.0.20
                 ├── proc-macro2 v1.0.60 (*)
                 ├── quote v1.0.28 (*)
                 └── unicode-ident feature "default" (*)
              ├── syn feature "clone-impls"
                 └── syn v2.0.20 (*)
              ├── syn feature "derive"
                 └── syn v2.0.20 (*)
              ├── syn feature "parsing"
                 └── syn v2.0.20 (*)
              ├── syn feature "printing"
                 ├── syn v2.0.20 (*)
                 └── syn feature "quote"
                     └── syn v2.0.20 (*)
              └── syn feature "proc-macro"
                  ├── syn v2.0.20 (*)
                  ├── proc-macro2 feature "proc-macro" (*)
                  ├── quote feature "proc-macro" (*)
                  └── syn feature "quote" (*)
           └── syn feature "full"
               └── syn v2.0.20 (*)
    ├── uefi-raw feature "default"
       └── uefi-raw v0.3.0
           ├── ptr_meta v0.2.0 (*)
           ├── bitflags feature "default" (*)
           └── uguid feature "default"
               └── uguid v2.0.0
    └── uguid feature "default" (*)

And we’re down to 13kB. What is there left to do? Well, we can pack it:

$ upx --best target/x86_64-unknown-uefi/release/four.efi
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2023
UPX 4.0.2       Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 30th 2023

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     12800 ->      7168   56.00%    win64/pe     four.efi                      

Packed 1 file.

And this is it: We’re down to roughly 7kB.


  1. Some of them aren’t even executables. 


Kommentare

Die eingegebenen Daten und der Anfang der IP-Adresse werden gespeichert. Die E-Mail-Adresse wird für Gravatar und Benachrichtungen genutzt, Letzteres nur falls gewünscht. - Fragen oder Bitte um Löschung? E-Mail an (mein Vorname)@ytvwld.de.