Logo

dev-resources.site

for different kinds of informations.

Using Rust main from a custom entry point

Published at
6/21/2022
Categories
rust
uefi
Author
ayush1325
Categories
2 categories in total
rust
open
uefi
open
Author
9 person written this
ayush1325
open
Using Rust main from a custom entry point

Hello everyone. In this post, we will deep dive into the crux of the main() function and look behind the scenes. By the end, we will have some understanding of Rust runtime. Primarily, I will describe my current implementation of efi_main to hook into Rust runtime.

C Runtime

First, we need to get some things out of the way. In “C”, the first function is not int main(int argc, char *argv[]). While this might surprise some of you, C does have a runtime, and it is called Crt0. It is mostly written in assembly and linked to almost every C Program. This example is for Linux x86-64 with AT&T syntax, without an actual C runtime.

.text

.globl _start

_start: # _start is the entry point known to the linker
    xor %ebp, %ebp # effectively RBP := 0, mark the end of stack frames
    mov (%rsp), %edi # get argc from the stack (implicitly zero-extended to 64-bit)
    lea 8(%rsp), %rsi # take the address of argv from the stack
    lea 16(%rsp,%rdi,8), %rdx # take the address of envp from the stack
    xor %eax, %eax # per ABI and compatibility with icc
    call main # %edi, %rsi, %rdx are the three args (of which first two are C standard) to main

    mov %eax, %edi # transfer the return of main to the first argument of _exit
    xor %eax, %eax # per ABI and compatibility with icc
    call _exit # terminate the program
Enter fullscreen mode Exit fullscreen mode

This _start function then calls the all too familiar main function in C. With this out of the way; we are now going to talk about Rust Runtime and all the things behind the scenes that make a simple “Hello World” program work.

Rust Runtime

Everyone can already guess that the Rust runtime is much more complicated than the C Runtime. Also, almost every OS is very well integrated with C, while Rust must do most of the heavy lifting of integrating with the OS itself. If you want a detailed explanation, you should look at Michael Gattozzi’s blog post which goes into great detail about it.

I will give you a quick tldr: “C” main() -> “Rust” lang_start -> “Rust” lang_start_internal -> “Rust” init() -> “Rust” sys::init() -> “Rust” main().

Is everyone still with me? Good. Now I will briefly explain all the functions I just mentioned.

C main()

