blob: ab14462d92943c8ff6319514ce55cc7e729c52fa [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include "dpi_memutil.h"
#include <cassert>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <libelf.h>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include "sv_scoped.h"
namespace {
// Convenience class for runtime errors when loading an ELF file
class ElfError : public std::exception {
public:
ElfError(const std::string &path, const std::string &msg) {
std::ostringstream oss;
oss << "Failed to load ELF file at `" << path << "': " << msg;
msg_ = oss.str();
}
const char *what() const noexcept override { return msg_.c_str(); }
private:
std::string msg_;
};
// Class wrapping an open ELF file
class ElfFile {
public:
ElfFile(const std::string &path) : path_(path) {
(void)elf_errno();
if (elf_version(EV_CURRENT) == EV_NONE) {
throw std::runtime_error(elf_errmsg(-1));
}
fd_ = open(path.c_str(), O_RDONLY, 0);
if (fd_ < 0) {
throw ElfError(path, "could not open file.");
}
ptr_ = elf_begin(fd_, ELF_C_READ, NULL);
if (!ptr_) {
close(fd_);
throw ElfError(path, elf_errmsg(-1));
}
if (elf_kind(ptr_) != ELF_K_ELF) {
elf_end(ptr_);
close(fd_);
throw ElfError(path, "not an ELF file.");
}
}
~ElfFile() {
elf_end(ptr_);
close(fd_);
}
size_t GetPhdrNum() {
size_t phnum;
if (elf_getphdrnum(ptr_, &phnum) != 0) {
throw ElfError(path_, elf_errmsg(-1));
}
return phnum;
}
const Elf32_Phdr *GetPhdrs() {
const Elf32_Phdr *phdrs = elf32_getphdr(ptr_);
if (!phdrs)
throw ElfError(path_, elf_errmsg(-1));
return phdrs;
}
std::string path_;
int fd_;
Elf *ptr_;
};
} // namespace
// Convert a string to a MemImageType, throwing a std::runtime_error
// if it's not a known name.
static MemImageType GetMemImageTypeByName(const std::string &name) {
if (name == "elf")
return kMemImageElf;
if (name == "vmem")
return kMemImageVmem;
std::ostringstream oss;
oss << "Unknown image type: `" << name << "'.";
throw std::runtime_error(oss.str());
}
// Return a MemImageType for the file at filepath or throw a std::runtime_error.
// Never returns kMemImageUnknown.
static MemImageType DetectMemImageType(const std::string &filepath) {
size_t ext_pos = filepath.find_last_of(".");
if (ext_pos == std::string::npos) {
// Assume ELF files if no file extension is given.
// TODO: Make this more robust by actually checking the file contents.
return kMemImageElf;
}
std::string ext = filepath.substr(ext_pos + 1);
MemImageType image_type = GetMemImageTypeByName(ext);
if (image_type == kMemImageUnknown) {
std::ostringstream oss;
oss << "Cannot auto-detect file type for `" << filepath << "'.";
throw std::runtime_error(oss.str());
}
return image_type;
}
// Generate a single array of bytes representing the contents of PT_LOAD
// segments of the ELF file. Like objcopy, this generates a single "giant
// segment" whose first byte corresponds to the first byte of the lowest
// addressed segment and whose last byte corresponds to the last byte of the
// highest address.
static std::vector<uint8_t> FlattenElfFile(const std::string &filepath) {
ElfFile elf(filepath);
size_t phnum = elf.GetPhdrNum();
const Elf32_Phdr *phdrs = elf.GetPhdrs();
// To mimic what objcopy does (that is, the binary target of BFD), we need to
// iterate over all loadable program headers, find the lowest address, and
// then copy in our loadable data based on their offset with respect to the
// found base address.
bool any = false;
Elf32_Addr low = 0, high = 0;
for (size_t i = 0; i < phnum; i++) {
const Elf32_Phdr &phdr = phdrs[i];
if (phdr.p_type != PT_LOAD) {
std::cout << "Program header number " << i << " in `" << filepath
<< "' is not of type PT_LOAD; ignoring." << std::endl;
continue;
}
if (phdr.p_filesz == 0) {
continue;
}
if (!any || phdr.p_paddr < low) {
low = phdr.p_paddr;
}
Elf32_Addr seg_top = phdr.p_paddr + (phdr.p_filesz - 1);
if (seg_top < phdr.p_paddr) {
std::ostringstream oss;
oss << "phdr for segment " << i << " has start 0x" << std::hex
<< phdr.p_paddr << " and size 0x" << phdr.p_filesz
<< ", which overflows the address space.";
throw ElfError(filepath, oss.str());
}
if (!any || seg_top > high) {
high = seg_top;
}
any = true;
}
// If any is false, there were no segments that contributed to the
// file. Return nothing.
if (!any)
return std::vector<uint8_t>();
// Otherwise, we know every valid byte of data has an address in the
// range [low, high] (inclusive).
assert(low <= high);
size_t file_size;
const char *file_data = elf_rawfile(elf.ptr_, &file_size);
assert(file_data);
StagedMem ret;
for (size_t i = 0; i < phnum; i++) {
const Elf32_Phdr &phdr = phdrs[i];
if (phdr.p_type != PT_LOAD) {
continue;
}
// Check the segment actually fits in the file
if (file_size < phdr.p_offset + phdr.p_filesz) {
std::ostringstream oss;
oss << "phdr for segment " << i << " claims to end at offset 0x"
<< std::hex << phdr.p_offset + phdr.p_filesz
<< ", but the file only has size 0x" << file_size << ".";
throw ElfError(filepath, oss.str());
}
if (phdr.p_filesz == 0)
continue;
uint32_t off = phdr.p_paddr - low;
std::vector<uint8_t> seg(phdr.p_filesz, 0);
memcpy(&seg[0], file_data + phdr.p_offset, phdr.p_filesz);
ret.AddSegment(off, std::move(seg));
}
return ret.GetFlat();
}
// Merge seg0 and seg1, overwriting any overlapping data in seg0 with
// that from seg1. rng0/rng1 is the base and top address of seg0/seg1,
// respectively.
static std::vector<uint8_t> MergeSegments(const AddrRange<uint32_t> &rng0,
std::vector<uint8_t> &&seg0,
const AddrRange<uint32_t> &rng1,
std::vector<uint8_t> &&seg1) {
// First, deal with the special case where seg1 completely contains
// seg0 (since there's no copying needed at all).
if (rng1.lo <= rng0.lo && rng0.hi <= rng1.hi) {
return std::move(seg1);
}
uint32_t new_bot = std::min(rng0.lo, rng1.lo);
uint32_t new_top = std::max(rng0.hi, rng1.hi);
assert(new_bot <= new_top);
size_t new_len = 1 + (size_t)(new_top - new_bot);
assert(seg0.size() <= new_len);
assert(seg1.size() <= new_len);
// We want to avoid copying if possible. The next most efficient
// case (after just returning seg1) is when seg0 doesn't stick out
// the left hand end. In this case, we can extend seg1 to the right
// (which might not cause a copy) and then copy just the bytes we
// need from seg0.
if (rng1.lo <= rng0.lo) {
assert(rng1.hi < rng0.hi);
assert(new_len == seg1.size() + (rng0.hi - rng1.hi));
size_t old_len = seg1.size();
std::vector<uint8_t> ret = std::move(seg1);
ret.resize(new_len);
// We know that that rng0 isn't completely contained in rng1 and
// that rng0 doesn't stick out of the left hand end. That means it
// must stick out of the right (so rng1.hi < rng0.hi). However, we
// also know that the two ranges overlap, so rng0.lo <= rng1.hi.
assert(rng0.lo <= rng1.hi);
// src_off is the index of the first byte that needs copying from
// seg0. Note that this is always at least 1 (because there is an
// actual overlap).
uint32_t src_off = 1 + (rng1.hi - rng0.lo);
assert(seg0.size() == src_off + (rng0.hi - rng1.hi));
memcpy(&ret[old_len], &seg0[src_off], rng0.hi - rng1.hi);
return ret;
}
// In this final case, seg0 sticks out the left hand end. That means
// we'll have to copy seg1 whatever happens (because we have to
// shuffle its elements to the right). Work by resizing seg0 and
// then writing seg1 where it's needed.
std::vector<uint8_t> ret = std::move(seg0);
ret.resize(new_len);
uint32_t off = rng1.lo - rng0.lo;
memcpy(&ret[off], &seg1[0], seg1.size());
return ret;
}
void StagedMem::AddSegment(uint32_t offset, std::vector<uint8_t> &&seg) {
if (seg.empty())
return;
uint32_t seg_top = offset + seg.size() - 1;
assert(seg_top >= offset);
min_addr_ = std::min(min_addr_, offset);
max_addr_ = std::max(max_addr_, seg_top);
segs_.Emplace(offset, seg_top, std::move(seg), MergeSegments);
}
std::vector<uint8_t> StagedMem::GetFlat() const {
// Since max_addr_ and min_addr_ are inclusive, the size to allocate
// is 1+(max-min). We cast to size_t to make sure the +1 doesn't
// overflow.
size_t len = (size_t)1 + (max_addr_ - min_addr_);
std::vector<uint8_t> ret(len, 0);
for (const auto &pr : segs_) {
const AddrRange<uint32_t> &rng = pr.first;
const std::vector<uint8_t> &seg = pr.second;
assert(seg.size() == 1 + (rng.hi - rng.lo));
assert(min_addr_ <= rng.lo);
uint32_t off = rng.lo - min_addr_;
assert(off + seg.size() <= ret.size());
memcpy(&ret[off], &seg[0], seg.size());
}
return ret;
}
void DpiMemUtil::RegisterMemoryArea(const std::string &name, uint32_t base,
const MemArea *mem_area) {
assert(mem_area);
// Check that we don't overflow the address space.
uint32_t size = mem_area->GetSizeBytes();
uint32_t addr_top = base + (size - 1);
if (addr_top < base) {
std::ostringstream oss;
oss << "Cannot register '" << name << "' at at 0x" << std::hex << base
<< " because it has size 0x" << size
<< " and the top would overflow the top of the address space.";
throw std::runtime_error(oss.str());
}
size_t new_idx = mem_areas_.size();
auto pr = name_to_mem_.emplace(name, new_idx);
if (!pr.second) {
const MemArea &stored = *mem_areas_[pr.first->second];
std::ostringstream oss;
oss << "Cannot register '" << name << "' at: '" << mem_area->GetScope()
<< "' (Previously defined at: '" << stored.GetScope() << "')"
<< std::endl;
throw std::runtime_error(oss.str());
}
auto clash = addr_to_mem_.EmplaceDisjoint(base, addr_top, std::move(new_idx));
if (clash) {
assert(*clash);
// Remove the entry we added to name_to_mem_ above
name_to_mem_.erase(pr.first);
std::ostringstream oss;
oss << "Cannot register '" << name << "' at 0x" << std::hex << base
<< " because its address range overlaps an existing area.";
throw std::runtime_error(oss.str());
}
mem_areas_.push_back(mem_area);
base_addrs_.push_back(base);
names_.push_back(name);
}
MemImageType DpiMemUtil::GetMemImageType(const std::string &path,
const char *type) {
return type ? GetMemImageTypeByName(type) : DetectMemImageType(path);
}
void DpiMemUtil::PrintMemRegions() const {
std::cout << "Registered memory regions:" << std::endl;
for (const auto &pr : name_to_mem_) {
const MemArea &mem = *mem_areas_[pr.second];
uint32_t base = base_addrs_[pr.second];
uint32_t top = base + mem.GetSizeBytes() - 1;
std::cout << "\t'" << pr.first << "' (" << mem.GetWidth()
<< "bits) at location: '" << mem.GetScope() << "'"
<< " (LMA range [0x" << std::hex << base << ", 0x" << top << "])"
<< std::dec << std::endl;
}
}
void DpiMemUtil::LoadFileToNamedMem(bool verbose, const std::string &name,
const std::string &filepath,
MemImageType type) {
// If the image type isn't specified, try to figure it out from the file name
if (type == kMemImageUnknown) {
type = DetectMemImageType(filepath);
}
assert(type != kMemImageUnknown);
// Search for corresponding registered memory based on the name
auto it = name_to_mem_.find(name);
if (it == name_to_mem_.end()) {
std::ostringstream oss;
oss << "`" << name
<< ("' is not the name of a known memory region. "
"Run with --meminit=list to get a list.");
throw std::runtime_error(oss.str());
}
if (verbose) {
std::cout << "Loading data from file `" << filepath << "' into memory `"
<< name << "'." << std::endl;
}
const MemArea &m = *mem_areas_[it->second];
try {
switch (type) {
case kMemImageElf:
m.Write(0, FlattenElfFile(filepath));
break;
case kMemImageVmem:
m.LoadVmem(filepath);
break;
default:
assert(0);
}
} catch (const SVScoped::Error &err) {
std::ostringstream oss;
oss << "No memory found at `" << err.scope_name_
<< "' (the scope associated with region `" << name << "').";
throw std::runtime_error(oss.str());
}
}
void DpiMemUtil::LoadElfToMemories(bool verbose, const std::string &filepath) {
// Load the contents of the ELF file into the staging area
StageElf(verbose, filepath);
for (const auto &pr : staging_area_) {
const std::string &mem_name = pr.first;
const StagedMem &staged_mem = pr.second;
auto mem_area_it = name_to_mem_.find(mem_name);
assert(mem_area_it != name_to_mem_.end());
const MemArea &mem_area = *mem_areas_[mem_area_it->second];
for (const auto &seg_pr : staged_mem.GetSegs()) {
const AddrRange<uint32_t> &seg_rng = seg_pr.first;
const std::vector<uint8_t> &seg_data = seg_pr.second;
assert(seg_rng.lo % mem_area.GetWidthByte() == 0);
uint32_t lo_word = seg_rng.lo / mem_area.GetWidthByte();
try {
mem_area.Write(lo_word, seg_data);
} catch (const SVScoped::Error &err) {
std::ostringstream oss;
oss << "No memory found at `" << err.scope_name_
<< "' (the scope associated with region `" << mem_name
<< "', used by a segment that starts at LMA 0x" << std::hex
<< base_addrs_[mem_area_it->second] + seg_rng.lo << ").";
throw std::runtime_error(oss.str());
}
}
}
}
void DpiMemUtil::StageElf(bool verbose, const std::string &path) {
// Clear out anything that was in the staging area before
staging_area_.clear();
ElfFile elf(path);
// Allow subclasses to get at the loaded ELF data if they need it
OnElfLoaded(elf.ptr_);
size_t file_size;
const char *file_data = elf_rawfile(elf.ptr_, &file_size);
assert(file_data);
size_t phnum = elf.GetPhdrNum();
const Elf32_Phdr *phdrs = elf.GetPhdrs();
for (size_t i = 0; i < phnum; ++i) {
const Elf32_Phdr &phdr = phdrs[i];
if (phdr.p_type != PT_LOAD)
continue;
if (phdr.p_filesz == 0)
continue;
size_t mem_area_idx =
GetRegionForSegment(path, i, phdr.p_paddr, phdr.p_filesz);
const MemArea &mem_area = *mem_areas_[mem_area_idx];
uint32_t mem_area_base = base_addrs_[mem_area_idx];
const std::string &name = names_[mem_area_idx];
// Check that the segment is aligned correctly for the memory
uint32_t local_base = phdr.p_paddr - mem_area_base;
if (local_base % mem_area.GetWidthByte()) {
std::ostringstream oss;
oss << "Segment " << i << " has LMA 0x" << std::hex << phdr.p_paddr
<< ", which starts at offset 0x" << local_base
<< " in the memory region `" << name
<< "'. This offset is not aligned to the region's word width of "
<< std::dec << mem_area.GetWidth() << " bits.";
throw ElfError(path, oss.str());
}
// Where does the segment finish in the file image? We don't need
// to worry about overflow here, because we're adding two
// uint32_t's into a size_t. But we do need to check the segment
// actually fits in the file
size_t off_end = (size_t)phdr.p_offset + phdr.p_filesz;
if (file_size < off_end) {
std::ostringstream oss;
oss << "phdr for segment " << i << " claims to end at offset 0x"
<< std::hex << off_end - 1 << ", but the file only has size 0x"
<< file_size << ".";
throw ElfError(path, oss.str());
}
if (verbose) {
std::cout << "Loading segment " << i << " from ELF file `" << path
<< "' into memory `" << name << "'." << std::endl;
}
// Get the StagedMem object associated with this memory area. If
// there isn't one, make a new empty one.
StagedMem &staged_mem = staging_area_[name];
const char *seg_data = file_data + phdr.p_offset;
std::vector<uint8_t> vec(phdr.p_filesz, 0);
memcpy(&vec[0], seg_data, phdr.p_filesz);
staged_mem.AddSegment(local_base, std::move(vec));
}
}
const StagedMem &DpiMemUtil::GetMemoryData(const std::string &mem_name) const {
auto it = staging_area_.find(mem_name);
return (it == staging_area_.end()) ? empty_ : it->second;
}
size_t DpiMemUtil::GetRegionForSegment(const std::string &path, int seg_idx,
uint32_t lma, uint32_t mem_sz) const {
assert(mem_sz > 0);
auto mem_area_it = addr_to_mem_.find(lma);
if (mem_area_it == addr_to_mem_.end()) {
std::ostringstream oss;
oss << "No memory region is registered that contains the address 0x"
<< std::hex << lma << " (the base address of segment " << seg_idx
<< ").";
throw ElfError(path, oss.str());
}
size_t mem_area_idx = mem_area_it->second;
const MemArea &mem_area = *mem_areas_[mem_area_idx];
uint32_t base_addr = base_addrs_[mem_area_idx];
assert(base_addr <= lma);
uint32_t lma_top = lma + (mem_sz - 1);
if (lma_top < lma) {
std::ostringstream oss;
oss << "Integer overflow for top address of segment " << seg_idx << ".";
throw ElfError(path, oss.str());
}
uint32_t local_base = lma - base_addr;
uint32_t local_top = lma_top - base_addr;
if (mem_area.GetSizeBytes() <= local_top) {
std::ostringstream oss;
oss << "Segment " << seg_idx << " has size 0x" << std::hex << mem_sz
<< " bytes. Its LMA of 0x" << lma << " is at offset 0x" << local_base
<< " in the memory region `" << names_[mem_area_idx]
<< "', so the segment finishes at offset 0x" << local_top
<< ", but the memory region is only 0x" << mem_area.GetSizeBytes()
<< " bytes long.";
throw ElfError(path, oss.str());
}
return mem_area_it->second;
}