I have the following Rust program:
//app.rs
#![no_main]
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
built by using
rustc --target armv7a-none-eabi app.rs
NOTE: target armv7a-none-eabi does not allow to have fn main.
It produces an application app and I ran in a Bare ARM platform:
# chmod +x app
# ls -l app
-rwxrwxrwx 1 root root 572 Dec 6 2022 app
# file app
app_rust: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
# ./app
Segmentation fault (core dumped)
Generally you want to use something like the #[entry] attribute from the cortex-m-rt crate
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
/* .. */
}
}
You will, of course, need to add the crate as a dependency first:
cargo add cortex-m-rt
I'm wanting to compile binaries for openwrt 19.07.7 on mips with limited space.
OpenWrt 19.07.7, r11306-c4a6851c72
-----------------------------------------------------
root#OpenWrt:~# df /
Filesystem 1K-blocks Used Available Use% Mounted on
overlayfs:/overlay 3392 1276 2116 38% /
root#OpenWrt:~# ls -s
662 crosshello 3 helloworld
root#OpenWrt:~# ldd --version
musl libc (mips-sf)
Version 1.1.24
Above you can see the size of crosshello (cross compiled using rust) and helloworld (cross compiled using gcc). There is limited space on the target as you can see.
I followed https://github.com/japaric/rust-cross and it works. However I had to reduce the size of the binary to get it to fit. Following https://github.com/johnthagen/min-sized-rust I got the binary (the "Cross compiling with cargo" hello world with clap from the first link) to fit. I haven't compiled my own program yet. I got stuck a the "Optimize libstd with build-std" section.
I'm getting the above error:
$ cargo +nightly build -Z build-std=std,panic_abort --target=mips-unknown-linux-musl --release
Compiling libc v0.2.106
...
error: could not find native static library `c`, perhaps an -L flag is missing?
error: could not compile `libc` due to previous error
warning: build failed, waiting for other jobs to finish...
error: build failed
How do I fix that? I don't want to static link against the system libc, I want it dynamically linked like other binaries on that platform. But not sure how to do that. If I use the standard libstd the binaries are dynamically linked just fine.
Alternatively I could try nostd but my code would need significant modification and I'm not sure that would fix this problem anyway. Alternatively I could just go ahead compile my actual project with libstd and it might fit. However my project is doing data logging, so the binary is competing for space with data I want to store, so I want to minimize space as much as possible.
I've also seen reference to cargo-bloat so I'll probably check that out to find what all the space is being used for in the binary.
I'm open to different strategies to achieve what I want if anyone has ideas?
On my dev system:
$ cat .cargo/config
[target.mips-unknown-linux-musl]
ar = "/home/alex/projects/openwrt-sdk-19.07.7-ath79-generic_gcc-7.5.0_musl.Linux-x86_64/staging_dir/toolchain-mips_24kc_gcc-7.5.0_musl/bin/mips-openwrt-linux-ar"
linker = "/home/alex/projects/openwrt-sdk-19.07.7-ath79-generic_gcc-7.5.0_musl.Linux-x86_64/staging_dir/toolchain-mips_24kc_gcc-7.5.0_musl/bin/mips-openwrt-linux-gcc"
$ cat Cargo.toml
#cargo-features = ["strip"]
[package]
name = "crosshello"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "3.0.0-beta.5"
[profile.release]
#strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
(The binaries are stripped, currently I'm stripping them manually.)
I am trying to create an embed-friendly executable (small footprint and without dependency on the Rust standard library) that uses a library (wasmi) that already has support for a no_std build. New to Rust, I am simply piecing together instructions, but the gist of it appears to be follow the steps.
For the executable:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn _start(_argc: isize, _argv: *const *const u8) -> ! {
interpret(_argc, _argv);
loop {}
}
That is to:
include #![no_std]
define our entry (not main since we don't have a runtime that will call it)
and define a panic handler since the Rust std lib is not included to define it for us.
My Cargo file to compile this looks like this:
[package]
name = "driver"
version = "0.1.0"
edition = "2018"
[dependencies.wasmi]
path = "../../github_dev/wasmi"
features = ["core"]
default-features = false
test=false
bench=false
[profile.release]
panic = "abort"
lto = true
incremental=false
debug=true
opt-level = "z"
test=false
bench=false
and produces a very small binary that excludes any standard library symbols (using nm to check) and runs as expected.
The problem occurs when I actually try to call a function from the wasmi library. It is built with no_std via the features=core line. Doing an nm on the files in release/deps/libwasmi-*.rlib shows no standard library symbols. However when linking occurs with this command:
rustc --release --verbose -- -C link-arg=-nostartfiles
it leads to:
Compiling driver v0.1.0 (/home/my_home/wasmi_embed/driver)
Running rustc --edition=2018 --crate-name driver src/main.rs --color always --crate-type bin --emit=dep-info,link -C opt-level=3 -C panic=abort -C lto -C link-arg=-nostartfiles -C metadata=957eda2e590447ba -C extra-filename=-957eda2e590447ba --out-dir /home/my_home/wasmi_embed/driver/target/release/deps -L dependency=/home/my_home/wasmi_embed/driver/target/release/deps --extern libc=/home/my_home/wasmi_embed/driver/target/release/deps/liblibc-f7fb773c7b059a14.rlib --extern wasmi=/home/my_home/wasmi_embed/driver/target/release/deps/libwasmi-534aef1926b4eb6c.rlib
and an error occurs:
error[E0152]: duplicate lang item found: panic_impl.
--> src/main.rs:31:1
|
31 | / pub extern fn panic(_info: &PanicInfo) -> ! {
32 | | loop {}
33 | | }
| |_^
|
= note: first defined in crate `std`.
It seems Rust is trying to link in standard library support for at least panic handling, but I don't know why.
I would like help to understand why and to understand how to prevent it.
If I remove the panic_impl attribute then my executable compiles, but it includes a lot of standard library symbols that I am trying to prevent.
The example symbols I see are:
my_home#my_puter:~/wasmi_embed/driver/target/release$ nm --demangle -A -a -B -s --line-number test_2018 2>/dev/null | grep std
driver:00000000000264c0 t rust_begin_unwind /rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/panicking.rs:311
driver:00000000000264a0 t rust_oom /rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/alloc.rs:203
driver:000000000001f490 t rust_panic /rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/panicking.rs:524
driver:0000000000025aa0 t _$LT$std..panicking..continue_panic_fmt..PanicPayload$LT$$u27$a$GT$$u20$as$u20$core..panic..BoxMeUp$GT$::get::he4f810e299a2e0b4 /rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/panicking.rs:372
driver:00000000000259a0 t _$LT$std..panicking..continue_panic_fmt..PanicPayload$LT$$u27$a$GT$$u20$as$u20$core..panic..BoxMeUp$GT$::box_me_up::hd8430725259668a8 /rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/panicking.rs:367
driver:0000000000021520 t _$LT$std..sys_common..process..DefaultEnvKey$u20$as$u20$core..borrow..Borrow$LT$std..ffi..os_str..OsStr$GT$$GT$::borrow::hbacd0cd7d7fbf1c1/rustc/8e2063d02062ee9f088274690a97826333847e17//src/libstd/sys_common/process.rs:27
driver:0000000000021570 t _$LT$std..error..$LT$impl$u20$core..convert..From$LT$alloc..string..String$GT$$u20$for$u20$alloc..boxed..Box$LT$$LP$dyn$u20$std..error..Err
... plus more
The above symbols are not found in any of the rlib files under the dep directory including libwasmi, nor are they found in the driver executable when not calling libwasmi code.
I've read a similar issue (hence my test=false and bench=false in the Cargo.toml) but that did not help. I've tried to build with just rustc with varying commands (excluding Cargo) but the error is the same. I've tried to compile wasmi as a static library (ar) and link it in, but being new to Rust I was spending a lot of time trying to link it in and it just wasn't happening.
I resolved this after seeking some help in the rust forums. enter link description here. Specifically, was not able to determine what was responsible for rust std lib being linked into my executable ... was it an issue with a crate or an issue with cargo or an issue with rustc or an issue with the linker. I did not know where the problem was born, but based on similar bugs filed I figured that somehow a crate was being compiled to bring in std lib unexpected. Turns out bug enter link description here was not related even though the error message was the same. I did not have an issue with unexpected propagations form different type of dependencies (dev-dependencies and build-dependencies). I tried all these techniques to pinpoint what was bringing in std lib:
I tried using cargo tree to list dependencies to list all the crate
dependencies:
wasmi v0.4.3 (/home/jlb6740/github_dev/wasmi)
├── byteorder v1.3.1 (/home/jlb6740/github_dev/byteorder)
├── hashbrown v0.1.8 (/home/jlb6740/github_dev/hashbrown)
│ ├── byteorder v1.3.1 (/home/jlb6740/github_dev/byteorder) ()
│ └── scopeguard v0.3.3 (/home/jlb6740/github_dev/scopeguard)
├── libm v0.1.2
├── memory_units v0.3.0
└── parity-wasm v0.31.0 (/home/jlb6740/github_dev/parity-wasm)
└── byteorder v1.3.1 (/home/jlb6740/github_dev/byteorder) ()
I tried using cargo rustc --verbose … but at this time verbose does
not indicate anything was using default features which may include
using std
I tried using cargo metadata … this generated a long list of
dependencies that was hard to parse, but I did see some instances
where scopeguard and byteorder had default features requiring std
support. I downloaded all of these crates and just hardcoded
attributes so that the crates would only build with no_std support.
I tried looking at the deps/ output and did an nm on all of the
rlibs to see if any of the libraries used symbols found in std. I
could not find that that was the case. I thought rlibs were like
static libraries and that anything they used would be included in
the rlib but apparently not.
I looked at cargo rustc -- -C --print-link-args to check out linker
flags but I could not find anything obvious telling me it was
bringing in std lib.
None of these things helped me to pinpoint what was introducing std lib. Ultimately the suggestion at the rust forums was to use cargo check for a target that does not allow std lib at all. Those with a * listed here: enter link description here have only core support. I tried that, running with --target=thumbv7m-none-eabi and saw:
error[E0463]: can’t find crate for alloc
–> /home/jlb6740/github_dev/hashbrown/src/lib.rs:44:1
|
44 | extern crate std as alloc;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ can’t find crate
Turns out it was hashbrown which was a dependency of a dependency of my executable. It built no_std by default but had an extern std linked under a different name and which was guarded by a feature called “nightly”. The guard was disabled in my efforts to not build anything but no_std. Nothing I’d tried alerted me to the crate which was responsible until this. Seems there should be a better way to get a more comprehensive list of crate dependencies than what cargo tree provided, but changing the wasmi cargo to make sure the nightly feature was set solved my issue.
If I build a Rust application using Cargo with some crate dependencies, will any code in those dependencies that is unused by my application be eliminated from the final executable?
It looks like it. I made a test lib and bin crate side by side:
// hellobin/src/main.rs
extern crate hellolib;
fn main() {
hellolib::func1();
}
For the lib:
// hellolib/src/main.rs
pub fn func1() {
println!("Hello, world!");
}
pub fn func2() {
println!("Hello, other world!");
}
Building my binary and then inspecting symbols with nm:
$ nm target/debug/helloworld | grep hello
0000000100001360 t __ZN10helloworld4main17h749f61fb726f0a10E
00000001000014b0 T __ZN8hellolib5func117hec0b5301559d46f6E
Only the used function has a symbol in the final binary.
You can compile with cargo rustc -- -C link-dead-code though and you will see both symbols are present, including the unused one:
$ nm target/debug/helloworld | grep hello
0000000100001270 t __ZN10helloworld4main17h3104b73b00fdd798E
00000001000013d0 T __ZN8hellolib5func117hec0b5301559d46f6E
0000000100001420 T __ZN8hellolib5func217hc9d0886874057b84E
I believe (but I'm not sure) that it's the linker removing the dead code, so it may have still been compiled and then removed during linking.
TL;DR: Yes, every unused function is going to be excluded.
This is actually the job of LLVM that will at least keep track of every unused function. Any unused code (as in codepaths in function not taken across the entire Application) may require LTO (Link Time Optimizations) to be activated to turn your crate into one compilation unit and give LLVM a fighting chance.
I find the approach and the way they define the language in the first two chapters of the documentation particularly interesting. So I decided to get my fingers wet and started out with "Hello, world!".
I did so on Windows 7 x64, btw.
fn main() {
println!("Hello, world!");
}
Issuing cargo build and looking at the result in targets\debug I found the resulting .exe being 3MB. After some searching (documentation of cargo command line flags is hard to find...) I found the --release option and created the release build. To my surprise, the .exe size has only become smaller by an insignificant amount: 2.99MB instead of 3MB.
My expectation would have been that a systems programming language would produce something compact.
Can anyone elaborate on what Rust is compiling to, how it can be possible it produces such huge images from a 3-line program? Is it compiling to a virtual machine? Is there a strip command I missed (debug info inside the release build?)? Anything else which might allow to understand what is going on?
By default, the Rust compiler optimizes for execution speed, compilation speed, and ease of debugging (by including symbols, for example), rather than minimal binary size.
For an overview of all of the ways to reduce the size of a Rust binary, see my min-sized-rust GitHub repository.
The current high level steps to reduce binary size are:
Use Rust 1.32.0 or newer (which doesn't include jemalloc by default)
Add the following to Cargo.toml:
[profile.release]
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*
* strip = true requires Rust 1.59+. On older Rust versions, run strip manually on the resulting binary.
Build in release mode using cargo build --release
There is more that can be done using nightly Rust, but I'll leave that information in min-sized-rust as it changes over time due to the use of unstable features.
You can also use #![no_std] to remove Rust's libstd. See min-sized-rust for details.
Rust uses static linking to compile its programs, meaning that all libraries required by even the simplest Hello world! program will be compiled into your executable. This also includes the Rust runtime.
To force Rust to dynamically link programs, use the command-line arguments -C prefer-dynamic; this will result in a much smaller file size but will also require the Rust libraries (including its runtime) to be available to your program at runtime.
This essentially means you will need to provide them if the computer does not have them, taking up more space than your original statically linked program takes up.
For portability I'd recommend you statically link the Rust libraries and runtime in the way you have been doing if you were to ever distribute your programs to others.
I don't have any Windows systems to try on, but on Linux, a statically compiled Rust hello world is actually smaller than the equivalent C. If you are seeing a huge difference in size, it is probably because you are linking the Rust executable statically and the C one dynamically.
With dynamic linking, you need to take the size of all the dynamic libraries into account too, not just the executable.
So, if you want to compare apples to apples, you need to make sure either both are dynamic or both are static. Different compilers will have different defaults, so you can't just rely on the compiler defaults to produce the same result.
If you're interested, here are my results:
-rw-r--r-- 1 aij aij 63 Apr 5 14:26 printf.c
-rwxr-xr-x 1 aij aij 6696 Apr 5 14:27 printf.dyn
-rwxr-xr-x 1 aij aij 829344 Apr 5 14:27 printf.static
-rw-r--r-- 1 aij aij 59 Apr 5 14:26 puts.c
-rwxr-xr-x 1 aij aij 6696 Apr 5 14:27 puts.dyn
-rwxr-xr-x 1 aij aij 829344 Apr 5 14:27 puts.static
-rwxr-xr-x 1 aij aij 8712 Apr 5 14:28 rust.dyn
-rw-r--r-- 1 aij aij 46 Apr 5 14:09 rust.rs
-rwxr-xr-x 1 aij aij 661496 Apr 5 14:28 rust.static
These were compiled with gcc (Debian 4.9.2-10) 4.9.2 and rustc 1.0.0-nightly (d17d6e7f1 2015-04-02) (built 2015-04-03), both with default options and with -static for gcc and -C prefer-dynamic for rustc.
I had two versions of the C hello world because I thought using puts() might link in fewer compilation units.
If you want to try reproducing it on Windows, here are the sources I used:
printf.c:
#include <stdio.h>
int main() {
printf("Hello, world!\n");
}
puts.c:
#include <stdio.h>
int main() {
puts("Hello, world!");
}
rust.rs
fn main() {
println!("Hello, world!");
}
Also, keep in mind that different amounts of debugging information, or different optimization levels would also make a difference. But I expect if you are seeing a huge difference it is due to static vs. dynamic linking.
When compiling with Cargo, you can use dynamic linking:
cargo rustc --release -- -C prefer-dynamic
This will dramatically reduce the size of the binary, as it is now dynamically linked.
On Linux, at least, you can also strip the binary of symbols using the strip command:
strip target/release/<binary>
This will approximately halve the size of most binaries.
Install rust nightly -
rustup toolchain install nightly, rustup default nightly
Now, make these changes in all the Cargo.toml files in your project.
Add cargo-features = ["strip"] before [package] at the top of the Cargo.toml
At the bottom, or between [dependencies] and [package] add,
[profile.release]
# strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
lto = true # Enable link time optimization
codegen-units = 1 # Reduce parallel code generation units
Now build with RUSTFLAGS='-C link-arg=-s' cargo build --release
I found these links useful - https://collabora.com/news-and-blog/blog/2020/04/28/reducing-size-rust-gstreamer-plugin/ and https://github.com/johnthagen/min-sized-rust and https://arusahni.net/blog/2020/03/optimizing-rust-binary-size.html
#![no_main]
#![no_std]
#[link(name = "msvcrt", kind = "dylib")]
extern {
fn puts(ptr: *const u8); // i8 or u8 doesn't matter in this case
}
#[no_mangle]
unsafe extern fn main() {
puts("Hello, World!\0".as_ptr());
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
with next profile
[profile.release]
debug = false
strip = true
opt-level = 'z'
codegen-units = 1
lto = true
panic = 'abort'
gives 9 kb with -r, while C
#include <stdio.h>
main() {
puts("Hello, World!");
}
gives 48 kb with GCC and -Os and 2 kb with TCC. Pretty impressive, isn't it?
This is a feature, not a bug!
You can specify the library versions (in the project's associated Cargo.toml file) used in the program (even the implicit ones) to ensure library version compatibility. This, on the other hand, requires that the specific library be statically linked to the executable, generating large run-time images.
Hey, it's not 1978 any more - many people have more than 2 MB RAM in their computers :-)