Inspect first binary(elf) built for RISC-V target

In the previous edition, RISC-V target is set, crate is asked to exclude std library and is built with core library with a necessary custom panic handler and with entry point into _start function.

Now, lets inspect what’s in it. For embedded developers, knowledge on command line tools to inspect a built binary in inevitable. Coz, static diagnosing is easier to catch rather live debugging. In this edition we will run into few command line tools usage; if you aren’t familiar with any, you should be able to find enough details in internet. I’m not going to baby step into how to use these command line tools but will briefly touch upon as and when needed.

  1. List the symbols in the binary (link dead code with rustflags)

Cargo build command creates a directory with set target and build profile(debug) workspace-root-dir/target/riscv32imac-unknown-none-elf/debug and places all the build output and artifacts here.

Though, both nm and objdump command can be used to get the symbols in binary, lets use nm here to make is simple. (objdump is more intrusive and can provide more information). nm command below with option -a to display all the symbols and -n to sort in address. ( I have removed few lines in output to save space, but be rest assured there isn’t any symbol)

bootstrap-ws $riscv64-unknown-elf-nm -an target/riscv32imac-unknown-none-elf/debug/execs
00000000 N 
00000000 N 
00000000 N 
00000000 N .Lline_table_start0
00000000 a 17yclaw1yejlrt4o
00000014 N 
0000002b N 
.... .... 
000004b7 N 
000004bd N 
bootstrap-ws $
bootstrap-ws $cat execs/src/main.rs 
#![no_std]
#![no_main]

use core::panic::PanicInfo;

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

fn _start() {

}

To our surprise, though the main.rs file clearly has symbols (_start, panic, etc), none is present in the execs elf from the nm command output. Its time to discuss and uncover few things about rust compiler. First, rust compiler is built to be more optimized and more efficient. Hence it would aggressively remove any un-used (dead code). In the code above _start function is empty and its not used anywhere yet making it easy for the compiler to throw it away without much difficulty. Now, its time to find a way to tell compiler to NOT throw away any function even if its a piece of dead code. How to do that? ie, to inform compiler in cargo workspace. Normally, you can always throw in some flags to tell rustc (rust compiler) to do this in command line.

But with cargo setup, cargo provides a environment variable RUSTFLAGS to set these flags to rustc compiler (to be precise its linker here). Lets try to pass flag link-dead-code to rustc to keep dead code at link stage in configuration as below.

ootstrap-ws $cat .cargo/config 
[build]
target = "riscv32imac-unknown-none-elf"

rustflags = [

"-C", "link-dead-code",
]

Lets, rebuild (cargo build) and get the nm command output of the execs binary.

bootstrap-ws $cargo build
   Compiling execs v0.1.0 (/Users/benixvincenttheogaraj/RUST/GIT-REPO-RUST/SEP01_2024/rust-on-rv32i/bootstrap-ws/execs)
   Compiling libs v0.1.0 (/Users/benixvincenttheogaraj/RUST/GIT-REPO-RUST/SEP01_2024/rust-on-rv32i/bootstrap-ws/libs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.89s
bootstrap-ws $riscv64-unknown-elf-nm -an target/riscv32imac-unknown-none-elf/debug/execs
00000000 N 
00000000 N 
00000000 N 
00000000 N 
00000000 N .Lline_table_start0
00000000 a 17yclaw1yejlrt4o
00000014 N 
0000002b N 
00000045 N 
0000005e N 
00000083 N 
... .... 
000004e9 N 
000110b4 t 
000110b4 t 
000110b4 t 
000110b4 t rust_begin_unwind
000110b6 t 
000110b8 t 
000110bc t 
000110bc t 
000110bc t 
000110bc t 
000110bc t 
000110bc t 
000110bc t 
000110bc t _ZN5execs6_start17h3de1a8ea11c8462eE
000110be t 
000110be t 
000110be t 
bootstrap-ws $

Now, few extra symbols popped out, but still don’t find _start. But the new symbol 000110bc t _ZN5execs6_start17h3de1a8ea11c8462eE that popped out has the name _start in it which gives the clue it might be distorted idiomatically mangled. Now, lets set an outer attribute #[no_mangle] for this _start function alone as below and get the nm output once again.

bootstrap-ws $cat execs/src/main.rs 
#![no_std]
#![no_main]

use core::panic::PanicInfo;

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

#[no_mangle]
fn _start() {

}
bootstrap-ws $riscv64-unknown-elf-nm -an target/riscv32imac-unknown-none-elf/debug/execs
00000000 N 
00000000 N 
00000000 N 
00000000 N 
00000000 N .Lline_table_start0
00000000 a 17yclaw1yejlrt4o
00000014 N 
0000002b N 
....
000004a0 N 
000004ae N 
000004b3 N 
000004b6 N 
000004be N 
000004c4 N 
000110b4 t 
000110b4 t 
000110b4 t 
000110b4 t rust_begin_unwind
000110b6 t 
......
000110bc t 
000110bc t 
000110bc T _start
000110be t 
000110be t 
000110be t 
bootstrap-ws $

Hooray! now, its visibly present in built elf confirming that the workspace setup is ready to start with RISC-V assembly to bootstrap into rust. But before that, we need to choose a hardware for development in our next edition.

Keep reading