Writing startup code into a library in a freestanding environnment

Hello ! I am trying to write programs to be used with the Linux kernel in the user land.
I did not want to rely on stdlib nor libc , and looking here and there I found about the x86_64-unknown-none target.
It is used for writing freestanding x86 binaries, I think that is the target I am looking for.
I first tried to write a minimal running program (userbin):

#![no_std]
#![no_main]

use core::panic::PanicInfo;

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

#[export_name = "_start"]
fn main() {
    loop {}
}

And running it worked just fine. Now I believe I might want to do some setup before calling main, I do not know yet what this setup will be exactly, but it might involve setting up a global allocator, or unwinding mechanisms. I intend to write many such programs, so I wanted to isolate the setup in a library.

I created a libstartup library package, next to the binary userbin package and wrote the following in libstartup/lib.rs:

#![no_std]

use core::panic::PanicInfo;

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

#[export_name = "_start"]
pub fn start_up() -> ! {
    extern "Rust" {
        fn main () -> !;
    }
    unsafe { main() };
}

and changed userbin/main.rs to:

#![no_std]
#![no_main]
use libstartup;

#[export_name = "main"]
fn main() {
    loop {}
}

running cargo build works fine, but running the compiled program results in a SIGSEGV(Address boundary error).
Looking at the disassembly of _start:

0000000000001240 <_start>:
 1240: 50                 pushq   %rax
 1241: ff 15 e9 10 00 00  callq   *0x10e9(%rip) # 0x2330 <userbin.25798e51bdcbbf78-cgu.0+0x2330>

It looks like it calls a symbol defined in userbin, but it's not main, even though the symbol main is defined. The compiler is aware of the link between the declared main in libstartup/lib.rs and the exported main in userbin.rs, because if I change to #[export_name = "not_wanted_main"] in userbin/main.rs the compilers complains about the undefined symbol main.

So I know the compiler is aware of the link, but it doesn't call the right function (it calls a symbol that is not a function).

Can you please help me get this working ?

To get here I read:

Have you tried exporting main() with the "C" calling convention, rather than using the "Rust" one?

Depending on how things are running, it may also be possible to attach a debugger to your program and see what the machine is doing at the time. I'm sure people working on Linux have already figured out a way to debug kernel code.

The random symbol you are seeing may be from the PLT or GOT and the program expects that symbol to be rewritten to point at main() at load time by the dynamic linker. I'm no expert, so I could be talking nonsense, but it may be something to look into.

1 Like

It indeed looks like it is a GOT call. x86_64-unknown-none uses the pic reloc model rather than the static reloc model. This was done to avoid relocations according to the commit message. The pic reloc model however needs relocations for the GOT when PLT usage is disabled. rustc_session: default to -Z plt=yes on non-x86_64 by durin42 · Pull Request #109982 · rust-lang/rust · GitHub causes the PLT to be disabled by default. If PLT usage is enabled, the linker is generally able to relax it into a direct pc-relative call without GOT load.

2 Likes

Thank you ! To be honest I did not understand everything you said. I think I'll I have to dig more on the linking process when using position independent code. For now I added the the flag -Z plt=yes and everything looks fine when running and the generated instructions.

The calling convention was not an issue here. The risv-rt crate also uses the C calling convention. Nonetheless I tried it and it didn't work.

1 Like