...but then the heaps would no longer line up.
However, if we had a second stage... we could have the first stage map the
second stage wayyy out of the way, and transfer control to it:
And with that little trick, we can load anything .
But let's start small.
Our actual plan has quite a few moving parts, and I definitely handwaved over
a few important bits, but, let's get started with something .
So, let's make a new cargo workspace, in a folder called minipak
, with the
following top-level Cargo manifest:
TOML markup
# in `Cargo.toml`
[ workspace ]
members = [
"crates/encore" ,
]
encore
, according to plan, will be our utility library on top of libcore
that provides a safe interface over pesky stuff like system calls.
Shell session
$ cargo new --lib crates/encore
Created library `crates/encore` package
Great. That way, we can open the minipak
folder in VS
Code and be able to edit all our crates.
(Of course, right now, we only have encore
).
But before we do that: we're going to need Rust nightly. Instead of switching
our default rustup channel, let's simply add a rust-toolchain
file at the
top.
TOML markup
# in `rust-toolchain`
[ toolchain ]
channel = "nightly-2021-02-14"
components = [ "rustfmt" , "clippy" ]
targets = [ "x86_64-unknown-linux-gnu" ]
I normally wouldn't pin to a specific version like nightly-2021-02-14
, but
since we're going to be using a bunch of unstable features, I feel like
it's justified here.
Note that we're installing the x86_64-unknown-linux-gnu
target, but in
reality, we're going to be using very little of it, since we do not actually
intend on using any C library — or any GNU stuff at all, for that matter.
In fact, let's opt out of a whole bunch of stuff immediately.
Since we're going to be needing that in a bunch of other crates (minipak
,
stage1
, and stage2
), let's put those items directly in encore
— that
way we have them everywhere!
Rust code
// in `crates/encore/src/lib.rs`
// Don't use libstd
#![ no_std]
// Allow using inline assembly
#![ feature( asm) ]
// Allow specifying our own eh_personality
#![ feature( lang_items) ]
// Allow using intrinsics
#![ feature( core_intrinsics) ]
// Bring in heap-allocated types like `Vec`, and `String`
extern crate alloc;
pub mod items;
And in that items.rs
file, we're going to immediately get down to business:
Rust code
// in `crates/encore/src/items.rs`
/// Panic handler
#[ panic_handler]
fn panic ( _info : & core:: panic:: PanicInfo ) -> ! {
core:: intrinsics:: abort ( ) ;
}
/// Exception handling personality
#[ lang = "eh_personality" ]
fn eh_personality ( ) { }
// Provides memset, memcpy
extern crate rlibc;
/// Used by parts of libcore that aren't panic=abort
#[ no_mangle]
#[ allow( non_snake_case) ]
unsafe extern "C" fn _Unwind_Resume ( ) { }
/// Provides bcmp (aliased below)
extern crate compiler_builtins;
#[ no_mangle]
unsafe extern "C" fn bcmp ( s1 : * const u8 , s2 : * const u8 , n : usize ) -> i32 {
compiler_builtins:: mem:: bcmp ( s1, s2, n)
}
What does all of this do? Well, even though we're not using libstd
, the
distribution of libcore we're using is built in a certain way. For example,
it assumes we're unwinding on panics, to show stack traces.
Something we are not going to do. Instead we are going to abort on panics,
as show in our panic handler above. Thus, we don't need (and don't want) to
compile in stack unwinding information in our various binaries.
For the parts we're using pre-compiled (mostly libcore), we provide a dummy
implementation of _Unwind_Resume
, which is actually never going to be used,
but will make the linker very happy.
The rest are just functions that rustc sort of assumes are there. Well, it's not
rustc so much as it is LLVM, its backend. If you write code that looks like
memcpy
, it'll actually use memcpy
. Same goes for memset
, etc.
Normally those would be provided by the C library, but since we don't have one,
we'll use this teeny tiny crate called rlibc
that provides just a few functions:
Rust code
$ cargo add rlibc@1.0 . 0 --manifest-path . /crates /encore/Cargo. toml
Updating ' https: //github.com/rust-lang/crates.io-index' index
Adding rlibc v1. 0 . 0 to dependencies
The cargo add
and cargo rm
subcommands are provided by the third-party tool
cargo-edit .
Similarly, bcmp
is something LLVM assumes is there (probably because we're
using a GNU target, now that I think about it), but never fear, we can fish
out an implementation for it as well, in the aptly-named
compiler_builtins crate:
Rust code
$ cargo add compiler_builtins@0.1 . 39 --manifest-path . /crates /encore/Cargo. toml
Updating ' https: //github.com/rust-lang/crates.io-index' index
Adding compiler_builtins v0. 1 . 39 to dependencies
And now, encore
should typecheck, and even build:
Shell session
$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build
Compiling rlibc v1.0.0
Compiling compiler_builtins v0.1.39
Compiling encore v0.1.0 (/home/amos/ftl/minipak/crates/encore)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
But if you're using VS Code like me, with the
rust-analyzer (RA for short) extension
enabled, you may notice a lot of red squigglies around panic
and
eh_personality
, with messages like "found duplicate lang item".
Well, that's because by default, the rust-analyzer extension checks all
targets, including test. And, well, the test harness used by cargo test
is
not no_std
friendly:
Shell session
$ cargo test --quiet
error[E0152]: found duplicate lang item `panic_impl`
--> crates/encore/src/items.rs:3:1
|
3 | fn panic(_info: &core::panic::PanicInfo) -> ! {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: the lang item is first defined in crate `std` (which `test` depends on)
= note: first definition in `std` loaded from /home/amos/.rustup/toolchains/nightly-2021-02-14-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-6f5658153d127ddd.so, /home/amos/.rustup/toolchains/nightly-2021-02-14-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-6f5658153d127ddd.rlib
= note: second definition in the local crate (`encore`)
error[E0152]: found duplicate lang item `eh_personality`
--> crates/encore/src/items.rs:9:1
|
9 | fn eh_personality() {}
| ^^^^^^^^^^^^^^^^^^^
|
= note: the lang item is first defined in crate `panic_unwind` (which `std` depends on)
= note: first definition in `panic_unwind` loaded from /home/amos/.rustup/toolchains/nightly-2021-02-14-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-b69e89b2ef7e009e.rlib
= note: second definition in the local crate (`encore`)
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0152`.
error: could not compile `encore`
To learn more, run the command again with --verbose.
That's actually a really good error message.
It is! So, for the purposes of minimizing the amount of red squigglies going
forward, we're going to add a VS Code workspace config, in which we're going to
tell the rust-analyzer extension to not check all targets, and we'll sprinkle
another couple options that will make completion better with procedural macros.
To open workspace settings, we can simply hit F1 to bring up VS Code's menu, and
start typing "workspace settings", eventually picking the "Preferences: Open Workspace
Settings (JSON)" option, which will create and open .vscode/setting.json
JSON
{
"rust-analyzer.checkOnSave.allTargets" : false ,
"rust-analyzer.procMacro.enable" : true ,
"rust-analyzer.cargo.loadOutDirsFromCheck" : true
}
This file has autocompletion with docs-on-hover and everything, thanks to
JSON schema .
Changing those settings require reloading the VS Code window, so, if you
haven't already, do that (it should prompt for you to do it, but if not, F1
=> Reload Window).
And just like that, the red squiggles are gone!
Now's a good time to make a first commit, if you're following along at home.
Unfortunately, since we didn't create our workspace with cargo new
, there's
no .gitignore
.
And since it is a cargo workspace, all the build artifacts are going to end up
in a folder named target
at the top of the workspace (for all the crates in
the workspace).
So, let's ignore it:
# in `.gitignore`
/target
Also, since we created minipak/crates/encore
with cargo new
before
initializing minipak
as a git repository itself, it created a Git repository
for us, which we can get rid of:
Shell session
$ rm -rf crates/encore/.git
Now let's make an executable:
Shell session
$ cargo new --bin crates/minipak
warning: compiling this new package may not work due to invalid workspace configuration
current package believes it's in a workspace when it's not:
current: /home/amos/ftl/minipak/crates/minipak/Cargo.toml
workspace: /home/amos/ftl/minipak/Cargo.toml
this may be fixable by adding `crates/minipak` to the `workspace.members` array of the manifest located at: /home/amos/ftl/minipak/Cargo.toml
Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.
Created binary (application) `crates/minipak` package
The warning, as often, is right on the money. If we want crates/minipak
to be
part of the workspace, we have to edit our top-level Cargo.toml
.
TOML markup
[ workspace ]
members = [
"crates/encore" ,
# 👇 new!
"crates/minipak" ,
]
Now then! The generated code for our new minipak
executable isn't quite
what we want.
Sure, it works:
Shell session
$ cargo run --bin minipak
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/minipak`
Hello, world!
But it depends on libc:
Shell session
$ ldd ./target/debug/minipak
linux-vdso.so.1 (0x00007ffc4828d000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007ff27639e000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007ff27637d000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007ff276376000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ff2761a9000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff276419000)
And libstd
:
Shell session
$ nm --demangle ./target/debug/minipak | grep std:: | head -5
00000000000180f0 T <std::error::<impl core::convert::From<alloc::string::String> for alloc::boxed::Box<dyn std::error::Error+core::marker::Send+core::marker::Sync>>::from::StringError as core::fmt::Debug>::fmt
00000000000180c0 T <std::error::<impl core::convert::From<alloc::string::String> for alloc::boxed::Box<dyn std::error::Error+core::marker::Send+core::marker::Sync>>::from::StringError as std::error::Error>::description
00000000000180d0 T <std::error::<impl core::convert::From<alloc::string::String> for alloc::boxed::Box<dyn std::error::Error+core::marker::Send+core::marker::Sync>>::from::StringError as core::fmt::Display>::fmt
00000000000480d8 b std::sys_common::at_exit_imp::LOCK
0000000000048100 b std::sys_common::at_exit_imp::QUEUE
So let's make it no_std
!
Rust code
// in `crates/minipak/src/lib.rs`
// 👇
// Opt out of libstd
#![ no_std]
fn main ( ) {
println ! ( "Hello, world!" ) ;
}
Of course, now it doesn't build anymore:
Shell session
$ cargo run --bin minipak
Compiling minipak v0.1.0 (/home/amos/ftl/minipak/crates/minipak)
error: cannot find macro `println` in this scope
--> crates/minipak/src/main.rs:5:5
|
5 | println!("Hello, world!");
| ^^^^^^^
error: language item required, but not found: `eh_personality`
error: `#[panic_handler]` function required, but not found
error: aborting due to 3 previous errors
error: could not compile `minipak`
To learn more, run the command again with --verbose.
The println!
, we can just remove:
Rust code
// in `crates/minipak/src/lib.rs`
// Opt out of libstd
#![ no_std]
fn main ( ) {
// muffin!
}
As for the language items, we have them in encore
!
Shell session
$ (cd crates/minipak && cargo add ../encore)
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding encore (unknown version) to dependencies
Using the --manifest-path
option that cargo-edit
accepts is complicated
here. If we cargo add crates/encore
, the path in minipak's Cargo manifest will
be wrong.
If we cargo add ../encore
, cargo-edit
will immediately fail as it cannot
read encore's Cargo manifest. I'm fairly sure this is a bug in cargo-edit
.
Anyway, cd
does the trick here.
Does it run now?
Shell session
$ cargo run --bin minipak
Compiling minipak v0.1.0 (/home/amos/ftl/minipak/crates/minipak)
error: language item required, but not found: `eh_personality`
error: `#[panic_handler]` function required, but not found
error: aborting due to 2 previous errors
error: could not compile `minipak`
To learn more, run the command again with --verbose.
Mh, no. It's almost like it doesn't actually include encore
in the build...
Can we force it to?
Rust code
// in `crates/minipak/src/lib.rs`
// below global attributes like `#![no_std]`, which must be the first thing in
// the main source file.
extern crate encore;
Shell session
$ cargo run --bin minipak
Compiling minipak v0.1.0 (/home/amos/ftl/minipak/crates/minipak)
error: no global memory allocator found but one is required; link to std or add `#[global_allocator]` to a static item that implements the GlobalAlloc trait.
error: `#[alloc_error_handler]` function required, but not found.
note: Use `#![feature(default_alloc_error_handler)]` for a default error handler.
error: aborting due to 2 previous errors
error: could not compile `minipak`
To learn more, run the command again with --verbose.
Yes!
So that's why we had extern crate rlibc
and extern crate alloc
. We want
them to be linked together with our code even if we don't explicitly use
any
of their symbols!
Correct! In fact, before the Rust 2018
Edition , one
had to add extern crate foobar
for all crates you wanted to use.
And now it's implicit? They're pulled in if you have a use
directive involving
them?
Let's move on to the next errors: it's complaining that we have no global
memory allocator. That's fair.
In encore
, we added extern crate alloc
, because we want to be able to use
types like Vec
and String
, but we haven't set an allocator. So, it fails
at compile time!
Let's pick one from another prolific Rust article writer, Philipp Oppermann:
linked_list_allocator .
Since we'll want an allocator pretty much everywhere, we'll add it directly
to encore
:
Shell session
$ (cd crates/encore && cargo add linked_list_allocator@0.8.11)
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding linked_list_allocator v0.8.11 to dependencies
Whenever you do cargo add
or cargo rm
, Cargo.toml
is edited outside
of VS Code, so the rust-analyzer extension does not pick up the changes (at
the time of this writing, at least).
However, if you bring up the VS Code menu with F1 and search for "Rust
Analyzer: Reload Workspace", you can have it pick up the new crates without
reloading the whole window.
What I like about this allocator is that it doesn't use heuristics to decide
whether to use brk
or mmap
to allocate memory. In fact, it has no
knowledge of Linux syscalls at all! It makes zero assumptions. All it wants
is a bottom address, and a size.
Rust code
// in `crates/encore/src/items.rs`
use linked_list_allocator:: LockedHeap;
// Set a global allocator
#[ global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap:: empty ( ) ;
/// Heap size, in megabytes
const HEAP_SIZE_MB: u64 = 128 ;
/// Initialize a global allocator that only uses `mmap`, with a fixed heap size.
///
/// # Safety
/// Calling this too late (or not at all) and doing a heap allocation will
/// fail. The `mmap` syscall can also fail, which would be disastrous.
pub unsafe fn init_allocator ( ) {
let heap_size = HEAP_SIZE_MB * 1024 * 1024 ;
let heap_bottom = MmapOptions:: new ( heap_size) . map ( ) . unwrap ( ) ;
ALLOCATOR. lock ( ) . init ( heap_bottom as _ , heap_size as _ ) ;
}
All we need to do now is remember to call init_allocator
from all the
crates that use encore
, and we're good to go!
Rust code
// in `crates/minipak/src/main.rs`
// Opt out of libstd
#![ no_std]
fn main ( ) {
unsafe {
encore:: items:: init_allocator ( ) ;
}
}
We no longer need extern crate encore
, because we're using a symbol from it!
And just like that, we should be good to go:
Shell session
$ cargo run --quiet --bin minipak
error[E0433]: failed to resolve: use of undeclared type `MmapOptions`
--> crates/encore/src/items.rs:43:23
|
43 | let heap_bottom = MmapOptions::new(heap_size).map().unwrap();
| ^^^^^^^^^^^ use of undeclared type `MmapOptions`
Oh, uh, no. Where did this MmapOptions
come from?
Getting our timelines crossed?
Yeah, that's what I get for doing prep work ahead of time...
Okay so, obviously, our current init_allocator
function uses some code that
does not exist yet. Well, no worries. We'll just stub a bunch of things!
Rust code
// in `crates/encore/src/lib.rs`
pub mod items;
pub mod error; // new!
pub mod items;
pub mod memmap; // also new!
Rust code
// in `crates/encore/src/error.rs`
#[ derive( Debug) ]
pub enum EncoreError { }
Rust code
// in `crates/encore/src/memmap.rs`
use crate :: error:: EncoreError;
pub struct MmapOptions {
len : u64 ,
}
impl MmapOptions {
/// Create a new set of mmap options
pub fn new ( len : u64 ) -> Self {
Self { len }
}
/// Create a memory mapping,
pub fn map ( & mut self ) -> Result < u64 , EncoreError > {
todo ! ( )
}
}
Rust code
// in `crates/encore/src/items.rs`
use crate :: memmap:: MmapOptions;
Finally, as requested, we'll add an allocation error handler, but this time,
we need to add it directly to minipak
- it can't be set in encore
.
Rust code
// in `crates/minipak/src/main.rs`
// Opt out of libstd
#![ no_std]
// Use the default allocation error handler
#![ feature( default_alloc_error_handler) ]
fn main ( ) {
unsafe {
encore:: items:: init_allocator ( ) ;
}
}
Surely now, minipak
will run:
Shell session
$ cargo run --quiet --bin minipak
error: requires `start` lang_item
Oh, uh, that's interesting. It complains that init_allocator
is not used
anywhere... but we call it from main
?
Do we even have a main
in a no_std
binary? Doesn't a main
need pre-main
initialization code that would normally be provided by a C library?
Ohhhh right! Our entry point is not main
, it's more like _start
, like
we've done before for assembly programs, or echidna
, our previous no_std
binary.
Well, the compiler output mentions the start
language item, but a quick
search reveals that it's far from stable (and also its documentation is kinda
lacking?).
Instead, let's simply opt out of Rust's main
handling altogether and
provide a symbol named _start
, unmangled, that we know our linker (GNU ld)
will pick up.
Since it's an entry point, it's not called , it's "jumped to". We also want
to avoid any stack allocations, since the first thing want to remember is the
value of %rsp
, the stack pointer, so that later on we can read our
arguments, environment variables, and auxiliary vectors.
So, we need to opt in and out of a bunch more stuff:
Rust code
// in `crates/minipak/src/main.rs`
// Opt out of libstd
#![ no_std]
// Let us worry about the entry point.
#![ no_main]
// Use the default allocation error handler
#![ feature( default_alloc_error_handler) ]
// Let us make functions without any prologue - assembly only!
#![ feature( naked_functions) ]
// Let us use inline assembly!
#![ feature( asm) ]
/// Our entry point.
#[ naked]
#[ no_mangle]
unsafe extern "C" fn _start ( ) {
asm ! ( "mov rdi, rsp" , "call pre_main" , options( noreturn) )
}
#[ no_mangle]
unsafe fn pre_main ( _stack_top : * mut u8 ) {
encore:: items:: init_allocator ( ) ;
}
Good, good. I like the look of this. Very low-level. Very "hardcore", as they
say. Much street cred.
Of course this doesn't actually link.
The actual linker output is quite lengthy, so allow me to highlight just
the bit that matters:
Shell session
$ cargo run --quiet --bin minipak
(cut)
= note: /usr/sbin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/Scrt1.o: in function `_start':
(.text+0x16): undefined reference to `__libc_csu_fini'
/usr/sbin/ld: (.text+0x1d): undefined reference to `__libc_csu_init'
/usr/sbin/ld: (.text+0x24): undefined reference to `main'
/usr/sbin/ld: (.text+0x2a): undefined reference to `__libc_start_main'
collect2: error: ld returned 1 exit status
AhAH! We're still getting some code from the C library linked in, and it
expects us to have a main
symbol. But we don't have one!
Let us worry about the entry point. Take a day off, glibc. After a few
decades, you definitely deserve it.
Let's just opt into one... more... unstable feature...
Rust code
// in `crates/minipak/src/main.rs`
// (omitted: other features / attributes)
// Let us pass arguments to the linker directly
#![ feature( link_args) ]
/// Don't link any glibc stuff, also, make this executable static.
#[ allow( unused_attributes) ]
#[ link_args = "-nostartfiles -nodefaultlibs -static" ]
extern "C" { }
// (omitted: entry point, pre_main)
And... now it compiles!
Rust code
$ cargo run --quiet --bin minipak
[ 1 ] 9205 illegal hardware instruction cargo run --quiet --bin minipak
...it even almost runs!
Where does it fail?
Shell session
$ gdb --quiet -ex "run" --args ./target/debug/minipak
Reading symbols from ./target/debug/minipak...
Starting program: /home/amos/ftl/minipak/target/debug/minipak
Program received signal SIGILL, Illegal instruction.
encore::items::panic (_info=0x7fffffffdb98) at /home/amos/ftl/minipak/crates/encore/src/items.rs:4
4 core::intrinsics::abort();
(gdb) bt
#0 encore::items::panic (_info=0x7fffffffdb98) at /home/amos/ftl/minipak/crates/encore/src/items.rs:4
#1 0x0000000000402301 in core::panicking::panic_fmt ()
at /rustc/8e54a21139ae96a2aca3129100b057662e2799b9//library/core/src/panicking.rs:92
#2 0x00000000004022cd in core::panicking::panic ()
at /rustc/8e54a21139ae96a2aca3129100b057662e2799b9//library/core/src/panicking.rs:50
#3 0x0000000000401164 in encore::memmap::MmapOptions::map (self=0x7fffffffdc48)
at /home/amos/ftl/minipak/crates/encore/src/memmap.rs:15
#4 0x0000000000401078 in encore::items::init_allocator () at /home/amos/ftl/minipak/crates/encore/src/items.rs:45
#5 0x000000000040101e in minipak::pre_main (_stack_top=0x7fffffffdc90)
at /home/amos/ftl/minipak/crates/minipak/src/main.rs:28
#6 0x0000000000401008 in minipak::_start () at /home/amos/ftl/minipak/crates/minipak/src/main.rs:23
When we panic! Of course! That's just what abort
does. It attempts to
execute an illegal instruction, which causes the program to brutally stop.
And, see, we don't need panic=unwind
, because we can just run our program
under GDB, and boom, we get a stack trace.
Although, it would be neat if our program printed something before it
crashed.
So, okay, fine, let's actually wrap some syscalls.
We've done this before, again, with echidna
, our previous no_std
program,
in Part 12 .
But we'll try to do it a little better this time, mh?
Let's add a couple crates to encore
that we'll need very soon:
Shell session
$ (cd crates/encore && cargo add bitflags@1.2.1 && cargo add displaydoc@0.1.7 --no-default-features)
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding bitflags v1.2.1 to dependencies
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding displaydoc v0.1.7 to dependencies
Both these crates are no_std
-friendly, provided you disable default feature
for displaydoc
, which we've just done in our cargo-edit
invocation (look
closer!).
Next up, we'll add a syscall
module to encore
:
Rust code
// in `crates/encore/src/lib.rs`
pub mod syscall;
Let's start at the beginning. It would be neat to write messages to the
standard output, affectionately nicknamed "stdout". For that we'll need the
write
syscall.
The first argument that the write
syscall takes is a file descriptor, which
on 64-bit Linux is just a 64-bit integer (or is it limited to 32-bit integers?
doesn't matter, all registers are 64-bit wide anyway).
Let's make a newtype just for file descriptors:
Rust code
// in `crates/encore/src/syscall.rs`
/// A file descriptor
#[ derive( Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) ]
pub struct FileDescriptor ( pub u64 ) ;
impl FileDescriptor {
/// Standard input file descriptor
pub const STDIN: Self = Self ( 0 ) ;
/// Standard output file descriptor
pub const STDOUT: Self = Self ( 1 ) ;
/// Standard error file descriptor
pub const STDERR: Self = Self ( 2 ) ;
}
/// # Safety
/// Calls into the kernel.
#[ inline( always) ]
pub unsafe fn write ( fd : FileDescriptor , buf : * const u8 , count : u64 ) -> u64 {
let syscall_number: u64 = 1 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) fd.0 ,
in( "rsi" ) buf,
in( "rdx" ) count,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
rax
}
Now, there's a lot I like about this wrapper. It forces you to consider that
the first argument is not just any integer, it's actually a file
descriptor. We provide values for stdin, stdout, and stderr, that have the
right type.
And we use lateout
to specify that rcx
and r11
are clobbered, which
means even in release (optimized) builds, everything should work. That's one
mistake we won't be making again!
We can use it right now, from minipak
:
Rust code
// in `crates/minipak/src/main.rs`
#[ no_mangle]
unsafe fn pre_main ( _stack_top : * mut u8 ) {
let s = "Hello from minipak!\n" ;
encore:: syscall:: write (
encore:: syscall:: FileDescriptor:: STDOUT,
s. as_bytes ( ) . as_ptr ( ) ,
s. as_bytes ( ) . len ( ) as _ ,
) ;
encore:: items:: init_allocator ( ) ;
}
Shell session
$ cargo run --quiet --bin minipak
Hello from minipak!
[1] 13309 illegal hardware instruction cargo run --quiet --bin minipak
It works fine, but it's not very ergonomic.
Well, we've been down the "write our custom formatting routines" road before.
It was fun! But we have a lot on our plate, so let's just accept a gift from
the gods libcore, shall we?
Rust code
// in `crates/encore/src/lib.rs`
pub mod utils;
Rust code
// in `crates/encore/src/utils.rs`
use core:: fmt;
#[ macro_export]
macro_rules! println {
( $( $arg: tt) * ) => {
{
use ::core::fmt::Write;
::core::writeln!( $crate::utils::Stdout, $( $arg) * ) .ok( ) ;
}
}
}
pub struct Stdout ;
impl fmt:: Write for Stdout {
fn write_str ( & mut self , s : & str ) -> fmt:: Result {
unsafe {
crate :: syscall:: write (
crate :: syscall:: FileDescriptor:: STDOUT,
s. as_ptr ( ) ,
s. len ( ) as _ ,
) ;
}
Ok ( ( ) )
}
}
Of course we have to define println!
ourselves because libcore
does not
assume we even have anywhere to print to.
Consider this:
Where would this print to? Huh? That's what I thought.
Our definition of println!
is not entirely trivial though, let's go through
it real quick:
Rust code
// We want to be able to access this macro from other crates, so it must be
// exported. Macro namespacing is a bit of a hard problem, because it changes
// *what's parsed*, yet the visibility of macros is also part of, well, what's
// parsed.
// More info here: https://github.com/rust-lang/rust/issues/54727
#[ macro_export]
// Our macro has the name `println`
macro_rules! println {
// It accepts 0 or more "token trees" - any token at all, including
// commas, parentheses, etc.
( $( $arg: tt) * ) => {
// defines a new scope...
{
// ...so that we can import this trait, which we need for the
// next macro invocation.
use ::core::fmt::Write;
// invoke the `writeln!` macro which *is* provided by `libcore`
// since it accepts a destination.
::core::writeln!( $crate::utils::Stdout, $( $arg) * ) .ok( ) ;
// note that `$crate::` is absolutely necessary here - we *cannot*
// just use `crate::`, because since a macro's contents are
// copy-pasted wherever it's used, it would end up referring to
// the crate that _invokes_ the macro, rather than the crate where
// it's defined. we could use `encore::` instead, but then we would
// have to change it if we ever decide to rename this crate, which
// I feel too lazy to do just _thinking_ about it.
}
}
}
Now, normally when you use libstd
, your program implicitly uses a prelude,
with a bunch of stuff in it, like Vec
, and... the println!
macro.
We can do the same with encore
:
Rust code
// in `crates/encore/src/lib.rs`
pub mod prelude;
Rust code
// in `crates/encore/src/prelude.rs`
pub use crate :: { error:: EncoreError, items:: init_allocator, memmap:: MmapOptions, println, syscall} ;
pub use alloc:: { fmt:: Write, format, string:: String, vec:: Vec} ;
And now, minipak
's main.rs
can look like this:
Rust code
// in `crates/minipak/src/main.rs`
// (omitted: everything up until and including `_start`)
use encore:: prelude:: * ;
#[ no_mangle]
unsafe fn pre_main ( _stack_top : * mut u8 ) {
println ! ( "Initializing allocator..." ) ;
init_allocator ( ) ;
println ! ( "Initializing allocator... done!" ) ;
}
And now we get:
Rust code
$ cargo run --quiet --bin minipak
Initializing allocator...
[ 1 ] 15284 illegal hardware instruction cargo run --quiet --bin minipak
Nice!
Of course we still can't initialize our allocator, and our panics still
straight up abort, which is not very informative.
Just in case this isn't our last panic, let's zhuzh up our panic handler:
Rust code
// in `crates/encore/src/items.rs`
/// Panic handler
#[ panic_handler]
fn panic ( info : & core:: panic:: PanicInfo ) -> ! {
crate :: println!( "{}" , info) ;
core:: intrinsics:: abort ( ) ;
}
Even though we defined the println!
macro in the utils
module of the
encore
crate, its full path is crate::println!
, not
crate::utils::println!
.
Told you macro namespacing was a bit fuzzy!
Now we have this:
Shell session
$ cargo run --quiet --bin minipak
Initializing allocator...
panicked at 'not yet implemented', crates/encore/src/memmap.rs:16:9
[1] 15709 illegal hardware instruction cargo run --quiet --bin minipak
Which feels much nicer.
Setting the RUST_BACKTRACE
environment variable to 1
does sweet nothing,
because stack unwinding is something libstd
does, and we've opted out of
it.
Speaking of, we should probably opt into panic=abort
as well, just so we
don't generate unnecessary code.
Since it goes into a "profile", it has to be in the top-level Cargo manifest,
that of the workspace, not any specific crate:
TOML markup
# in `Cargo.toml`
[ workspace ]
members = [
"crates/encore" ,
"crates/minipak" ,
]
[ profile . dev ]
panic = "abort"
[ profile . release ]
debug = true
lto = "thin"
panic = "abort"
While we're at it, I've also signed us up for debug symbols in release builds,
and Thin link-time optimization .
Just by switching to panic = "abort"
, our binary size reduced from 1.1 MiB
to 799 KiB. Not bad! It's one of the tricks recommended in min-sized
rust .
Our release binary is currently 89 KiB unstripped, 17 KiB stripped. That's
fairly close to being optimal, given that we have 4 sections:
One with the ELF headers
One with the code
One with constants
One with read-write globals
...so the minimum size is 4*4KiB = 16KiB. Taking a quick look at a stripped
version of our release build reveals that our code takes up slightly more than
4 KiB:
Shell session
$ readelf -Wl ./target/release/minipak
Elf file type is EXEC (Executable file)
Entry point 0x402560
There are 8 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000224 0x000224 R 0x1000
👇
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x0015d5 0x0015d5 R E 0x1000
LOAD 0x003000 0x0000000000403000 0x0000000000403000 0x0004c8 0x0004c8 R 0x1000
LOAD 0x003e90 0x0000000000404e90 0x0000000000404e90 0x000170 0x000170 RW 0x1000
Doesn't our program still segfault?
Ah, right! We never actually wrapped mmap
.
Well, let's do so now.
The mmap
syscall is a tad more complex than open
, at least in its interface.
It accepts two kinds of flags: the memory protection (readable, writable,
executable), and some options (private, fixed, anonymous).
For flags like those, the bitflags crate
will come in really handy:
Rust code
// in `crates/encore/src/syscall.rs`
use bitflags:: * ;
bitflags ! {
/// Memory protection: readable, writable, executable
#[ derive( Default) ]
pub struct MmapProt: u64 {
/// The mapping will be readable
const READ = 0x1 ;
/// The mapping will be writable
const WRITE = 0x2 ;
/// The mapping will be executable
const EXEC = 0x4 ;
}
}
bitflags ! {
/// Flags for the `mmap` syscall
pub struct MmapFlags: u64 {
/// Create a private copy-on-write mapping.
const PRIVATE = 0x02 ;
/// Don't interpret addr as a hint: place the mapping at exactly that address.
const FIXED = 0x10 ;
/// Mapping is not backed by any file
const ANONYMOUS = 0x20 ;
}
}
/// # Safety
/// Calls into the kernel. May unmap running code.
/// One of the most unsafe syscalls in existence.
#[ inline( always) ]
pub unsafe fn mmap (
addr : u64 ,
len : u64 ,
prot : MmapProt ,
flags : MmapFlags ,
fd : FileDescriptor ,
off : u64 ,
) -> u64 {
let syscall_number: u64 = 9 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) addr,
in( "rsi" ) len,
in( "rdx" ) prot.bits( ) ,
in( "r10" ) flags.bits( ) ,
in( "r8" ) fd.0 ,
in( "r9" ) off,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
rax
}
The bitflags!
macro lets us define a custom type that behaves like
bitflags: the BitAnd
and BitOr
traits are implemented, for example, so we
can do MmapFlags::PRIVATE | MmapFlags::FIXED
and things just work.
The bits
method returns the underlying type, here u64
, so that we can
"pass it to assembly".
So, we have an mmap
wrapper! Still, that's a lot of parameters. Calling
it directly is not ergonomic at all. Not one bit.
Let's try to make a safer abstraction on top of it!
There's already a handful of Rust crates that try to abstract mmap
safely,
but they rely on libc, which we cannot have, and also, they have opinions that
are slightly different from mine.
I'd like mine to be able to:
Allocate big blocks of memory, read-write
Occasionally map entire files, read-only
Check that offsets are page-aligned before doing the syscall
The builder pattern seems
like a good fit here, so let's go for it:
Rust code
// in `crates/encore/src/memmap.rs`
use syscall:: FileDescriptor;
use crate :: {
error:: EncoreError,
syscall:: { self , MmapFlags, MmapProt} ,
} ;
/// Options for creating a memory mapping
pub struct MmapOptions {
prot : MmapProt ,
flags : MmapFlags ,
len : u64 ,
file : Option < FileOpts > ,
at : Option < u64 > ,
}
/// Options for mapping a file
#[ derive( Default, Clone) ]
pub struct FileOpts {
/// An open file descriptor
pub fd : FileDescriptor ,
/// The offset at which to map the file
pub offset : u64 ,
}
impl MmapOptions {
pub fn new ( len : u64 ) -> Self {
Self {
prot : MmapProt:: READ | MmapProt:: WRITE,
flags : MmapFlags:: ANONYMOUS | MmapFlags:: PRIVATE,
len,
file : None,
at : None,
}
}
/// Specify a file that should be mapped
pub fn file ( & mut self , file : FileOpts ) -> & mut Self {
self . file = Some ( file) ;
self
}
/// Sets protections - defaults to READ+WRITE
pub fn prot ( & mut self , prot : MmapProt ) -> & mut Self {
self . prot = prot;
self
}
/// Set flags. Note that `ANONYMOUS` and `PRIVATE` are the default,
/// and this overwrites them. If `at` is set, `FIXED` is also used.
pub fn flags ( & mut self , flags : MmapFlags ) -> & mut Self {
self . flags = flags;
self
}
/// Specify a fixed address for this mapping (sets the `FIXED` flag)
pub fn at ( & mut self , at : u64 ) -> & mut Self {
self . at = Some ( at) ;
self
}
/// Actually create the mapping
pub fn map ( & mut self ) -> Result < u64 , EncoreError > {
let mut flags = self . flags ;
if let Some( at) = & self . at {
if !is_aligned ( * at) {
return Err ( EncoreError:: MmapMemUnaligned ( * at) ) ;
}
flags. insert ( MmapFlags:: FIXED) ;
}
if let Some( file) = & self . file {
if !is_aligned ( file. offset ) {
return Err ( EncoreError:: MmapFileUnaligned ( file. offset ) ) ;
}
flags. remove ( MmapFlags:: ANONYMOUS) ;
}
let file = self . file . clone ( ) . unwrap_or_default ( ) ;
let addr = self . at . unwrap_or_default ( ) ;
let res = unsafe { syscall:: mmap ( addr, self . len , self . prot , flags, file. fd , file. offset ) } ;
if res as i64 == -1 {
return Err ( EncoreError:: MmapFailed) ;
}
Ok ( res)
}
}
fn is_aligned ( x : u64 ) -> bool {
x & 0xFFF == 0
}
There!
Note that we don't have to worry about flags much. mmap
can do a lot more,
like for example sharing memory across multiple processes, but we don't need
that here — all our mappings are PRIVATE
.
They're also ANONYMOUS
, unless the user passes a FileOpts
, in which case
they're able to specify a FileDescriptor
and an offset in one fell swoop.
And finally, calling at
automatically sets the FIXED
flag. Oh, and any
offsets specified are checked for alignment. We assume 4KiB pages because...
it's convenient.
We've used EncoreError
variants that don't exist yet, though: let's fix that.
Normally, I'd use a crate like thiserror ,
but it is unfortunately not no_std
friendly, so instead, it's time to
give Jane's wonderful displaydoc a try.
Rust code
// in `crates/encore/src/error.rs`
#[ derive( Debug) ]
pub enum EncoreError {
/// mmap fixed address provided was not aligned to 0x1000: {0}
MmapMemUnaligned( u64 ) ,
/// mmap file offset provided was not aligned to 0x1000: {0}
MmapFileUnaligned( u64 ) ,
/// mmap syscall failed
MmapFailed,
}
Et voilà! Le tour est joué.
Shell session
$ cargo run --quiet --bin minipak
Initializing allocator...
Initializing allocator... done!
[1] 19138 illegal hardware instruction cargo run --quiet --bin minipak
Oh, forgot about exit
.
Rust code
// in `crates/encore/src/syscall.rs`
#[ inline( always) ]
pub fn exit ( code : i32 ) -> ! {
let syscall_number: u64 = 60 ;
unsafe {
asm ! (
"syscall" ,
in( "rax" ) syscall_number,
in( "rdi" ) code,
options( noreturn, nostack) ,
) ;
}
}
Rust code
// in `crates/minipak/src/main.rs`
#[ no_mangle]
unsafe fn pre_main ( _stack_top : * mut u8 ) {
println ! ( "Initializing allocator..." ) ;
init_allocator ( ) ;
println ! ( "Initializing allocator... done!" ) ;
syscall:: exit ( 0 ) ;
}
Shell session
$ cargo run --quiet --bin minipak
Initializing allocator...
Initializing allocator... done!
Cool! It doesn't crash anymore! It's time to wish y'all a good week, as I go
back to my cave day job until ne-
...but we didn't do anything?
Oh what, planning doesn't count? Plus we did a ton of preparation!
Right, but that's really boring. And we've done it all before, with
echidna
. There was nothing new here, nothing new at all!
Ahhhh well we've done it before, but not that well. You can tell that this
time, we're preparing for war. We have proper string formatting, a println!
macro, a nice panic handler, even a memory allocator.
Speaking of, we haven't even tried using that memory allocator! How do we know
it works??
You know what bear, that's fair. Let's have a little bit of fun before we
close out. Just to showcase what we can do from our no_std
binary.
For example, we could print the contents of /etc/hosts
- that's always fun!
So first we open a file, and then we... ah crap, I forgot about open
.
Fine, FINE, we'll make an abstraction for files, but then I'm out of here.
First, a couple syscalls:
Rust code
// in `encore/src/syscall.rs`
bitflags ! {
/// Flags for the `open` syscall
pub struct OpenFlags: u64 {
/// Read-only (open flag)
const RDONLY = 0o0 ;
/// Read-write (open flag)
const RDWR = 0o2 ;
/// Create (open flag)
const CREAT = 0o100 ;
/// Truncate (open flag)
const TRUNC = 0o1000 ;
}
}
/// # Safety
/// Calls into the kernel.
#[ inline( always) ]
pub unsafe fn open ( filename : * const u8 , flags : OpenFlags , mode : u64 ) -> FileDescriptor {
let syscall_number: u64 = 2 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) filename,
in( "rsi" ) flags.bits( ) ,
in( "rdx" ) mode,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
FileDescriptor ( rax)
}
#[ repr( C) ]
pub struct Stat {
// As found using `offsetof` and `sizeof`
_unused1 : [ u8 ; 48 ] ,
pub size : u64 ,
_unused2 : [ u8 ; 88 ] ,
}
/// # Safety
/// Calls into the kernel.
#[ inline( always) ]
pub unsafe fn fstat ( fd : FileDescriptor , buf : * mut Stat ) -> u64 {
let syscall_number: u64 = 5 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) fd.0 ,
in( "rsi" ) buf,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
rax
}
/// # Safety
/// Calls into the kernel.
#[ inline( always) ]
pub unsafe fn close ( fd : FileDescriptor ) -> u64 {
let syscall_number: u64 = 3 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) fd.0 ,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
rax
}
/// # Safety
/// Calls into the kernel.
#[ inline( always) ]
pub unsafe fn munmap < T > ( addr : * const T , len : u64 ) -> u64 {
let syscall_number: u64 = 11 ;
let mut rax = syscall_number;
asm ! (
"syscall" ,
inout( "rax" ) rax,
in( "rdi" ) addr,
in( "rsi" ) len,
lateout( "rcx" ) _, lateout( "r11" ) _,
options( nostack) ,
) ;
rax
}
And then, an fs
module!
Rust code
// in `crates/encore/src/lib.rs`
pub mod fs;
Rust code
// in `crates/encore/src/fs.rs`
use crate :: {
error:: EncoreError,
memmap:: { FileOpts, MmapOptions} ,
syscall:: { self , FileDescriptor, MmapProt, OpenFlags, Stat} ,
} ;
use alloc:: { format, string:: String} ;
use core:: {
mem:: MaybeUninit,
ops:: { Index, Range} ,
} ;
/// A read-only file
pub struct File {
path : String ,
fd : FileDescriptor ,
}
#[ allow( clippy::clippy::len_without_is_empty) ]
impl File {
/// Opens a file (read-only)
pub fn open ( path : & str ) -> Result < Self , EncoreError > {
Self:: raw_open ( path, OpenFlags:: RDONLY, 0 )
}
/// Creates a file (for writing)
pub fn create ( path : & str , mode : u64 ) -> Result < Self , EncoreError > {
Self:: raw_open (
path,
OpenFlags:: RDWR | OpenFlags:: CREAT | OpenFlags:: TRUNC,
mode,
)
}
/// Internal: open a file with given flags and mode
fn raw_open ( path : & str , flags : OpenFlags , mode : u64 ) -> Result < Self , EncoreError > {
let nul_path = format ! ( "{}\0" , path) ;
let fd = unsafe { syscall:: open ( nul_path. as_ptr ( ) , flags, mode) } ;
if ( fd. 0 as i64 ) < 0 {
return Err ( EncoreError:: Open ( path. into ( ) ) ) ;
}
Ok ( Self {
path : path. into ( ) ,
fd,
} )
}
/// Write a whole buffer to this file, or fail. This may involve
/// multiple syscalls.
pub fn write_all ( & mut self , mut buf : & [ u8 ] ) -> Result < ( ) , EncoreError > {
while !buf. is_empty ( ) {
let written = unsafe { syscall:: write ( self . fd , buf. as_ptr ( ) , buf. len ( ) as u64 ) } ;
if written as i64 == -1 {
return Err ( EncoreError:: Write ( self . path . clone ( ) ) ) ;
}
buf = & buf[ written as usize ..] ;
}
Ok ( ( ) )
}
/// Returns the length of the file, in bytes
pub fn len ( & self ) -> Result < u64 , EncoreError > {
let mut stat = MaybeUninit :: < Stat > :: uninit ( ) ;
let ret = unsafe { syscall:: fstat ( self . fd ( ) , stat. as_mut_ptr ( ) ) } ;
if ret != 0 {
return Err ( EncoreError:: Stat ( self . path . clone ( ) ) ) ;
}
let stat = unsafe { stat. assume_init ( ) } ;
Ok ( stat. size )
}
/// Returns the file descriptor
pub fn fd ( & self ) -> FileDescriptor {
self . fd
}
/// Map this file in memory (read-only)
pub fn map ( & self ) -> Result < Map < ' _ > , EncoreError > {
let self_data = MmapOptions:: new ( self . len ( ) ?)
. file ( FileOpts {
fd : self . fd ,
offset : 0 ,
} )
. prot ( MmapProt:: READ)
. map ( ) ? as * const u8 ;
let data = unsafe { core:: slice:: from_raw_parts ( self_data, self . len ( ) ? as _ ) } ;
Ok ( Map { file : self , data } )
}
}
impl Drop for File {
fn drop ( & mut self ) {
// Close on drop
unsafe { syscall:: close ( self . fd ) } ;
}
}
pub struct Map < ' a > {
/// This file exists so the file isn't closed until the mapping is dropped.
#[ allow( unused) ]
file : & ' a File ,
data : & ' a [ u8 ] ,
}
impl < ' a > Drop for Map < ' a > {
fn drop ( & mut self ) {
// Munmap on drop
unsafe { syscall:: munmap ( self . data . as_ptr ( ) , self . data . len ( ) as _ ) } ;
}
}
impl < ' a > AsRef < [ u8 ] > for Map < ' a > {
fn as_ref ( & self ) -> & [ u8 ] {
self . data
}
}
impl < ' a > Index < Range < usize > > for Map < ' a > {
type Output = [ u8 ] ;
fn index ( & self , index : Range < usize > ) -> & Self:: Output {
& self . data [ index]
}
}
Add the missing error variants:
Rust code
// in `crates/encore/src/error.rs`
use alloc:: string:: String;
#[ derive( Debug) ]
pub enum EncoreError {
/// Could not open file `0`
Open( String ) ,
/// Could not write to file `0`
Write( String ) ,
/// Could not statfile `0`
Stat( String ) ,
/// mmap fixed address provided was not aligned to 0x1000: {0}
MmapMemUnaligned( u64 ) ,
/// mmap file offset provided was not aligned to 0x1000: {0}
MmapFileUnaligned( u64 ) ,
/// mmap syscall failed
MmapFailed,
}
And for good measure, let's add some of these to our custom prelude:
Rust code
// in `crates/encore/src/prelude.rs`
pub use crate :: {
error:: EncoreError,
fs:: File,
items:: init_allocator,
memmap:: MmapOptions,
println,
syscall:: { self , MmapFlags, MmapProt, OpenFlags} ,
} ;
pub use alloc:: {
fmt:: Write,
format,
string:: { String, ToString} ,
vec:: Vec,
} ;
And now we can print the contents of some file:
Rust code
// in `crates/minipak/src/main.rs`
#[ no_mangle]
unsafe fn pre_main ( _stack_top : * mut u8 ) {
init_allocator ( ) ;
main ( ) . unwrap ( ) ;
syscall:: exit ( 0 ) ;
}
fn main ( ) -> Result < ( ) , EncoreError > {
let file = File:: open ( "/etc/lsb-release" ) ?;
let map = file. map ( ) ?;
let s = core:: str:: from_utf8 ( & map[ ..] ) . unwrap ( ) ;
for l in s. lines ( ) {
println ! ( "> {}" , l) ;
}
Ok ( ( ) )
}
Shell session
$ cargo run --quiet --bin minipak
> DISTRIB_DESCRIPTION="Arch Linux"
> DISTRIB_RELEASE=rolling
> DISTRIB_ID=Arch
> LSB_VERSION=1.4
Hurray!
Cool, cool, but... we haven't done anything with executables in this article.
And yet it takes about an hour to read from end to end! Let this be your
lesson for the day bear: things that don't feel like work, but are
necessary for other work to happen, are still work.
And we did damn good work. It'll serve us well in the future.
Grhmph. I still would've liked to load an ELF object. Just one.
Fine, you want to load an executable? Here you go:
Rust code
// in `crates/minipak/src/main.rs`
fn main ( ) -> Result < ( ) , EncoreError > {
let file = File:: open ( "/lib64/ld-linux-x86-64.so.2" ) ?;
let map = file. map ( ) ?;
let there_you_go = core:: str:: from_utf8 ( & map[ 1 ..4 ] ) . unwrap ( ) ;
println ! ( "{}" , there_you_go) ;
Ok ( ( ) )
}
Shell session
$ cargo run --quiet --bin minipak
ELF
There 😇
Grahhhhhhhhhhh that's not what I m-
Goodbye!