Implement a tiny C runtime (crt) — the low-level code that sits between the OS kernel and a C program's main() — built entirely in Rust with no dependency on the system libc.
Learning how programs are linked and what happens before main() is called. This project replaces glibc/musl's startup code, heap allocator, and basic I/O with Rust equivalents, linked into a standalone static library.
┌──────────────────────────┐
│ C user program │ csrc/test.c — calls printf, fopen, malloc, etc.
├──────────────────────────┤
│ C variadic shim │ csrc/shims.c — variadic printf/fprintf/sprintf/snprintf
├──────────────────────────┤
│ ┌──────────────────────┐│
│ │ entry (_start) ││ ELF entry point → calls main(argc, argv, envp)
│ ├──────────────────────┤│
│ │ exit (atexit/abort) ││ exit handlers, _Exit, SIGABRT
│ ├──────────────────────┤│
│ │ heap (malloc/free) ││ First-fit free-list allocator, backed by mmap
│ ├──────────────────────┤│
│ │ fs (FILE stdio) ││ Buffered fopen/fread/fwrite/fflush/fseek, etc.
│ ├──────────────────────┤│
│ │ io (printf family) ││ Formatted output: vprintf/vfprintf/vsprintf/vsnprintf
│ ├──────────────────────┤│
│ │ string (mem/str) ││ strlen/strcpy/memcpy/memset/strcmp, etc.
│ ├──────────────────────┤│
│ │ sys (Linux layer) ││ Raw syscall wrappers: read/write/mmap/exit, etc.
│ └──────────────────────┘│
├──────────────────────────┤
│ Linux kernel │
└──────────────────────────┘
GNU/Linux in x86_64/arm64.
make build-linux-x86
make build-linux-arm- Rust toolchain (nightly not required)
- C compiler (for variadic shim and test programs)
make
make test
./target/test_progThe test binary is linked with -nostartfiles -nodefaultlibs — no glibc involved.
# Build Rust static lib
cargo build --release
# Compile C code
cc -c csrc/shims.c -o shims.o
cc -c my_prog.c -o my_prog.o
# Link — must provide our own _start, no libc
cc -nostartfiles -nodefaultlibs \
my_prog.o shims.o target/release/libcrt_rs.a \
-o my_progRust cannot define C variadic functions (...). The C file csrc/shims.c provides thin wrappers that unpack va_list and call the Rust-implemented vprintf/vfprintf/vsprintf/vsnprintf functions.
Similarly, C expects stdin/stdout/stderr as macros. The header csrc/shims.h defines them as calls to crt_stdin()/crt_stdout()/crt_stderr() which return *mut FILE pointers to the Rust-managed static FILE objects.
The printf engine supports: %d %i %u %x %X %o %s %c %p %% with flags - (left-align) and 0 (zero-pad) and minimum field width.
- First-fit free-list search
- Block coalescing on
free() - Backed by anonymous
mmap - Alignment: 16 bytes (64-bit) / 8 bytes (32-bit)
- Minimum block data size: 16 bytes
- Per-stream 1024-byte buffer
- Dirty-flag tracking for deferred writes
- Read buffering: batch reads into buffer, serve from buffer
- Write buffering: accumulate in buffer, flush when full or on
fflush()/fseek()
Unsafe is confined to:
- Raw syscall invocations (inline assembly)
extern "C"function interfaces receiving raw pointers from Cstatic mutaccess for the heap free-list, FILE pool, and atexit registry- Raw pointer dereference in string operations called from C
Internal helpers use safe Rust where possible — e.g., slice indexing instead of pointer arithmetic in int_to_str, &mut [u8] instead of *mut u8.
- No thread safety (no locks, no
errno, no thread-local storage) - No signal handling
- No environment variable access (
getenv) - No
longjmp/setjmp - No locale support
- No floating-point formatting in printf
- No
tmpfile/tmpnam - Linux-only syscall module
- Single-threaded only
MIT