Write! macro in no_std linux executable causes segment fault

As a part of my hobby OS project, I'm working on a loader that will load an ELF into memory and run.
Having Linux as a backgroud platform for testing, I came up with the following test implementation of a console:

#![no_std]
#![no_main]
#![feature(asm)]
#![feature(const_fn_trait_bound)]

use core::fmt::Write;

#[no_mangle]
fn _start() {
    let msg = "hi there!\n";
    let mut console = console::Console::new(platform::StdOut);
    console.write_str(&msg);
    write!(&mut console, "hi");
    platform::exit(0);
}

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

mod platform {
    use crate::console::ConsoleDevice;

    pub fn exit(code: isize) -> ! {
        unsafe {
            asm!(
                "syscall",
                in("rax") 60,
                in("rdi") code,
                options(nomem, nostack, noreturn)
            )
        }
    }

    pub fn write(s: &str) -> usize {
        let chars_written: usize;
        unsafe {
            asm!(
                "syscall",
                inlateout("rax") 1_usize => chars_written,
                in("rdx") s.len(),
                in("rdi") 1,
                in("rsi") s.as_ptr(),
                options(nostack)
            )
        }
        chars_written
    }

    pub struct StdOut;

    impl ConsoleDevice for StdOut {
        fn write(&mut self, s: &str) {
            write(s);
        }
    }
}

mod console {
    use core::fmt::{Result, Write};

    pub trait ConsoleDevice {
        fn write(&mut self, s: &str);
    }

    pub struct Console<T: ConsoleDevice> {
        device: T
    }

    impl<T: ConsoleDevice> Console<T> {
        pub const fn new(device: T) -> Self {
            Console { device }
        }
    }

    impl<T: ConsoleDevice> Write for Console<T> {
        fn write_str(&mut self, s: &str) -> Result {
            self.device.write(s);
            Ok(())
        }
    }
}

The problem is write_str works fine, but write! macro segfaults. I use panic = "abort" and build as RUSTFLAGS="-C link-arg=-nostartfiles" cargo build`.

Release profile compiles fine. For the debug profile, the linker complains about not being able to find references to memcpy and memset. This is the issue I believe, but I remember reading about using write! in no_std bare-metal apps, and that it works fine.

What am I doing wrong and how to properly resolve the issue? Am I to implement memcpy and memset myself?

You should be able to link in the compiler-builtins crate to get memset and friends.

Thanks for the crate! Actually, it could not be compiled because of the use of -C link-arg=-nostartfiles. So I just added implementations of memcpy and memset from std. Debug profile worked fine, but release profile still segfaulted because of optimizations. I had to disable sse and use soft-float instead. For now, this is sufficient for testing purposes.

My fast guess would be incorrectly specified clobbered registers/flags and/or assembly options for the syscall. Just because on call works doesn't mean something else won't get spooked later on.

The syscall instruction clobbers rcx (for the return instruction pointer) and r11 (for rflags). In addition are you sure using the nostack option is correct?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.