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
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
:
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. This function signature is as follows:
;
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:
,
argc: isize,
argv: *const *const u8,
) >
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:
unsafe ;
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:
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:
- The
efi_main
currently calls to “C”main
, so we are jumping language boundaries here. - 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. 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:
pub unsafe extern "efiapi"
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:
pub unsafe
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.
// 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.
unsafe
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.
Consider supporting me if you like my work.