Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first party C tests #764

Merged
merged 1 commit into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As our tests evolve we'll need a more comprehensive march string. We use rv64gcv_zcb_zfa_zba_zbb_zbc_zbs_zfh_zicboz_zicbop_zicbom_zicond_zbkb_zbkx_zknd_zkne_zknh for our tests. We really want to enable every extension so that any instruction can be used in a test file for the model.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But some extensions are incompatible, such as Zcf and Zclsd, each test should be able to specify its own match string

One example is gcc's test framework, you can specify it in the comments (for reference)

gcc/testsuite/gcc.target/riscv/cmo-zicbom-1.c

/* { dg-do compile } */
/* { dg-options "-march=rv64gc_zicbom -mabi=lp64" } */
...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a very good point. Not sure that we need that level of complexity yet in the first version of this PR (would rather get some testing in instead of waiting for it to be perfect and continue with almost no tests), but definitely something we will need to address eventually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completely agree, would be great to see this merged soon.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah my plan was to embed a list of march and model configs inside the source files and then we would run each test for all of those. Probably worth waiting until after the new config system is done though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable. We should probably open an issue to track this after this PR is merged.

# 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
Loading