blob: 5a79e1e167345361e798029441804d3509155137 [file]
// Copyright 2023 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
//
// https://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_arm_semihost.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <string>
#include <utility>
#include "absl/functional/bind_front.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "mpact/sim/generic/register.h"
#include "mpact/sim/util/memory/memory_interface.h"
namespace mpact {
namespace sim {
namespace riscv {
RiscVArmSemihost::RiscVArmSemihost(BitWidth bit_width,
util::MemoryInterface *i_memory_if,
util::MemoryInterface *d_memory_if)
: i_memory_if_(i_memory_if),
d_memory_if_(d_memory_if),
// Put the functions that implement the different semihosting operations
// into a map according to their function code.
semihost_operations_(
{{kSysClose, absl::bind_front(&RiscVArmSemihost::SysClose, this)},
{kSysClock, absl::bind_front(&RiscVArmSemihost::SysClock, this)},
{kSysElapsed, absl::bind_front(&RiscVArmSemihost::SysElapsed, this)},
{kSysErrno, absl::bind_front(&RiscVArmSemihost::SysErrno, this)},
{kSysException,
absl::bind_front(&RiscVArmSemihost::SysException, this)},
{kSysFlen, absl::bind_front(&RiscVArmSemihost::SysFlen, this)},
{kSysGetCmdline,
absl::bind_front(&RiscVArmSemihost::SysGetCmdline, this)},
{kSysHeapInfo,
absl::bind_front(&RiscVArmSemihost::SysHeapInfo, this)},
{kSysIsError, absl::bind_front(&RiscVArmSemihost::SysIsError, this)},
{kSysIsTty, absl::bind_front(&RiscVArmSemihost::SysIsTty, this)},
{kSysOpen, absl::bind_front(&RiscVArmSemihost::SysOpen, this)},
{kSysRead, absl::bind_front(&RiscVArmSemihost::SysRead, this)},
{kSysReadc, absl::bind_front(&RiscVArmSemihost::SysReadc, this)},
{kSysRemove, absl::bind_front(&RiscVArmSemihost::SysRemove, this)},
{kSysRename, absl::bind_front(&RiscVArmSemihost::SysRename, this)},
{kSysSeek, absl::bind_front(&RiscVArmSemihost::SysSeek, this)},
{kSysSystem, absl::bind_front(&RiscVArmSemihost::SysSystem, this)},
{kSysTickFreq,
absl::bind_front(&RiscVArmSemihost::SysTickFreq, this)},
{kSysTime, absl::bind_front(&RiscVArmSemihost::SysTime, this)},
{kSysTmpnam, absl::bind_front(&RiscVArmSemihost::SysTmpnam, this)},
{kSysWrite, absl::bind_front(&RiscVArmSemihost::SysWrite, this)},
{kSysWritec, absl::bind_front(&RiscVArmSemihost::SysWritec, this)},
{kSysWrite0,
absl::bind_front(&RiscVArmSemihost::SysWrite0, this)}}) {
is_32_bit_ = bit_width == BitWidth::kWord32;
// Pre-allocate some fixed length data buffers that are used across semihost
// calls. Set the length according to the register width for those used to
// fetch parameters from data memory.
db_inst_ = db_factory_.Allocate<uint32_t>(3);
db1_ = is_32_bit_ ? db_factory_.Allocate<uint32_t>(1)
: db_factory_.Allocate<uint64_t>(1);
db2_ = is_32_bit_ ? db_factory_.Allocate<uint32_t>(2)
: db_factory_.Allocate<uint64_t>(2);
db3_ = is_32_bit_ ? db_factory_.Allocate<uint32_t>(3)
: db_factory_.Allocate<uint64_t>(3);
db4_ = is_32_bit_ ? db_factory_.Allocate<uint32_t>(4)
: db_factory_.Allocate<uint64_t>(4);
// stdin
fd_map_.insert(std::make_pair(0, 0));
// stdout
fd_map_.insert(std::make_pair(1, 1));
// stderr
fd_map_.insert(std::make_pair(2, 2));
}
RiscVArmSemihost::~RiscVArmSemihost() {
db_inst_->DecRef();
db1_->DecRef();
db2_->DecRef();
db3_->DecRef();
db4_->DecRef();
}
bool RiscVArmSemihost::IsSemihostingCall(const Instruction *inst) {
if (inst == nullptr) return false;
// Load the instruction words on either side of the ebreak instruction.
uint64_t address = inst->address() - 4;
i_memory_if_->Load(address, db_inst_, nullptr, nullptr);
// Check to see if this is a semihosting call, if not, return.
if ((db_inst_->Get<uint32_t>(0) != kSlliNop1f) ||
(db_inst_->Get<uint32_t>(1) != kEBreak) ||
(db_inst_->Get<uint32_t>(2) != kSraiNop7)) {
return false;
}
return true;
}
void RiscVArmSemihost::OnEBreak(const Instruction *inst) {
if (!IsSemihostingCall(inst)) return;
// Handle the semihosting call.
auto registers = inst->state()->registers();
auto iter = registers->find(kA0Name);
auto *a0 = iter == registers->end() ? nullptr : iter->second;
iter = registers->find(kA1Name);
auto *a1 = iter == registers->end() ? nullptr : iter->second;
if ((a0 == nullptr) || (a1 == nullptr)) {
LOG(ERROR) << "Failed to fetch semihost argument registers";
}
// Read the op number and the address of the parameter block from the
// registers.
uint64_t op_num = 0;
uint64_t parameter = 0;
if (is_32_bit_) {
op_num = a0->data_buffer()->Get<uint32_t>(0);
parameter = a1->data_buffer()->Get<uint32_t>(0);
} else {
op_num = a0->data_buffer()->Get<uint64_t>(0);
parameter = a1->data_buffer()->Get<uint64_t>(0);
}
// Make sure the op number is valid.
auto op = semihost_operations_.find(op_num);
if (op == semihost_operations_.end()) {
LOG(ERROR) << absl::StrCat("Illegal semihost operation (",
absl::Hex(op_num), ")");
return;
}
// Call the semihosting op.
uint64_t ret_val = 0;
auto status = op->second(parameter, &ret_val);
// In case of error, set an error code and log the error message.
if (!status.ok()) {
LOG(ERROR) << absl::StrCat("Semihost error: ", status.message());
if (is_32_bit_) {
a0->data_buffer()->Set<uint32_t>(0, std::numeric_limits<uint32_t>::max());
} else {
a0->data_buffer()->Set<uint64_t>(0, std::numeric_limits<uint64_t>::max());
}
} else {
if (is_32_bit_) {
a0->data_buffer()->Set<uint32_t>(0, static_cast<uint32_t>(ret_val));
} else {
a0->data_buffer()->Set<uint64_t>(0, ret_val);
}
}
}
absl::Status RiscVArmSemihost::SysClose(uint64_t parameter, uint64_t *ret_val) {
// Load the file descriptor from the parameter block.
d_memory_if_->Load(parameter, db1_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db1_->Get<uint32_t>(0))
: static_cast<int>(db1_->Get<uint64_t>(0));
// Check to see that the target file descriptor is in the map, if not,
// the fd is not for an opened file.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
// Get the host fd and close the file.
int host_fd = iter->second;
// If it's stdin, stdout, or stderr, ignore, just return ok.
if (host_fd <= 2) {
*ret_val = 0;
return absl::OkStatus();
}
int ret = close(host_fd);
if (!ret) {
sys_errno_ = errno;
} else {
// Remove the file descriptor from the map.
fd_map_.erase(iter);
}
*ret_val = static_cast<uint64_t>(ret);
return absl::OkStatus();
}
// Currently not implemented, will implement once there is need for it.
absl::Status RiscVArmSemihost::SysClock(uint64_t parameter, uint64_t *ret_val) {
return absl::UnimplementedError("SysClock not implemented");
// TODO: Complete implementation.
}
// Currently not implemented, will implement once there is need for it.
absl::Status RiscVArmSemihost::SysElapsed(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysElapsed not implemented");
// TODO: Complete implementation.
}
// Return the value of the simulated errno.
absl::Status RiscVArmSemihost::SysErrno(uint64_t parameter, uint64_t *ret_val) {
*ret_val = sys_errno_;
return absl::OkStatus();
}
// Exception notification. The program should be terminated.
absl::Status RiscVArmSemihost::SysException(uint64_t parameter, uint64_t *) {
// In gcc it seems like the parameter value is passed in the parameter
// register for RV32, but stored in memory, and then a pointer passed in the
// parameter register for RV64. A bit odd...
uint32_t param_value;
if (is_32_bit_) {
param_value = static_cast<uint32_t>(parameter);
} else {
d_memory_if_->Load(parameter, db1_, nullptr, nullptr);
param_value = static_cast<uint32_t>(db1_->Get<uint64_t>(0));
}
if (param_value == kAdpStoppedApplicationExit) {
if (exit_callback_ != nullptr) {
exit_callback_();
return absl::OkStatus();
}
LOG(ERROR) << "Program exit not caught in ARM semihosting - no callback";
return absl::NotFoundError("Exit callback not valid in ARM semihosting");
}
LOG(ERROR) << "Exceptions other than ApplicationExit are not implemented";
return absl::UnimplementedError(
"Exceptions other than ApplicationExit are not implemented");
}
// Return the length of a file given by the file descriptor.
absl::Status RiscVArmSemihost::SysFlen(uint64_t parameter, uint64_t *ret_val) {
// Load the targete file descriptor.
d_memory_if_->Load(parameter, db1_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db1_->Get<uint32_t>(0))
: static_cast<int>(db1_->Get<uint64_t>(0));
// Verify that the file descriptor is valid.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
auto host_fd = iter->second;
// Get the file info, and get the file length.
struct stat statbuf;
int res = fstat(host_fd, &statbuf);
if (res < 0) {
sys_errno_ = errno;
*ret_val = -1ULL;
return absl::OkStatus();
}
*ret_val = static_cast<uint64_t>(statbuf.st_size);
return absl::OkStatus();
}
// Currently unimplemented. Will implement if there is a demand.
absl::Status RiscVArmSemihost::SysGetCmdline(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysGetCmdline not implemented");
// TODO: Complete implementation.
}
// Returns 0 information indicating that the call doesn't provide this info.
absl::Status RiscVArmSemihost::SysHeapInfo(uint64_t parameter,
uint64_t *ret_val) {
d_memory_if_->Load(parameter, db1_, nullptr, nullptr);
uint64_t block_address = is_32_bit_
? static_cast<int>(db1_->Get<uint32_t>(0))
: static_cast<int>(db1_->Get<uint64_t>(0));
// Return all zeros.
auto *db = db_factory_.Allocate<uint32_t>(4);
d_memory_if_->Load(block_address, db, nullptr, nullptr);
db->Set<uint32_t>(0, 0);
db->Set<uint32_t>(1, 0);
db->Set<uint32_t>(2, 0);
db->Set<uint32_t>(3, 0);
d_memory_if_->Store(block_address, db);
db->DecRef();
return absl::OkStatus();
}
// This function is not implemented for now. Will look into it if there is
// demand.
absl::Status RiscVArmSemihost::SysIsError(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysGetCmdline not implemented");
// TODO: Complete implementation.
}
// Check if the fd is a tty.
absl::Status RiscVArmSemihost::SysIsTty(uint64_t parameter, uint64_t *ret_val) {
// Load the target file descriptor.
d_memory_if_->Load(parameter, db1_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db1_->Get<uint32_t>(0))
: static_cast<int>(db1_->Get<uint64_t>(0));
// Check if the fd is valid.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
auto host_fd = iter->second;
// Return the value of the isatty call.
int ret = isatty(host_fd);
if (!ret) {
sys_errno_ = errno;
}
*ret_val = static_cast<uint64_t>(ret);
return absl::OkStatus();
}
// Open a file, return the file descriptor if successful.
absl::Status RiscVArmSemihost::SysOpen(uint64_t parameter, uint64_t *ret_val) {
// Load the parameter block consisiting of pointer to a string, the file open
// mode, and the length of the string.
d_memory_if_->Load(parameter, db3_, nullptr, nullptr);
uint64_t string_address = is_32_bit_
? static_cast<uint64_t>(db3_->Get<uint32_t>(0))
: static_cast<uint64_t>(db3_->Get<uint64_t>(0));
int mode = is_32_bit_ ? static_cast<int>(db3_->Get<int32_t>(1))
: static_cast<int>(db3_->Get<int64_t>(1));
uint64_t file_name_len = is_32_bit_
? static_cast<int>(db3_->Get<uint32_t>(2))
: static_cast<int>(db3_->Get<uint64_t>(2));
// Allocate a data buffer for the file name string, load it, and initialize
// a string variable with it.
std::string file_name;
if (file_name_len > 0) {
auto *db_c = db_factory_.Allocate<uint8_t>(file_name_len);
d_memory_if_->Load(string_address, db_c, nullptr, nullptr);
file_name =
std::string(static_cast<char *>(db_c->raw_ptr()), file_name_len);
db_c->DecRef();
}
// If the name is ":tt" then it's either cin or cout depending on the mode.
// In this case just dup the corresponding host fd's.
int host_fd;
int target_fd;
if (file_name == ":tt") {
if (mode == O_RDONLY) {
host_fd = 0;
target_fd = dup(0);
} else {
host_fd = 1;
target_fd = dup(1);
}
} else {
// Open the file, create if needed for write/update modes.
if (mode == O_RDONLY) {
host_fd = open(file_name.c_str(), 0, mode);
} else {
host_fd = open(file_name.c_str(), O_CREAT, mode);
}
target_fd = dup(host_fd);
}
*ret_val = static_cast<uint64_t>(target_fd);
if (target_fd < 0) {
sys_errno_ = errno;
return absl::OkStatus();
}
fd_map_.insert(std::make_pair(target_fd, host_fd));
return absl::OkStatus();
}
absl::Status RiscVArmSemihost::SysRead(uint64_t parameter, uint64_t *ret_val) {
// Load the parameter block, consisting of the target file descriptor, the
// target buffer address, and buffer length.
d_memory_if_->Load(parameter, db3_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db3_->Get<uint32_t>(0))
: static_cast<int>(db3_->Get<uint64_t>(0));
uint64_t buffer_address = is_32_bit_
? static_cast<uint64_t>(db3_->Get<uint32_t>(1))
: static_cast<uint64_t>(db3_->Get<uint64_t>(1));
int length = is_32_bit_ ? static_cast<int>(db3_->Get<int32_t>(2))
: static_cast<int>(db3_->Get<int64_t>(2));
// Check that the file descriptor is valid.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
auto host_fd = iter->second;
// Allocate a data buffer sufficient for the target buffer length.
auto *db = db_factory_.Allocate<uint8_t>(length);
// Read from the file/
int res = read(host_fd, db->raw_ptr(), length);
*ret_val = static_cast<uint64_t>(res);
if (res < 0) {
sys_errno_ = errno;
return absl::OkStatus();
}
// Write to the buffer.
d_memory_if_->Store(buffer_address, db);
db->DecRef();
return absl::OkStatus();
}
// Read a byte from the debug console. This is not implemented for now.
absl::Status RiscVArmSemihost::SysReadc(uint64_t parameter, uint64_t *ret_val) {
return absl::UnimplementedError("SysReadc not implemented");
// TODO: Complete implementation.
}
// Remove a file from the host file system. This will not be implemented.
absl::Status RiscVArmSemihost::SysRemove(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysRemove not implemented");
}
// Rename a file in the host file system. This will not be implemented.
absl::Status RiscVArmSemihost::SysRename(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysRename not implemented");
}
// Seek in the file specified by the target file descriptor.
absl::Status RiscVArmSemihost::SysSeek(uint64_t parameter, uint64_t *ret_val) {
// Load the parameters consisting of the target fd and the desired seek
// offset.
d_memory_if_->Load(parameter, db2_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db2_->Get<uint32_t>(0))
: static_cast<int>(db2_->Get<uint64_t>(0));
uint64_t offset = is_32_bit_ ? static_cast<int>(db2_->Get<uint32_t>(1))
: static_cast<int>(db2_->Get<uint64_t>(1));
// Verify that the target fd is valid.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
auto host_fd = iter->second;
// Perform the seek relative to the beginning of the file.
auto res = lseek(host_fd, offset, SEEK_SET);
if (res < 0) {
sys_errno_ = errno;
res = -1;
} else {
res = 0;
}
*ret_val = static_cast<uint64_t>(res);
return absl::OkStatus();
}
// Execute a command in the shell of the host. This will not be implemented.
absl::Status RiscVArmSemihost::SysSystem(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysSystem not implemented");
}
// Return the system tick frequency. For now just return -1 to indicate that
// this call is not fully supported.
absl::Status RiscVArmSemihost::SysTickFreq(uint64_t parameter,
uint64_t *ret_val) {
*ret_val = -1ULL;
return absl::OkStatus();
}
// Return unix time in seconds.
absl::Status RiscVArmSemihost::SysTime(uint64_t parameter, uint64_t *ret_val) {
*ret_val = time(nullptr);
return absl::OkStatus();
}
// Return a temporary file name.
absl::Status RiscVArmSemihost::SysTmpnam(uint64_t parameter,
uint64_t *ret_val) {
// Load parameters consisting of a pointer to a buffer, an int (0-255) that
// is a target identifier for this filename, and the length of the buffer.
d_memory_if_->Load(parameter, db3_, nullptr, nullptr);
uint64_t buffer_address = is_32_bit_
? static_cast<int>(db3_->Get<uint32_t>(0))
: static_cast<int>(db3_->Get<uint64_t>(0));
int target_id = is_32_bit_ ? static_cast<uint64_t>(db3_->Get<uint32_t>(1))
: static_cast<uint64_t>(db3_->Get<uint64_t>(1));
int length = is_32_bit_ ? static_cast<int>(db3_->Get<int32_t>(2))
: static_cast<int>(db3_->Get<int64_t>(2));
// Validate the target_id.
if (target_id < 0 || target_id > 255) {
*ret_val = -1ULL;
return absl::OkStatus();
}
// The length of the buffer has to be at least L_tmpnam
if (length < L_tmpnam) {
*ret_val = -1ULL;
return absl::OkStatus();
}
// Allocate a data buffer and call tmpnam, then write the name to the buffer.
auto tmpnam_db = db_factory_.Allocate<uint8_t>(length);
tmpnam(static_cast<char *>(tmpnam_db->raw_ptr()));
d_memory_if_->Store(buffer_address, tmpnam_db);
tmpnam_db->DecRef();
return absl::OkStatus();
}
// Write data to a file.
absl::Status RiscVArmSemihost::SysWrite(uint64_t parameter, uint64_t *ret_val) {
// Load parameters consisting of target fd, target buffer address, and
// length.
d_memory_if_->Load(parameter, db3_, nullptr, nullptr);
int target_fd = is_32_bit_ ? static_cast<int>(db3_->Get<uint32_t>(0))
: static_cast<int>(db3_->Get<uint64_t>(0));
uint64_t buffer_address = is_32_bit_
? static_cast<uint64_t>(db3_->Get<uint32_t>(1))
: static_cast<uint64_t>(db3_->Get<uint64_t>(1));
int length = is_32_bit_ ? static_cast<int>(db3_->Get<int32_t>(2))
: static_cast<int>(db3_->Get<int64_t>(2));
// Verify that the target fd is valid.
auto iter = fd_map_.find(target_fd);
if (iter == fd_map_.end()) {
*ret_val = -1ULL;
return absl::OkStatus();
}
auto host_fd = iter->second;
// Allocate the data buffer necessary to read the data to be written to the
// file.
auto *db = db_factory_.Allocate<uint8_t>(length);
d_memory_if_->Load(buffer_address, db, nullptr, nullptr);
// Write the data to the file.
int res = write(host_fd, db->raw_ptr(), length);
*ret_val = static_cast<uint64_t>(res);
if (res < 0) {
sys_errno_ = errno;
}
*ret_val = static_cast<uint64_t>(res);
db->DecRef();
return absl::OkStatus();
}
// Write a byte to the degbug console. This is not implemented for now.
absl::Status RiscVArmSemihost::SysWritec(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysWritec not implemented");
}
// Write a null terminated string to the debug console. This is not implemented
// for now.
absl::Status RiscVArmSemihost::SysWrite0(uint64_t parameter,
uint64_t *ret_val) {
return absl::UnimplementedError("SysWrite0 not implemented");
}
} // namespace riscv
} // namespace sim
} // namespace mpact