From c426e88d996544472b60ebadfc64fb39bf104c58 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:03:37 -0400 Subject: [PATCH 1/6] crates/asm: add bcmp and memcmp for static linking without libc riscv64 codegen emits calls to bcmp for byte comparisons, which is undefined when linking with -static and no libc. Provide both bcmp and memcmp implementations alongside the existing memcpy/memset/strlen. --- crates/asm/src/lib.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 1589a1d..273cc35 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -55,6 +55,33 @@ pub unsafe extern "C" fn memset(s: *mut u8, c: i32, n: usize) -> *mut u8 { s } +/// Compares two byte sequences. +/// +/// # Safety +/// +/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn bcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { + for i in 0..n { + let a = unsafe { *s1.add(i) }; + let b = unsafe { *s2.add(i) }; + if a != b { + return i32::from(a) - i32::from(b); + } + } + 0 +} + +/// Compares two byte sequences. +/// +/// # Safety +/// +/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { + unsafe { bcmp(s1, s2, n) } +} + /// Calculates the length of a null-terminated string. /// /// # Safety From 1539533c54626ca26f87de535d8ff7bbec818e16 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:03:59 -0400 Subject: [PATCH 2/6] build: move link flags to build.rs Binary-specific link flags (-nostartfiles, -static, section stripping) now use cargo:rustc-link-arg-bin so they don't break proc-macro or build-script linking. ld-wrapper only strips sections from static binaries. --- .cargo/config.toml | 21 +++------------------ microfetch/build.rs | 17 +++++++++++++++++ scripts/ld-wrapper | 14 +++++++++----- 3 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 microfetch/build.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d9f659e..2623c5a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,29 +3,14 @@ # it as a post-link step. # See: # +# +# Binary-specific link flags live in microfetch/build.rs via cargo:rustc-link-arg-bin +# so they only affect the final binary and don't break proc-macro or build-script linking. [target.'cfg(target_os = "linux")'] linker = "scripts/ld-wrapper" rustflags = [ - # No C runtime, we provide _start ourselves - "-C", - "link-arg=-nostartfiles", - # Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead - "-C", - "link-arg=-static", - # Static PIE is incompatible with -static :( - "-C", - "relocation-model=static", # Suppress .eh_frame emission from our own codegen (does not cover compiler_builtins; # those remnants are removed by the linker wrapper via objcopy post-link) "-C", "force-unwind-tables=no", - # Linker flags - "-C", - "link-arg=-Wl,--gc-sections", # remove unreferenced input sections - "-C", - "link-arg=-Wl,--strip-all", # strip all symbol table entries - "-C", - "link-arg=-Wl,--build-id=none", # omit the .note.gnu.build-id section - "-C", - "link-arg=-Wl,-z,norelro", # disable RELRO (removes relro_padding) ] diff --git a/microfetch/build.rs b/microfetch/build.rs new file mode 100644 index 0000000..3f43fef --- /dev/null +++ b/microfetch/build.rs @@ -0,0 +1,17 @@ +fn main() { + // These flags only apply to the microfetch binary, not to proc-macro crates + // or other host-compiled artifacts. + + // No C runtime, we provide _start ourselves + println!("cargo:rustc-link-arg-bin=microfetch=-nostartfiles"); + // Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead + println!("cargo:rustc-link-arg-bin=microfetch=-static"); + // Remove unreferenced input sections + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--gc-sections"); + // Strip all symbol table entries + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--strip-all"); + // Omit the .note.gnu.build-id section + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--build-id=none"); + // Disable RELRO (removes relro_padding) + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,-z,norelro"); +} diff --git a/scripts/ld-wrapper b/scripts/ld-wrapper index cf2cb21..c907b83 100755 --- a/scripts/ld-wrapper +++ b/scripts/ld-wrapper @@ -2,20 +2,23 @@ # Invoke mold, then strip junk sections from the output binary with objcopy. # This (more or less) removes sections that mold cannot discard itself, suck as: # - .eh_frame / .eh_frame_hdr - unwind tables from compiler_builtins -# - dynstr - mold emits this, even for fully static binaries +# - .dynstr - mold emits this, even for fully static binaries # - .comment - compiler version string # # We forward everything to mold via -fuse-ld, then post-process the output in place. set -eu -# Locate the output file +# Locate the output file and detect static linking +IS_STATIC=0 OUTPUT="" prev="" for arg in "$@"; do + case "$arg" in + -static) IS_STATIC=1 ;; + esac if [ "$prev" = "-o" ]; then OUTPUT="$arg" - break fi prev="$arg" done @@ -23,8 +26,9 @@ done # Invoke mold via the cc driver, forward all original arguments cc -fuse-ld=mold "$@" -# Remove sections that mold cannot discard -if [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then +# Only strip sections from fully static binaries. +# Dynamic executables (i.e. build scripts, proc-macros) need .dynstr at runtime. +if [ "$IS_STATIC" = 1 ] && [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then objcopy \ --remove-section=.eh_frame \ --remove-section=.eh_frame_hdr \ From c4b7afbf46aee65186d72f203ede0c3a01074b2a Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Sat, 11 Apr 2026 02:05:16 -0400 Subject: [PATCH 3/6] crates/asm: fix aarch64 stack alignment in _start --- crates/asm/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 273cc35..1ed703c 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -225,7 +225,9 @@ mod entry { // Move stack pointer to first argument register "mov x0, sp", // Align stack to 16-byte boundary (AArch64 ABI requirement) - "and sp, sp, -16", + "mov x9, sp", + "and x9, x9, #-16", + "mov sp, x9", // Call into Rust code "bl {entry_rust}", // Move return code to syscall argument From 9f7fa83c6b2aa1e819a500e5c6cbc657878d9eb1 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Sat, 11 Apr 2026 02:52:54 -0400 Subject: [PATCH 4/6] ci: replace cargo-cross with setup-cross-toolchain-action --- .github/workflows/rust.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 945ed5c..ce87619 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,12 +34,14 @@ jobs: - name: "Make Mold the default linker" uses: rui314/setup-mold@v1 - - name: "Install cross" - run: cargo install cross --git https://github.com/cross-rs/cross + - name: "Setup cross-compilation toolchain" + uses: taiki-e/setup-cross-toolchain-action@v1 + with: + target: ${{ matrix.target }} - name: "Build" - run: cross build --verbose --target ${{ matrix.target }} + run: cargo build --verbose - name: "Run tests" if: matrix.target == 'x86_64-unknown-linux-gnu' - run: cross test --verbose --target ${{ matrix.target }} + run: cargo test --verbose From 2d4bb2e371560bd46f4c77226fffaef2b1db156f Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:04:19 -0400 Subject: [PATCH 5/6] microfetch: move _start to binary crate, gate entry_rust with cfg(not(test)) --- crates/asm/src/lib.rs | 150 ++--------------------------------------- microfetch/src/main.rs | 51 +++++++++++++- 2 files changed, 54 insertions(+), 147 deletions(-) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 1ed703c..19abc0e 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -98,12 +98,15 @@ pub const unsafe extern "C" fn strlen(s: *const u8) -> usize { /// Function pointer type for the main application entry point. /// The function receives argc and argv and should return an exit code. +#[cfg(not(test))] pub type MainFn = unsafe extern "C" fn(i32, *const *const u8) -> i32; +#[cfg(not(test))] static mut MAIN_FN: Option = None; /// Register the main function to be called from the entry point. /// This must be called before the program starts (e.g., in a constructor). +#[cfg(not(test))] pub fn register_main(main_fn: MainFn) { unsafe { MAIN_FN = Some(main_fn); @@ -125,6 +128,7 @@ pub fn register_main(main_fn: MainFn) { /// ```rust,ignore /// unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32` /// ``` +#[cfg(not(test))] #[unsafe(no_mangle)] pub unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { // Read argc and argv from stack @@ -141,155 +145,11 @@ pub unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { // External main function that must be defined by the binary using this crate. // Signature: `unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> // i32` +#[cfg(not(test))] unsafe extern "C" { fn main(argc: i32, argv: *const *const u8) -> i32; } -#[cfg(target_arch = "x86_64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `x86_64` Linux at program start: - /// - /// - `[rsp]` = argc - /// - `[rsp+8]` = argv[0] - /// - `[rsp+16]` = argv[1] - /// - ... - /// - `[rsp+8n]` = NULL - /// - `[rsp+8n+8]` = envp[0] - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`rsp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[rsp]` without validating the pointer - /// - Modifies `rsp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via syscall) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. This has been - /// your safety notice. I WILL put UB in your Rust program. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mov rdi, rsp", - // Align stack to 16-byte boundary (System V AMD64 ABI requirement) - "and rsp, -16", - // Call into Rust code - "call {entry_rust}", - // Move return code to syscall argument - "mov rdi, rax", - // Exit syscall - "mov rax, 60", // SYS_exit - "syscall", - entry_rust = sym super::entry_rust, - ); - } -} - -#[cfg(target_arch = "aarch64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `aarch64` Linux at program start, the stack layout is identical - /// to x86_64: - /// - /// - `[sp]` = argc - /// - `[sp+8]` = argv[0] - /// - ... - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`sp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[sp]` without validating the pointer - /// - Modifies `sp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via SVC instruction) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mov x0, sp", - // Align stack to 16-byte boundary (AArch64 ABI requirement) - "mov x9, sp", - "and x9, x9, #-16", - "mov sp, x9", - // Call into Rust code - "bl {entry_rust}", - // Move return code to syscall argument - "mov x0, x0", - // Exit syscall - "mov x8, 93", // SYS_exit - "svc #0", - entry_rust = sym super::entry_rust, - ); - } -} - -#[cfg(target_arch = "riscv64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `riscv64` Linux at program start, the stack layout is identical - /// to x86_64: - /// - /// - `[sp]` = argc - /// - `[sp+8]` = argv[0] - /// - ... - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`sp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[sp]` without validating the pointer - /// - Modifies `sp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via ECALL instruction) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mv a0, sp", - // Align stack to 16-byte boundary (RISC-V ABI requirement) - "andi sp, sp, -16", - // Call into Rust code - "call {entry_rust}", - // Move return code to syscall argument - "mv a0, a0", - // Exit syscall - "li a7, 93", // SYS_exit - "ecall", - entry_rust = sym super::entry_rust, - ); - } -} - -// Re-export the entry point -#[cfg(target_arch = "x86_64")] pub use entry::_start; -#[cfg(target_arch = "aarch64")] pub use entry::_start; -#[cfg(target_arch = "riscv64")] pub use entry::_start; - /// Direct syscall to open a file /// /// # Returns diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 25e855c..fcefcc8 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -3,12 +3,59 @@ extern crate alloc; -use core::panic::PanicInfo; +use core::{arch::naked_asm, panic::PanicInfo}; use microfetch_alloc::BumpAllocator; // Re-export libc replacement functions from asm crate pub use microfetch_asm::{memcpy, memset, strlen}; -use microfetch_asm::{sys_exit, sys_write}; +use microfetch_asm::{entry_rust, sys_exit, sys_write}; + +#[cfg(target_arch = "x86_64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mov rdi, rsp", + "and rsp, -16", + "call {entry_rust}", + "mov rdi, rax", + "mov rax, 60", + "syscall", + entry_rust = sym entry_rust, + ); +} + +#[cfg(target_arch = "aarch64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mov x0, sp", + "mov x9, sp", + "and x9, x9, #-16", + "mov sp, x9", + "bl {entry_rust}", + "mov x0, x0", + "mov x8, 93", + "svc #0", + entry_rust = sym entry_rust, + ); +} + +#[cfg(target_arch = "riscv64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mv a0, sp", + "andi sp, sp, -16", + "call {entry_rust}", + "mv a0, a0", + "li a7, 93", + "ecall", + entry_rust = sym entry_rust, + ); +} // Global allocator #[global_allocator] From bcc3f2ab96ad5a7bb2ea0400648af75b2b8d0689 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Sat, 11 Apr 2026 03:02:09 -0400 Subject: [PATCH 6/6] ci: run tests on all platforms --- .github/workflows/rust.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ce87619..99668ae 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -43,5 +43,4 @@ jobs: run: cargo build --verbose - name: "Run tests" - if: matrix.target == 'x86_64-unknown-linux-gnu' - run: cargo test --verbose + run: cargo test --workspace --exclude microfetch --verbose