Hello world for qemu serial
authorKit Rhett Aultman <kit@kitaultman.com>
Mon, 24 Jun 2024 04:06:17 +0000 (00:06 -0400)
committerKit Rhett Aultman <kit@kitaultman.com>
Mon, 24 Jun 2024 04:06:17 +0000 (00:06 -0400)
This commit establishes the overall project structure needed for doing a bare
metal executive on RISC-V.  This currently supports the qemu risc64 emulator
because that is a target I can develop across multiple environments as I have
the free time.

This contains:
* Basic project structure
* rustc cross-compilation to riscv64-unknown-none-elf target
* Cargo integration for running qemu
* linking scripts for the qemu riscv64 virt "board" memory layout
* do-nothing panic handler
* _start function that contains the minimal RISC-V initialization: setting up
  the global pointer (gp), reserving a stack, and clearing the bss region
* Very simple use of the uart_16550 crate to print a "Hello world" message

This all came together mostly in an afternoon's work and wouldn't have been
possible without these exceptional resources:

"Running Rust code on RISC-V in Qemu"
https://meyerzinn.tech/posts/2023/03/05/running-rust-code-on-risc-v-in-qemu/

"RISC-V from scratch 2: Hardware layouts, linker scripts, and C runtimes"
https://twilco.github.io/riscv-from-scratch/2019/04/27/riscv-from-scratch-2.html

.cargo/config.toml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
rust-toolchain.toml [new file with mode: 0644]
src/linker.ld [new file with mode: 0644]
src/main.rs [new file with mode: 0644]

diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644 (file)
index 0000000..f69d0bd
--- /dev/null
@@ -0,0 +1,16 @@
+[target.riscv64gc-unknown-none-elf]
+rustflags = ["-C", "link-args=-Tsrc/linker.ld --omagic"]
+# Add -S to wait for the debugger
+runner = """ qemu-system-riscv64
+  -machine virt
+  -cpu rv64
+  -m 128M
+  -s
+  -nographic
+  -serial mon:stdio
+  -bios """
+
+[build]
+target = "riscv64gc-unknown-none-elf"
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..ea8c4bf
--- /dev/null
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..c0b5a87
--- /dev/null
@@ -0,0 +1,59 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "raw-cpuid"
+version = "10.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "riscv"
+version = "0.1.0"
+dependencies = [
+ "uart_16550",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
+name = "uart_16550"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dc00444796f6c71f47c85397a35e9c4dbf9901902ac02386940d178e2b78687"
+dependencies = [
+ "bitflags",
+ "rustversion",
+ "x86",
+]
+
+[[package]]
+name = "x86"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2781db97787217ad2a2845c396a5efe286f87467a5810836db6d74926e94a385"
+dependencies = [
+ "bit_field",
+ "bitflags",
+ "raw-cpuid",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..56aec2c
--- /dev/null
@@ -0,0 +1,19 @@
+[package]
+name = "riscv"
+version = "0.1.0"
+edition = "2021"
+
+[build]
+target = "riscv64gc-unknown-none-elf"
+
+[dependencies]
+uart_16550 = "0.3.0"
+
+# We need to turn off panic unwinding
+# so we set the panic behavior to abort
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644 (file)
index 0000000..5d56faf
--- /dev/null
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
diff --git a/src/linker.ld b/src/linker.ld
new file mode 100644 (file)
index 0000000..21ec60b
--- /dev/null
@@ -0,0 +1,46 @@
+OUTPUT_ARCH(riscv)
+
+MEMORY {
+  RAM   (wxa) : ORIGIN = 0x80000000, LENGTH = 128M
+}
+
+PHDRS {
+  text PT_LOAD;
+  data PT_LOAD;
+  bss PT_LOAD;
+}
+
+SECTIONS {
+  . = ORIGIN(RAM); # start at 0x8000_0000
+
+  .text : { # put code first
+    *(.text.init) # start with anything in the .text.init section
+    *(.text .text.*) # then put anything else in .text
+  } >RAM AT>RAM :text # put this section into the text segment
+
+  # we have to provide a pointer at the start of the rodata/data/bss area
+  # in order to facilitate linker relaxation.  This is a process whereby the
+  # linker can use simpler instructions for variable accesses that exist within
+  # a 12-bit offset from the global pointer
+  PROVIDE(_global_pointer = .); # we have to provide the global_pointer at the beg
+
+  .rodata : { # next, read-only data
+    *(.rodata .rodata.*)
+  } >RAM AT>RAM :text # goes into the text segment as well (since instructions are generally read-only)
+
+  .data : { # and the data section
+    *(.sdata .sdata.*) *(.data .data.*)
+  } >RAM AT>RAM :data # this will go into the data segment
+
+  .bss :{ # finally, the BSS
+    PROVIDE(_bss_start = .); # define a variable for the start of this section
+    *(.sbss .sbss.*) *(.bss .bss.*)
+    PROVIDE(_bss_end = .); # ... and one at the end
+  } >RAM AT>RAM :bss # and this goes into the bss segment
+
+  # start the stack at the end of memory and let it grow downward
+  PROVIDE(_stack_top = ORIGIN(RAM) + LENGTH(RAM));
+}
+
+# make the linker error unless we provide a _start
+ENTRY(_start)
diff --git a/src/main.rs b/src/main.rs
new file mode 100644 (file)
index 0000000..da4170b
--- /dev/null
@@ -0,0 +1,68 @@
+#![no_std]
+#![no_main]
+#![feature(naked_functions)] // enable functions that are pure inline assembly
+
+use core::panic::PanicInfo;
+
+// cfg not test is set here because the analyzer and the test system
+// complained about the panic handler being doubly-defined
+#[cfg(not(test))]
+#[panic_handler]
+fn on_panic(_info: &PanicInfo) -> ! {
+       loop {}
+}
+
+/*
+ * Global entry point, specified in the linker script.
+ * Requires C-style linkage in order for the linker to identify the name
+ * of the function and place it at the entry point address.
+ *
+ * The main purpose here is to "initialize the runtime".  In this case, this
+ * means setting up the global pointer for linker relaxation, establishing
+ * the stack, and zeroing-out the bss section
+ */
+#[naked]
+#[no_mangle] // C-style linkage needs an unmangled function
+#[link_section = ".text.init"]
+unsafe extern "C" fn _start() -> ! {
+  use core::arch::asm;
+  asm!(
+    // The linker will try to emit the `la` pseudo-instruction when it believes
+    // the referenced variable will be within a 12-bit offset of the _global_pointer,
+    // so we must initialize the `gp` register to this at runtime.
+    // (see the linker script file for more details)
+    ".option push",
+    ".option norelax",
+    "la gp, _global_pointer",
+    ".option pop",
+    
+    // set the stack pointer
+    "la sp, _stack_top",
+
+    // clear the BSS section
+    "la t0, _bss_start",
+    "la t1, _bss_end",
+    "bgeu t0, t1, 2f",
+"1:",
+    "sb zero, 0(t0)",
+    "addi t0, t0, 1",
+    "bne t0, t1, 1b",
+"2:",
+
+    // "tail-call" to {entry} (call without saving a return address)
+    "tail {entry}",
+    entry = sym entry, // {entry} refers to the function [entry] below
+    options(noreturn) // we must handle "returning" from assembly
+  );
+}
+
+#[no_mangle] // Again, being mindful of the C calling convention
+extern "C" fn entry() -> ! {
+    use uart_16550::MmioSerialPort;
+    const SERIAL_BASE: usize = 0x1000_0000;
+    let mut serial_port = unsafe { MmioSerialPort::new(SERIAL_BASE) };
+    for byte in "KIT -- Hello World!\n".bytes() {
+        serial_port.send(byte)
+    }
+    loop {}
+}