Rust provides powerful tools for writing low-level, systems-oriented code. This includes the ability to disable the standard library, define custom panic handlers, and control function behavior and memory layout using attributes like #[link_section], #[no_mangle], and extern "C".

Disabling the Standard Library

In environments without an operating system, such as embedded systems or kernel modules, the standard library (std) can be replaced with the lightweight core library by using #![no_std]. The core library offers essential Rust features like Option, Result, and basic traits but lacks OS abstractions like file I/O and threading.

Disabling std requires alternative handling for certain runtime features, such as panic handling and heap allocation. For example, in #![no_std] environments, the program cannot rely on the default panic behavior and must define a custom panic handler.

#![no_std]
 
use core::panic::PanicInfo;
 
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}
 
#[no_mangle]
pub extern "C" fn main() -> ! {
    loop {}
}

Here, the #![no_std] directive excludes the standard library, and a minimal custom panic handler is implemented to avoid linking to the standard library’s panic machinery.

No main

The #![no_main] attribute tells the Rust compiler not to include the standard main function or any runtime setup provided by the standard library. It is used in combination #[feature(start] to enable the usage of a #[start] attribute to define a custom entry point for the program. Unlike main, the #[start] function is responsible for handling initialization logic, such as setting up memory, preparing the stack, or invoking the main program logic and does not receive arguments in the same way as main. Instead, it takes:

  • _argc: The number of command-line arguments.
  • _argv: A pointer to the command-line arguments as a raw pointer

_start

In absence of an explicitly defined #[start], Rust will expect the initial function to be named _start. This name originates from the CRT and dynamic linkers, debuggers, and disassemblers expect it because it has been de-facto standard for decades

A Custom Panic Handler

When a panic occurs, the default Rust runtime handles it either by unwinding the stack (to run destructors Drop) or by aborting the program. In #![no_std], the runtime features needed for unwinding are unavailable, so you typically define a panic handler that halts execution in a controlled way.

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

This panic handler avoids unwinding and instead enters an infinite loop, ensuring stability in constrained environments.

The #[link_section] attribute allows placement of functions or static variables into specific sections of a binary. This is essential in low-level programming tasks where precise control over memory layout is required, such as in bootloaders or kernel code.

#[link_section = ".init_array"]
pub static INIT: unsafe extern "C" fn() = init_function;
 
#[no_mangle]
pub unsafe extern "C" fn init_function() {
    // Code to run during initialization
}

By placing init_function in the .init_array section, the function is executed during the program’s initialization phase. This is useful for embedding custom initialization logic in binaries.

No Mangle

Rust uses name mangling to ensure symbol uniqueness, but the #[no_mangle] attribute disables this behavior, preserving the function name as written. This is critical for interoperability with other languages or tools that expect specific function names.

#[no_mangle]
pub extern "C" fn my_function() {
    // The symbol "my_function" will appear as-is in the binary
}

The #[no_mangle] attribute ensures that tools like linkers or system loaders can locate and call the function without requiring knowledge of Rust’s mangling scheme.

Extern “C”

The extern "C" declaration specifies the C calling convention for a function, ensuring compatibility with C programs and system interfaces. It controls how arguments are passed, how the stack is managed, and ensures predictable behavior when interfacing with non-Rust code.

#[no_mangle]
pub extern "C" fn my_entry_point() {
    // Entry point for integration with C or system-level tools
}

This declaration ensures the function follows the C ABI, making it callable from C programs or directly by the operating system.

Release profile optimizations

This example of the Cargo.toml section shows several optimization of the release profile

[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1

Panic configuration

It is common to configure a different panic behavior from the default unwind. The alternative, abort, means the program will terminate without unwinding stack or cleaning resources, and this reduces binary size and runtime overhead

Opt-level

This sets the optimization level for the compiler. “z” optimizes for the smallest possible binary size, often at the expense of runtime performance. This is useful for embedded systems or situations with strict size constraints.

Other options include “0” (no optimization), “1”, “2” (default), and “3” (highest optimization for performance).

Link Time Optimization (LTO) enables additional optimizations during the linking phase.

  • When lto = true, the compiler performs cross-module optimizations, potentially reducing binary size and improving performance.
  • There are other values, such as “fat” (aggressive LTO) or “thin” (more lightweight and parallelizable).

Codegen units

This controls the number of code generation units the compiler splits the crate into for parallel compilation. A value of 1 ensures no split happen, leading to better optimizations at the cost of slower compile times. Higher values (default is 16) improve compile times but can reduce optimization quality.