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