How fat does a fat binary need to be? (2021)
justine.lol
justine.lol
Justine's impressive work on this subject makes me wonder if it's possible to build truly native applications using more complete toolsets and more modern languages. Multiplatform toy programs have existed for ages, but this SDK of sorts makes it possible to write executables that don't require manually tricking the compiler or adjusting your code for portability with inline assembly. As long as the program is statically compiled and contains the necessary drivers for things like syscalls, this should be perfectly possible, though the current implementation isn't exactly user friendly, relying on giant headers like https://justine.lol/cosmopolitan/cosmopolitan.h, statically linked components like https://justine.lol/cosmopolitan/crt.o and https://justine.lol/cosmopolitan/ape.o, and other such tweaks.
I'd love a Go/Rust compile target that just works regardless of operating system. As an added bonus, the bare metal nature would be excellent for platforms like Firecracker to run services without even needing to boot a kernel!
And, if you're shipping into an app store, there's really no point in using something like this.
BTW I also worked on a small in-house system that was just a run-it-from-the-desktop install (aka xcopy install) and it worked perfectly. Boss insisted we needed a "proper" installer so I tried the installer-maker provided by MS. Jesus what a fucked-up mess that made of the whole process, a day of trying to get that working (while it sprayed guids and subdirectories everywhere and could't even uninstall what it installed) and I gave up.
I also have a fun story of an MSSQL uninstall that went wrong on a live server.
executive summary is that xcopy installs work very well IME.
Which you realistically don't, hence the point of the comparison.
> executive summary is that xcopy installs work very well IME.
The point isn't the initial install, it's the subsequent lookups (including update, uninstallation, etc.) when you have a bunch of programs to deal with. When you're dealing with 1 program, you can treat it like your pet. But when you're dealing with 50, you can't do that anymore. You need to realize you have cattle and adjust accordingly.
Phone numbers are nothing like programs. The comparison is false.
> The point isn't the initial install, it's the subsequent lookups (including update, uninstallation, etc.).
Well perhaps, but if you can delete a program by literally deleting the file, and update it literally by replacing it then you are onto a good thing - unix-like simplicity. Dependencies may be a problem, or not.
That wasn't the comparison in the first place.
Finding the location of the program and its uninstaller is very much like an address or phone number lookup. Updating it is like updating an address book. And you need a database to maintain it all and find what you need, and something has to update that database. That's one major thing an installer does.
Possible, but a lot harder than doing it the Linux way.
Some examples of apps spanning many files and directories are configuration files, cached data, and plugins. For uninstallation, much of that stuff should definitely be deleted; for upgrades, there are occasionally conflicts which installer systems can help to manage (though, often enough, they can only do part of what needs to be done).
Especially for porting existing projects, I recommend against using the amalgamation as sometimes including everything all the time causes problems.
> Linux, Mac, Windows, FreeBSD, OpenBSD, NetBSD, and BIOS
1. Windows
2. Linux
3. Mac (which is a descendent of BSD, though with different kernel.)
4. BSD (including FreeBSD, OpenBSD, NetBSD)
The "OS" in BIOS doesn't stand for Operating System! It's Basic Input/Output System.
And of course it would list every BSD instance. Supporting them isn’t automatic when you’re taking about pre-compiles binaries because they can have subtly different ABIs. POSIX helps here but mostly at the source level, an ELF compiled for FreeBSD wouldn’t automatically run on OpenBSD nor NetBSD. The fact this does is unusual and deserves calling out.
Sorry to be a pedant, but DragonflyBSD is missing.
If so, ok, sure, then you have a point. Otherwise, no, those are three distinct OSes, and it's entirely correct to list them as such.
I know you can't mix macOS into that, so the "or 4 if I'm generous" is pretty unfair.
> The "OS" in BIOS doesn't stand for Operating System! It's Basic Input/Output System.
I don't think the author is claiming it means "Operating System", just that "BIOS" is a simple shorthand for "bare metal". Which no, isn't an "operating system" in the classical sense, but it is a standardized set of very low-level system services that sorta vaguely kinda acts like one. And if we consider UEFI rather than old-school PC-BIOS, it's even more like an OS.
Your comment just feels like pedantry for pedantry's sake (and not particularly correct pedantry, at that). Not sure what you're trying to accomplish here by minimizing the achievement.
I'm going to strongly disagree; BSD hasn't been a single operating system since 1995 (4.4BSD-Lite Release 2, the last BSD version from Berkeley). The extant BSDs are closely related, derive from a common original source, and semi-frequently share code back and forth, but they're separate systems with different code and different ABIs.
How Fat Does a Fat Binary Need to Be? - https://news.ycombinator.com/item?id=26103769 - Feb 2021 (67 comments)
I laughed at this. I am a Go programmer, FWIW :)
https://hn.algolia.com/?dateRange=all&page=0&prefix=false&qu...
package main
func main() {
println("hello world")
}
then: go build -ldflags -s -gcflags all=-l hello.go
ls hello
result: Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022-08-30 7:47 PM 747520 hello.exe .byte 0x66,0x2e,0x0f,0x1f,0x84,0x00,0x00,0x00 # f.▼ä
.byte 0x00,0x00
ld.bfd uses them to fill empty spaces. See also `o//tool/decode/x86opinfo.com 66 2e 0f 1f 84 00 00 00 00 00` and https://justine.lol/dox/modrm.txt and https://justine.lol/dox/geek.txtBTW, once I enable windows I think there's padding at the end of the binary section that's hard to notice, it might be good to replace it with something visible. I think it's coming from `ape_idata_iatend`.
That's one of my very few gripes with Rust: compared to what Zig / C / C++ can produce, Rust's release binaries are quite huge. There's likely a lot of possible work to be done in the area of tree shaking and generally only leaving in what's actually used in the final binary. I hope this becomes a priority soon-ish.
Example: you use a crate with 13 functions. You only use 1 but it depends on 2 others in the same crate that don't depend on anything else. Why shouldn't the end result only compile 3 of the 13 functions?
Or maybe I am misunderstanding or I am badly informed (not a compiler / linker engineer so I'm likely with naive assumptions) -- apologies if so.
I understand it's not easy, I am just a bit surprised that Rust's team is not chasing this a bit more aggressively, let's say. It puts a slight stain on an otherwise excellent project.
I'm no Rust expert though.
If your deps ask for 1.2.3, 1.2.4, and 2.0.0, your binary will include the necessary functions from 1.2.4 and 2.0.0.
(Disclaimer: I'm mostly guessing here, but am I missing something important?)
AFAIU the choice here also considers how easily things will build and link, and whether your package manager/build tool will need a SAT-solver to figure out which version of each library to use, and even then you can still run into unsatisfiable restrictions (`Could not resolve dependencies`) if libraries are not adequately updated/maintained.
It seems that by allowing duplication "only" pay for the libraries that are not updated (including all their deps), which means that you are trading computer resources (disk, cpu?) for human time updating and debugging, which might be a really good deal, especially if you only end up with duplicates in the cases where you lack the human time to keep all deps updated.
One could argue that the time cost of maintaining the libraries can only be deferred so there's no benefit deferring it, but the time people using the libraries save because they don't need the libraries to get updated if they have enough disk is probably what made Rust and NPM just duplicate dependencies.
I still wish at one point binary sizes are taken seriously though. But I am aware that it's likely (a) not at all a priority currently, and (b) a gigantic effort.
Guess I'll dig out the guides for reducing binary sizes but last time I needed tokio + opentelemetry + a Prometheus adapter my release binary was always at least 5MB.
Also sorry, didn't mean to come off as dismissive. It's just that to me 5MB is quite a lot and should be shrinkable. I keep wondering if the compiler/linker tech can help Rust there.
Having said that, if your binary ended up including a hyper-based HTTP server and a TLS implementation, and was built with the default release profile (optimizing for speed rather than size), I can believe it would reach 5MB stripped.
Debug symbols stripped it clocks at 5.9M, all symbols stripped it's at 5.0M.
Obviously this is not critical. But I have to admit I envy Zig and OCaml. :)
[profile.release]
codegen-units = 1
lto = "fat"
opt-level = "z"
panic = "abort"
Take out that last line if you need to recover from panics at runtime. So far, in my limited use of Rust, I haven't had to. Anyway, those are all the easy tweaks to reduce binary size.From 5.0M to 3.0M, not bad!
Turned the optimization for speed back on -- 4.4M! Wow.
https://github.com/golang/go/issues/51900
& from what I gather, APE is tightly-bound to x86_64, & depends on an embedded emulator for other architectures:
> All we have to do embed an ARM build of the emulator above within our x86 executables, and have them morph and re-exec appropriately, similar to how Cosmopolitan is already doing doing with qemu-x86_64, except that this wouldn't need to be installed beforehand. The tradeoff is that, if we do this, binaries will only be 10x smaller than Go's Hello World, instead of 100x smaller.
could still be useful as an optional build target though... (i.e. GOOS=cosmopolitan)
From the robustness/survivability perspective, the mono-cultures we have are bad enough.
Whether it's batteries included, or batteries sold separately, we'll still be stuck with batteries. If I want to run Linux on my Mac, that's what UTM is for.
Despite my reservations, nothing but respect, & enjoyed your Feross presentation.
The binary code is for Intel processors. Silicon chips have a completely different instruction set.
For this same reason Android, iOS and RTos (for embeded systems) are not included among the targets.
"Website that lets you download a 1KB long executable that can be run on 7 _kernels_ and x86_64 architectures, but actually it's only been tested on Intel and will probably not work on Android even if technically it's still a Linux kernel, also Linux < 2.6 is unsupported so YMMV"
I agree fully with what you're saying.
So you needed additional steps for macos. Not sure if that has been recently resolved?
https://github.com/jart/zsh/commit/94a4bc14bb2e415ec3d10cf71...
Not sure if it was merged.
Her approach is not bash-only, zsh is the odd one out on this one.
bash -c './redbean.com'
or whatever your executable is called. ZSH is doing something incorrectly that prevents running these binaries."Apple Silicon" is just a way to say chips designed by Apple vs a 3rd party like Intel.
The instructions are Arm vs x86. This has nothing to do with the underlying substrate of silicon.
There are two kinds of ARM licenses. One is for chipset designs (e.g. Cortex - https://en.wikipedia.org/wiki/ARM_Cortex-A) that you get fabricated to get your CPUs.
The other ARM license is an ISA license. You get no chip design. All you get is the instruction set architecture but you have to design the chip yourself. That's what Apple has been doing.
Only thing I had to do was rename the file to open it in Finder first because of gatekeeper requirements.
arch -x86_64 ./hello2
hello world
./hello2
hello worldAgreed. The cosmopolitan project is incredible hacking and I appreciate the author's ingenuity, but this has always felt a little bit to me like a solution in search of a problem. How is cosmopolitan software intended to be distributed? If the answer is on the internet, I don't see how a single fat binary helps the end user dramatically more than separate download links. For the developer, cross compilation can be automated as part of deployment. The platform specific binaries should both be smaller than the universal ape binaries and won't resort to hackery to work properly.
Also, no. The operating system integration code is tiny in the grand scheme of things. Consider Fabrice Bellard's JavaScript interpreter. If I build it for six operating systems, then it's 782kB:
master jart@nightmare:~/cosmo2$ ls -hal o/tiny/third_party/quickjs/qjs.com
-rwxr-xr-x 1 jart jart 782K Aug 30 10:53 o/tiny/third_party/quickjs/qjs.com
If I build it for just Linux, then it's 710kB: master jart@nightmare:~/cosmo2$ ls -hal o/tinylinux/third_party/quickjs/qjs.com
-rwxr-xr-x 1 jart jart 710K Aug 30 10:52 o/tinylinux/third_party/quickjs/qjs.com
That's a 72kB difference, which includes all those Windows NT hacks and bare metal support. Why not just stuff it in there and be done with it? That way you only have to list one file on your download site.Do end users commonly want to copy programs between OSes, or will they usually just revisit the project's website and download it again? My feeling is it would be the latter.
And if we're talking about running multiple OSes on the same computer, is it common to share filesystems between those OSes? It's been a while since I ran more than one OS, but trying to do that wasn't remotely pleasant.
I think this is a huge technical achievement, and I do think there are some use cases for it here and there, but I personally don't see a big benefit over just using a CI system to automatically create a build for each OS/arch combination at release time.
I also wonder about the trade offs involved in using a different libc. As an example, when I was looking into musl, I realized that it didn't use NSS (unsurprising, since that would defeat the whole zero-dependency static linking thing), and therefore had no support for multicast DNS (well, not that NSS is required, but they also didn't, and had no plans to, integrate mDNS support of their own). I've pretty much abandoned using musl (and Alpine for containers) due to random quirks and gotchas like this that end up biting me somewhere down the line.
There's one deliverable.
One.
Users cannot download the wrong build, because the wrong build doesn't exist.
Can it make a binary that's both x86 and x64 for Windows? Is that even possible?
x64
> Can it make a binary that's both x86 and x64 for Windows?
Seems to be a PE32+, so it won't run on 32-bit Windows.
> Is that even possible?
Yes, it should be. A 32-bit process on 64-bit Windows can run 64-bit code by changing the segment selector - make a far call or jump to 0x33:<address>. See [0] for more details.
There are some caveats though. kernel32.dll refuses to be loaded as both the 32-bit and 64-bit versions in the same executable. So the executable can probably only import ntdll. One thing one might do is just compile both a 32-bit and 64-bit version of the actual program as dll's and embed them inside the launcher and then extract and load them using only api's in ntdll. In the case of APE it would probably just embed an x64-on-x86 emulator and run the executable in that in case it's being run on x86.
Another solution might be to create a .NET AnyCPU executable which will run as 64-bit on 64-bit versions of Windows.
[0]: https://www.malwaretech.com/2014/02/the-0x33-segment-selecto...
But will that run on all the other OSes?
If you really wanted to, you could hack your own "fat binary" format by packing the 64-bit EXE as a resource within an x86 binary that detects the current architecture, but that's not very exciting.
PE requires a single machine type to be specified, but these days there are support for hybrid binaries with the ARM64EC ABI (which follows x64 conventions and can include both x64 and ARM64 code) and fat binaries with the ARM64X ABI (can include both ARM64EC and ARM64 code).
Great work jart! I hope this catches on.
I guess that, and shared libraries ABI, is the biggest challenge now.
The Wikipedia page for .com suggests that 64 bit Windows NT+ systems can't run the original style .com files anymore due to lacking the MS-DOS emulation subsystem, so the point may be moot.
Totally cheating would be to integrate the DOS application functionally into the Windows DOS stub. Instead of printing "This application requires Microsoft Windows" it could even use overlays to circumvent the 64kB com restriction.
It's 12 kB, aka 96 kb.
The HN submission's title is not even the article's title, which is "How Fat Does a Fat Binary Need To Be?"
HN auto-title cases submissions titles and removes any numbers prefixing the title string. So “7 KB” would be changed to “Kb”.
wat