Skip to content

[nvx] E: Add nvx-crt0 startup crate (skeleton)#24

Open
esaurez wants to merge 1 commit into
devfrom
feat/nvx-crt0-skeleton
Open

[nvx] E: Add nvx-crt0 startup crate (skeleton)#24
esaurez wants to merge 1 commit into
devfrom
feat/nvx-crt0-skeleton

Conversation

@esaurez

@esaurez esaurez commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a new nvx-crt0 static library that owns the executable entry point (_do_start), the _start Rust function it dispatches to, the C / Rust trampolines that bridge to the application's main function, and the argc / argv / environ parsing logic.

This PR is purely additive: the crate is created, registered in the workspace, and built into the sysroot as libnvx_crt0.a, but no consumer has been migrated to use it yet. All existing behaviour is unchanged.

Why

Today the nvx runtime crate exposes startup symbols (_do_start, _start, c_trampoline, etc.) behind its staticlib feature, and libposix enables that feature via its own staticlib = ["nvx/staticlib"] wiring. As a result, libposix.a embeds a strong undefined reference to main (via the c_trampoline's extern "C" fn main(int, char **) declaration). That UND is what makes extension .so files fail to link against libposix.a -- .sos never define main. The current workaround is a build-harness post-process (rust-objcopy --weaken-symbol=main libposix.a) so the UND becomes weak and the System V ABI's "weak undef -> 0" rule (implemented in the dlfcn loader by #22) can absorb it at dlopen time.

The proper fix is to stop carrying startup symbols in libposix.a at all. Splitting them into a dedicated nvx-crt0 crate is step one: executables explicitly link libnvx_crt0.a, libraries (notably libposix) link only the runtime helpers in nvx.

This PR delivers only the new crate. The follow-up PRs cut existing consumers over.

What this PR contains

File Change
src/libs/nvx-crt0/Cargo.toml (new) New package, crate-type = [lib, staticlib]. Direct dependency on nvx for the shared init / cleanup / pie::relocate_pie_binary helpers. Features c-main (default) and rust-main select the trampoline; a compile_error! guard rejects builds with neither or both set.
src/libs/nvx-crt0/src/lib.rs (new, ~340 LOC) Startup code factored out of nvx's lib.rs without behavioural change: global_asm! defining _do_start, _start, both trampolines, ARGC / ARGV / environ globals, build_string_table / parse_argp / parse_envp helpers. Doc comments expanded to call out the feature-set contract and the kernel-trap-frame ABI.
src/libs/nvx/src/lib.rs Minimal touch: pub mod pie; (was mod pie;) and pub fn init() / pub fn cleanup() (were private) so the new crate can call them. No symbols are moved out of nvx in this PR -- that happens in the follow-up cut-over PR.
Cargo.toml (workspace) Register src/libs/nvx-crt0 as a member; expose nvx-crt0 = { path = "src/libs/nvx-crt0", default-features = false }.
Cargo.lock Auto-updated for the new crate.
Makefile Add nvx-crt0 to ALL_GUEST_STATIC_LIBS so the sysroot ships libnvx_crt0.a for downstream C consumers (e.g. nanvix/cpython).
build/make/generic-guest-staticlibs.mk Introduce two new building blocks needed to support the new crate alongside posix: (1) per-package feature overrides (GUEST_STATICLIB_FEATURES_<pkg> -> GUEST_STATICLIB_PKG_FEATURES lookup) so posix keeps its staticlib [+ standalone] features while nvx-crt0 builds with c-main; (2) an artifact-name helper (lib<pkg-with-_>.a) so the cargo crate name nvx-crt0 maps correctly to the produced libnvx_crt0.a for cp / rm paths.

Why "modified Option A" (and not the alternatives)

Three approaches were considered:

  • Option A (chosen): nvx-crt0 is a thin crate that owns the startup glue and reuses nvx's runtime helpers (heap setup, TDA setup, PIE relocation, panic handler, allocator). One source of truth per concern; nvx-crt0 depends on nvx.
  • Option B: Make nvx-crt0 fully self-contained -- duplicate init/cleanup/pie into the new crate, so the crate has no nvx dependency. Avoids one dependency edge but doesn't actually eliminate duplicate-symbol risk because nvx-crt0 and libposix.a still share other deps (sys, sysalloc, config, ...). Pure churn.
  • Option C: Split nvx more aggressively into nvx-rt + nvx-crt0, then have posix depend only on nvx-rt. Cleanest end-state but a bigger refactor than what's needed to remove the main UND, and the boundary between nvx and a hypothetical nvx-rt isn't obviously well-defined today.

Option A delivers the architectural separation the upstreaming chain needs without changing what nvx does for its existing consumers.

Validation

  • Full ./z build of the all target succeeds: [OK] Build complete.
  • i686-nanvix-nm libnvx_crt0.a reports:
    • T _do_start (kernel-entry stub)
    • T _start (Rust entry, dispatches to trampoline)
    • T c_trampoline (mangled)
    • B environ
    • U main (the intended undefined reference that an executable's int main(...) resolves at link time)
  • i686-nanvix-nm libposix.a is byte-identical to the pre-PR build -- no consumer cut-over yet.
  • CPython 3.12 + numpy 1.26.4 end-to-end on the Nanvix microvm still produces NUMPY_TEST_OK.
  • ./z build -- format-check rust-lint-check spellcheck all green; pre-commit hook passes.
  • Verified the per-package feature override path: cargo build -p nvx-crt0 --features c-main works in isolation, and the batched all-guest-staticlibs target produces both libposix.a (with staticlib standalone) and libnvx_crt0.a (with c-main) without feature unification issues.

Compatibility

  • No public API change in nvx. init / cleanup were private; making them pub is an extension. The mod pie; -> pub mod pie; switch exposes the existing public relocate_pie_binary function under the conventional path.
  • No consumer touches. Every Rust no_std binary, every test, every benchmark, libposix, cpython, downstream .sos -- all continue to build exactly as before.
  • No runtime behaviour change. libnvx_crt0.a is produced and installed but nothing links it yet.

Follow-up PRs

  1. Cut libposix.a over: stop enabling nvx/staticlib, add the libnvx_crt0.a link directive to the cpython Makefile.nanvix.
  2. Cut every Rust no_std executable over: add nvx-crt0 as a cargo dependency on the ~14 affected binaries, drop extern crate nvx; in favour of extern crate nvx_crt0;, remove the startup symbols from nvx itself, and delete the temporary POST_STATICLIB_HOOK_posix objcopy workaround currently sitting in our local build harness.

Introduces a new `nvx-crt0` static library that owns the executable
entry point (`_do_start`), the `_start` Rust function it dispatches to,
the C / Rust trampolines that bridge to the application's `main`
function, and the `argc` / `argv` / `environ` parsing logic.

This PR is **purely additive**: the crate is created, registered in the
workspace, and built into the sysroot as `libnvx_crt0.a`, but no
consumer has been migrated to use it yet.  All existing behaviour --
`libposix.a`'s embedded startup, every Rust no_std executable's link
graph, every test, the cpython link line -- is unchanged.

The split lets a follow-up PR cut `libposix.a` over to depend only on
the runtime helpers in `nvx`, removing the strong undefined `main`
reference that currently forces a workaround (`rust-objcopy
--weaken-symbol=main libposix.a`) in the build harness.  The companion
loader-side fix that resolves weak undefined symbols to zero at dlopen
time landed in `[syscall] E: Honour STB_WEAK undefined symbols`.

What this PR contains:

  - `src/libs/nvx-crt0/Cargo.toml` -- new package, `crate-type = [lib,
    staticlib]`, dependencies mirror `nvx` plus a direct `nvx`
    dependency for the shared `init` / `cleanup` / `pie::relocate_pie_binary`
    helpers.  Features `c-main` (default) and `rust-main` select which
    trampoline gets compiled; a `compile_error!` guard rejects builds
    with neither or both set.

  - `src/libs/nvx-crt0/src/lib.rs` (~340 LOC) -- the startup code,
    factored out of `src/libs/nvx/src/lib.rs` without behavioural
    change.  The `global_asm!` block defining `_do_start`, the `_start`
    Rust function, both trampolines, the `ARGC` / `ARGV` / `environ`
    globals, and `build_string_table` / `parse_argp` / `parse_envp`
    helpers all move here.  Doc comments expanded to call out the
    new feature-set contract and the kernel-trap-frame ABI.

  - `src/libs/nvx/src/lib.rs` -- minimal touch: `pub mod pie;`
    (was `mod pie;`) and `pub fn init() / pub fn cleanup()` (were
    private) so the new crate can call them.  No symbols moved out of
    `nvx` in this PR -- that happens in the follow-up cut-over PR.

  - `Cargo.toml` (workspace) -- register `src/libs/nvx-crt0` as a
    member and expose the `nvx-crt0` workspace dependency with
    `default-features = false`.

  - `Makefile` -- add `nvx-crt0` to `ALL_GUEST_STATIC_LIBS` so the
    sysroot ships `libnvx_crt0.a` for downstream C consumers.

  - `build/make/generic-guest-staticlibs.mk` -- introduce per-package
    feature overrides (`GUEST_STATICLIB_FEATURES_<pkg>`) and an
    artifact-name helper (`lib<pkg-with-_>.a`) so the existing batched
    build path keeps working: `posix` continues to use `staticlib
    [+ standalone]`, while `nvx-crt0` is built with `c-main` for the
    sysroot copy.  Rust no_std binaries that want the `rust-main`
    flavour will pull `nvx-crt0` directly via cargo with the matching
    feature toggle in a future PR.

Validation:

  - `./z build` of the full `all` target succeeds.
  - `i686-nanvix-nm libnvx_crt0.a` reports `T _do_start`, `T _start`,
    `T c_trampoline`, `B environ`, `U main` (the intended UND that an
    executable's `int main(...)` resolves at link time).
  - `i686-nanvix-nm libposix.a` is byte-identical to the pre-PR build
    (no consumer cut-over yet).
  - CPython + numpy end-to-end on the Nanvix microvm still produces
    `NUMPY_TEST_OK` -- behaviour unchanged.
  - `cargo fmt --check`, `cargo clippy -- -D warnings`, and the
    pipeline spellcheck all pass.

Follow-up PRs:

  1. Cut `libposix.a` over: stop enabling `nvx/staticlib`, add the
     `libnvx_crt0.a` link directive to the cpython Makefile.
  2. Cut every Rust no_std executable over: add `nvx-crt0` as a cargo
     dependency, drop `extern crate nvx;` in favour of `extern crate
     nvx_crt0;`, and remove the startup symbols from `nvx` itself.
     Also remove the temporary `POST_STATICLIB_HOOK_posix` objcopy
     workaround in `generic-guest-staticlibs.mk`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant