Skip to content

Commit

Permalink
Add first party C tests
Browse files Browse the repository at this point in the history
Add first party C tests that can be used for tests specific to this model. This can use Clang or RISC-V GCC to compile the tests. It includes a copy of `nanoprinf` for printing to the UART.
  • Loading branch information
Timmmm authored Mar 7, 2025
1 parent 91b55d3 commit 40d26b5
Show file tree
Hide file tree
Showing 15 changed files with 7,281 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: |
# Ninja is used because the CMake Makefile generator doesn't
# build top-level targets in parallel unfortunately.
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DFIRST_PARTY_TESTS=TRUE
# By default only the rv32d and rv64d emulators are build,
# but we want to build more targets here to ensure they
# can at least build without errors.
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ add_subdirectory("model")
# Emulator binary.
add_subdirectory("c_emulator")

# Old pre-compiled riscv-tests.
add_subdirectory("test/riscv-tests")
# Old pre-compiled riscv-tests & first-party tests.
add_subdirectory("test")

# Release packaging.
if (NOT CPACK_GENERATOR)
Expand Down
9 changes: 9 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
add_subdirectory("riscv-tests")

# This is off by default so we don't require people who
# just want to build the model to have Clang or RISC-V GCC
# installed.
option(FIRST_PARTY_TESTS "Compile & run first party tests (requires Clang or RISC-V GCC).")
if (FIRST_PARTY_TESTS)
add_subdirectory("first_party")
endif()
2 changes: 0 additions & 2 deletions test/README

This file was deleted.

4 changes: 4 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tests

