diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..abf44e2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,15 @@ +[target.x86_64-unknown-linux-gnu] +linker = "x86_64-linux-gnu-gcc" +rustflags = [ + "-Clink-args=-nostartfiles", + "-Clink-args=-Wl,-n,-N,--no-dynamic-linker,--build-id=none", + "-Crelocation-model=static" +] + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +rustflags = [ + "-Clink-args=-nostartfiles", + "-Clink-args=-Wl,-n,-N,--no-dynamic-linker,--build-id=none", + "-Crelocation-model=static" +] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e7f16a0..8070a74 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,30 +12,18 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y make nasm binutils binutils-aarch64-linux-gnu + sudo apt-get install -y curl make nasm qemu-user-static \ + gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu + curl https://sh.rustup.rs -sSf | sh -s -- -y - - name: Build via Makefile - run: make ci_build + - name: Build and Test + run: | + . "$HOME/.cargo/env" + make -s ci_tests - - name: Public artifact - uses: actions/upload-artifact@v1 + - name: Publish artifact + uses: actions/upload-artifact@v4 with: name: Build Artifact path: out/ - - test: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - - name: Install dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y make nasm binutils binutils-aarch64-linux-gnu - sudo wget https://github.com/multiarch/qemu-user-static/releases/download/v7.1.0-2/qemu-aarch64-static -O /usr/sbin/qemu-aarch64-static - sudo chmod +x /usr/sbin/qemu-aarch64-static - - - name: Run Tests - run: make ci_tests diff --git a/.gitignore b/.gitignore index 553042b..c771476 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea out/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6ddf31f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "rs-nap" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cfdc123 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +resolver = "2" +members = ["rs-nap"] + +[profile.release] +strip = true +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true diff --git a/Makefile b/Makefile index 5fd520f..5887c8c 100644 --- a/Makefile +++ b/Makefile @@ -9,21 +9,12 @@ ci_build: x86_64-linux-gnu-ld -m elf_x86_64 -z noseparate-code -z noexecstack --strip-all -o out/nap nap.o && rm nap.o aarch64-linux-gnu-as nap-aarch64.s -o nap-aarch64.o aarch64-linux-gnu-ld -z noseparate-code -z noexecstack --strip-all -o out/nap-aarch64 nap-aarch64.o && rm nap-aarch64.o + cargo build --release --target-dir target --target x86_64-unknown-linux-gnu && cp target/x86_64-unknown-linux-gnu/release/rs-nap out/rs-nap-x86_64 + cargo build --release --target-dir target --target aarch64-unknown-linux-gnu && cp target/aarch64-unknown-linux-gnu/release/rs-nap out/rs-nap-aarch64 ls -la out/ ci_tests: ci_build - if [ $$(arch) = "x86_64" ]; then \ - echo "[x86_64] Testing 1s nap"; \ - timeout 3s out/nap 1 2>&1 > /dev/null; \ - echo "[x86_64] Testing 10s default/bad input nap"; \ - timeout 12s out/nap bad_arg ; if [ $$? = "1" ]; then true; else false; fi; \ - else \ - echo "x86_64 testing not available on aarch64 platform"; \ - fi - echo "[aarch64] Testing 1s nap" - timeout 3s qemu-aarch64-static out/nap-aarch64 1 2>&1 > /dev/null - echo "[aarch64] Testing 10s default/bad input nap" - timeout 12s qemu-aarch64-static out/nap-aarch64 bad_arg ; if [ $$? = "1" ]; then true; else false; fi + @bash run-tests.sh tests: install if [ $$(arch) = "x86_64" ]; then \ diff --git a/rs-nap/Cargo.lock b/rs-nap/Cargo.lock new file mode 100644 index 0000000..6ddf31f --- /dev/null +++ b/rs-nap/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "rs-nap" +version = "0.1.0" diff --git a/rs-nap/Cargo.toml b/rs-nap/Cargo.toml new file mode 100644 index 0000000..dab6002 --- /dev/null +++ b/rs-nap/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rs-nap" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/rs-nap/README.md b/rs-nap/README.md new file mode 100644 index 0000000..64681f5 --- /dev/null +++ b/rs-nap/README.md @@ -0,0 +1,60 @@ +# Rust rewrite + +## Goals + +### Feature parity + +- [x] Maintain roughly the same binary size +- [x] Zero requirements other than the linux kernel +- [x] Increased readability over assembly +- [x] Shared code base for all architectures + +### Architecture support + +- [x] x86_64 implementation +- [x] aarch64 implementation + +#### Architecture specific implementation notes + +For each new architecture: + +1. Add a file to the `support` subfolder which implements + - The startup code (`_start`) to get arguments from the stack + - and the following linux kernel service calls + - `sys_exit` to call `exit` + - `sys_write` to call `write` to stdout 1 + - and `sys_sleep` +2. Add a platform specific entry to `support.rs` in the form of a `cfg_attr` +3. Add a platform target to `.cargo/config.toml` +4. Add a toolchain target to `rust-toolchain.toml` +5. Update this `README.md` with the new platform build and run commands +6. Update any CI/CD (future) to build the binary for the target platform + +## Run on a native processor + +```sh +# Build +cargo build --release +# Display file size +ls -lah ./target/release/rs-nap +# Run for 3 seconds +./target/release/rs-nap 3 +``` + +## Run on an emulated processor + +```sh +# Install cross compiler and emulation layer + +# Debian-based distros +sudo apt -y install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu qemu-user-static +``` + +```sh +# Build +cargo build --target aarch64-unknown-linux-gnu --release +# Display file size +ls -lah target/aarch64-unknown-linux-gnu/release/rs-nap +# Run for 3 seconds +qemu-aarch64-static target/aarch64-unknown-linux-gnu/release/rs-nap 3 +``` diff --git a/rs-nap/src/main.rs b/rs-nap/src/main.rs new file mode 100644 index 0000000..967cc17 --- /dev/null +++ b/rs-nap/src/main.rs @@ -0,0 +1,53 @@ +#![no_std] +#![no_main] +#![feature(naked_functions)] + +mod support; +use support::*; +use core::slice::from_raw_parts as mkslice; + +#[no_mangle] +pub unsafe fn nap(args: &[*const u8]) -> ! { + let mut sleep_time:usize = 10; + let mut good_input:bool = false; + + if args.len() > 1 { + (sleep_time, good_input) = get_sleep_time(args[1]); + } + + sleep(sleep_time, good_input); + + print_str(b"Done!\n"); + sys_exit(0) +} + +pub unsafe fn sleep(mut sleep_time: usize, good_input: bool) { + if good_input == false { + sleep_time = 10; + print_str(b"Bad input. "); + } + + print_str(b"Sleeping for "); + print_num(sleep_time); + print_str(b" seconds...\n"); + + sys_sleep(sleep_time); +} + +unsafe fn get_sleep_time(arg: *const u8) -> (usize, bool) { + let (seconds,_) = from_radix_10(mkslice(arg, strlen(arg))); + (seconds, seconds > 0 && seconds < 1000000000) +} + +#[no_mangle] +unsafe fn get_args(stack_top: *const u8) { + let argc = *(stack_top as *const usize); + let argv = stack_top.add(8) as *const *const u8; + let args = mkslice(argv, argc as usize); + nap(args) +} + +#[panic_handler] +unsafe fn my_panic(_info: &core::panic::PanicInfo) -> ! { + sys_exit(255) +} diff --git a/rs-nap/src/support.rs b/rs-nap/src/support.rs new file mode 100644 index 0000000..47e42b6 --- /dev/null +++ b/rs-nap/src/support.rs @@ -0,0 +1,8 @@ +#[cfg_attr(all(target_arch="x86_64", target_os="linux"), path="support/x86_64.rs")] +#[cfg_attr(all(target_arch="aarch64", target_os="linux"), path="support/aarch64.rs")] +mod support; +pub use support::*; + +#[path="support/noarch.rs"] +mod noarch; +pub use noarch::*; diff --git a/rs-nap/src/support/aarch64.rs b/rs-nap/src/support/aarch64.rs new file mode 100644 index 0000000..d430bb1 --- /dev/null +++ b/rs-nap/src/support/aarch64.rs @@ -0,0 +1,47 @@ +use core::arch::{asm,naked_asm}; + +mod interop; +use interop::timespec; + +#[no_mangle] +#[naked] +unsafe extern "C" fn _start() { + // Move the stack pointer before it gets clobbered + naked_asm!( + "mov fp, sp", + "mov x0, fp", + "bl get_args" + ) +} + +pub unsafe fn sys_exit(exit_code:usize) -> ! { + asm!("svc 0", + in("w8") 93, + in("x0") exit_code, + options(nostack, noreturn) + ) +} + +pub unsafe fn sys_write(buffer: *const u8, count: usize) { + asm!("svc #0", + inout("x0") 1 => _, + inout("x1") buffer => _, + inout("x2") count => _, + inout("x8") 64 => _, + options(nostack) + ) +} + +pub unsafe fn sys_sleep(seconds: usize) { + let sleep_time = timespec { + tv_sec: seconds as isize, + tv_nsec: 0 + }; + + asm!("svc 0", + in("x0") &sleep_time, + in("x1") 0, + in("x8") 101, + options(nostack, preserves_flags) + ) +} diff --git a/rs-nap/src/support/interop.rs b/rs-nap/src/support/interop.rs new file mode 100644 index 0000000..02220fe --- /dev/null +++ b/rs-nap/src/support/interop.rs @@ -0,0 +1,5 @@ +#[repr(C)] +pub struct timespec { + pub tv_sec: isize, + pub tv_nsec: isize, +} diff --git a/rs-nap/src/support/noarch.rs b/rs-nap/src/support/noarch.rs new file mode 100644 index 0000000..8f33330 --- /dev/null +++ b/rs-nap/src/support/noarch.rs @@ -0,0 +1,65 @@ +use crate::support::sys_write; + +pub unsafe fn strlen(mut s: *const u8) -> usize { + let mut count = 0; + while *s != b'\0' { + count += 1; + s = s.add(1); + } + count +} + +pub fn print_str(s: &[u8]) { + unsafe { + sys_write(s.as_ptr(), s.len()); + } +} + +pub fn print_num(n: usize) { + if n > 9 { + print_num(n / 10); + } + let c = b'0' + (n % 10) as u8; + print_str(&[c]); +} + +fn nth(n: u8) -> usize +{ + let mut i:usize = 0; + for _ in 0..n { + i = i + 1; + } + i +} + +pub fn ascii_to_digit(character: u8) -> Option { + match character { + b'0' => Some(nth(0)), + b'1' => Some(nth(1)), + b'2' => Some(nth(2)), + b'3' => Some(nth(3)), + b'4' => Some(nth(4)), + b'5' => Some(nth(5)), + b'6' => Some(nth(6)), + b'7' => Some(nth(7)), + b'8' => Some(nth(8)), + b'9' => Some(nth(9)), + _ => None, + } +} + +pub fn from_radix_10(text: &[u8]) -> (usize, usize) { + let mut index:usize = 0; + let mut number:usize = 0; + while index != text.len() { + if let Some(digit) = ascii_to_digit(text[index]) { + number *= nth(10); + number += digit; + index += 1; + } else { + break; + } + } + (number, index) +} + diff --git a/rs-nap/src/support/x86_64.rs b/rs-nap/src/support/x86_64.rs new file mode 100644 index 0000000..beb0a64 --- /dev/null +++ b/rs-nap/src/support/x86_64.rs @@ -0,0 +1,47 @@ +use core::arch::{asm,naked_asm}; + +mod interop; +use interop::timespec; + +#[no_mangle] +#[naked] +unsafe extern "C" fn _start() { + // Move the stack pointer before it gets clobbered + naked_asm!( + "mov rdi, rsp", + "call get_args" + ) +} + +pub unsafe fn sys_exit(exit_code:usize) -> ! { + asm!("syscall", + in("rax") 60, + in("rdi") exit_code, + options(nostack, noreturn) + ) +} + +pub unsafe fn sys_write(buffer: *const u8, count: usize) { + asm!("syscall", + inout("rax") 1 => _, + in("rdi") 1, + in("rsi") buffer, + in("rdx") count, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ) +} + +pub unsafe fn sys_sleep(seconds: usize) { + let sleep_time = timespec { + tv_sec: seconds as isize, + tv_nsec: 0 + }; + + asm!("syscall", + in("rax") 35, + in("rdi") &sleep_time, + in("rsi") 0, + options(nostack)) +} diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..812731f --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +# Runs a qemu test for a given architecture +# Ideally all builds will use the method below +run_qemu() { + local arch=$1 + local seconds=$2 + local binary=$3 + local qemu="qemu-$arch-static" + if [[ $binary == *"rs-"* ]]; then + icon="🦀" + else + icon="🦬" + fi + + echo "$icon $arch - Testing ${seconds}s nap" + time timeout $(($seconds+1))s $qemu $binary $seconds &>/dev/null +} + +## NASM tests - likely problems in the code somewhere + +# NASM amd64 tests (does not work on aarch64 qemu) +if [ $(arch) == "x86_64" ]; then + echo "🦬 x86_64 - Testing 1s nap" + timeout 2s ./out/nap 1 &>/dev/null + echo "🦬 x86_64 - Testing 10s nap" + timeout 11s ./out/nap 10 &>/dev/null +fi + +# NASM arm64 tests (does not work on x86_64 qemu) +if [ $(arch) == "aarch64" ]; then + echo "🦬 aarch64 - Testing 1s nap" + timeout 2s ./out/nap-aarch64 1 &>/dev/null + echo "🦬 aarch64 - Testing 10s nap" + timeout 11s ./out/nap-aarch64 10 &>/dev/null +fi + +## Rust tests - rust works everywhere + +# Rust amd64 tests +run_qemu x86_64 1 ./out/rs-nap-x86_64 +run_qemu x86_64 10 ./out/rs-nap-x86_64 + +# Rust arm64 tests +run_qemu aarch64 1 ./out/rs-nap-aarch64 +run_qemu aarch64 10 ./out/rs-nap-aarch64 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..440fdae --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,7 @@ +[toolchain] +channel = "nightly" +components = [ "rustfmt", "clippy" ] +targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu" +]