Rust custom bare metal compile target: linker expects "_start" symbol and discards unused ones: How can I specify a custom entry symbol?

Viewed 689

I'm cross compiling bare metal 32-bit code for x86 with Rust and I'm facing the problem, that the final object file is empty, if the entry function is not exactly called _start; the linker throws all code away because it sees it as dead. I'm familiar with the fact, that _start is a well-known entry point name, but the question is still:

What part in Rust, LLVM or Linker forces this? Also attributes like extern "C" fn ..., #[no_mangle] or #[export_name = "foobar"] do not work (get thrown away by the linker). My guess is, that it's not the Rust compiler but the linker. As you can see, I use rust-lld as linker and ld.lld as linker-flavor in my case (see below).

  1. Where does the required _start-come from? Why does the linker throw my other code away?
  2. Whats the best option to specify my custom entry point to the linker?

x86-unknown-bare_metal.json

{
  "llvm-target": "i686-unknown-none",
  "data-layout": "e-m:e-i32:32-f80:128-n8:16:32-S128-p:32:32",
  "arch": "x86",
  "target-endian": "little",
  "target-pointer-width": "32",
  "target-c-int-width": "32",
  "os": "none",
  "executables": true,
  "linker-flavor": "ld.lld",
  "linker": "rust-lld",
  "panic-strategy": "abort",
  "disable-redzone": true,
  "features": "+soft-float,+sse"
}

I'm using Rust 1.54.0 nightly and built it on a Linux 5.8.0-system.

I spend some time searching the internet and found discussions, that Rust should eventually get a #[entrypoint="foobar annotation or something like this, but I didn't found an usable solution unfortunately.

My attempt was to append

"pre-link-args": {
  "ld.lld": [
    "-e,foobar"
  ]
}

to the target definition (function also called foobar) but the object file is still empty. An other attempt was to keep all dead code. This works but this solution is dirty.

Minimal code example:

// disable rust standard library
#![no_std]
// disables Rust runtime init,
#![no_main]

// see https://docs.rust-embedded.org/embedonomicon/smallest-no-std.html
#![feature(lang_items)]

// see https://docs.rust-embedded.org/embedonomicon/smallest-no-std.html
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

use core::panic::PanicInfo;
use core::sync::atomic;
use core::sync::atomic::Ordering;

#[no_mangle]
/// The name **must be** `_start`, otherwise the compiler doesn't output anything
/// to the object file. I don't know why it is like this.
/// Also `pub` or `pub extern "C"` doesn't work
fn _start() -> ! {
    loop {}
}

#[inline(never)]
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {
        atomic::compiler_fence(Ordering::SeqCst);
    }
}
1 Answers

New Answer [Solution]

The real solution is quite simple but was hard to find, because it's hard to digg for possible options and solutions in this relatively undocumented field. I found out, that llvm-ld uses the same options, as GNU ld. So I checked against the GNU ld link options and found the solution. It has to be

"pre-link-args": {
  "ld.lld": [
    "--entry=entry_32_bit"
  ]
}

and the value is the name of the function in the file. The function must be annotated with #[no_mangle].

Also see: https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html

Old Answer:

A quick and really dirty solution would be

#[no_mangle]
/// The name **must be** `_start`, otherwise the compiler doesn't output anything
/// to the object file. I don't know it is like this.
fn _start() -> ! {
    entry_32_bit();
}

#[no_mangle]
#[inline(never)]
fn entry_32_bit() -> ! {
    loop {}
}

With this, one can directly jump to symbol entry_32_bit from assembly. But this "solution" is far from ideal! Especially when you want to link multiple bins together, there will be a name collision.

Related