blob: f97703aa069baa95166c0b93377e9284969a7254 [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 "verilator_memutil.h"
#include <cassert>
#include <cstring>
#include <fcntl.h>
#include <getopt.h>
#include <iostream>
#include <libelf.h>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include "sv_scoped.h"
// DPI Exports
extern "C" {
/**
* Write |file| to a memory
*
* @param file path to a SystemVerilog $readmemh()-compatible file (VMEM file)
*/
extern void simutil_verilator_memload(const char *file);
/**
* Write a 32 bit word |val| to memory at index |index|
*
* @return 1 if successful, 0 otherwise
*/
extern int simutil_verilator_set_mem(int index, const svBitVecVal *val);
}
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) {
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
// Print a usage message to stdout
static void PrintHelp() {
std::cout << "Simulation memory utilities:\n\n"
"-r|--rominit=FILE\n"
" Initialize the ROM with FILE (elf/vmem)\n\n"
"-m|--raminit=FILE\n"
" Initialize the RAM with FILE (elf/vmem)\n\n"
"-f|--flashinit=FILE\n"
" Initialize the FLASH with FILE (elf/vmem)\n\n"
"-l|--meminit=NAME,FILE[,TYPE]\n"
" Initialize memory region NAME with FILE [of TYPE]\n"
" TYPE is either 'elf' or 'vmem'\n\n"
"-E|--load-elf=FILE\n"
" Load ELF file, using segment LMAs to pick memory regions\n\n"
"-l list|--meminit=list\n"
" Print registered memory regions\n\n"
"--verbose-mem-load\n"
" Print a message for each memory load\n\n"
"-h|--help\n"
" Show help\n\n";
}
// Convert a string to a MemImageType, returning kMemImageUnknown if it's not a
// known name.
static MemImageType GetMemImageTypeByName(const std::string &name) {
if (name == "elf")
return kMemImageElf;
if (name == "vmem")
return kMemImageVmem;
return kMemImageUnknown;
}
// 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;
}
namespace {
// An instruction to load the file at filepath to the memory called name. If
// name is the empty string then type must be kMemImageElf and this is an
// instruction to load an ELF file, picking memories by LMA.
struct LoadArg {
std::string name;
std::string filepath;
MemImageType type;
};
} // namespace
// Parse a meminit command-line argument. This should be of the form
// mem_area,file[,type]. Throw a std::runtime_error if something looks wrong.
static LoadArg ParseMemArg(std::string mem_argument) {
std::array<std::string, 3> args;
size_t pos = 0;
size_t end_pos = 0;
size_t i;
for (i = 0; i < 3; ++i) {
end_pos = mem_argument.find(",", pos);
// Check for possible exit conditions
if (pos == end_pos) {
std::ostringstream oss;
oss << "empty field in: `" << mem_argument << "'.";
throw std::runtime_error(oss.str());
}
if (end_pos == std::string::npos) {
args[i] = mem_argument.substr(pos);
break;
}
args[i] = mem_argument.substr(pos, end_pos - pos);
pos = end_pos + 1;
}
// mem_argument is not empty as getopt requires an argument,
// but not a valid argument for memory initialization
if (i == 0) {
std::ostringstream oss;
oss << "meminit must be in the format `name,file[,type]'. Got: `"
<< mem_argument << "'.";
throw std::runtime_error(oss.str());
}
MemImageType type;
if (i == 1) {
// Type not set explicitly
type = DetectMemImageType(args[1]);
} else {
type = GetMemImageTypeByName(args[2]);
}
return {.name = args[0], .filepath = args[1], .type = 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.
//
// The data for any intermediate addresses that don't appear in a segment are
// set to zero.
static std::vector<uint8_t> FlattenElfFile(const std::string &filepath) {
(void)elf_errno();
if (elf_version(EV_CURRENT) == EV_NONE) {
throw std::runtime_error(elf_errmsg(-1));
}
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_memsz == 0) {
continue;
}
if (!any || phdr.p_paddr < low) {
low = phdr.p_paddr;
}
if (!any || phdr.p_paddr + phdr.p_memsz > high) {
high = phdr.p_paddr + phdr.p_memsz;
}
any = true;
}
assert(low <= high);
size_t len_bytes = high - low;
size_t file_size;
const char *file_data = elf_rawfile(elf.ptr_, &file_size);
assert(file_data);
std::vector<uint8_t> buf(len_bytes);
for (size_t i = 0; i < phnum; i++) {
const Elf32_Phdr &phdr = phdrs[i];
if (phdr.p_type != PT_LOAD || phdr.p_filesz == 0) {
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());
}
// Actually copy the data across. We have just checked that there are
// p_filesz bytes of data available, and the loop that picked low/high
// above ensured that we have space to store for p_memsz bytes: use the
// smaller of the two numbers.
memcpy(&buf[phdr.p_paddr - low], file_data + phdr.p_offset,
std::min(phdr.p_filesz, phdr.p_memsz));
}
return buf;
}
// Write a "segment" of data to the given memory area. Reads file_sz bytes from
// data. If mem_sz > file_sz, appends mem_sz - file_sz bytes of zeros to the
// end.
static void WriteSegment(const MemArea &m, uint32_t offset, uint32_t file_sz,
uint32_t mem_sz, const uint8_t *data) {
assert(m.width_byte <= 32);
assert(m.addr_loc.size == 0 || mem_sz + offset <= m.addr_loc.size);
assert((offset % m.width_byte) == 0);
// Valid ELF files probably have file_sz <= mem_sz, but it sort of makes sense
// even if not: we are just reading the initial portion of some data stored in
// the file.
file_sz = std::min(file_sz, mem_sz);
// If this fails to set scope, it will throw an error which should
// be caught at this function's callsite.
SVScoped scoped(m.location.data());
// This "mini buffer" is used to transfer each write to SystemVerilog. It's
// not massively efficient, but doing so ensures that we pass 256 bits (32
// bytes) of initialised data each time. This is for simutil_verilator_set_mem
// (defined in prim_util_memload.svh), whose "val" argument has SystemVerilog
// type bit [255:0].
uint8_t minibuf[32];
memset(minibuf, 0, sizeof minibuf);
assert(m.width_byte <= sizeof minibuf);
uint32_t all_words = (mem_sz + m.width_byte - 1) / m.width_byte;
uint32_t full_data_words = file_sz / m.width_byte;
uint32_t part_data_word_len = file_sz % m.width_byte;
bool has_part_data_word = part_data_word_len != 0;
uint32_t all_data_words = full_data_words + (has_part_data_word ? 1 : 0);
assert(all_data_words <= all_words);
uint32_t zero_words = all_words - all_data_words;
uint32_t word_offset = offset / m.width_byte;
// Copy the full data words
for (uint32_t i = 0; i < full_data_words; ++i) {
uint32_t dst_word = word_offset + i;
uint32_t src_byte = i * m.width_byte;
memcpy(minibuf, &data[src_byte], m.width_byte);
if (!simutil_verilator_set_mem(dst_word, (svBitVecVal *)minibuf)) {
std::ostringstream oss;
oss << "Could not set `" << m.name << "' memory at byte offset 0x"
<< std::hex << dst_word * m.width_byte << ".";
throw std::runtime_error(oss.str());
}
}
// Copy any partial data, zeroing minibuf first to ensure that the latter
// bytes in the word are zero.
if (has_part_data_word) {
memset(minibuf, 0, sizeof minibuf);
uint32_t dst_word = word_offset + full_data_words;
uint32_t src_byte = full_data_words * m.width_byte;
memcpy(minibuf, &data[src_byte], part_data_word_len);
if (!simutil_verilator_set_mem(dst_word, (svBitVecVal *)minibuf)) {
std::ostringstream oss;
oss << "Could not set `" << m.name << "' memory at byte offset 0x"
<< std::hex << dst_word * m.width_byte << " (partial data word).";
throw std::runtime_error(oss.str());
}
}
// Zero minibuf again and splat any remaining words.
if (zero_words > 0) {
memset(minibuf, 0, sizeof minibuf);
for (uint32_t i = 0; i < zero_words; ++i) {
uint32_t dst_word = word_offset + full_data_words + i;
if (!simutil_verilator_set_mem(dst_word, (svBitVecVal *)minibuf)) {
std::ostringstream oss;
oss << "Could not set `" << m.name << "' memory at byte offset 0x"
<< std::hex << dst_word * m.width_byte << " (zero word).";
throw std::runtime_error(oss.str());
}
}
}
}
static void WriteElfToMem(const MemArea &m, const std::string &filepath) {
std::vector<uint8_t> elf_data = FlattenElfFile(filepath);
WriteSegment(m, 0, elf_data.size(), elf_data.size(), &elf_data[0]);
}
static void WriteVmemToMem(const MemArea &m, const std::string &filepath) {
SVScoped scoped(m.location.data());
// TODO: Add error handling.
simutil_verilator_memload(filepath.data());
}
bool VerilatorMemUtil::RegisterMemoryArea(const std::string name,
const std::string location) {
// Default to 32bit width and no address
return RegisterMemoryArea(name, location, 32, nullptr);
}
bool VerilatorMemUtil::RegisterMemoryArea(const std::string name,
const std::string location,
size_t width_bit,
const MemAreaLoc *addr_loc) {
assert((width_bit <= 256) &&
"TODO: Memory loading only supported up to 256 bits.");
assert(width_bit % 8 == 0);
// First, create and register the memory by name
MemArea mem = {.name = name,
.location = location,
.width_byte = (uint32_t)width_bit / 8,
.addr_loc = {.base = 0, .size = 0}};
auto ret = name_to_mem_.emplace(name, mem);
if (ret.second == false) {
std::cerr << "ERROR: Can not register \"" << name << "\" at: \"" << location
<< "\" (Previously defined at: \"" << ret.first->second.location
<< "\")" << std::endl;
return false;
}
MemArea *stored_mem_area = &ret.first->second;
// If we have no address information, there's nothing more to do. However, if
// we do have address information, we should add an entry to addr_to_mem_.
if (!addr_loc) {
return true;
}
// Check that the size of the new area is positive, and that we don't overflow
// the address space.
if (addr_loc->size == 0) {
std::cerr << "ERROR: Can not register '" << name
<< "' because it has zero size.\n";
return false;
}
uint32_t addr_top = addr_loc->base + (addr_loc->size - 1);
if (addr_top < addr_loc->base) {
std::cerr << "ERROR: Can not register '" << name
<< "' because it overflows the top of the address space.\n";
return false;
}
// If the existing map is non-empty, we must check for overlaps
if (!addr_to_mem_.empty()) {
// We start by checking for an overlap "from the right". This would be a
// region that starts strictly above addr_loc->base, but where it's low
// address is still less than addr_top. We can use std::map::upper_bound to
// find the first region strictly above addr_loc->base (which returns the
// end iterator if there isn't one).
auto right_it = addr_to_mem_.upper_bound(addr_loc->base);
if (right_it != addr_to_mem_.end()) {
const MemAreaLoc &ub_loc = right_it->second->addr_loc;
assert(ub_loc.size != 0);
if (ub_loc.base <= addr_top) {
std::cerr << "ERROR: Can not register '" << name
<< "' because its address range overlaps to left of '"
<< right_it->second->name << "'.\n";
return false;
}
}
// We also need to check from the other side. This would be a region that
// starts at or before addr_loc->base and extends past it. If right_it is
// addr_to_mem_.begin(), there is no such region (because the lowest
// addressed region already starts above addr_loc->base). Otherwise,
// decrement right_it to get the highest addressed region that starts at or
// before addr_loc->base. Note this still works if right_it is the end
// iterator: we just pick up the last region, which we know exists because
// addr_to_mem_ is not empty.
if (right_it != addr_to_mem_.begin()) {
auto left_it = std::prev(right_it);
const MemAreaLoc &lb_loc = left_it->second->addr_loc;
assert(lb_loc.size != 0);
uint32_t lb_max = lb_loc.base + lb_loc.size;
if (addr_loc->base <= lb_max) {
std::cerr << "ERROR: Can not register '" << name
<< "' because its address range overlaps to right of '"
<< left_it->second->name << "'.\n";
return false;
}
}
}
// Phew, no overlap!
addr_to_mem_.insert(std::make_pair(addr_loc->base, stored_mem_area));
stored_mem_area->addr_loc = *addr_loc;
return true;
}
bool VerilatorMemUtil::ParseCLIArguments(int argc, char **argv,
bool &exit_app) {
const struct option long_options[] = {
{"rominit", required_argument, nullptr, 'r'},
{"raminit", required_argument, nullptr, 'm'},
{"flashinit", required_argument, nullptr, 'f'},
{"meminit", required_argument, nullptr, 'l'},
{"verbose-mem-load", no_argument, nullptr, 'V'},
{"load-elf", required_argument, nullptr, 'E'},
{"help", no_argument, nullptr, 'h'},
{nullptr, no_argument, nullptr, 0}};
std::vector<LoadArg> load_args;
bool verbose = false;
// Reset the command parsing index in-case other utils have already parsed
// some arguments
optind = 1;
while (1) {
int c = getopt_long(argc, argv, ":r:m:f:l:E:h", long_options, nullptr);
if (c == -1) {
break;
}
// Disable error reporting by getopt
opterr = 0;
switch (c) {
case 0:
break;
case 'r':
load_args.push_back(
{.name = "rom", .filepath = optarg, .type = kMemImageUnknown});
break;
case 'm':
load_args.push_back(
{.name = "ram", .filepath = optarg, .type = kMemImageUnknown});
break;
case 'f':
load_args.push_back(
{.name = "flash", .filepath = optarg, .type = kMemImageUnknown});
break;
case 'l':
if (strcasecmp(optarg, "list") == 0) {
PrintMemRegions();
exit_app = true;
return true;
}
// --meminit / -l
try {
load_args.emplace_back(ParseMemArg(optarg));
} catch (const std::runtime_error &err) {
std::cerr << "ERROR: " << err.what() << std::endl;
return false;
}
break;
case 'V':
verbose = true;
break;
case 'E':
load_args.push_back(
{.name = "", .filepath = optarg, .type = kMemImageElf});
break;
case 'h':
PrintHelp();
return true;
case ':': // missing argument
std::cerr << "ERROR: Missing argument." << std::endl << std::endl;
return false;
case '?':
default:;
// Ignore unrecognized options since they might be consumed by
// other utils
}
}
for (const LoadArg &arg : load_args) {
try {
if (!arg.name.empty()) {
LoadFileToNamedMem(verbose, arg.name, arg.filepath, arg.type);
} else {
assert(arg.type == kMemImageElf);
LoadElfToMemories(verbose, arg.filepath);
}
} catch (const std::exception &err) {
std::cerr << "ERROR: " << err.what() << std::endl;
return false;
}
}
return true;
}
void VerilatorMemUtil::PrintMemRegions() const {
std::cout << "Registered memory regions:" << std::endl;
for (const auto &pr : name_to_mem_) {
const MemArea &m = pr.second;
std::cout << "\t'" << m.name << "' (" << m.width_byte * 8
<< "bits) at location: '" << m.location << "'";
if (m.addr_loc.size) {
uint32_t low = m.addr_loc.base;
uint32_t high = m.addr_loc.base + m.addr_loc.size - 1;
std::cout << " (LMA range [0x" << std::hex << low << ", 0x" << high
<< "])" << std::dec;
}
std::cout << std::endl;
}
}
void VerilatorMemUtil::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 = it->second;
try {
switch (type) {
case kMemImageElf:
WriteElfToMem(m, filepath);
break;
case kMemImageVmem:
WriteVmemToMem(m, 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 `" << m.name << "').";
throw std::runtime_error(oss.str());
}
}
void VerilatorMemUtil::LoadElfToMemories(bool verbose,
const std::string &filepath) {
(void)elf_errno();
if (elf_version(EV_CURRENT) == EV_NONE) {
throw std::runtime_error(elf_errmsg(-1));
}
ElfFile elf(filepath);
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_memsz == 0)
continue;
const MemArea *mem_area = FindRegionForAddr(phdr.p_paddr);
if (!mem_area) {
std::ostringstream oss;
oss << "No memory region is registered that contains the address 0x"
<< std::hex << phdr.p_paddr << " (the base address of segment " << i
<< ").";
throw ElfError(filepath, oss.str());
}
assert(mem_area->addr_loc.base <= phdr.p_paddr);
uint32_t lma_top = phdr.p_paddr + (phdr.p_memsz - 1);
uint32_t off_end = phdr.p_offset + phdr.p_filesz;
// Overflow checks
if (lma_top < phdr.p_paddr) {
std::ostringstream oss;
oss << "Integer overflow for top address of segment " << i << ".";
throw ElfError(filepath, oss.str());
}
if (off_end < phdr.p_offset) {
std::ostringstream oss;
oss << "Integer overflow for top file offset of segment " << i << ".";
throw ElfError(filepath, oss.str());
}
uint32_t local_base = phdr.p_paddr - mem_area->addr_loc.base;
uint32_t local_top = lma_top - mem_area->addr_loc.base;
if (mem_area->addr_loc.size <= local_top) {
std::ostringstream oss;
oss << "Segment " << i << " has size 0x" << std::hex << phdr.p_memsz
<< " bytes. Its LMA of 0x" << phdr.p_paddr << " is at offset 0x"
<< local_base << " in the memory region `" << mem_area->name
<< "', so the segment finishes at offset 0x" << local_top
<< ", but the memory region is only 0x" << mem_area->addr_loc.size
<< " bytes long.";
throw ElfError(filepath, oss.str());
}
// Check that the segment is aligned correctly for the memory
if (local_base % mem_area->width_byte) {
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 `" << mem_area->name
<< "'. This offset is not aligned to the region's word width of "
<< std::dec << 8 * mem_area->width_byte << " bits.";
throw ElfError(filepath, oss.str());
}
// Check the segment actually fits in the file
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(filepath, oss.str());
}
if (verbose) {
std::cout << "Loading segment " << i << " from ELF file `" << filepath
<< "' into memory `" << mem_area->name << "'." << std::endl;
}
const char *seg_data = file_data + phdr.p_offset;
try {
WriteSegment(*mem_area, local_base, phdr.p_filesz, phdr.p_memsz,
(const uint8_t *)seg_data);
} catch (const SVScoped::Error &err) {
std::ostringstream oss;
oss << "No memory found at `" << err.scope_name_
<< "' (the scope associated with region `" << mem_area->name
<< "', used by segment " << i << ", which starts at LMA 0x"
<< std::hex << phdr.p_paddr << ").";
throw std::runtime_error(oss.str());
}
}
}
const MemArea *VerilatorMemUtil::FindRegionForAddr(uint32_t lma) const {
// To find the memory area containing lma, use upper_bound to find the first
// region strictly after it, and then std::prev to step backwards. This fails
// if either the map is empty (obviously!) or if ub_it is already the
// beginning of the map.
if (addr_to_mem_.empty())
return nullptr;
auto ub_it = addr_to_mem_.upper_bound(lma);
if (ub_it == addr_to_mem_.begin())
return nullptr;
const MemArea *m = std::prev(ub_it)->second;
// Every entry in addr_to_mem_ should have a valid pointer to a MemArea with a
// valid location.
assert(m != nullptr);
assert(m->addr_loc.size != 0);
// What's more, the base of the location should be less than equal to lma
// (because it's strictly less than the smallest entry strictly greater).
assert(m->addr_loc.base <= lma);
// This means that we've found the right region iff the top of the region
// includes lma.
uint32_t addr_top = m->addr_loc.base + (m->addr_loc.size - 1);
return (lma <= addr_top) ? m : nullptr;
}