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 😁
464b
😎
❯ cargo +nightly install anonlink
❯ anonlink
❯ cargo +nightly run --release
Hello World!
❯ cargo +nightly build --release && (Get-Item ".\target\release\min-sized-rust-windows.exe").Length
Compiling min-sized-rust-windows v0.1.0 (**\min-sized-rust-windows)
Finished release [optimized] target(s) in 1.33s
464
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 ofmain.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.
- Using the LINK.exe
- No imports.
- To avoid having an extra
.idata
section (more bytes and cannot be merged into.text
section usingLINK.exe
) we do the following. - Resolve stdout handle from
PEB
's process parameters (thanks ChrisSD). [3][4] - Invoke
NtWriteFile
/ZwWriteFile
using syscall0x80
. [5][6]- This is undocumented behaviour in windows, syscalls change over time. [5]
- 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.
- To avoid having an extra
- 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.
- A custom built stub created to remove
- Drop debug info in pe header.
- Add
/EMITPOGOPHASEINFO /DEBUG:NONE
flags. - Credits to @Frago9876543210 for finding, and implementing this.
- Add
- Using strategies shown in [2] we could post process the exe and merge headers to get closer to the 600-500b mark although we start straying away from the goal of this project.
- Provided the call signature of
ZwWriteFile
I could usebuild.rs
to make a script to dynamically resolve the syscall number fromntdll
using something like iced-x86. - Go pure assembly (drop type definitions for PEB).
- https://github.com/johnthagen/min-sized-rust
- www.catch22.net/tuts/win32/reducing-executable-size#use-the-right-linker-settings
- https://github.com/pts/pts-tinype
- https://news.ycombinator.com/item?id=25266892 (Thank you anonunivgrad & ChrisSD!)
- 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
- https://j00ru.vexillium.org/syscalls/nt/64/
- https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntwritefile
- @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
😁