I'm trying to write an x86 bootloader and operating system completely in Rust (no separate assembly files, only inline assembly within Rust).
My bootloader works completely as intended within the QEMU emulator, I'm far into the development of my kernel and my bootloader has never failed. I'm just worried if my bootloader has any undefined behavior and this behavior just happens to work for me.
One of the first "bootloader" things I need to do is set the stack pointer to a valid area of memory to act as my bootloader's stack. My bootloader uses local variables, and also sets the stack pointer with inline assembly.
// extreme oversimplification of what I have as my bootloader
#![no_std]
#![no_main]
#[no_mangle]
fn entry() -> !
{
// place the stack area just before the boot sector
unsafe { core::arch::asm!
(
"mov sp, 0x7c00"
)}
let bootloader_variable_1 = ...;
let bootloader_variable_2 = ...;
// do things with bootloader variables
}
My main concern is that the compiler allocates some space for local variables on the stack before anything within my entry
function is ran and the compiler expects these variables to be at specific offsets, but then I manually change the stack pointer, invalidating all of the offsets.
Looking at the disassembly (x86 Intex syntax) of the generated binary when built in release mode, I see...
push bx
sub sp, 12
mov sp, 0x7c00
...
The generated assembly runs two commands before my function, both of which edit the stack pointer, and then I overwrite it. I'm surprised there haven't been any problems up to this point.
I'm not the most fluent in x86 assembly, but it seems like all instances of local variables are being optimized away from the stack and are used in processor registers instead, which is my best guess on why my bootloader works right now.
Is this cause for concern? Can I safely set the stack pointer in a no_std
environment (at the very start of the program) and not corrupt any local variables? Will this scheme work on any and all Rust-compliant compilers? If not, is there any way to do this without some external assembly file?
I excluded my full bootloader stage 1 code and the full disassembly, but I can add it if anyone thinks it could help.
The way to do this would be to make _entry
a naked function, for which the compiler will not generate any additional assembly code beyond exactly what you specify.
Naked functions are currently an experimental feature and will require a nightly compiler and the feature #![feature(naked_functions)]
. Or, you can use the naked_function crate, which implements naked functions using the global_asm!
macro available since Rust 1.59.0.
Either way, you will then write your entry function like so:
#[naked]
pub extern "C" fn entry() -> ! {
unsafe {
asm!(
"mov sp, 0x7c00",
"call {main}",
"hlt",
main = sym main,
options(noreturn)
);
}
}
This sets up the stack pointer, then calls a main
function as the real entry point; main can then be written as a regular Rust function as the stack has been set up. It then issues a HLT to stop the CPU, in case main returns.