lang (typesystem | machine | asm)
naked_fns
Add support for generating naked (prologue/epilogue-free) functions via a new function attribute.
Some systems programming tasks require that the programmer have complete control over function stack layout and interpretation, generally in cases where the compiler lacks support for a specific use case. While these cases can be addressed by building the requisite code with external tools and linking with Rust, it is advantageous to allow the Rust compiler to drive the entire process, particularly in that code may be generated via monomorphization or macro expansion.
When writing interrupt handlers for example, most systems require additional state be saved beyond the usual ABI requirements. To avoid corrupting program state, the interrupt handler must save the registers which might be modified before handing control to compiler-generated code. Consider a contrived interrupt handler for x86_64:
unsafe fn isr_nop() {
asm!("push %rax"
/* Additional pushes elided */ :::: "volatile");
let n = 0u64;
asm!("pop %rax"
/* Additional pops elided */ :::: "volatile");
}
The generated assembly for this function might resemble the following (simplified for readability):
isr_nop:
sub $8, %rsp
push %rax
movq $0, 0(%rsp)
pop %rax
add $8, %rsp
retq
Here the programmer's need to save machine state conflicts with the compiler's
assumption that it has complete control over stack layout, with the result that
the saved value of rax
is clobbered by the compiler. Given that details of
stack layout for any given function are not predictable (and may change with
compiler version or optimization settings), attempting to predict the stack
layout to sidestep this issue is infeasible.
When interacting with FFIs that are not natively supported by the compiler, a similar situation arises where the programmer knows the expected calling convention and can implement a translation between the foreign ABI and one supported by the compiler.
Support for naked functions also allows programmers to write functions that would otherwise be unsafe, such as the following snippet which returns the address of its caller when called with the C ABI on x86.
mov 4(%ebp), %eax
ret
Because the compiler depends on a function prologue and epilogue to maintain storage for local variable bindings, it is generally unsafe to write anything but inline assembly inside a naked function. The LLVM language reference describes this feature as having "very system-specific consequences", which the programmer must be aware of.
Add a new function attribute to the language, #[naked]
, indicating the
function should have prologue/epilogue emission disabled.
Because the calling convention of a naked function is not guaranteed to match any calling convention the compiler is compatible with, calls to naked functions from within Rust code are forbidden unless the function is also declared with a well-defined ABI.
Defining a naked function with the default (Rust) ABI is an error, because the
Rust ABI is unspecified and the programmer can never write a function which is
guaranteed to be compatible. For example, The function declaration of foo
in
the following code block is an error.
#[naked]
unsafe fn foo() { }
The following variant is not an error because the C calling convention is well-defined and it is thus possible for the programmer to write a conforming function:
#[naked]
extern "C" fn foo() { }
Because the compiler cannot verify the correctness of code written in a naked
function (since it may have an unknown calling convention), naked functions must
be declared unsafe
or contain no non-unsafe
statements in the body. The
function error
in the following code block is a compile-time error, whereas
the functions correct1
and correct2
are permitted.
#[naked]
extern "C" fn error(x: &mut u8) {
*x += 1;
}
#[naked]
unsafe extern "C" fn correct1(x: &mut u8) {
*x += 1;
}
#[naked]
extern "C" fn correct2(x: &mut u8) {
unsafe {
*x += 1;
}
}
The following example illustrates the possible use of a naked function for implementation of an interrupt service routine on 32-bit x86.
use std::intrinsics;
use std::sync::atomic::{self, AtomicUsize, Ordering};
#[naked]
#[cfg(target_arch="x86")]
unsafe extern "C" fn isr_3() {
asm!("pushad
call increment_breakpoint_count
popad
iretd" :::: "volatile");
intrinsics::unreachable();
}
static bp_count: AtomicUsize = ATOMIC_USIZE_INIT;
#[no_mangle]
pub fn increment_breakpoint_count() {
bp_count.fetch_add(1, Ordering::Relaxed);
}
fn register_isr(vector: u8, handler: unsafe extern "C" fn() -> ()) { /* ... */ }
fn main() {
register_isr(3, isr_3);
// ...
}
The current support for extern
functions in rustc
generates a minimum of two
basic blocks for any function declared in Rust code with a non-default calling
convention: a trampoline which translates the declared calling convention to the
Rust convention, and a Rust ABI version of the function containing the actual
implementation. Calls to the function from Rust code call the Rust ABI version
directly.
For naked functions, it is impossible for the compiler to generate a Rust ABI version of the function because the implementation may depend on the calling convention. In cases where calling a naked function from Rust is permitted, the compiler must be able to use the target calling convention directly rather than call the same function with the Rust convention.
The utility of this feature is extremely limited to most users, and it might be misused if the implications of writing a naked function are not carefully considered.
Do nothing. The required functionality for the use case outlined can be
implemented outside Rust code and linked in as needed. Support for additional
calling conventions could be added to the compiler as needed, or emulated with
external libraries such as libffi
.
It is easy to quietly generate wrong code in naked functions, such as by causing
the compiler to allocate stack space for temporaries where none were
anticipated. There is currently no restriction on writing Rust statements inside
a naked function, while most compilers supporting similar features either
require or strongly recommend that authors write only inline assembly inside
naked functions to ensure no code is generated that assumes a particular stack
layout. It may be desirable to place further restrictions on what statements are
permitted in the body of a naked function, such as permitting only asm!
statements.
The unsafe
requirement on naked functions may not be desirable in all cases.
However, relaxing that requirement in the future would not be a breaking change.
Because a naked function may use a calling convention unknown to the compiler, it may be useful to add a "unknown" calling convention to the compiler which is illegal to call directly. Absent this feature, functions implementing an unknown ABI would need to be declared with a calling convention which is known to be incorrect and depend on the programmer to avoid calling such a function incorrectly since it cannot be prevented statically.