refactor vspace - remove most memory simulation

All of it can be derived from translate_dynamic().
This commit is contained in:
Lephenixnoir 2022-03-27 18:32:28 +01:00
parent e90ef447fc
commit 1f475b0863
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
8 changed files with 279 additions and 325 deletions

View File

@ -14,6 +14,7 @@ flex_target(LoadAsm lib/load-asm.l
"${CMAKE_CURRENT_BINARY_DIR}/load-asm.yy.cpp" COMPILE_FLAGS -s)
set(fxos_core_SOURCES
lib/AbstractMemory.cpp
lib/disassembly.cpp
lib/lang.cpp
lib/memory.cpp

View File

@ -0,0 +1,81 @@
//---------------------------------------------------------------------------//
// 1100101 |_ mov #0, r4 __ //
// 11 |_ <0xb380 %5c4> / _|_ _____ ___ //
// 0110 |_ 3.50 -> 3.60 | _\ \ / _ (_-< //
// |_ base# + offset |_| /_\_\___/__/ //
//---------------------------------------------------------------------------//
// fxos/AbstractMemory: Simulated memory
//
// This header defines the typeclass for simulated memory within a 32-bit
// address space. The class provides functions to identify the simulated range
// within the address space, as well as basic read and search operations.
//
// Suclasses of this only need to implement the translate_dynamic() method
// which gives low-level access to part of the simulated memory.
//
// Note that currently, given the implementation of virtual space bindings and
// AbstractMemory methods, most operations only try to translate_dynamic()
// once -- which means that they fail if the requested range is not simulated
// in a single block. This is usually not a problem because virtual space
// bindings simulate small memory areas separated by huge gaps, so they never
// extend each other.
//---
#ifndef FXOS_ABSTRACTMEMORY_H
#define FXOS_ABSTRACTMEMORY_H
#include <fxos/memory.h>
#include <fxos/util/Addressable.h>
#include <cstdint>
namespace FxOS {
/* A common interface for simulated memory. All non-virtual methods are
provided by the base class and need not be implemented. */
class AbstractMemory
{
public:
/* Checks if an address or interval is simulated (in its entirety) */
bool covers(uint32_t addr, int size=1);
/* Check if a full region is simulated */
bool covers(MemoryRegion const &region);
/* Returns the data located at the provided virtual address, nullptr if it
is not entirely covered. */
char const *translate(uint32_t addr, int size=1);
/* Returns the data located at the provided virtual address, and indicates
how much is available in *size. The pointer is null if [addr] itself is
not covered, in which case *size is also set to 0. */
virtual char const *translate_dynamic(uint32_t addr, int *size) = 0;
/* Search a binary pattern in the specified area. Returns the virtual
address of the first occurrence if any is found, [end] otherwise
(including if the range is empty or exceeds simulated memory). */
uint32_t search(uint32_t start, uint32_t end, void const *pat, int size);
/* Read a simple object from memory. The following methods all assume that
the specified address is simulated, and return a default value if it's
not -- you should probably check beforehand! Alignment constraints are
not checked either, that's up to you.
The return type has the value in [.value] and remembers the address in
[.address], which is sometimes useful. It implicitly converts to the
data type, see <fxos/util/Addressable.h>. */
/* Various sizes of integers with sign-extension or zero-extension. */
Addressable<int8_t> read_i8 (uint32_t addr);
Addressable<uint8_t> read_u8 (uint32_t addr);
Addressable<int16_t> read_i16(uint32_t addr);
Addressable<uint16_t> read_u16(uint32_t addr);
Addressable<int32_t> read_i32(uint32_t addr);
Addressable<uint32_t> read_u32(uint32_t addr);
/* Read a non-NUL-terminated string */
Addressable<std::string> read_str(uint32_t addr, size_t len);
};
} /* namespace FxOS */
#endif /* FXOS_ABSTRACTMEMORY_H */

View File

@ -1,13 +1,24 @@
//---------------------------------------------------------------------------//
// 1100101 |_ mov #0, r4 __ //
// 11 |_ <0xb380 %5c4> / _|_ _____ ___ //
// 0110 |_ 3.50 -> 3.60 | _\ \ / _ (_-< //
// |_ base# + offset |_| /_\_\___/__/ //
//---------------------------------------------------------------------------//
// fxos/vspace: Virtual address space with loaded code and analyses
//
// This is the main structure/entry point of fxos. A Virtu
//---
// fxos.vspace: A virtual space where code is being studied
//---
#ifndef LIBFXOS_VSPACE_H
#define LIBFXOS_VSPACE_H
#ifndef FXOS_VSPACE_H
#define FXOS_VSPACE_H
#include <fxos/memory.h>
#include <fxos/os.h>
#include <fxos/symbols.h>
#include <fxos/AbstractMemory.h>
#include <fxos/util/Buffer.h>
#include <fxos/util/Addressable.h>
@ -18,148 +29,65 @@
namespace FxOS {
/* A common interface for simulated memory */
class AbstractMemory
{
public:
/* Checks if an address or interval is simulated */
virtual bool covers(uint32_t addr, int size=1) const noexcept = 0;
/* Check if a full region is simulated */
virtual bool covers(MemoryRegion const &region) const noexcept;
/* Returns the data located at the provided virtual address, nullptr if it
is not entirely covered. */
virtual char const *translate(uint32_t addr, int size=1) const = 0;
/* Returns the data located at the provided virtual address, and indicates
how much is available in *size. The pointer is null if [addr] itself is
not covered, in which case *size is also set to 0. */
virtual char const *translate_dynamic(uint32_t addr, int *size) const = 0;
/* Read data from the memory. The following methods read data of
various types. (Not a template because of the restriction about
template specialization in non-namespaces scopes still in g++.)
When reading data, provide a virtual address. The address is saved
in the returned object for later printing or inspection. The
returned object Addressable<T> automatically converts to T when
used, and supports [.address] which returns the original address.
The size parameter is only meaningful for variable-sized types such
as string, and ignored for fixed-size types such as integers. If the
desired object is not within the range of the simulated memory, returns
a dummy value of address -1. You should check the range beforehand! */
/* Read integers with signed or unsigned extension. These functions do
not check alignment, because exceptionally the processor supports
unaligned operations (eg. movual.l). */
Addressable<int8_t> read_i8 (uint32_t addr) const;
Addressable<uint8_t> read_u8 (uint32_t addr) const;
Addressable<int16_t> read_i16(uint32_t addr) const;
Addressable<uint16_t> read_u16(uint32_t addr) const;
Addressable<int32_t> read_i32(uint32_t addr) const;
Addressable<uint32_t> read_u32(uint32_t addr) const;
/* Read a non-NUL-terminated string */
Addressable<std::string> read_str(uint32_t addr, size_t len) const;
/* Search a binary pattern in the specified area. Returns the virtual
address of the first occurrence if any is found, [end] otherwise
(including if the range is empty or exceeds simulated memory). */
virtual uint32_t search(uint32_t start, uint32_t end,
void const *pattern, int size) const = 0;
};
/* A binding of a data buffer into a memory region of the target. */
struct Binding: public AbstractMemory
{
/* Constructor from data buffer. An error is raised if the buffer is
not at least of the size of the region. In this case, a new buffer
can be constructed with the required size. */
Binding(MemoryRegion region, Buffer buffer);
/* Constructor from data buffer. An error is raised if the buffer is
not at least of the size of the region. In this case, a new buffer
can be constructed with the required size. */
Binding(MemoryRegion region, Buffer buffer);
/* Targeted region, might overlap with other bindings */
MemoryRegion region;
/* Underlying buffer (copy of the original one) */
Buffer buffer;
/* Targeted region, might overlap with other bindings */
MemoryRegion region;
/* Underlying buffer (copy of the original one) */
Buffer buffer;
bool covers(uint32_t addr, int size=1) const noexcept override;
bool covers(MemoryRegion const &region) const noexcept override;
char const *translate(uint32_t addr, int size=1) const override;
char const *translate_dynamic(uint32_t addr, int *size) const override;
uint32_t search(uint32_t start, uint32_t end, void const *pattern,
int size) const override;
};
/* A target description in the database; loadable, but not loaded yet */
struct Target
{
/* Just a list of bindings to be formed */
using Binding = std::pair<MemoryRegion,std::string>;
std::vector<Binding> bindings;
/* Also the MPU to be set */
std::string mpu;
char const *translate_dynamic(uint32_t addr, int *size) override;
};
/* A composite space where regions can be bound dynamically */
class VirtualSpace: public AbstractMemory
{
public:
/* Create an empty space with no regions */
VirtualSpace();
/* Create an empty space with no regions */
VirtualSpace();
/* Create a new virtual space with a target loaded. */
VirtualSpace(Target const &target,std::vector<std::string> const &folders);
/* MPU used by this target, or an empty string if unspecified */
std::string mpu;
/* MPU used by this target, or an empty string if unspecified */
std::string mpu;
/* List of bindings (most recent first) */
std::vector<Binding> bindings;
/* List of bindings */
std::vector<Binding> const &bindings() const {
return m_bindings;
}
/* OS analysis; performed on-demand. Returns the new or cached OS analysis,
and nullptr only if OS cannot be analyzed */
OS *os_analysis(bool force=false);
/* OS analysis; performed on-demand. Returns the new or cached OS analysis,
and nullptr only if OS cannot be analyzed */
OS *os_analysis(bool force=false);
/* Cursor position, used by the interactive shell */
uint32_t cursor;
/* Cursor position, used by the interactive shell */
uint32_t cursor;
/* Symbol table */
SymbolTable symbols;
/* Symbol table */
SymbolTable symbols;
/* Bind a memory region from a buffer. The region can either be
standard (see <fxos/memory.h>) or custom.
/* Bind a memory region from a buffer. The region can either be
standard (see <fxos/memory.h>) or custom.
If several loaded regions overlap on some addresses, *the last
loaded region will be used*. Thus, new regions can be loaded to
selectively override parts of the target.
If several loaded regions overlap on some addresses, *the last
loaded region will be used*. Thus, new regions can be loaded to
selectively override parts of the target.
An error is raised if the buffer is smaller than the region being
bound. */
void bind_region(MemoryRegion const &region, Buffer const &buffer);
An error is raised if the buffer is smaller than the region being
bound. */
void bind_region(MemoryRegion const &region, Buffer const &buffer);
/* Implementation of AbstractMemory primitives */
bool covers(uint32_t addr, int size=1) const noexcept override;
bool covers(MemoryRegion const &region) const noexcept override;
char const *translate(uint32_t addr, int size=1) const override;
char const *translate_dynamic(uint32_t addr, int *size) const override;
uint32_t search(uint32_t start, uint32_t end, void const *pattern,
int size) const override;
char const *translate_dynamic(uint32_t addr, int *size) override;
private:
/* Bound regions (in order of binding) */
std::vector<Binding> m_bindings;
/* Buffers owned by the target (when loaded from description) */
std::vector<Buffer> m_buffers;
/* Current OS analyzer */
std::unique_ptr<OS> m_os;
/* OS analysis results */
std::unique_ptr<OS> m_os;
};
} /* namespace FxOS */
#endif /* LIBFXOS_VSPACE_H */
#endif /* FXOS_VSPACE_H */

104
lib/AbstractMemory.cpp Normal file
View File

@ -0,0 +1,104 @@
//---------------------------------------------------------------------------//
// 1100101 |_ mov #0, r4 __ //
// 11 |_ <0xb380 %5c4> / _|_ _____ ___ //
// 0110 |_ 3.50 -> 3.60 | _\ \ / _ (_-< //
// |_ base# + offset |_| /_\_\___/__/ //
//---------------------------------------------------------------------------//
#include <fxos/AbstractMemory.h>
#include <cstring>
namespace FxOS {
bool AbstractMemory::covers(uint32_t addr, int size)
{
return (this->translate(addr, size) != nullptr);
}
bool AbstractMemory::covers(MemoryRegion const &region)
{
return this->covers(region.start, region.size());
}
char const *AbstractMemory::translate(uint32_t addr, int size)
{
int actual_size;
char const *ptr = this->translate_dynamic(addr, &actual_size);
return (ptr && actual_size >= size) ? ptr : nullptr;
}
Addressable<int8_t> AbstractMemory::read_i8(uint32_t addr)
{
int8_t *i8 = (int8_t *)this->translate(addr, 1);
if(!i8)
return Addressable((int8_t)-1);
return Addressable(addr, *i8);
}
Addressable<uint8_t> AbstractMemory::read_u8(uint32_t addr)
{
uint8_t *u8 = (uint8_t *)this->translate(addr, 1);
if(!u8)
return Addressable((uint8_t)-1);
return Addressable(addr, *u8);
}
Addressable<int16_t> AbstractMemory::read_i16(uint32_t addr)
{
uint8_t *i16 = (uint8_t *)this->translate(addr, 2);
if(!i16)
return Addressable((int16_t)-1);
int16_t v = (i16[0] << 8) | i16[1];
return Addressable(addr, v);
}
Addressable<uint16_t> AbstractMemory::read_u16(uint32_t addr)
{
uint8_t *u16 = (uint8_t *)this->translate(addr, 2);
if(!u16)
return Addressable((uint16_t)-1);
uint16_t v = (u16[0] << 8) | u16[1];
return Addressable(addr, v);
}
Addressable<int32_t> AbstractMemory::read_i32(uint32_t addr)
{
uint8_t *i32 = (uint8_t *)this->translate(addr, 4);
if(!i32)
return Addressable((int32_t)-1);
int32_t v = (i32[0] << 24) | (i32[1] << 16) | (i32[2] << 8) | i32[3];
return Addressable(addr, v);
}
Addressable<uint32_t> AbstractMemory::read_u32(uint32_t addr)
{
uint8_t *u32 = (uint8_t *)this->translate(addr, 4);
if(!u32)
return Addressable((uint32_t)-1);
uint32_t v = (u32[0] << 24) | (u32[1] << 16) | (u32[2] << 8) | u32[3];
return Addressable(addr, v);
}
Addressable<std::string> AbstractMemory::read_str(uint32_t addr, size_t len)
{
char const *str = this->translate(addr, len);
if(!str)
return Addressable(std::string());
return Addressable(addr, std::string(str, len));
}
uint32_t AbstractMemory::search(uint32_t start, uint32_t end,
void const *pattern, int size)
{
void const *data = translate(start, end-start);
if(!data)
return end;
void const *occurrence = memmem(data, end - start, pattern, size);
if(!occurrence)
return end;
return start + ((char *)occurrence - (char *)data);
}
} /* namespace FxOS */

View File

@ -181,8 +181,8 @@ void OS::parse_footer()
// Checksum
//---
static uint32_t accumulate_range(VirtualSpace const &m_space, uint32_t start,
uint32_t end)
static uint32_t accumulate_range(VirtualSpace &m_space,
uint32_t start, uint32_t end)
{
uint32_t sum = 0;

View File

@ -4,123 +4,25 @@
namespace FxOS {
//---
// Simulated memory access primitives
//---
bool AbstractMemory::covers(MemoryRegion const &region) const noexcept
{
return covers(region.start, region.size());
}
Addressable<int8_t> AbstractMemory::read_i8(uint32_t addr) const
{
int8_t *i8 = (int8_t *)translate(addr, 1);
if(!i8)
return Addressable((int8_t)-1);
return Addressable(addr, *i8);
}
Addressable<uint8_t> AbstractMemory::read_u8(uint32_t addr) const
{
uint8_t *u8 = (uint8_t *)translate(addr, 1);
if(!u8)
return Addressable((uint8_t)-1);
return Addressable(addr, *u8);
}
Addressable<int16_t> AbstractMemory::read_i16(uint32_t addr) const
{
uint8_t *i16 = (uint8_t *)translate(addr, 2);
if(!i16)
return Addressable((int16_t)-1);
int16_t v = (i16[0] << 8) | i16[1];
return Addressable(addr, v);
}
Addressable<uint16_t> AbstractMemory::read_u16(uint32_t addr) const
{
uint8_t *u16 = (uint8_t *)translate(addr, 2);
if(!u16)
return Addressable((uint16_t)-1);
uint16_t v = (u16[0] << 8) | u16[1];
return Addressable(addr, v);
}
Addressable<int32_t> AbstractMemory::read_i32(uint32_t addr) const
{
uint8_t *i32 = (uint8_t *)translate(addr, 4);
if(!i32)
return Addressable((int32_t)-1);
int32_t v = (i32[0] << 24) | (i32[1] << 16) | (i32[2] << 8) | i32[3];
return Addressable(addr, v);
}
Addressable<uint32_t> AbstractMemory::read_u32(uint32_t addr) const
{
uint8_t *u32 = (uint8_t *)translate(addr, 4);
if(!u32)
return Addressable((uint32_t)-1);
uint32_t v = (u32[0] << 24) | (u32[1] << 16) | (u32[2] << 8) | u32[3];
return Addressable(addr, v);
}
Addressable<std::string> AbstractMemory::read_str(uint32_t addr, size_t len)
const
{
char const *str = translate(addr, len);
if(!str)
return Addressable(std::string());
return Addressable(addr, std::string(str, len));
}
//---
// Bindings of data buffers into memory regions
//---
Binding::Binding(MemoryRegion source_region, Buffer source_buffer):
region(source_region), buffer(source_buffer)
region {source_region}, buffer {source_buffer}
{
/* Extend the buffer if it's not at least as large as the region */
if(buffer.size < region.size()) {
buffer = Buffer(buffer, region.size());
}
/* Extend the buffer if it's not at least as large as the region */
if(buffer.size < region.size())
buffer = Buffer(buffer, region.size());
}
bool Binding::covers(uint32_t addr, int size) const noexcept
char const *Binding::translate_dynamic(uint32_t addr, int *size)
{
return size >= 0 && addr >= region.start && addr + size <= region.end;
}
bool Binding::covers(MemoryRegion const &r) const noexcept
{
return covers(r.start, r.size());
}
char const *Binding::translate(uint32_t addr, int size) const
{
if(!covers(addr, size)) return nullptr;
return buffer.data.get() + (addr - region.start);
}
char const *Binding::translate_dynamic(uint32_t addr, int *size) const
{
if(!covers(addr, 1)) return nullptr;
*size = region.end - addr;
return buffer.data.get() + (addr - region.start);
}
uint32_t Binding::search(uint32_t start, uint32_t end, void const *pattern,
int size) const
{
void const *data = translate(start, end-start);
if(!data)
return end;
void const *occurrence = memmem(data, end - start, pattern, size);
if(!occurrence)
return end;
return start + ((char *)occurrence - (char *)data);
if(addr >= region.start && addr < region.end) {
*size = region.end - addr;
return buffer.data.get() + (addr - region.start);
}
return nullptr;
}
//---
@ -128,96 +30,34 @@ uint32_t Binding::search(uint32_t start, uint32_t end, void const *pattern,
//---
VirtualSpace::VirtualSpace():
m_bindings {}, m_buffers {}, m_os {nullptr}
mpu {}, bindings {}, m_os {nullptr}
{
}
VirtualSpace::VirtualSpace(Target const &target,
std::vector<std::string> const &folders):
VirtualSpace()
{
for(auto binding: target.bindings)
{
MemoryRegion region = binding.first;
ssize_t size = (region.size() > 0 ? region.size() : -1);
auto b = m_buffers.emplace(m_buffers.end(),
binding.second, folders, size);
bind_region(region, *b);
}
this->mpu = target.mpu;
}
OS *VirtualSpace::os_analysis(bool force)
{
if(!m_os || force) {
m_os = std::make_unique<OS>(*this);
/* We don't keep an OS analysis result that failed */
if(m_os->type == OS::UNKNOWN)
m_os = nullptr;
}
return m_os.get();
if(!m_os || force) {
m_os = std::make_unique<OS>(*this);
/* We don't keep an OS analysis result that failed */
if(m_os->type == OS::UNKNOWN)
m_os = nullptr;
}
return m_os.get();
}
void VirtualSpace::bind_region(MemoryRegion const &region,Buffer const &buffer)
void VirtualSpace::bind_region(MemoryRegion const &region, Buffer const &buf)
{
Binding b(region, buffer);
m_bindings.push_back(b);
this->bindings.emplace(this->bindings.begin(), region, buf);
}
bool VirtualSpace::covers(uint32_t addr, int size) const noexcept
char const *VirtualSpace::translate_dynamic(uint32_t addr, int *size)
{
for(auto it = m_bindings.crbegin(); it != m_bindings.crend(); it++)
{
if(it->covers(addr, size)) return true;
}
return false;
}
bool VirtualSpace::covers(MemoryRegion const &region) const noexcept
{
return covers(region.start, region.size());
}
char const *VirtualSpace::translate(uint32_t addr, int size) const
{
for(auto it = m_bindings.crbegin(); it != m_bindings.crend(); it++)
{
char const *ptr = it->translate(addr, size);
if(ptr) return ptr;
}
return nullptr;
}
char const *VirtualSpace::translate_dynamic(uint32_t addr, int *size) const
{
for(auto it = m_bindings.crbegin(); it != m_bindings.crend(); it++)
{
char const *ptr = it->translate_dynamic(addr, size);
if(ptr) return ptr;
}
return nullptr;
}
uint32_t VirtualSpace::search(uint32_t start, uint32_t end,
void const *pattern, int size) const
{
uint32_t occurrence;
if(end < start || !covers(start, end - start))
return end;
for(auto it = m_bindings.crbegin(); it != m_bindings.crend(); it++)
{
if(it->covers(start, end - start))
{
occurrence = it->search(start, end, pattern, size);
if(occurrence != end) return occurrence;
}
}
return end;
for(auto &b: this->bindings) {
char const *ptr = b.translate_dynamic(addr, size);
if(ptr)
return ptr;
}
return nullptr;
}
} /* namespace FxOS */

View File

@ -113,11 +113,11 @@ void _afh(Session &session, char const *reference, char const *pattern,
if(distance < 0) distance = 32;
if(align <= 0) align = 1;
VirtualSpace const &v = *session.current_space;
VirtualSpace &v = *session.current_space;
/* If no region is specified, explore the regions for all bindings */
if(regions.size() == 0) {
for(auto &b: v.bindings())
for(auto &b: v.bindings)
regions.push_back(b.region);
}
@ -129,8 +129,7 @@ void _afh(Session &session, char const *reference, char const *pattern,
std::vector<std::pair<uint32_t,int>> pending;
for(auto const &r: regions) {
uint32_t region_size = r.end - r.start;
char const *data = v.translate(r.start, region_size);
char const *data = v.translate(r.start, r.size());
if(!data) throw CommandError("region 0x{:08x} .. 0x{:08x} is not "
"fully bound", r.start, r.end);
@ -139,7 +138,7 @@ void _afh(Session &session, char const *reference, char const *pattern,
while((r.start + i) % align != 0) i++;
/* Search patterns for (size) bytes inside (data) */
for(; i <= (int)region_size - (int)size; i += align) {
for(; i <= (int)r.size() - (int)size; i += align) {
if(!matches(data + i, reference, pattern, size)) continue;
uint32_t start = r.start + i;
@ -150,7 +149,7 @@ void _afh(Session &session, char const *reference, char const *pattern,
if(p.first + p.second + distance < start) {
Range r;
r.start = pending[0].first - distance;
r.end = p.first + p.second + distance;
r.end = p.first + p.second + distance - 1;
if(output_started) fmt::print("...\n");
_h_hexdump(session, r, pending);
@ -166,14 +165,15 @@ void _afh(Session &session, char const *reference, char const *pattern,
if(match_count >= 128) break;
}
if(match_count >= 128) break;
}
/* Print the last pending elements */
if(pending.size()) {
auto const &p = pending[pending.size() - 1];
Range r = { pending[0].first-distance, p.first+p.second+distance };
if(output_started) fmt::print("...\n");
_h_hexdump(session, r, pending);
/* Print the last pending elements */
if(pending.size()) {
auto const &p = pending[pending.size() - 1];
Range r = { pending[0].first-distance, p.first+p.second+distance };
if(output_started) fmt::print("...\n");
_h_hexdump(session, r, pending);
}
pending.clear();
}
if(match_count == 0)

View File

@ -33,13 +33,13 @@ static void show_vspace(std::string name, VirtualSpace &s, Session &session)
if(is_current) fmt::print("* ");
fmt::print(theme(11), "{}\n", name);
if(s.bindings().size() == 0) {
if(s.bindings.size() == 0) {
fmt::print(" (no bindings)\n");
return;
}
fmt::print(" Region Start End File\n");
for(auto &b: s.bindings()) {
for(auto &b: s.bindings) {
MemoryRegion const *ref = MemoryRegion::region_for(b.region);
fmt::print(" {:<7s} 0x{:08x} .. 0x{:08x}", (ref ? ref->name : ""),
b.region.start, b.region.end);
@ -168,7 +168,7 @@ void _vm(Session &session, std::string file, std::vector<MemoryRegion> regions)
Buffer contents(path);
/* If no files are loaded yet, set the PC to the first loaded region */
if(!session.current_space->bindings().size())
if(!session.current_space->bindings.size())
session.pc = regions[0].start;
for(auto &r: regions)