Skip to content

mcountryman/min-sized-rust-windows

Repository files navigation

Minimum Binary Size Windows

CI

The smallest hello world I could get on win10 x64 in rust. This isn't something meant to be used in production, more of a challenge. I'm in no ways an expert and I have seen windows binaries get smaller on windows. [2] If you can go smaller let me know how you did it 😁

Results

268b 😎

❯ cargo pack
   Compiling rivet-mini v0.1.0 (**\min-sized-rust-windows\rivet-mini)
    Finished `release` profile [optimized] target(s) in 1.69s
     Running `target\release\packer.exe`
   Compiling min-sized-rust-windows v0.1.0 (**\min-sized-rust-windows)
    Finished `release` profile [optimized] target(s) in 3.10s
Wrote 268 bytes to ./target/release/msrw.exe (40.2% reduction)

❯ .\target\release\msrw.exe && (Get-Item ".\target\release\msrw.exe").Length
Hello World!
268

Strategies

I'm excluding basic strategies here such as enabling lto and setting opt-level = 'z'. [0]

  • no_std
  • no_main
  • Merge .rdata and .pdata sections into .text section linker flag. [1]
    • Using the LINK.exe /MERGE
      flag found at the bottom of main.rs.
    • Section definitions add more junk to the final output, and I believe they have a min-size. For this example we really don't care about readonly data (.rdata) or exception handlers (.pdata) so we "merge" these empty sections into the .text sections.
  • No imports.
    • To avoid having an extra .idata section (more bytes and cannot be merged into .text section using LINK.exe) we do the following.
    • Resolve stdout handle from PEB's process parameters (thanks ChrisSD). [3][4]
    • Invoke NtWriteFile/ZwWriteFile using syscall 0x80. [5][6]
      1. This is undocumented behaviour in windows, syscalls change over time. [5]
      2. I can't guarantee this will work on your edition of windows.. it's tested on my local machine (W10) and on GH actions (windows-2022 and windows-2019) server editions.
  • Custom LINK.exe stub.
    • A custom built stub created to remove Rich PE header. More information can be found here.
    • Credits to @Frago9876543210 for finding, and implementing this.
  • Drop debug info in pe header.
    • Add /EMITPOGOPHASEINFO /DEBUG:NONE flags.
    • Credits to @Frago9876543210 for finding, and implementing this.
  • Custom Packer (rivet-mini)
    • We implemented a custom packer that overlaps the PE header with the DOS header and trims the Optional Header to the absolute minimum.
    • It also shifts sections to close gaps and merges overlay data.
    • Achieved 268 bytes.

Future

  • Provided the call signature of ZwWriteFile I could use build.rs to make a script to dynamically resolve the syscall number from ntdll using something like iced-x86.
  • Go pure assembly (drop type definitions for PEB).

References

  1. https://github.com/johnthagen/min-sized-rust
  2. www.catch22.net/tuts/win32/reducing-executable-size#use-the-right-linker-settings
  3. https://github.com/pts/pts-tinype
  4. https://news.ycombinator.com/item?id=25266892 (Thank you anonunivgrad & ChrisSD!)
  5. https://processhacker.sourceforge.io/doc/struct___r_t_l___u_s_e_r___p_r_o_c_e_s_s___p_a_r_a_m_e_t_e_r_s.html
  6. https://j00ru.vexillium.org/syscalls/nt/64/
  7. https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntwritefile

Credits

  • @Frago9876543210 - Brought binary size from 760b -> 600b 😁

  • @Frago9876543210 - Brought binary size from 600b -> 560b 😁

  • @ironhaven - Brought binary size from 560b -> 536b 😁

  • @StackOverflowExcept1on - Brought binary size from 536b -> 464b 😁

  • @realJoshByrnes - Brought binary size from 464b -> 448b 😁

  • @realJoshByrnes (rivet-mini packer) - Brought binary size from 448b -> 268b 🚀

About

🦀 464b rust binary on windows

Topics

Resources

License

Stars

Watchers

Forks

Contributors 8

Languages