Closed polarathene closed 1 week ago
Unified example for the -gnu
(glibc & eyra) + -musl
targets for nostd
:
src/main.rs
:
#![no_std]
#![no_main]
// This approach to include eyra specific lines seems acceptable since Eyra only works with nightly:
// https://github.com/sunfishcode/c-ward/issues/144
#![cfg_attr(feature = "eyra", feature(cfg_match, lang_items), allow(internal_features))]
cfg_match! {
cfg(feature = "eyra") => {
extern crate eyra;
#[global_allocator]
static GLOBAL_ALLOCATOR: rustix_dlmalloc::GlobalDlmalloc = rustix_dlmalloc::GlobalDlmalloc;
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
}
}
// NOTE: This differs from the official example,
// Provides visible feedback to stdout at the expense of extra size:
#[no_mangle]
pub extern "C" fn main() -> isize {
const HELLO: &'static str = "Hello, world!\n";
unsafe { write(1, HELLO.as_ptr() as *const i8, HELLO.len()) };
0
}
extern "C" {
fn write(fd: i32, buf: *const i8, count: usize) -> isize;
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
build.rs
:
fn main() {
let target_is_musl = std::env::var("CARGO_CFG_TARGET_ENV")
.is_ok_and(|v| v == "musl");
let target_is_glibc = std::env::var("CARGO_CFG_TARGET_ENV")
.is_ok_and(|v| v == "gnu");
// Pass `-nostartfiles` to the linker, when Eyra is enabled.
if cfg!(feature = "eyra") {
println!("cargo:rustc-link-arg=-nostartfiles");
} else {
// NOTE: Not required when building with `cargo zigbuild`:
if target_is_musl {
println!("cargo:rustc-link-arg=-lc");
}
// NOTE: Absolute paths specific to Fedora 41 used here.
// Not providing the static libraries will dynamically link libc and segfault at runtime.
if target_is_glibc {
println!("cargo:rustc-link-arg=/usr/lib64/libc.a");
println!("cargo:rustc-link-arg=/usr/lib/gcc/x86_64-redhat-linux/14/libgcc_eh.a");
}
}
}
Cargo.toml
:
[package]
name = "example"
version = "0.1.0"
edition = "2021"
[dependencies]
eyra = { version = "0.17.0", default-features = false, optional = true }
rustix-dlmalloc = { version = "0.1.0", features = ["global"], optional = true }
[features]
eyra = ["dep:eyra", "dep:rustix-dlmalloc"]
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
opt-level = "z"
strip = true
rust-toolchain.toml
: (optional, pins nightly version used)
[toolchain]
profile = "minimal"
channel = "nightly-2024-10-04"
components = ["rust-src"]
targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
# NOTE: The `-Z build-std*` args aren't doing much for the above `nostd` focused example:
# LLD as the linker benefits musl notably in size (~14KB => ~4KB), gnu targets minimally.
RUSTFLAGS="-Z location-detail=none -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static" \
cargo +nightly build --target x86_64-unknown-linux-gnu --release \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort
# NOTE: The `-Z build-std*` args aren't doing much for the above `nostd` focused example:
# LLD as the linker benefits musl notably in size (~14KB => ~4KB), gnu targets minimally.
RUSTFLAGS="-Z location-detail=none -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static" \
cargo +nightly build --target x86_64-unknown-linux-gnu --release \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort \
--features eyra
# LLD as the linker benefits musl notably in size (~14KB => ~4KB), gnu targets minimally.
RUSTFLAGS="-Z location-detail=none -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static" \
cargo +nightly build --target x86_64-unknown-linux-musl --release \
-Z build-std=std,panic_abort \
-Z build-std-features=panic_immediate_abort
Thanks!
Also, if you're using #![no_std]
and #![no_main]
, you may also be interested in using origin directly, which can produce even smaller binaries.
No worries!
This was just from having some time spare to go over my prior notes on the topic and do a revision / summary over my original issue ( #27 ).
I was a bit surprised with some of the musl insights, especially when changing to lld for the linker having a notable improvement.
Also, if you're using
#![no_std]
and#![no_main]
, you may also be interested in using origin directly, which can produce even smaller binaries.
I hit a bit of a snag not long after the docs PR here was merged nightly releases were failing due to a change that affected unwinding 0.2.2
, but while unwinding 0.2.3
resolved that, in some builds with Eyra I encountered a new failure that didn't apply with unwinding 0.2.2
.
Unfortunately due to the semver resolution (despite not changing my Eyra version), the unwinding
crate would resolve to 0.2.3
(which will fail on my pinned nightly version in rust-toolchain.toml
). So presently I need to pin unwinding 0.2.2
in my Cargo.lock
or via an addition to my Cargo.toml
along with the nightly pin, or run my patched unwinding 0.2.3
which apparently is not a valid fix (would break builds for others).
Just adding that context for anyone that lands here and attempts to reproduce the examples without a Cargo.lock
😅
As for size yes I got 352 bytes with your origin example IIRC, while a similar "Hello World" with rustix
directly was 344 bytes. At that extreme -gnu
/ -musl
target was irrelevant due to no libc
usage?
I had come across this blog article prior to trying your origin example (they managed 640 bytes), where at the end their "Hello World" string version was 888 bytes.
The article (at least at the time I read it) was lacking information to reproduce their final result but after I saw the syscall usage to avoid libc
I immediately thought rustix
might let me do that without having to think about managing syscalls directly (I had not used rustix
, but it was a nice reason to try it).
After that I got around to looking at the origin examples and realized you did roughly the same (but with proper error handling), and I got that down to 504 bytes:
# The nightly `-Z build-std` flags trim off almost 500 more bytes:
RUSTFLAGS='-C link-arg=-Wl,--build-id=none,--omagic,-z,nognustack -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static -C link-arg=-nostartfiles' \
cargo build --release --target x86_64-unknown-linux-gnu \
-Z build-std=core,panic_abort \
-Z build-std-features=panic_immediate_abort
# 720 bytes down to 504 bytes (nightly `.comment` content is larger than stable toolchain due to version info):
objcopy -R .comment target/x86_64-unknown-linux-gnu/release/example
NOTE: That was with origin = 0.23.0
, since the current 0.23.1
release sets unwinding = 0.2.3
as the minimum, preventing me from using cargo update unwinding --precise 0.22
.
With my rustix
attempt, I do remember a bit of a slow down when looking at how to approach the exit()
call as while I found it in rustix
source, the docs didn't cover it.
runtime
feature to access it (seems like someone else had that UX issue too).Off-topic: Eyra docs.rs
are failing to build, last successful docs publish was 0.16.0
.
For anyone interested in reproducing this, I'll share it, but at this point it doesn't help evaluate Origin or Eyra as it's too simple now there is no overhead that they can reduce:
#![no_std]
#![no_main]
#[no_mangle]
pub extern "C" fn _start() -> ! {
exit(); // +8 bytes to size vs using `loop() {}`
}
fn exit() -> ! { unsafe { rustix::runtime::exit_thread(42) } }
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
[package]
name = "example"
version = "0.0.0"
edition = "2021"
[dependencies]
rustix = { version = "0.38.37", default-features = false, features = ["runtime"] }
[profile.release]
lto = true
panic = "abort"
opt-level = "z"
strip = true
# Current stable Rust (1.81.0):
$ RUSTFLAGS='-C link-arg=-Wl,--build-id=none,--nmagic,-z,nognustack -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static -C link-arg=-nostartfiles' \
cargo build --release --target x86_64-unknown-linux-gnu
# Remove some extra weight:
$ objcopy -R .comment target/x86_64-unknown-linux-gnu/release/example
# Only 344 bytes:
$ du --bytes target/x86_64-unknown-linux-gnu/release/example
344 target/x86_64-unknown-linux-gnu/release/example
$ ldd target/x86_64-unknown-linux-gnu/release/example
not a dynamic executable
# It works:
$ target/x86_64-unknown-linux-gnu/release/example
$ echo $?
42
For a little more functionality, add the stdio
feature to the rustix
dep, and in src/main.rs
update _start()
to call this method before exit()
:
#[inline(always)]
fn hello_world() {
rustix::io::write(
unsafe { rustix::stdio::stdout() },
"Hello, world!\n".as_bytes()
).unwrap();
}
This will have some extra content we can trim away via other flags (if min size was the goal, these aren't always advised of course):
# Additional linker arg `--no-eh-frame-hdr`:
$ RUSTFLAGS='-C link-arg=-Wl,--build-id=none,--nmagic,-z,nognustack,--no-eh-frame-hdr -C link-arg=-fuse-ld=lld -C relocation-model=static -C target-feature=+crt-static -C link-arg=-nostartfiles' \
cargo build --release --target x86_64-unknown-linux-gnu
# Also remove `.eh_frame`:
# NOTE: `--build-id=none` above is more optimal vs `-R .note.gnu.build-id` post-build:
$ objcopy -R .comment -R .eh_frame target/x86_64-unknown-linux-gnu/release/example
# Only 584 bytes:
$ du --bytes target/x86_64-unknown-linux-gnu/release/example
584 target/x86_64-unknown-linux-gnu/release/example
$ ldd target/x86_64-unknown-linux-gnu/release/example
not a dynamic executable
$ target/x86_64-unknown-linux-gnu/release/example
Hello, world!
Alternatively, the --no-eh-frame-hdr
and objcopy -R .eh_frame
aren't relevant if you use -Z build-std=core -Z build-std-features=panic_immediate_abort
, which when --nmagic
is swapped for --omagic
in this case results in 456 bytes (provides no improvement for the original 344 bytes version).
Summary
~13KB
difference to a6KB
difference.Reproduction
musl (25KB)
musl + zig (26.4KB)
glibc (834KB)
eyra (24.7KB)
nostd
referenceFor an additional reference the
nostd
example that was added at a later date:Presently builds with eyra to
5,592
bytes 😎 (with the sameCargo.toml
release profile used below)-Z build-std
args increases to10,144
bytes, or7,608
bytes by adding-C linker-plugin-lto -C linker=clang -C link-arg=-flto=full -C link-arg=-fuse-ld=mold
(note: eyra fails to build if-C link-arg=-fuse-ld=lld
is used with-C linker-plugin-lto
).For the musl target (without eyra):
13,464
bytes, but requires-C link-arg=-lc
to build successfully. Adding-C link-arg=-fuse-ld=lld
reduces this down to3,776
bytes.3,200
bytes, or2,928
bytes after stripping some ELF strings (-C link-arg=-lc
not required, Zig also defaults the linker to LLD already):Static glibc gnu target (without eyra):
also builds smaller at(Nevermind that segfaults and unexpectedly dynamic links glibc. Without4,760
bytes. This also requires-C link-arg=-lc
to be successful.+crt-static
it'll build dynamically linked successfully at4,440
bytes)-C link-arg=/usr/lib64/libc.a -C link-arg=/usr/lib/gcc/x86_64-redhat-linux/14/libgcc_eh.a
(at least on Fedora 41 with theglibc-static
package) to successfully build and weighs in at692,976
bytes with the-Z
build-std flags (otherwise 64 bytes larger at693,040
bytes).-C linker-plugin-lto -C linker=clang -C link-arg=-flto=full
for cross-language LTO, but no improvement.