* `riscv-tests` - a collection of very old pre-compiled ELFs from [the `riscv-tests` repo](https://github.com/riscv-software-src/riscv-tests). These are bare minimum tests; not very exhaustive at all.
* `first_party` - tests specifically designed for this Sail model. These tests are not designed to test all the features of RISC-V. Rather they are for testing new code that we add, and bug fixes.
143 changes: 143 additions & 0 deletions test/first_party/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@

# First party tests

# There are three options for cross-compilation of an RV32 program.
#
# 1. Clang, which is sensibly designed to be able to cross-compile to any architecture it supports.
# Unfortunately it is possible to build it without RISC-V support, e.g. RHEL 8 does this, so we
# need to check that. You can download a binary from https://github.com/llvm/llvm-project/releases
# 2. GCC via riscv32-unknown-elf-gcc. Unlike Clang you have to specifically build GCC as a cross
# compiler. You can download a binary from https://github.com/riscv-collab/riscv-gnu-toolchain/releases
# 3. GCC via riscv64-unknown-elf-gcc, but only if it has "multilib" support, so we'll try riscv32-... first.

find_program(CLANG_BIN "clang")
find_program(GCC_BIN_RV32 "riscv32-unknown-elf-gcc")
find_program(GCC_BIN_RV64 "riscv64-unknown-elf-gcc")

if (CLANG_BIN)
message(STATUS "Found clang: ${CLANG_BIN}")
endif()
if (GCC_BIN_RV32)
message(STATUS "Found riscv32-unknown-elf-gcc: ${GCC_BIN_RV32}")
endif()
if (GCC_BIN_RV64)
message(STATUS "Found riscv64-unknown-elf-gcc: ${GCC_BIN_RV64}")
endif()

set(CROSS_COMPILER_COMMAND_RV32)
set(CROSS_COMPILER_COMMAND_RV64)

# Prefer Clang.
if (CLANG_BIN)
# Check it supports RISC-V.
execute_process(
COMMAND clang -print-targets
OUTPUT_VARIABLE clang_targets
COMMAND_ERROR_IS_FATAL ANY
)

# Check if `riscv32` is present in the output
if (clang_targets MATCHES "riscv32")
set(CROSS_COMPILER_COMMAND_RV32 ${CLANG_BIN} --target=riscv32)
set(CROSS_COMPILER_COMMAND_RV64 ${CLANG_BIN} --target=riscv64)
else()
message(WARNING "Your Clang compiler does not support RISC-V (see '${CLANG_BIN} -print-targets'). Please download one from https://github.com/llvm/llvm-project/releases")
endif()
endif()

# Prefer riscv32-unknown-elf-gcc to riscv64-unknown-elf-gcc for RV32.
if (GCC_BIN_RV32)
if (NOT CROSS_COMPILER_COMMAND_RV32)
set(CROSS_COMPILER_COMMAND_RV32 ${GCC_BIN_RV32})
endif()
endif()

if (GCC_BIN_RV64)
if (NOT CROSS_COMPILER_COMMAND_RV32)
# It might support multilib.
set(CROSS_COMPILER_COMMAND_RV32 ${GCC_BIN_RV64})
endif()
if (NOT CROSS_COMPILER_COMMAND_RV64)
set(CROSS_COMPILER_COMMAND_RV64 ${GCC_BIN_RV64})
endif()
endif()

if (NOT CROSS_COMPILER_COMMAND_RV32 OR NOT CROSS_COMPILER_COMMAND_RV64)
message(FATAL_ERROR "No suitable cross-compiler found. We recommend downloading Clang from https://github.com/llvm/llvm-project/releases")
endif()

set(common_deps
"src/common/crt0.S"
"src/common/encoding.h"
"src/common/link.ld"
"src/common/nanoprintf.c"
"src/common/nanoprintf.h"
"src/common/runtime.c"
"src/common/runtime.h"
)

set(tests
"test_hello_world.c"
"test_minstret.S"
)

foreach (xlen IN ITEMS 32 64)
foreach (test_source IN LISTS tests)
set(arch "rv${xlen}d")
if (xlen EQUAL 32)
set(mabi "ilp32")
else()
set(mabi "lp64")
endif()

set(elf "${arch}_${test_source}.elf")

add_custom_command(
OUTPUT ${elf}
DEPENDS ${common_deps} "src/${test_source}"
COMMAND ${CROSS_COMPILER_COMMAND_RV${xlen}}
# The ISA string to compile for.
-march=rv${xlen}gc
# Calling convention to use. Valid values are 'ilp32' or 'lp64' for 32/64-bit,
# optionally followed by 'f' or 'd' for hard-float. All combinations are valid.
-mabi=${mabi}d
# Required for compatibility with old versions of LLD. Otherwise you get an error
# "relocation R_RISCV_ALIGN requires unimplemented linker relaxation; recompile with -mno-relax"
-mno-relax
# Indicate we are building in a standalone environment. Implies -fno-builtin
# so the compiler won't assume that memcpy etc. are available.
-ffreestanding
# Don't try to link with libc or libm.
-nostdlib
# Generate a statically linked binary.
-static
# The relocation model. This compiles the code so that it can
# be linked at any address. This means the linker script
# doesn't have to e.g. put all code in the first 2GB of memory.
-mcmodel=medany
# Generate debug info.
-g
# Optimise code generation for a good debugging experience.
-Og
# Enable warnings and upgrade them to errors.
-Wall
-Werror
# The linker script.
-T "${CMAKE_CURRENT_SOURCE_DIR}/src/common/link.ld"
-o ${elf}
"${CMAKE_CURRENT_SOURCE_DIR}/src/${test_source}"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/crt0.S"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/nanoprintf.c"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/runtime.c"
VERBATIM
COMMENT "Compiling ${test_source}"
)

add_custom_target(build_${arch}_${test_source} ALL DEPENDS ${elf})

add_test(
NAME "first_party_${arch}_${test_source}"
COMMAND $<TARGET_FILE:riscv_sim_${arch}> --pmp-count 16 ${elf}
)
endforeach()
endforeach()
199 changes: 199 additions & 0 deletions test/first_party/src/common/crt0.S
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#include "encoding.h"

#define MSTATUS_MPP_OFFSET 11

.section .text

.global _start
_start:
j reset_handler

init_pmp:
// Save the current mtvec before changing it
csrr s1, mtvec
// Set a trap handler that would just jump to 1:, in case PMPs are not supported
la t0, 1f
csrw mtvec, t0
// Set a full match pmp address
li t0, (1 << (31 + (__riscv_xlen / 64) * (53 - 31))) - 1
csrw pmpaddr0, t0
// Configure the above pmp to have full access
li t0, PMP_NAPOT | PMP_R | PMP_W | PMP_X
csrw pmpcfg0, t0
// Fence and flush all virtual addresses
sfence.vma
// Align target address stored in mtvec to 64 bytes for compatibility with CLIC.
.balign 64
1:
// Restore mtvec to the previous value and return
csrw mtvec, s1
csrw mcause, x0
ret

// Trap handler is stored in mtvec and must be 4-byte aligned for CLINT but
// must be aligned to 2^6 for compatibility with CLIC.
.p2align 6
.global trap_handler
trap_handler:
// For some reason when compiling with riscv64-unknown-elf-gcc it
// will happily compile `printf()` etc and just assume it's running
// on Linux. This means it does a `write()` syscall which means
// writing the syscall number to a7, and then `ecall`. We can
// emulate Linux for `write()` to stdout or stderr so that
// `printf()` works.
csrr t5, mcause
// Clear bits [xlen-2 .. 16], since CLIC puts some irrelevant stuff there.
li t6, 0x1FFFF
not t6, t6
srli t6, t6, 1
not t6, t6
and t5, t5, t6

li t6, CAUSE_USER_ECALL
beq t5, t6, ecall_handler
li t6, CAUSE_SUPERVISOR_ECALL
beq t5, t6, ecall_handler
li t6, CAUSE_MACHINE_ECALL
beq t5, t6, ecall_handler

// Unhandled trap.
li a0, 1001
tail htif_exit

.global ecall_handler
ecall_handler:
// Handle syscalls.

// We're going to mret from this so mret to the following instructions.
csrr t6, mepc
addi t6, t6, 4
csrw mepc, t6

// Syscall number is in a7, arguments are in a0..a6, return values in
// a0, a1. See https://man7.org/linux/man-pages/man2/syscall.2.html

// Custom syscalls e.g. to set/get the privilege can be added here like this:
//
// li t5, SYSCALL_SET_PRIVILEGE
// beq a7, t5, syscall_set_privilege

// syscalls return -1 on failure.
li a0, -1
mret

#define FROM_1_TO_31 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31

.global reset_handler
reset_handler:
// Initialise registers to avoid X issues.
.irp i, FROM_1_TO_31
li x\i, 0
.endr

// Init float registers to avoid X issues. This requires enabling the
// FPU, but we also have to skip it if fmv.w.x is an illegal instruction.
// Note this can be true even if mstatus[FS] is non-zero, to allow for
// emulation. We also restore the original value of mstatus so that
// tests that check its initial value still work.
#ifdef __riscv_flen
la t0, 9f
csrw mtvec, t0
li t0, MSTATUS_FS
csrrs t0, mstatus, t0
.irp i, 0, FROM_1_TO_31
fmv.w.x f\i, x0
.endr
.p2align 6
9:
#endif

// Initialize pmp
call init_pmp

// Initialise trap handler.
la t0, trap_handler
csrw mtvec, t0

// Initialise stack pointer.
csrr a0, mhartid

la tp, _stack

# Give each core 128KB of stack and Thread Local Storage.
#define STKSHIFT 17
add sp, a0, 1
sll sp, sp, STKSHIFT
add sp, sp, tp
sll a2, a0, STKSHIFT
add tp, tp, a2

// Initialize global pointer.
.option push
.option norelax
la gp, __global_pointer$
.option pop

// Store a frame pointer on the stack. This is necessary if you don't use
// -fomit-frame-pointer because main() will load it.
// For simplicity unconditionally store 8 bytes even on RV32.
addi sp, sp, -8
sw zero, 0(sp)
sw zero, 4(sp)

call main

// Main return value is in a0 which we pass straight to htif_exit.
tail htif_exit


// HTIF (Host Target InterFace) MMIO device.
//
// bitfield htif_cmd : bits(64) = {
// device : 63 .. 56,
// cmd : 55 .. 48,
// payload : 47 .. 0
// }
//
// The upper byte must be written second if doing two 4-byte writes.
// Device is:
//
// 0 (syscall-proxy): if payload[0] is 1, exit with code payload[..1]
// otherwise do nothing.
// 1 (terminal): if command is 0, terminal input (unimplemented in Sail)
// if command is 1, terminal output (write lowest byte of payload)

.section .bss.mmio

// HTIF devices.

.balign 8
.global tohost
tohost:
.fill 8

// fromhost is not used by Sail but its presence allows the ELF to run on Spike.
.balign 8
.global fromhost
fromhost:
.fill 8

.section .text

.global htif_exit
htif_exit:
la t0, tohost
sll a0, a0, 1
or a0, a0, 1
1:
sw a0, 0(t0)
sw zero, 4(t0)
j 1b

.global htif_putc
htif_putc:
la t0, tohost
sw a0, 0(t0)
// device=1 (terminal), cmd=1 (output)
li a0, 0x01010000
sw a0, 4(t0)
ret
Loading

0 comments on commit 40d26b5

Please sign in to comment.