Add binary program support in kelvin_sim
Refactor the LoadImage function to KelvinTop so it can be shared with kelvin_sim and Renode interfaces. This allows kelvin_sim to run a binary blob of the ELF program the same as in Renode and FPGA.
PiperOrigin-RevId: 562868209
diff --git a/sim/BUILD b/sim/BUILD
index dc21c6e..922f393 100644
--- a/sim/BUILD
+++ b/sim/BUILD
@@ -168,6 +168,8 @@
"@com_google_absl//absl/flags:flag",
"@com_google_absl//absl/flags:parse",
"@com_google_absl//absl/flags:usage",
+ "@com_google_absl//absl/log",
+ "@com_google_absl//absl/log:initialize",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
diff --git a/sim/kelvin_sim.cc b/sim/kelvin_sim.cc
index 744be98..b8c3388 100644
--- a/sim/kelvin_sim.cc
+++ b/sim/kelvin_sim.cc
@@ -2,7 +2,10 @@
#include <cstdint>
#include <cstdlib>
+#include <fstream>
+#include <ios>
#include <iostream>
+#include <optional>
#include <string>
#include <vector>
@@ -11,6 +14,8 @@
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/flags/usage.h"
+#include "absl/log/initialize.h"
+#include "absl/log/log.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
@@ -25,6 +30,11 @@
ABSL_FLAG(bool, i, false, "Interactive mode");
ABSL_FLAG(bool, interactive, false, "Interactive mode");
+ABSL_FLAG(uint32_t, bin_memory_offset, 0,
+ "Memory offset to load the binary file");
+ABSL_FLAG(std::optional<uint32_t>, entry_point, std::nullopt,
+ "Optionally set the entry point of the program.");
+
// Static pointer to the top instance. Used by the control-C handler.
static kelvin::sim::KelvinTop *top = nullptr;
@@ -99,13 +109,27 @@
return true;
}
+// Use ELF file's magic word to determine if the input file is an ELF file.
+static bool IsElfFile(std::string &file_name) {
+ std::ifstream image_file;
+ image_file.open(file_name, std::ios::in | std::ios::binary);
+ if (image_file.good()) {
+ uint32_t magic_word;
+ image_file.read(reinterpret_cast<char *>(&magic_word), sizeof(magic_word));
+ image_file.close();
+ return magic_word == 0x464c457f; // little endian ELF magic word.
+ }
+ return false;
+}
+
int main(int argc, char **argv) {
+ absl::InitializeLog();
absl::SetProgramUsageMessage("Kelvin MPACT-Sim based CLI tool");
auto out_args = absl::ParseCommandLine(argc, argv);
argc = out_args.size();
argv = &out_args[0];
if (argc != 2) {
- std::cerr << "Only a single input file allowed" << std::endl;
+ LOG(ERROR) << "Only a single input file allowed";
return -1;
}
std::string file_name = argv[1];
@@ -121,23 +145,59 @@
sa.sa_handler = &sim_sigint_handler;
sigaction(SIGINT, &sa, nullptr);
+ bool interactive = absl::GetFlag(FLAGS_i) || absl::GetFlag(FLAGS_interactive);
+ auto is_elf_file = IsElfFile(file_name);
+
+ uint32_t entry_point = 0;
// Load the elf segments into memory.
mpact::sim::util::ElfProgramLoader elf_loader(kelvin_top.memory());
- auto load_result = elf_loader.LoadProgram(file_name);
- if (!load_result.ok()) {
- std::cerr << "Error while loading '" << file_name
- << "': " << load_result.status().message();
+ if (!is_elf_file && interactive) {
+ LOG(ERROR) << "Interactive mode may misbehave without the ELF symbol";
+ return -1;
+ }
+ if (is_elf_file) {
+ auto load_result = elf_loader.LoadProgram(file_name);
+ if (!load_result.ok()) {
+ LOG(ERROR) << "Error while loading '" << file_name
+ << "': " << load_result.status().message();
+ return -1;
+ }
+ auto elf_entry_point = load_result.value();
+ // Set the program entry point to based on the ELF info but can
+ // be overridden by the `entry_point` flag.
+ entry_point = (absl::GetFlag(FLAGS_entry_point).has_value())
+ ? absl::GetFlag(FLAGS_entry_point).value()
+ : elf_entry_point;
+ if (elf_entry_point != entry_point) {
+ LOG(ERROR) << absl::StrFormat(
+ "ELF recorded entry point 0x%08x is different from the flag value "
+ "0x%08x. The program may not start properly",
+ elf_entry_point, entry_point);
+ }
+ } else { // Load binary file from the specified memory offset.
+ // Required the flag `entry_point` to be specified for binary file.
+ if (!absl::GetFlag(FLAGS_entry_point).has_value()) {
+ LOG(ERROR) << "Need to specify the program entry point";
+ return -1;
+ }
+ entry_point = absl::GetFlag(FLAGS_entry_point).value();
+ auto res =
+ kelvin_top.LoadImage(file_name, absl::GetFlag(FLAGS_bin_memory_offset));
+ if (!res.ok()) {
+ LOG(ERROR) << "Error while loading '" << file_name
+ << "': " << res.message();
+ return -1;
+ }
}
// Initialize the PC to the entry point.
- uint32_t entry_point = load_result.value();
auto pc_write = kelvin_top.WriteRegister("pc", entry_point);
if (!pc_write.ok()) {
- std::cerr << "Error writing to pc: " << pc_write.message();
+ LOG(ERROR) << "Error writing to pc: " << pc_write.message();
+ return -1;
}
// Determine if this is being run interactively or as a batch job.
- bool interactive = absl::GetFlag(FLAGS_i) || absl::GetFlag(FLAGS_interactive);
if (interactive) {
mpact::sim::riscv::DebugCommandShell cmd_shell(
{{&kelvin_top, &elf_loader}});
@@ -155,12 +215,12 @@
auto run_status = kelvin_top.Run();
if (!run_status.ok()) {
- std::cerr << run_status.message() << std::endl;
+ LOG(ERROR) << run_status.message();
}
auto wait_status = kelvin_top.Wait();
if (!wait_status.ok()) {
- std::cerr << wait_status.message() << std::endl;
+ LOG(ERROR) << wait_status.message();
}
auto sec = absl::ToDoubleSeconds(absl::Now() - t0);
std::cout << "Total cycles: " << kelvin_top.GetCycleCount() << std::endl;
diff --git a/sim/kelvin_top.cc b/sim/kelvin_top.cc
index 9955c3f..b71173d 100644
--- a/sim/kelvin_top.cc
+++ b/sim/kelvin_top.cc
@@ -2,7 +2,9 @@
#include <sys/stat.h>
+#include <algorithm>
#include <cerrno>
+#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
@@ -520,6 +522,11 @@
if (run_status_ != RunStatus::kHalted) {
return absl::FailedPreconditionError("ReadMemory: Core must be halted");
}
+ if (address >= state_->max_physical_address()) {
+ return absl::InvalidArgumentError("Memory address invalid");
+ }
+ length =
+ std::min<size_t>(length, state_->max_physical_address() - address + 1);
auto *db = db_factory_.Allocate(length);
// Load bypassing any watch points/semihosting.
state_->memory()->Load(address, db, nullptr, nullptr);
@@ -534,6 +541,11 @@
if (run_status_ != RunStatus::kHalted) {
return absl::FailedPreconditionError("WriteMemory: Core must be halted");
}
+ if (address >= state_->max_physical_address()) {
+ return absl::InvalidArgumentError("Memory address invalid");
+ }
+ length =
+ std::min<size_t>(length, state_->max_physical_address() - address + 1);
auto *db = db_factory_.Allocate(length);
std::memcpy(db->raw_ptr(), buffer, length);
// Store bypassing any watch points/semihosting.
@@ -620,6 +632,37 @@
return disasm;
}
+absl::Status KelvinTop::LoadImage(const std::string &image_path,
+ uint64_t start_address) {
+ std::ifstream image_file;
+ constexpr size_t kBufferSize = 4096;
+ image_file.open(image_path, std::ios::in | std::ios::binary);
+ char buffer[kBufferSize];
+ size_t gcount = 0;
+ uint64_t load_address = start_address;
+ if (!image_file.good()) {
+ return absl::Status(absl::StatusCode::kInternal, "Failed to open file");
+ }
+ do {
+ // Fill buffer.
+ image_file.read(buffer, kBufferSize);
+ // Get the number of bytes that was read.
+ gcount = image_file.gcount();
+ // Write to the simulator memory.
+ auto res = WriteMemory(load_address, buffer, gcount);
+ // Check that the write succeeded, increment address if it did.
+ if (!res.ok()) {
+ return absl::InternalError("Memory write failed");
+ }
+ if (res.value() != gcount) {
+ return absl::InternalError("Failed to write all the bytes");
+ }
+ load_address += gcount;
+ } while (image_file.good() && (gcount > 0));
+ image_file.close();
+ return absl::OkStatus();
+}
+
void KelvinTop::RequestHalt(HaltReason halt_reason,
const mpact::sim::generic::Instruction *inst) {
// First set the halt_reason_, then the half flag.
diff --git a/sim/kelvin_top.h b/sim/kelvin_top.h
index b279ebd..551d127 100644
--- a/sim/kelvin_top.h
+++ b/sim/kelvin_top.h
@@ -79,6 +79,8 @@
void RequestHalt(HaltReason halt_reason,
const mpact::sim::generic::Instruction *inst);
+ // Load a binary image of the SW program.
+ absl::Status LoadImage(const std::string &image_path, uint64_t start_address);
// Accessors.
sim::KelvinState *state() const { return state_; }
mpact::sim::util::MemoryInterface *memory() const { return memory_; }
diff --git a/sim/renode/kelvin_renode.cc b/sim/renode/kelvin_renode.cc
index a1df4e7..c0ba412 100644
--- a/sim/renode/kelvin_renode.cc
+++ b/sim/renode/kelvin_renode.cc
@@ -97,6 +97,11 @@
return kelvin_top_->GetDisassembly(address);
}
+absl::Status KelvinRenode::LoadImage(const std::string &image_path,
+ uint64_t start_address) {
+ return kelvin_top_->LoadImage(image_path, start_address);
+}
+
absl::StatusOr<uint64_t> KelvinRenode::ReadRegister(uint32_t reg_id) {
auto ptr = RiscVDebugInfo::Instance()->debug_register_map().find(reg_id);
if (ptr == RiscVDebugInfo::Instance()->debug_register_map().end()) {
diff --git a/sim/renode/kelvin_renode.h b/sim/renode/kelvin_renode.h
index e2a5591..0469eed 100644
--- a/sim/renode/kelvin_renode.h
+++ b/sim/renode/kelvin_renode.h
@@ -75,6 +75,9 @@
absl::Status GetRenodeRegisterInfo(int32_t index, int32_t max_len, char *name,
RenodeCpuRegister &info) override;
+ absl::Status LoadImage(const std::string &image_path,
+ uint64_t start_address) override;
+
private:
KelvinTop *kelvin_top_ = nullptr;
};
diff --git a/sim/renode/renode_debug_interface.h b/sim/renode/renode_debug_interface.h
index 4368763..04c910d 100644
--- a/sim/renode/renode_debug_interface.h
+++ b/sim/renode/renode_debug_interface.h
@@ -2,6 +2,7 @@
#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_DEBUG_INTERFACE_H_
#include <cstdint>
+#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
@@ -34,6 +35,9 @@
virtual absl::Status GetRenodeRegisterInfo(int32_t index, int32_t max_len,
char *name,
RenodeCpuRegister &info) = 0;
+
+ virtual absl::Status LoadImage(const std::string &image_path,
+ uint64_t start_address) = 0;
};
} // namespace kelvin::sim::renode
diff --git a/sim/renode/renode_mpact.cc b/sim/renode/renode_mpact.cc
index 51e111e..5a1035d 100644
--- a/sim/renode/renode_mpact.cc
+++ b/sim/renode/renode_mpact.cc
@@ -1,9 +1,6 @@
#include "sim/renode/renode_mpact.h"
-#include <cstddef>
#include <cstdint>
-#include <fstream>
-#include <ios>
#include <limits>
#include <string>
@@ -211,35 +208,11 @@
return -1;
}
auto *dbg = dbg_iter->second;
- // Open up the image file.
- std::ifstream image_file;
- image_file.open(file_name, std::ios::in | std::ios::binary);
- if (!image_file.good()) {
- LOG(ERROR) << "LoadImage: Input file not in good state";
+ auto res = dbg->LoadImage(std::string(file_name), address);
+ if (!res.ok()) {
+ LOG(ERROR) << "Failed to load image: " << res.message();
return -1;
}
- char buffer[kBufferSize];
- size_t gcount = 0;
- uint64_t load_address = address;
- do {
- // Fill buffer.
- image_file.read(buffer, kBufferSize);
- // Get the number of bytes that was read.
- gcount = image_file.gcount();
- // Write to the simulator memory.
- auto res = dbg->WriteMemory(load_address, buffer, gcount);
- // Check that the write succeeded, increment address if it did.
- if (!res.ok()) {
- LOG(ERROR) << "LoadImage: Memory write failed";
- return -1;
- }
- if (res.value() != gcount) {
- LOG(ERROR) << "LoadImage: Memory write failed to write all the bytes";
- return -1;
- }
- load_address += gcount;
- } while (image_file.good() && (gcount > 0));
- image_file.close();
return 0;
}
diff --git a/sim/renode/test/BUILD b/sim/renode/test/BUILD
index 50538d3..4b1d191 100644
--- a/sim/renode/test/BUILD
+++ b/sim/renode/test/BUILD
@@ -37,7 +37,6 @@
"@com_google_googletest//:gtest_main",
"@com_google_mpact-riscv//riscv:riscv_debug_info",
"@com_google_mpact-sim//mpact/sim/generic:core",
- "@com_google_mpact-sim//mpact/sim/generic:core_debug_interface",
"@com_google_mpact-sim//mpact/sim/util/program_loader:elf_loader",
],
)
diff --git a/sim/renode/test/kelvin_renode_test.cc b/sim/renode/test/kelvin_renode_test.cc
index 82c01e7..397dcf6 100644
--- a/sim/renode/test/kelvin_renode_test.cc
+++ b/sim/renode/test/kelvin_renode_test.cc
@@ -1,9 +1,6 @@
#include "sim/renode/kelvin_renode.h"
-#include <cstddef>
#include <cstdint>
-#include <fstream>
-#include <ios>
#include <string>
#include "sim/kelvin_top.h"
@@ -13,7 +10,6 @@
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "riscv/riscv_debug_info.h"
-#include "mpact/sim/generic/core_debug_interface.h"
#include "mpact/sim/util/program_loader/elf_program_loader.h"
namespace {
@@ -82,26 +78,11 @@
TEST_F(KelvinRenodeTest, RunBinProgram) {
std::string file_name = absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
- constexpr uint32_t kBufferSize = 1024;
constexpr uint64_t kBinFileAddress = 0x0;
constexpr uint64_t kBinFileEntryPoint = 0x0;
- char buffer[kBufferSize];
- size_t gcount = 0;
- uint64_t load_address = kBinFileAddress;
- std::ifstream image_file;
- image_file.open(file_name, std::ios::in | std::ios::binary);
- EXPECT_TRUE(image_file.good());
- do {
- image_file.read(buffer, kBufferSize);
- gcount = image_file.gcount();
- auto result = top_->WriteMemory(load_address, buffer, gcount);
- EXPECT_TRUE(result.ok());
- EXPECT_EQ(result.value(), gcount);
- load_address += gcount;
- } while (image_file.good() && (gcount > 0));
- image_file.close();
-
+ auto res = top_->LoadImage(file_name, kBinFileAddress);
+ EXPECT_TRUE(res.ok());
// Run the program.
testing::internal::CaptureStdout();
EXPECT_TRUE(top_->WriteRegister("pc", kBinFileEntryPoint).ok());
diff --git a/sim/renode/test/renode_mpact_test.cc b/sim/renode/test/renode_mpact_test.cc
index c4fa7d1..d639225 100644
--- a/sim/renode/test/renode_mpact_test.cc
+++ b/sim/renode/test/renode_mpact_test.cc
@@ -111,6 +111,16 @@
EXPECT_EQ(mem_bytes[0], kBytes[4]);
res = read_memory(sim_id_, 0x100, reinterpret_cast<char *>(mem_bytes), 8);
for (int i = 0; i < 8; i++) EXPECT_EQ(kBytes[i], mem_bytes[i]);
+
+ // Read memory from out of bound address
+ constexpr uint64_t kOutOfBoundAddress = 0x3'FFFF'FFFFULL;
+ res = read_memory(sim_id_, kOutOfBoundAddress,
+ reinterpret_cast<char *>(mem_bytes), 1);
+ EXPECT_EQ(res, 0);
+ // Write to out of bound memory address
+ res = write_memory(sim_id_, kOutOfBoundAddress,
+ reinterpret_cast<const char *>(mem_bytes), 1);
+ EXPECT_EQ(res, 0);
}
TEST_F(RenodeMpactTest, LoadImage) {
diff --git a/sim/test/BUILD b/sim/test/BUILD
index 9dbc184..58b2fa5 100644
--- a/sim/test/BUILD
+++ b/sim/test/BUILD
@@ -59,6 +59,7 @@
"kelvin_top_test.cc",
],
data = [
+ "testfiles/hello_world_mpause.bin",
"testfiles/hello_world_mpause.elf",
"testfiles/hello_world_rv32imf.elf",
"testfiles/kelvin_vldvst.elf",
diff --git a/sim/test/kelvin_top_test.cc b/sim/test/kelvin_top_test.cc
index 2c7b9b7..88f938e 100644
--- a/sim/test/kelvin_top_test.cc
+++ b/sim/test/kelvin_top_test.cc
@@ -26,6 +26,7 @@
using ::mpact::sim::util::FlatDemandMemory;
using HaltReason = ::mpact::sim::generic::CoreDebugInterface::HaltReason;
+constexpr char kMpauseBinaryFileName[] = "hello_world_mpause.bin";
constexpr char kMpauseElfFileName[] = "hello_world_mpause.elf";
constexpr char kRV32imfElfFileName[] = "hello_world_rv32imf.elf";
constexpr char kRV32iElfFileName[] = "rv32i.elf";
@@ -40,6 +41,8 @@
// Maximum memory size used by riscv programs build for userspace.
constexpr uint64_t kRiscv32MaxAddress = 0x3'ffff'ffffULL;
+constexpr uint64_t kBinaryAddress = 0;
+
class KelvinTopTest : public testing::Test {
protected:
KelvinTopTest() {
@@ -139,6 +142,70 @@
EXPECT_EQ("Program exits properly\n", stdout_str);
}
+TEST_F(KelvinTopTest, LoadImageFailed) {
+ const std::string input_file_name =
+ absl::StrCat(kDepotPath, "testfiles/", kMpauseBinaryFileName);
+ auto result = kelvin_top_->LoadImage("wrong_file", kBinaryAddress);
+ EXPECT_FALSE(result.ok());
+ // Set the memory to be smaller than the loaded image
+ kelvin_top_->state()->set_max_physical_address(0);
+ result = kelvin_top_->LoadImage(input_file_name, kBinaryAddress);
+ EXPECT_FALSE(result.ok());
+ kelvin_top_->state()->set_max_physical_address(0xf);
+ result = kelvin_top_->LoadImage(input_file_name, kBinaryAddress);
+ EXPECT_FALSE(result.ok());
+}
+
+// Directly read/write to memory addresses that are out-of-bound
+TEST_F(KelvinTopTest, ReadWriteOutOfBoundMemory) {
+ // Set the machine to have 16-byte physical memory
+ constexpr uint64_t kTestMemerySize = 0x10;
+ kelvin_top_->state()->set_max_physical_address(kTestMemerySize - 1);
+ uint8_t mem_bytes[kTestMemerySize + 4] = {0};
+ // Read the memory with the length greater than the physical memory size. The
+ // read operation is successful within the physical memory size range.
+ auto result =
+ kelvin_top_->ReadMemory(kBinaryAddress, mem_bytes, sizeof(mem_bytes));
+ EXPECT_OK(result);
+ EXPECT_EQ(result.value(), kTestMemerySize);
+ // Read the memory with the staring address out of the physical memory range.
+ // The read operation returns error.
+ result = kelvin_top_->ReadMemory(kTestMemerySize + 4, mem_bytes,
+ sizeof(mem_bytes));
+ EXPECT_FALSE(result.ok());
+
+ // Write the memory with the length greater than the physical memory size. The
+ // write operation is successful within the physical memory size range.
+ result =
+ kelvin_top_->WriteMemory(kBinaryAddress, mem_bytes, sizeof(mem_bytes));
+ EXPECT_OK(result);
+ EXPECT_EQ(result.value(), kTestMemerySize);
+ // Write the memory with the staring address out of the physical memory range.
+ // The write operation returns error.
+ result = kelvin_top_->WriteMemory(kTestMemerySize + 4, mem_bytes,
+ sizeof(mem_bytes));
+ EXPECT_FALSE(result.ok());
+}
+
+// Runs the binary program from beginning to end
+TEST_F(KelvinTopTest, RunHelloMpauseBinaryProgram) {
+ const std::string input_file_name =
+ absl::StrCat(kDepotPath, "testfiles/", kMpauseBinaryFileName);
+ constexpr uint32_t kBinaryEntryPoint = 0;
+ testing::internal::CaptureStdout();
+ auto result = kelvin_top_->LoadImage(input_file_name, kBinaryAddress);
+ CHECK_OK(result);
+ EXPECT_OK(kelvin_top_->WriteRegister("pc", kBinaryEntryPoint));
+ EXPECT_OK(kelvin_top_->Run());
+ EXPECT_OK(kelvin_top_->Wait());
+ auto halt_result = kelvin_top_->GetLastHaltReason();
+ CHECK_OK(halt_result);
+ EXPECT_EQ(static_cast<int>(halt_result.value()),
+ static_cast<int>(HaltReason::kUserRequest));
+ const std::string stdout_str = testing::internal::GetCapturedStdout();
+ EXPECT_EQ("Program exits properly\n", stdout_str);
+}
+
// Runs the rv32i program with arm semihosting.
TEST_F(KelvinTopTest, RunRV32IProgram) {
absl::SetFlag(&FLAGS_use_semihost, true);