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);