| // 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; |
| } |