7 ms·
Show HN: Tiny 1k Rust binary on Windows
- mcountryman 6y agoThis is my attempt at making the smallest exe with rust for windows using the msvc toolchain. If y'all have any pointers how to get it smaller let me know :)
- mytailorisrich 6y agoOn Windows the smallest exe can be achieved by writing in assembly and compiling as .com executable. These literally just contain the compiled assembly and are usually used on, for example, the 4k demoscene. 1k with rust is certainly a nice squeeze.
- poizan42 6y agoThat's a DOS executable though, and won't run on 64-bit Windows.
- mytailorisrich 6y agoNot out of the box but with something like vDos it should.
- weinzierl 6y agoRegular com programs won't run, but I'm waiting for the day the vulnerability is discovered that let's you run a specially crafted binary because of left over code from the old com launcher. Is the com extension still recognized in 64-bit windows?
- zinekeller 6y agoNo, like really impossible on a hardware level (most 16-bit instructions are hidden on hardware level on entering 64-bit mode) and on a software level (NTVDM is separate from Windows NT from day one (since it only works on x86 hardware and not on DEC Alpha for example) and it was an optional component since Vista but only on an x86 installation though).
- poizan42 6y agoConsidering that COM programs are headerless I can't imagine any way a specially crafted binary could do anything since there is no parsing going on. The "COM loader" just hands it off to NTVDM which does not exist on 64-bit Windows (there are 3rd-party implementations though) And yes, the com extension is still recognized and will pop up a dialog telling you "This app can't run on your PC". But actually .com and .exe files are handled equivalently, so you can just rename an .exe (i.e. PE executable) to .com and it will run - that doesn't make it a COM executable though...
- jariRG 6y agoThe size limited demos are use techniques like in the OP, scrapping the runtime, no std, filling the unused exe header sections with own data, quantizing floating point values, etc. And/or exe packers like Crinkler and kkrunchy. COM's are no longer used.
- userbinator 6y agoThese literally just contain the compiled assembly and are usually used on, for example, the 4k demoscene. There are plenty of 1k demos too, on Windows and otherwise: http://www.pouet.net/prodlist.php?type%5B%5D=1k http://www.pouet.net/prodlist.php?type%5B%5D=1k
- stephen82 6y agoI have found this https://github.com/HugoPlatzer/tiny-rust-binaries https://github.com/HugoPlatzer/tiny-rust-binaries. Can you test it and see how it behaves with msvc toolchain?
- quietbritishjim 6y agoThat contains a lot of platform-specific build instructions for Linux (and for the ELF file format used on Linux but not Windows), so it's never going to work with the msvc toolchain.
- stephen82 6y agoFor some reason, I saw ELF as EXE! hahaha ^_^ My bad everyone, my bad!
- mcountryman 6y agoI linked these guys here https://github.com/pts/pts-tinype https://github.com/pts/pts-tinype. They managed to do something similar with the PE headers, and I might have to try that next although I'm doubtful win64 exes are permitted to run if they are >1Kb
- pantalaimon 6y agoI'm disappointed. In C I can get a 1k Hello Word binary simply be writing int main(void) { puts("Hello World!"); return 0; } The Rust version is a mess [0] [0] https://github.com/mcountryman/min-sized-rust-windows/blob/master/src/main.rs https://github.com/mcountryman/min-sized-rust-windows/blob/m...
- deleted 6y ago[deleted]
- slezyr 6y agoYou're comparing C+StdLib vs. Rust without StdLib. Disable stdlib for C version and use the following program: #include <windows.h> int _start() { const char str[] = "Hello world!\n"; HANDLE stdout = GetStdHandle(STD_OUTPUT_HANDLE); DWORD written; WriteFile(stdout, str, sizeof(str), &written, NULL); ExitProcess(12); return 0; } https://stackoverflow.com/a/42536990 https://stackoverflow.com/a/42536990 Which is similar to what the Rust program does. The result should be significantly less than 1k
- zamadatix 6y agoI think the thing being measured by GP is "easiest way to get to the 1kb mentioned in the title" not "most complicated way to get as small as possible".
- ChrisSD 6y agoI tried your program with MSVC using the following command: cl small2.c /MD /link /entry:_start /subsystem:CONSOLE kernel32.lib It's 5kb.
- slezyr 6y agoIf I remember correctly you need a /nodefaultlib flag to disable stdlibs https://docs.microsoft.com/en-us/cpp/build/reference/nodefaultlib-ignore-libraries?redirectedfrom=MSDN&view=msvc-160 https://docs.microsoft.com/en-us/cpp/build/reference/nodefau...
- KwanEsq 6y agoI notice you moved from asm! to llvm_asm! [0]. Since the commit message doesn't explain why, would you be able to do so here? (Purely for my own curiosity) [0] https://github.com/mcountryman/min-sized-rust-windows/commit/86ac67a46b6ffcc470a922c60d28bcbd7643e7ba https://github.com/mcountryman/min-sized-rust-windows/commit...
- cdirkx 6y agoThey are the same: asm! was replaced by llvm_asm! because it currently only supports llvm asm and the name might suggest otherwise [1]. A backend independant syntax for asm! was recently specified [2], but that will take some time to implement. [1] https://github.com/rust-lang/rfcs/pull/2843 https://github.com/rust-lang/rfcs/pull/2843 [2] https://github.com/rust-lang/rfcs/pull/2873 https://github.com/rust-lang/rfcs/pull/2873
- steveklabnik 6y agoThey are not the same, and the asm! version is already implemented; the RFC came with an implementation. That first PR moved the old asm to llvm_asm, and then the new asm was built after a period of time so folks could migrate their code to llvm_asm, as a temporary measure. This commit happened after that transition so this has to be from the new asm back to the transition version.
- cdirkx 6y agoOops, my bad, I was working with an outdated understanding of the situation. Thanks for correcting me.
- steveklabnik 6y agoNo problem :) It was a feature that had no changes for a long time, and now has had a bunch of improvements and is finally on a path to stable. It can be hard to keep up with things sometimes.
- mcountryman 6y ago
- winter_blue 6y agoZig actually does a good job of cutting final output bytes automatically, even with standard library use. I think (?) its compiler sort-of implements a more advanced version of Unix strip. Andrew Kelley could probably chime in more on it.
- acqq 6y agoIt's very easy and fast to try: using 0.7.0+51d7c14ce from https://ziglang.org/download/ https://ziglang.org/download/ and: const std = @import("std"); pub fn main() void { std.debug.print("Hello, world!\n", .{}); } ---------- zig build-exe hello.zig -O ReleaseSmall --strip --single-threaded -target x86_64-windows Resulting hello.exe is 3072 bytes. From these 2560 are zero bytes. The only system calls from the resulting binary are to ExitProcess, GetLastError and WriteFile from KERNEL32.dll Additionally, changing the release optimizations: zig build-exe hello.zig -O ReleaseFast --single-threaded --strip -target x86_64-windows produces hello.exe of 2560 bytes, 1849 from which are zeroes. Note all: winter_blue has right to mention Zig. It's about what's provided out of the box in that new language and the provided optimizations are really effective. Edit: asking mcountryman to explain: "the compiled rust exe has 480 non-zero bytes" -- I think you mean not just "compiled" but "hand-optimized writing hundreds of lines of the rust source code and not using standard library"? https://github.com/mcountryman/min-sized-rust-windows/blob/master/src/main.rs https://github.com/mcountryman/min-sized-rust-windows/blob/m... https://github.com/mcountryman/min-sized-rust-windows/blob/master/src/types.rs https://github.com/mcountryman/min-sized-rust-windows/blob/m... Edit2: Please note that zig doesn't eventually link to printf and libc there, but links and resolves the std calls from the source to the calls to KERNEL32.dll.
- mcountryman 6y agoJust for fun, the compiled rust exe has 480 non-zero bytes, the zig exe had 512 bytes.
- mcountryman 6y agoAh, yeah, I wasn't trying to shrug off the usefulness of zig, sorry. I think when it comes to compile size zig does it a bit better due to it's smaller std although, when compiling rust with just a link to libc and a printf call I believe you get an exe ~2048b when optimizing for size so I'm not certain how well that scales. The 100 line hack I used was to noop out the `.idata` section which link.exe refused to merge/shrink, that only saved me 1Kb in the end. Looking at the section info of the zig exe it looks like there is some black magic going on with it's alignment so I may have to dig deeper to see what zig is doing here. Also thank you both for pointing zig out, I haven't heard of it until now and it may come in handy for something else I'm planning on making :)
- ziml77 6y agoI know making tiny Windows executables like this is done just for fun, but I'm curious if there's technically a performance loss from merging sections. IIRC the sections are split up and 4k aligned because they have to be page aligned in memory anyway.
- anonunivgrad 6y agoIf you're willing to rely on undocumented behavior, you can always skip kernel32 and use ntdll.dll directly. Or you could go even further and direct invoke the right syscalls. But either way, you'd be relying on behavior that Microsoft can change without notice.
- ChrisSD 6y agoThe Win32 loader will always load kernel32 no matter what. You don't even need it in your import table. It's just a matter of finding where it's loaded and calling into the right functions.
- mcountryman 6y agoBetween a 40b strcmp method (If I had/if it was more widely supported I could use sse4.2 strcmp) and ~74b of resolving said exports a couple syscalls sounds pretty nice provided the context switch isn't more expensive than resolving the imports.
- deleted 6y ago[deleted]
- ChrisSD 6y agoWell if you really want to you can get the output handle from the PEB[0]. You can then call NtWriteFile[1] using syscall `0x0008` (Windows 10 x64 only)[2]. [0]: 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://processhacker.sourceforge.io/doc/struct___r_t_l___u_... [1]: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntwritefile https://docs.microsoft.com/en-us/windows-hardware/drivers/dd... [2]: https://j00ru.vexillium.org/syscalls/nt/64/ https://j00ru.vexillium.org/syscalls/nt/64/
- mcountryman 6y agoI completely forgot stdout handle is in the PEB! Thanks! After learning how the windows x64 syscall calling convention works I got it working on win10. https://i.imgur.com/ErHU7Kr.png https://i.imgur.com/ErHU7Kr.png https://github.com/mcountryman/min-sized-rust-windows/commit/4d54424edef96d4a3bca91e8ee75d534bda25d43 https://github.com/mcountryman/min-sized-rust-windows/commit...
- pornel 6y agoNote that a typical "Hello World" executable written in C does not contain any code capable of actually printing anything. It links to libc shipped with the operating system. That is about 30MB of code that C executables get for free. You can link libc statically and dead-code-eliminate everything except print. That's what Rust does too, but Rust has fancier machinery for formatting and handling of panics with backtraces. These bits don't get dead-code-eliminated as thoroughly in Rust (probably because the print could theoretically fail).
- colejohnson66 6y agoRant: And below libc is probably X or Wayland which tells the kernel where to put pixels. And below that is the kernel and graphics driver which figures out how to do that and communicates it to the GPU. And below that is the actual GPU which figures out how to modulate an array of pixels into an HDMI signal. When do we stop quibbling about the size of binaries? It’s pointless to point out that small code relies on other abstractions. No one complains about tiny demo scene programs using tons of other stuff built into the OS. So why here?
- naikrovek 6y agoYour understanding of computers does not seem correct at all... But maybe it's my understanding that wrong. I don't know. C64 demos and 8088 DOS demos use very little OS infrastructure. Obviously things like JavaScript demos do. The challenges presented by a particular piece of hardware or software in the demo scene are chosen for different reasons. Sometimes, the demo author wants to show how well they can use the existing OS infrastructure. Sometimes they want to show what can be done without it. In both cases, small sizes are impressive.
- pornel 6y agoI'm not complaining that a Hello World exe doesn't come with its own operating system. It's just worth knowing when you compare exe sizes that C doesn't have an order of magnitude more efficient code generation. It just had a >30-year head start to get its standard library shipped with every operating system. Other languages typically don't have that advantage, and either have to reuse libc, or take a hit from statically linking their own libstd.