This is generated by rustc. Taking a look at code at compiler/rustc_codegen_ssa/src/base.rs:

    fn create_entry_fn<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
        cx: &'a Bx::CodegenCx,
        rust_main: Bx::Value,
        rust_main_def_id: DefId,
        use_start_lang_item: bool,
    ) -> Bx::Function {
        // The entry function is either `int main(void)` or `int main(int argc, char **argv)`,
        // depending on whether the target needs `argc` and `argv` to be passed in.
        let llfty = if cx.sess().target.main_needs_argc_argv {
            cx.type_func(&[cx.type_int(), cx.type_ptr_to(cx.type_i8p())], cx.type_int())
        } else {
            cx.type_func(&[], cx.type_int())
        };
...

As we can see, one of the two signatures of `main` is used. Incidentally, as you can see, neither of these main functions has a signature valid for UEFI, but that’s not too important right now.

This generated `main` basically calls the following function on the list, i.e., `lang_start`.

### Rust lang\_start()

This function is pretty simple, it just calls the `lang_start_internal`. Incidently, this can also be defined by us if we want. The issue tracking this can be found [here](https://github.com/rust-lang/rust/issues/29633). This function signature is as follows:

Enter fullscreen mode Exit fullscreen mode


rust
fn lang_start(
main: fn() -> T,
argc: isize,
argv: *const *const u8,
) -> isize;


### Rust lang\_start\_internal()

It basically calls the `init` and then the `main` function. It also prevents unwinding in the `init` and `main` functions, which is a requirement. The function signature is as follows:

Enter fullscreen mode Exit fullscreen mode


rust
fn lang_start_internal(
main: &(dyn Fn() -> i32 + Sync + crate::panic::RefUnwindSafe),
argc: isize,
argv: *const *const u8,
) -> Result {


### Rust init()

This function sets up stack\_guard for the current thread. It also calls the `sys::init()` function. The signature for it is the following:

Enter fullscreen mode Exit fullscreen mode


rust
unsafe fn init(argc: isize, argv: *const *const u8);


This is also the function where heap memory starts coming to play.

### Rust sys::init()

This function sets up platform-specific stuff. This is just an empty function on some platforms, while it does a lot of stuff on others. It is generally defined under `std/sys/<platform>/mod.rs` The function signature is as follows:

Enter fullscreen mode Exit fullscreen mode


rust
pub fn init(argc: isize, argv: *const *const u8) {


### Rust main()

This is the function where most normal programs start execution. By this point, it is assumed that all the `std` stuff that needs initialization must be done and available for the user.

## But wait, who calls the C main()?

This is precisely the question I had after reading about all these functions. The answer, though, is a bit less clear. It depends on the platform. The OS `crt0` on most platforms calls the C `main`. On others, well, people just seem to use a custom entry point like `efi_main` and not use `main()`. Since I wanted to use `main` but had a custom entry point with a custom signature, I had to do a hacky implementation to make things work.



## Using efi\_main() with Rust runtime

Since we have established that Rust generates a C `main` function for every Rust program, all we need to do is call `main` and be done with it. However, the problem is how to pass SystemTable and SystemHandle to Rust. Without those pointers, we cannot do much in UEFI. Thus they need to be stored globally somewhere.

After some thought, I have concluded that I will do it in the `sys::init()` function rather than the `efi_main`. The reasons for this are as follows:

1. The `efi_main` currently calls to “C” `main`, so we are jumping language boundaries here.
2. At some point, I would like to replace the `efi_main` with an autogenerated function or even a function written in assembly. For now, I am writing it in Rust, but that might not be the case in the future. Thus it should be as less complicated as possible.
3. `sys::init` seems kinda the natural place for it.

Now, the question is how to get it to reach `sys::init()`. The answer is pretty simple. We can use pointers.

My current implementation looks like the following:

Enter fullscreen mode Exit fullscreen mode


rust

[no_mangle]

pub unsafe extern "efiapi" fn efi_main(
handle: efi::Handle,
st: *mut efi::SystemTable,
) -> efi::Status {
const argc: isize = 2;
let handle_ptr: *const u8 = handle.cast();
let st_ptr: *const u8 = st.cast();
let argv: *const *const u8 = [handle_ptr, st_ptr, core::ptr::null()].as_ptr();

match main(argc, argv) {
    0 => {
        print_pass(st);
        efi::Status::SUCCESS
    }
    _ => {
        print_fail(st);
        efi::Status::ABORTED
    } // Or some other status code
}
Enter fullscreen mode Exit fullscreen mode

}


I just cast both SystemTable and SystemHandle pointers as `*const u8` in the `efi_main`. Then in the `sys::init()`, I cast them back to their original selves. The null at the end is something someone in zulipchat suggested.

And well, it kind of works. This simple hack allows us to get the SystemTable and SystemHandle all the way to `sys::init()`. They can be accessed in the following way:

Enter fullscreen mode Exit fullscreen mode


rust
pub unsafe fn init(argc: isize, argv: *const *const u8) {
let args: &[*const u8] = unsafe { crate::slice::from_raw_parts(argv, argc as usize) };

let handle: r_efi::efi::Handle = args[0] as r_efi::efi::Handle;
let st: *mut r_efi::efi::SystemTable = args[1] as *mut r_efi::efi::SystemTable;
Enter fullscreen mode Exit fullscreen mode

}


Now comes the catch, if we look at the function calling this, the line where the new Thread is created needs an allocator, or else it panics.

Enter fullscreen mode Exit fullscreen mode


rust
// One-time runtime initialization.
// Runs before main.
// SAFETY: must be called only once during runtime initialization.
// NOTE: this is not guaranteed to run, for example when Rust code is called externally.

[cfg_attr(test, allow(dead_code))]

unsafe fn init(argc: isize, argv: *const *const u8) {
unsafe {
sys::init(argc, argv);

    let main_guard = sys::thread::guard::init();
    // Next, set up the current Thread with the guard information we just
    // created. Note that this isn't necessary in general for new threads,
    // but we just do this to name the main thread and to give it correct
    // info about the stack bounds.
    let thread = Thread::new(Some(rtunwrap!(Ok, CString::new("main"))));
    thread_info::set(main_guard, thread);
}
Enter fullscreen mode Exit fullscreen mode

}




We can add a `return` before the thread creation and get to `main` perfectly. However, that is a bit of cheating, so this is where I will leave it for now.



## Conclusion

Initially, I set out to print “Hello World” from `main` in this post. However, after getting burned multiple times, I have finally decided to save it for later. The following post will look at creating and initializing the System Allocator. Spoiler, the `thread_info::set` will start panicking after that, so we will not be able to print “Hello World” even in the next post. Still, we are one step closer to a usable std for UEFI.



## Helpful Links

1. [Rust’s Runtime Post by Michael Gattozzi](https://blog.mgattozzi.dev/rusts-runtime/)
Enter fullscreen mode Exit fullscreen mode
uefi Article's
30 articles in total
Favicon
Debian Secure Boot: To be, or not to be, that is the question!
Favicon
Hosting a HTTP server over UEFI
Favicon
I want to create a menubar setup page in EDK2
Favicon
Understanding the Linux Boot Sequence
Favicon
Need a help with UEFI modifying.
Favicon
Handling ExitBootServices Event in Rust std
Favicon
How to Reverse UEFI modules (DXE Driver)
Favicon
UEFI Rust std has a new home
Favicon
How to make custom UEFI Protocol
Favicon
Reading PCR value from UEFI
Favicon
How to build DXE Driver by EDK2
Favicon
How to add DXE Driver to BIOS image
Favicon
Writing an Allocator for UEFI
Favicon
Using Rust main from a custom entry point
Favicon
Use Restricted std in UEFI
Favicon
Install Windows 11 (UEFI) via USB 3.0 Drive
Favicon
Getting Serial Output from UART on UP2 Pro Board
Favicon
Setup Intel DCI Debugging on UP Squared Board
Favicon
Read&Write BIOS Image using Dediprog SF100 SPI NOR Flash Programmer
Favicon
TPM2_NV_DefineSpace from UEFI
Favicon
TPM2_GetCapability from UEFI
Favicon
TPM2_GetRandom from UEFI
Favicon
Temporarily Suspend BitLocker for firmware updates with reboot
Favicon
How to password protect your computer your BIOS or UEFI
Favicon
How to create UEFI-only bootable USB live media?
Favicon
How to resolve the "Could not create MokListXRT: Out of Resources" Debian boot error on Dell computers
Favicon
The history of BIOS and UEFI
Favicon
First days installing Ubuntu alongside with Windows 10 on an UEFI device
Favicon
UEFI & GPT
Favicon
9pfsPkg: Network Boot from Bell Labs

Featured ones: