blob: df72f6e07568a2db9bae602e66bd3a1c951154b0 [file]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "riscv/riscv_renode.h"
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include "absl/functional/bind_front.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "mpact/sim/generic/core_debug_interface.h"
#include "mpact/sim/generic/type_helpers.h"
#include "mpact/sim/proto/component_data.pb.h"
#include "mpact/sim/util/memory/atomic_memory.h"
#include "mpact/sim/util/memory/flat_demand_memory.h"
#include "mpact/sim/util/memory/memory_interface.h"
#include "mpact/sim/util/memory/memory_watcher.h"
#include "mpact/sim/util/memory/single_initiator_router.h"
#include "mpact/sim/util/other/instruction_profiler.h"
#include "riscv/debug_command_shell.h"
#include "riscv/riscv32_decoder.h"
#include "riscv/riscv64_decoder.h"
#include "riscv/riscv_arm_semihost.h"
#include "riscv/riscv_cli_forwarder.h"
#include "riscv/riscv_clint.h"
#include "riscv/riscv_debug_info.h"
#include "riscv/riscv_debug_interface.h"
#include "riscv/riscv_instrumentation_control.h"
#include "riscv/riscv_register.h"
#include "riscv/riscv_register_aliases.h"
#include "riscv/riscv_renode_cli_top.h"
#include "riscv/riscv_renode_register_info.h"
#include "riscv/riscv_state.h"
#include "riscv/riscv_top.h"
#include "riscv/stoull_wrapper.h"
#include "src/google/protobuf/text_format.h"
namespace mpact {
namespace sim {
namespace riscv {
using ::mpact::sim::proto::ComponentData;
using ::mpact::sim::proto::ComponentValueEntry;
using ::mpact::sim::riscv::RiscVClint;
using ::mpact::sim::util::AtomicMemoryOpInterface;
using ::mpact::sim::util::MemoryWatcher;
using HaltReasonValueType =
::mpact::sim::generic::CoreDebugInterface::HaltReasonValueType;
using HaltReason = ::mpact::sim::generic::CoreDebugInterface::HaltReason;
using RunStatus = ::mpact::sim::generic::CoreDebugInterface::RunStatus;
// Configuration names.
constexpr std::string_view kMemoryBase = "memoryBase";
constexpr std::string_view kMemorySize = "memorySize";
constexpr std::string_view kClintMMRBase = "clintMMRBase";
constexpr std::string_view kCLIPort = "cliPort";
constexpr std::string_view kWaitForCLI = "waitForCLI";
constexpr std::string_view kInstProfile = "instProfile";
constexpr std::string_view kMemProfile = "memProfile";
constexpr std::string_view kStackEnd = "stackEnd";
constexpr std::string_view kStackSize = "stackSize";
constexpr std::string_view kICache = "iCache";
constexpr std::string_view kDCache = "dCache";
constexpr char kStackEndSymbolName[] = "__stack_end";
constexpr char kStackSizeSymbolName[] = "__stack_size";
RiscVRenode::RiscVRenode(std::string name, MemoryInterface* renode_sysbus,
RiscVXlen xlen)
: name_(name), renode_sysbus_(renode_sysbus) {
router_ = new util::SingleInitiatorRouter(name + "_router");
renode_router_ = new util::SingleInitiatorRouter(name + "_renode_router");
auto* data_memory = static_cast<MemoryInterface*>(router_);
// Instantiate memory profiler, but disable it until the config information
// has been received.
mem_profiler_ = new MemoryUseProfiler(data_memory);
mem_profiler_->set_is_enabled(false);
// Set up state, decoder, and top.
rv_state_ = new RiscVState("RiscVRenode", xlen, mem_profiler_,
static_cast<AtomicMemoryOpInterface*>(router_));
rv_fp_state_ = new RiscVFPState(rv_state_->csr_set(), rv_state_);
rv_state_->set_rv_fp(rv_fp_state_);
std::string reg_name;
if (xlen == RiscVXlen::RV32) {
rv_decoder_ = new RiscV32Decoder(rv_state_, data_memory);
// Make sure the architectural and abi register aliases are added.
for (int i = 0; i < 32; i++) {
reg_name = absl::StrCat(RiscVState::kXregPrefix, i);
(void)rv_state_->AddRegister<RV32Register>(reg_name);
(void)rv_state_->AddRegisterAlias<RV32Register>(
reg_name, ::mpact::sim::riscv::kXRegisterAliases[i]);
}
} else {
rv_decoder_ = new RiscV64Decoder(rv_state_, data_memory);
for (int i = 0; i < 32; i++) {
reg_name = absl::StrCat(RiscVState::kXregPrefix, i);
(void)rv_state_->AddRegister<RV64Register>(reg_name);
(void)rv_state_->AddRegisterAlias<RV64Register>(
reg_name, ::mpact::sim::riscv::kXRegisterAliases[i]);
}
}
for (int i = 0; i < 32; i++) {
reg_name = absl::StrCat(RiscVState::kFregPrefix, i);
(void)rv_state_->AddRegister<RVFpRegister>(reg_name);
(void)rv_state_->AddRegisterAlias<RVFpRegister>(
reg_name, ::mpact::sim::riscv::kFRegisterAliases[i]);
}
riscv_top_ = new RiscVTop(name, rv_state_, rv_decoder_);
// Set up the memory router with the system bus. Other devices are added once
// config info has been received. Add a tagged default memory transactor, so
// that any tagged loads/stores are forward to the sysbus without tags.
CHECK_OK(router_->AddDefaultTarget<MemoryInterface>(renode_sysbus));
// Create memory. These memories will be added to the core router when there
// is configuration data for the address space that belongs to the core. The
// memories will be added to the renode router immediately as the default
// target, since memory references from ReNode are only in the memory range
// exposed on the sysbus.
memory_ = new util::FlatDemandMemory();
atomic_memory_ = new util::AtomicMemory(memory_);
CHECK_OK(renode_router_->AddDefaultTarget<MemoryInterface>(memory_));
// Set up semihosting.
semihost_ = new RiscVArmSemihost(xlen == RiscVXlen::RV32
? RiscVArmSemihost::BitWidth::kWord32
: RiscVArmSemihost::BitWidth::kWord64,
data_memory, data_memory);
// Set up special handlers (ebreak, wfi, ecall).
riscv_top_->state()->AddEbreakHandler([this](const Instruction* inst) {
if (this->semihost_->IsSemihostingCall(inst)) {
this->semihost_->OnEBreak(inst);
return true;
}
if (this->riscv_top_->HasBreakpoint(inst->address())) {
this->riscv_top_->RequestHalt(HaltReason::kSoftwareBreakpoint, nullptr);
return true;
}
return false;
});
riscv_top_->state()->set_on_wfi([](const Instruction*) { return true; });
riscv_top_->state()->set_on_ecall([](const Instruction*) { return false; });
semihost_->set_exit_callback([this]() {
LOG(INFO) << "Simulation halting due to semihosting exit";
this->riscv_top_->RequestHalt(HaltReason::kProgramDone, nullptr);
});
}
RiscVRenode::~RiscVRenode() {
// Halt the core just to be safe.
(void)riscv_top_->Halt();
// Write out instruction profile.
if (inst_profiler_ != nullptr) {
std::string inst_profile_file_name =
absl::StrCat("./mpact_riscv_", name_, "_inst_profile.csv");
std::fstream inst_profile_file(inst_profile_file_name.c_str(),
std::ios_base::out);
if (!inst_profile_file.good()) {
LOG(ERROR) << "Failed to write profile to file";
} else {
inst_profiler_->WriteProfile(inst_profile_file);
}
inst_profile_file.close();
}
if (mem_profiler_ != nullptr) {
std::string mem_profile_file_name =
absl::StrCat("./mpact_riscv_", name_, "_mem_profile.csv");
std::fstream mem_profile_file(mem_profile_file_name.c_str(),
std::ios_base::out);
if (!mem_profile_file.good()) {
LOG(ERROR) << "Failed to write profile to file";
} else {
mem_profiler_->WriteProfile(mem_profile_file);
}
mem_profile_file.close();
}
// Export counters.
auto component_proto = std::make_unique<ComponentData>();
CHECK_OK(riscv_top_->Export(component_proto.get()))
<< "Failed to export proto";
std::string proto_file_name;
proto_file_name = absl::StrCat("./mpact_riscv_", name_, ".proto");
std::fstream proto_file(proto_file_name.c_str(), std::ios_base::out);
std::string serialized;
if (!proto_file.good()) {
LOG(ERROR) << "Failed to open proto file for writing";
} else if (!google::protobuf::TextFormat::PrintToString(*component_proto,
&serialized)) {
LOG(ERROR) << "Failed to serialize protos";
} else {
proto_file << serialized;
}
proto_file.close();
// Clean up.
delete renode_router_;
delete mem_profiler_;
delete inst_profiler_;
delete instrumentation_control_;
delete program_loader_;
delete cmd_shell_;
delete socket_cli_;
delete riscv_renode_cli_top_;
delete riscv_cli_forwarder_;
delete rv_decoder_;
delete rv_fp_state_;
delete riscv_top_;
delete rv_state_;
delete semihost_;
delete router_;
delete atomic_memory_;
delete memory_;
delete clint_;
}
absl::StatusOr<uint64_t> RiscVRenode::LoadExecutable(const char* elf_file_name,
bool for_symbols_only) {
program_loader_ = new ElfProgramLoader(this);
uint64_t entry_pt = 0;
if (for_symbols_only) {
auto res = program_loader_->LoadSymbols(elf_file_name);
if (!res.ok()) {
return res.status();
}
entry_pt = res.value();
} else {
auto res = program_loader_->LoadProgram(elf_file_name);
if (!res.ok()) {
return res.status();
}
entry_pt = res.value();
}
auto res = program_loader_->GetSymbol("tohost");
// Add watchpoint for tohost if the symbol exists.
if (res.ok()) {
// If there is a 'tohost' symbol, set up a write watchpoint on that address
// to catch writes that mark program exit.
uint64_t tohost_addr = res.value().first;
// Add to_host watchpoint that halts the execution when program exit is
// signaled.
auto* db = riscv_top_->state()->db_factory()->Allocate<uint32_t>(2);
auto status = riscv_top_->memory_watcher()->SetStoreWatchCallback(
MemoryWatcher::AddressRange{tohost_addr,
tohost_addr + 2 * sizeof(uint32_t) - 1},
[this, tohost_addr, db](uint64_t addr, int sz) {
static DataBuffer* load_db = db;
if (load_db == nullptr) return;
memory_->Load(tohost_addr, load_db, nullptr, nullptr);
uint32_t code = load_db->Get<uint32_t>(0);
if (code & 0x1) {
// The return code is in the upper 31 bits.
code >>= 1;
LOG(INFO) << absl::StrCat(
"Simulation halting due to tohost write: exit ",
absl::Hex(code));
(void)riscv_top_->RequestHalt(HaltReason::kProgramDone, nullptr);
load_db->DecRef();
}
});
}
// Add instruction profiler it hasn't already been added.
if (inst_profiler_ == nullptr) {
inst_profiler_ = new util::InstructionProfiler(*program_loader_, 2);
riscv_top_->counter_pc()->AddListener(inst_profiler_);
riscv_top_->counter_pc()->SetIsEnabled(false);
} else {
// If it has been added already, set the elf loader, and make sure the pc
// counter is enabled.
inst_profiler_->SetElfLoader(program_loader_);
riscv_top_->counter_pc()->SetIsEnabled(true);
}
return entry_pt;
}
// Each of the following methods checks to see if the command line enabled
// "top" interface is null. If it is not, it uses that to control the simulator,
// as it provides proper prioritization and handling of both ReNode and command
// line commands. Otherwise it uses the riscv_top interface directly.
absl::StatusOr<int> RiscVRenode::Step(int num) {
if (riscv_renode_cli_top_ != nullptr)
return riscv_renode_cli_top_->RenodeStep(num);
return riscv_top_->Step(num);
}
absl::StatusOr<HaltReasonValueType> RiscVRenode::GetLastHaltReason() {
if (riscv_renode_cli_top_ != nullptr)
return riscv_renode_cli_top_->RenodeGetLastHaltReason();
return riscv_top_->GetLastHaltReason();
}
// Perform direct read of the memory through the renode router. The renode
// router avoids routing the request back out to the sysbus.
absl::StatusOr<size_t> RiscVRenode::ReadMemory(uint64_t address, void* buf,
size_t length) {
auto* db = db_factory_.Allocate<uint8_t>(length);
renode_router_->Load(address, db, nullptr, nullptr);
std::memcpy(buf, db->raw_ptr(), length);
db->DecRef();
return length;
}
// Perform direct write of the memory through the renode router. The renode
// router avoids routing the request back out to the sysbus.
absl::StatusOr<size_t> RiscVRenode::WriteMemory(uint64_t address,
const void* buf,
size_t length) {
auto* db = db_factory_.Allocate<uint8_t>(length);
std::memcpy(db->raw_ptr(), buf, length);
renode_router_->Store(address, db);
db->DecRef();
return length;
}
absl::StatusOr<uint64_t> RiscVRenode::ReadRegister(uint32_t reg_id) {
auto ptr = RiscVDebugInfo::Instance()->debug_register_map().find(reg_id);
if (ptr == RiscVDebugInfo::Instance()->debug_register_map().end()) {
return absl::NotFoundError(
absl::StrCat("Not found reg id: ", absl::Hex(reg_id)));
}
if (riscv_renode_cli_top_ != nullptr)
return riscv_renode_cli_top_->RenodeReadRegister(ptr->second);
return riscv_top_->ReadRegister(ptr->second);
}
absl::Status RiscVRenode::WriteRegister(uint32_t reg_id, uint64_t value) {
auto ptr = RiscVDebugInfo::Instance()->debug_register_map().find(reg_id);
if (ptr == RiscVDebugInfo::Instance()->debug_register_map().end()) {
return absl::NotFoundError(
absl::StrCat("Not found reg id: ", absl::Hex(reg_id)));
}
if (riscv_renode_cli_top_ != nullptr)
return riscv_renode_cli_top_->RenodeWriteRegister(ptr->second, value);
return riscv_top_->WriteRegister(ptr->second, value);
}
int32_t RiscVRenode::GetRenodeRegisterInfoSize() const {
return RiscVRenodeRegisterInfo::GetRenodeRegisterInfo().size();
}
absl::Status RiscVRenode::GetRenodeRegisterInfo(int32_t index, int32_t max_len,
char* name,
RenodeCpuRegister& info) {
auto const& register_info = RiscVRenodeRegisterInfo::GetRenodeRegisterInfo();
if ((index < 0) || (index >= register_info.size())) {
return absl::OutOfRangeError(
absl::StrCat("Register info index (", index, ") out of range"));
}
info = register_info[index];
auto const& reg_map = RiscVDebugInfo::Instance()->debug_register_map();
auto ptr = reg_map.find(info.index);
if (ptr == reg_map.end()) {
name[0] = '\0';
} else {
strncpy(name, ptr->second.c_str(), max_len);
}
return absl::OkStatus();
}
static absl::StatusOr<uint64_t> ParseNumber(const std::string& number) {
if (number.empty()) {
return absl::InvalidArgumentError("Empty number");
}
absl::StatusOr<uint64_t> res;
if ((number.size() > 2) && (number.substr(0, 2) == "0x")) {
res = riscv::internal::stoull(number.substr(2), nullptr, 16);
} else if (number[0] == '0') {
res = riscv::internal::stoull(number.substr(1), nullptr, 8);
} else {
res = riscv::internal::stoull(number, nullptr, 10);
}
if (!res.ok()) {
LOG(ERROR) << "Invalid number: " << number;
return absl::InvalidArgumentError(absl::StrCat("Invalid number: ", number));
}
return res.value();
}
absl::Status RiscVRenode::SetConfig(const char* config_names[],
const char* config_values[], int size) {
std::string icache_cfg;
std::string dcache_cfg;
uint64_t memory_base = 0;
uint64_t memory_size = 0;
uint64_t clint_mmr_base = 0;
uint64_t stack_end_value = 0;
bool stack_end_set = false;
uint64_t stack_size_value = 0;
bool stack_size_set = false;
bool do_inst_profile = false;
int cli_port = 0;
int wait_for_cli = 0;
for (int i = 0; i < size; ++i) {
std::string name(config_names[i]);
if (name == kICache) {
icache_cfg = config_values[i];
} else if (name == kDCache) {
dcache_cfg = config_values[i];
} else {
auto res = ParseNumber(config_values[i]);
if (!res.ok()) {
return res.status();
}
auto value = res.value();
if (name == kMemoryBase) {
memory_base = value;
} else if (name == kMemorySize) {
memory_size = value;
} else if (name == kClintMMRBase) {
clint_mmr_base = value;
} else if (name == kCLIPort) {
cli_port = value;
} else if (name == kWaitForCLI) {
wait_for_cli = value;
} else if (name == kInstProfile) {
do_inst_profile = value != 0;
} else if (name == kMemProfile) {
mem_profiler_->set_is_enabled(value != 0);
} else if (name == kStackEnd) {
stack_end_value = value;
stack_end_set = true;
} else if (name == kStackSize) {
stack_size_value = value;
stack_size_set = true;
} else {
LOG(ERROR) << "Unknown config name: " << name << " "
<< config_values[i];
}
}
}
if (memory_size == 0) {
return absl::InvalidArgumentError("tagged_memory_size is 0");
}
// Add the memory targets.
CHECK_OK(router_->AddTarget<AtomicMemoryOpInterface>(
atomic_memory_, memory_base, memory_base + memory_size - 1));
CHECK_OK(router_->AddTarget<MemoryInterface>(memory_, memory_base,
memory_base + memory_size - 1));
// Memory mapped devices.
if (clint_mmr_base != 0) {
clint_ = new RiscVClint(/*period=*/100, riscv_top_->state()->mip());
riscv_top_->counter_num_cycles()->AddListener(clint_);
// Core local interrupt controller - clint.
CHECK_OK(router_->AddTarget<MemoryInterface>(clint_, clint_mmr_base,
clint_mmr_base + 0xffffULL));
}
// Instruction profiler.
if (do_inst_profile) {
if (inst_profiler_ == nullptr) {
if (program_loader_ == nullptr) {
// If the program loader is null, assume that it will be added later,
// but don't enable the trace until it is.
inst_profiler_ = new util::InstructionProfiler(2);
riscv_top_->counter_pc()->SetIsEnabled(false);
} else {
inst_profiler_ = new util::InstructionProfiler(*program_loader_, 2);
riscv_top_->counter_pc()->SetIsEnabled(true);
}
riscv_top_->counter_pc()->AddListener(inst_profiler_);
}
}
// If the cli port has been specified, then instantiate the requisite classes.
if (cli_port != 0 && (riscv_renode_cli_top_ == nullptr)) {
riscv_renode_cli_top_ =
new RiscVRenodeCLITop(riscv_top_, wait_for_cli != 0);
riscv_cli_forwarder_ = new RiscVCLIForwarder(riscv_renode_cli_top_);
cmd_shell_ = new DebugCommandShell();
instrumentation_control_ =
new RiscVInstrumentationControl(cmd_shell_, riscv_top_, mem_profiler_);
cmd_shell_->AddCore(
{static_cast<RiscVDebugInterface*>(riscv_cli_forwarder_),
[this]() { return program_loader_; }});
cmd_shell_->AddCommand(
instrumentation_control_->Usage(),
absl::bind_front(&RiscVInstrumentationControl::PerformShellCommand,
instrumentation_control_));
socket_cli_ =
new SocketCLI(cli_port, *cmd_shell_,
absl::bind_front(&RiscVRenodeCLITop::SetConnected,
riscv_renode_cli_top_));
if (!socket_cli_->good()) {
return absl::InternalError(
absl::StrCat("Failed to create socket CLI (", errno, ")"));
}
}
// Set up the stack if so specified (either by symbol in executable or by
// configuration settings).
bool initialize_stack = false;
uint64_t stack_end = 0;
if (program_loader_ != nullptr) {
auto res = program_loader_->GetSymbol(kStackEndSymbolName);
if (res.ok()) {
stack_end = res.value().first;
initialize_stack = true;
}
}
if (stack_end_set) {
stack_end = stack_end_value;
initialize_stack = true;
}
if (initialize_stack) {
uint64_t stack_size = 32 * 1024;
// Does the executable have a valid GNU_STACK segment? If so, override the
// default
if (program_loader_ != nullptr) {
auto loader_res = program_loader_->GetStackSize();
if (loader_res.ok()) {
stack_end = loader_res.value();
}
// If the __stack_size symbol is defined then override.
auto res = program_loader_->GetSymbol(kStackSizeSymbolName);
if (res.ok()) {
stack_size = res.value().first;
}
}
// If the flag is set, then override.
if (stack_size_set) stack_size = stack_size_value;
auto sp_write = riscv_top_->WriteRegister("sp", stack_end + stack_size);
if (!sp_write.ok()) return sp_write;
}
if (!icache_cfg.empty()) {
ComponentValueEntry icache_value;
icache_value.set_name("icache");
icache_value.set_string_value(icache_cfg);
auto* cfg = riscv_top_->GetConfig("icache");
auto status = cfg->Import(&icache_value);
if (!status.ok()) return status;
}
if (!dcache_cfg.empty()) {
ComponentValueEntry dcache_value;
dcache_value.set_name("dcache");
dcache_value.set_string_value(dcache_cfg);
auto* cfg = riscv_top_->GetConfig("dcache");
auto status = cfg->Import(&dcache_value);
if (!status.ok()) return status;
// Hook the cache into the memory port.
auto* dcache = riscv_top_->dcache();
dcache->set_memory(riscv_top_->state()->memory());
riscv_top_->state()->set_memory(dcache);
}
return absl::OkStatus();
}
absl::Status RiscVRenode::SetIrqValue(int32_t irq_num, bool irq_value) {
switch (irq_num) {
case *riscv::InterruptCode::kMachineExternalInterrupt:
riscv_top_->state()->mip()->set_meip(irq_value);
return absl::OkStatus();
case *riscv::InterruptCode::kMachineTimerInterrupt:
riscv_top_->state()->mip()->set_mtip(irq_value);
return absl::OkStatus();
case *riscv::InterruptCode::kMachineSoftwareInterrupt:
riscv_top_->state()->mip()->set_msip(irq_value);
return absl::OkStatus();
default:
return absl::NotFoundError(
absl::StrCat("Unsupported irq number: ", irq_num));
}
}
} // namespace riscv
} // namespace sim
} // namespace mpact