Cleanup windows-emulator and extract classes (#139)

This commit is contained in:
Maurice Heumann
2025-02-10 13:56:37 +01:00
committed by GitHub
12 changed files with 980 additions and 923 deletions

View File

@@ -0,0 +1,46 @@
#pragma once
#include <type_traits>
namespace utils
{
class moved_marker
{
public:
moved_marker() = default;
moved_marker(const moved_marker& copy) = default;
moved_marker& operator=(const moved_marker&) = default;
moved_marker(moved_marker&& obj) noexcept
: moved_marker()
{
this->operator=(std::move(obj));
}
moved_marker& operator=(moved_marker&& obj) noexcept
{
if (this != &obj)
{
this->was_moved_ = obj.was_moved_;
obj.was_moved_ = true;
}
return *this;
}
~moved_marker() = default;
bool was_moved() const
{
return this->was_moved_;
}
void mark_as_moved()
{
this->was_moved_ = true;
}
private:
bool was_moved_{false};
};
}

View File

@@ -0,0 +1,243 @@
#include "emulator_thread.hpp"
#include "context_frame.hpp"
#include "process_context.hpp"
namespace
{
template <typename T>
emulator_object<T> allocate_object_on_stack(x64_emulator& emu)
{
const auto old_sp = emu.reg(x64_register::rsp);
const auto new_sp = align_down(old_sp - sizeof(T), std::max(alignof(T), alignof(x64_emulator::pointer_type)));
emu.reg(x64_register::rsp, new_sp);
return {emu, new_sp};
}
void unalign_stack(x64_emulator& emu)
{
auto sp = emu.reg(x64_register::rsp);
sp = align_down(sp - 0x10, 0x10) + 8;
emu.reg(x64_register::rsp, sp);
}
void setup_stack(x64_emulator& emu, const uint64_t stack_base, const size_t stack_size)
{
const uint64_t stack_end = stack_base + stack_size;
emu.reg(x64_register::rsp, stack_end);
}
void setup_gs_segment(x64_emulator& emu, const emulator_allocator& allocator)
{
struct msr_value
{
uint32_t id;
uint64_t value;
};
const msr_value value{IA32_GS_BASE_MSR, allocator.get_base()};
emu.write_register(x64_register::msr, &value, sizeof(value));
}
bool is_object_signaled(process_context& c, const handle h, const uint32_t current_thread_id)
{
const auto type = h.value.type;
switch (type)
{
default:
break;
case handle_types::event: {
if (h.value.is_pseudo)
{
return true;
}
auto* e = c.events.get(h);
if (e)
{
return e->is_signaled();
}
break;
}
case handle_types::mutant: {
auto* e = c.mutants.get(h);
return !e || e->try_lock(current_thread_id);
}
case handle_types::semaphore: {
auto* s = c.semaphores.get(h);
if (s)
{
return s->try_lock();
}
break;
}
case handle_types::thread: {
const auto* t = c.threads.get(h);
if (t)
{
return t->is_terminated();
}
break;
}
}
throw std::runtime_error("Bad object: " + std::to_string(h.value.type));
}
}
emulator_thread::emulator_thread(memory_manager& memory, const process_context& context, const uint64_t start_address,
const uint64_t argument, const uint64_t stack_size, const uint32_t id)
: memory_ptr(&memory),
stack_size(page_align_up(std::max(stack_size, static_cast<uint64_t>(STACK_SIZE)))),
start_address(start_address),
argument(argument),
id(id),
last_registers(context.default_register_set)
{
this->stack_base = memory.allocate_memory(this->stack_size, memory_permission::read_write);
this->gs_segment = emulator_allocator{
memory,
memory.allocate_memory(GS_SEGMENT_SIZE, memory_permission::read_write),
GS_SEGMENT_SIZE,
};
this->teb = this->gs_segment->reserve<TEB64>();
this->teb->access([&](TEB64& teb_obj) {
// Skips GetCurrentNlsCache
// This hack can be removed once this is fixed:
// https://github.com/momo5502/emulator/issues/128
reinterpret_cast<uint8_t*>(&teb_obj)[0x179C] = 1;
teb_obj.ClientId.UniqueProcess = 1ul;
teb_obj.ClientId.UniqueThread = static_cast<uint64_t>(this->id);
teb_obj.NtTib.StackLimit = reinterpret_cast<std::uint64_t*>(this->stack_base);
teb_obj.NtTib.StackBase = reinterpret_cast<std::uint64_t*>(this->stack_base + this->stack_size);
teb_obj.NtTib.Self = &this->teb->ptr()->NtTib;
teb_obj.ProcessEnvironmentBlock = context.peb.ptr();
});
}
void emulator_thread::mark_as_ready(const NTSTATUS status)
{
this->pending_status = status;
this->await_time = {};
this->await_objects = {};
// TODO: Find out if this is correct
if (this->waiting_for_alert)
{
this->alerted = false;
}
this->waiting_for_alert = false;
}
bool emulator_thread::is_terminated() const
{
return this->exit_status.has_value();
}
bool emulator_thread::is_thread_ready(process_context& process)
{
if (this->is_terminated())
{
return false;
}
if (this->waiting_for_alert)
{
if (this->alerted)
{
this->mark_as_ready(STATUS_ALERTED);
return true;
}
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_TIMEOUT);
return true;
}
return false;
}
if (!this->await_objects.empty())
{
bool all_signaled = true;
for (uint32_t i = 0; i < this->await_objects.size(); ++i)
{
const auto& obj = this->await_objects[i];
const auto signaled = is_object_signaled(process, obj, this->id);
all_signaled &= signaled;
if (signaled && this->await_any)
{
this->mark_as_ready(STATUS_WAIT_0 + i);
return true;
}
}
if (!this->await_any && all_signaled)
{
this->mark_as_ready(STATUS_SUCCESS);
return true;
}
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_TIMEOUT);
return true;
}
return false;
}
if (this->await_time.has_value())
{
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_SUCCESS);
return true;
}
return false;
}
return true;
}
void emulator_thread::setup_registers(x64_emulator& emu, const process_context& context) const
{
setup_stack(emu, this->stack_base, this->stack_size);
setup_gs_segment(emu, *this->gs_segment);
CONTEXT64 ctx{};
ctx.ContextFlags = CONTEXT64_ALL;
unalign_stack(emu);
context_frame::save(emu, ctx);
ctx.Rip = context.rtl_user_thread_start;
ctx.Rcx = this->start_address;
ctx.Rdx = this->argument;
const auto ctx_obj = allocate_object_on_stack<CONTEXT64>(emu);
ctx_obj.write(ctx);
unalign_stack(emu);
emu.reg(x64_register::rcx, ctx_obj.value());
emu.reg(x64_register::rdx, context.ntdll->image_base);
emu.reg(x64_register::rip, context.ldr_initialize_thunk);
}

View File

@@ -0,0 +1,199 @@
#pragma once
#include "handles.hpp"
#include "emulator_utils.hpp"
#include "memory_manager.hpp"
#include <utils/moved_marker.hpp>
struct process_context;
class emulator_thread : public ref_counted_object
{
public:
emulator_thread(memory_manager& memory)
: memory_ptr(&memory)
{
}
emulator_thread(utils::buffer_deserializer& buffer)
: emulator_thread(buffer.read<memory_manager_wrapper>().get())
{
}
emulator_thread(memory_manager& memory, const process_context& context, uint64_t start_address, uint64_t argument,
uint64_t stack_size, uint32_t id);
emulator_thread(const emulator_thread&) = delete;
emulator_thread& operator=(const emulator_thread&) = delete;
emulator_thread(emulator_thread&& obj) noexcept = default;
emulator_thread& operator=(emulator_thread&& obj) noexcept = default;
~emulator_thread() override
{
this->release();
}
utils::moved_marker marker{};
memory_manager* memory_ptr{};
uint64_t stack_base{};
uint64_t stack_size{};
uint64_t start_address{};
uint64_t argument{};
uint64_t executed_instructions{0};
uint32_t id{};
std::u16string name{};
std::optional<NTSTATUS> exit_status{};
std::vector<handle> await_objects{};
bool await_any{false};
bool waiting_for_alert{false};
bool alerted{false};
std::optional<std::chrono::steady_clock::time_point> await_time{};
std::optional<NTSTATUS> pending_status{};
std::optional<emulator_allocator> gs_segment;
std::optional<emulator_object<TEB64>> teb;
std::vector<std::byte> last_registers{};
void mark_as_ready(NTSTATUS status);
bool is_await_time_over() const
{
return this->await_time.has_value() && this->await_time.value() < std::chrono::steady_clock::now();
}
bool is_terminated() const;
bool is_thread_ready(process_context& process);
void save(x64_emulator& emu)
{
this->last_registers = emu.save_registers();
}
void restore(x64_emulator& emu) const
{
emu.restore_registers(this->last_registers);
}
void setup_if_necessary(x64_emulator& emu, const process_context& context)
{
if (!this->executed_instructions)
{
this->setup_registers(emu, context);
}
if (this->pending_status.has_value())
{
const auto status = *this->pending_status;
this->pending_status = {};
emu.reg<uint64_t>(x64_register::rax, static_cast<uint64_t>(status));
}
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
if (this->marker.was_moved())
{
throw std::runtime_error("Object was moved!");
}
buffer.write(this->stack_base);
buffer.write(this->stack_size);
buffer.write(this->start_address);
buffer.write(this->argument);
buffer.write(this->executed_instructions);
buffer.write(this->id);
buffer.write_string(this->name);
buffer.write_optional(this->exit_status);
buffer.write_vector(this->await_objects);
buffer.write(this->await_any);
buffer.write(this->waiting_for_alert);
buffer.write(this->alerted);
buffer.write_optional(this->await_time);
buffer.write_optional(this->pending_status);
buffer.write_optional(this->gs_segment);
buffer.write_optional(this->teb);
buffer.write_vector(this->last_registers);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
if (this->marker.was_moved())
{
throw std::runtime_error("Object was moved!");
}
this->release();
buffer.read(this->stack_base);
buffer.read(this->stack_size);
buffer.read(this->start_address);
buffer.read(this->argument);
buffer.read(this->executed_instructions);
buffer.read(this->id);
buffer.read_string(this->name);
buffer.read_optional(this->exit_status);
buffer.read_vector(this->await_objects);
buffer.read(this->await_any);
buffer.read(this->waiting_for_alert);
buffer.read(this->alerted);
buffer.read_optional(this->await_time);
buffer.read_optional(this->pending_status);
buffer.read_optional(this->gs_segment, [this] { return emulator_allocator(*this->memory_ptr); });
buffer.read_optional(this->teb, [this] { return emulator_object<TEB64>(*this->memory_ptr); });
buffer.read_vector(this->last_registers);
}
void leak_memory()
{
this->marker.mark_as_moved();
}
private:
void setup_registers(x64_emulator& emu, const process_context& context) const;
void release()
{
if (this->marker.was_moved())
{
return;
}
if (this->stack_base)
{
if (!this->memory_ptr)
{
throw std::runtime_error("Emulator was never assigned!");
}
this->memory_ptr->release_memory(this->stack_base, this->stack_size);
this->stack_base = 0;
}
if (this->gs_segment)
{
this->gs_segment->release(*this->memory_ptr);
this->gs_segment = {};
}
}
};

View File

@@ -0,0 +1,199 @@
#include "exception_dispatch.hpp"
#include "process_context.hpp"
#include "context_frame.hpp"
#include <platform/status.hpp>
namespace
{
using exception_record = EMU_EXCEPTION_RECORD<EmulatorTraits<Emu64>>;
using exception_record_map = std::unordered_map<const exception_record*, emulator_object<exception_record>>;
emulator_object<exception_record> save_exception_record(emulator_allocator& allocator,
const exception_record& record,
exception_record_map& record_mapping)
{
const auto record_obj = allocator.reserve<exception_record>();
record_obj.write(record);
if (record.ExceptionRecord)
{
record_mapping.emplace(&record, record_obj);
emulator_object<exception_record> nested_record_obj{allocator.get_memory()};
const auto nested_record = record_mapping.find(reinterpret_cast<exception_record*>(record.ExceptionRecord));
if (nested_record != record_mapping.end())
{
nested_record_obj = nested_record->second;
}
else
{
nested_record_obj = save_exception_record(
allocator, *reinterpret_cast<exception_record*>(record.ExceptionRecord), record_mapping);
}
record_obj.access([&](exception_record& r) {
r.ExceptionRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(nested_record_obj.ptr());
});
}
return record_obj;
}
emulator_object<exception_record> save_exception_record(emulator_allocator& allocator,
const exception_record& record)
{
exception_record_map record_mapping{};
return save_exception_record(allocator, record, record_mapping);
}
uint32_t map_violation_operation_to_parameter(const memory_operation operation)
{
switch (operation)
{
default:
case memory_operation::read:
return 0;
case memory_operation::write:
case memory_operation::exec:
return 1;
}
}
size_t calculate_exception_record_size(const exception_record& record)
{
std::unordered_set<const exception_record*> records{};
size_t total_size = 0;
const exception_record* current_record = &record;
while (current_record)
{
if (!records.insert(current_record).second)
{
break;
}
total_size += sizeof(*current_record);
current_record = reinterpret_cast<exception_record*>(record.ExceptionRecord);
}
return total_size;
}
struct machine_frame
{
uint64_t rip;
uint64_t cs;
uint64_t eflags;
uint64_t rsp;
uint64_t ss;
};
void dispatch_exception_pointers(x64_emulator& emu, const uint64_t dispatcher,
const EMU_EXCEPTION_POINTERS<EmulatorTraits<Emu64>> pointers)
{
constexpr auto mach_frame_size = 0x40;
constexpr auto context_record_size = 0x4F0;
const auto exception_record_size =
calculate_exception_record_size(*reinterpret_cast<exception_record*>(pointers.ExceptionRecord));
const auto combined_size = align_up(exception_record_size + context_record_size, 0x10);
assert(combined_size == 0x590);
const auto allocation_size = combined_size + mach_frame_size;
const auto initial_sp = emu.reg(x64_register::rsp);
const auto new_sp = align_down(initial_sp - allocation_size, 0x100);
const auto total_size = initial_sp - new_sp;
assert(total_size >= allocation_size);
std::vector<uint8_t> zero_memory{};
zero_memory.resize(total_size, 0);
emu.write_memory(new_sp, zero_memory.data(), zero_memory.size());
emu.reg(x64_register::rsp, new_sp);
emu.reg(x64_register::rip, dispatcher);
const emulator_object<CONTEXT64> context_record_obj{emu, new_sp};
context_record_obj.write(*reinterpret_cast<CONTEXT64*>(pointers.ContextRecord));
emulator_allocator allocator{emu, new_sp + context_record_size, exception_record_size};
const auto exception_record_obj =
save_exception_record(allocator, *reinterpret_cast<exception_record*>(pointers.ExceptionRecord));
if (exception_record_obj.value() != allocator.get_base())
{
throw std::runtime_error("Bad exception record position on stack");
}
const emulator_object<machine_frame> machine_frame_obj{emu, new_sp + combined_size};
machine_frame_obj.access([&](machine_frame& frame) {
const auto& record = *reinterpret_cast<CONTEXT64*>(pointers.ContextRecord);
frame.rip = record.Rip;
frame.rsp = record.Rsp;
frame.ss = record.SegSs;
frame.cs = record.SegCs;
frame.eflags = record.EFlags;
});
}
}
void dispatch_exception(x64_emulator& emu, const process_context& proc, const DWORD status,
const std::vector<EmulatorTraits<Emu64>::ULONG_PTR>& parameters)
{
CONTEXT64 ctx{};
ctx.ContextFlags = CONTEXT64_ALL;
context_frame::save(emu, ctx);
exception_record record{};
memset(&record, 0, sizeof(record));
record.ExceptionCode = status;
record.ExceptionFlags = 0;
record.ExceptionRecord = 0;
record.ExceptionAddress = emu.read_instruction_pointer();
record.NumberParameters = static_cast<DWORD>(parameters.size());
if (parameters.size() > 15)
{
throw std::runtime_error("Too many exception parameters");
}
for (size_t i = 0; i < parameters.size(); ++i)
{
record.ExceptionInformation[i] = parameters[i];
}
EMU_EXCEPTION_POINTERS<EmulatorTraits<Emu64>> pointers{};
pointers.ContextRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(&ctx);
pointers.ExceptionRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(&record);
dispatch_exception_pointers(emu, proc.ki_user_exception_dispatcher, pointers);
}
void dispatch_access_violation(x64_emulator& emu, const process_context& proc, const uint64_t address,
const memory_operation operation)
{
dispatch_exception(emu, proc, STATUS_ACCESS_VIOLATION,
{
map_violation_operation_to_parameter(operation),
address,
});
}
void dispatch_illegal_instruction_violation(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_ILLEGAL_INSTRUCTION, {});
}
void dispatch_integer_division_by_zero(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_INTEGER_DIVIDE_BY_ZERO, {});
}
void dispatch_single_step(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_SINGLE_STEP, {});
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <x64_emulator.hpp>
#include <platform/traits.hpp>
#include <platform/primitives.hpp>
struct process_context;
void dispatch_exception(x64_emulator& emu, const process_context& proc, DWORD status,
const std::vector<EmulatorTraits<Emu64>::ULONG_PTR>& parameters);
template <typename T>
requires(std::is_integral_v<T> && !std::is_same_v<T, DWORD>)
void dispatch_exception(x64_emulator& emu, const process_context& proc, const T status,
const std::vector<EmulatorTraits<Emu64>::ULONG_PTR>& parameters)
{
dispatch_exception(emu, proc, static_cast<DWORD>(status), parameters);
}
void dispatch_access_violation(x64_emulator& emu, const process_context& proc, uint64_t address,
memory_operation operation);
void dispatch_illegal_instruction_violation(x64_emulator& emu, const process_context& proc);
void dispatch_integer_division_by_zero(x64_emulator& emu, const process_context& proc);
void dispatch_single_step(x64_emulator& emu, const process_context& proc);

View File

@@ -1,5 +1,7 @@
#pragma once
#include <serialization.hpp>
struct handle_types
{
enum type : uint16_t

View File

@@ -10,10 +10,6 @@
namespace
{
constexpr auto ALLOCATION_GRANULARITY = 0x0000000000010000ULL;
constexpr auto MIN_ALLOCATION_ADDRESS = 0x0000000000010000ULL;
constexpr auto MAX_ALLOCATION_ADDRESS = 0x00007ffffffeffffULL;
void split_regions(memory_manager::committed_region_map& regions, const std::vector<uint64_t>& split_points)
{
for (auto i = regions.begin(); i != regions.end(); ++i)

View File

@@ -8,6 +8,10 @@
#include <memory_interface.hpp>
constexpr auto ALLOCATION_GRANULARITY = 0x0000000000010000ULL;
constexpr auto MIN_ALLOCATION_ADDRESS = 0x0000000000010000ULL;
constexpr auto MAX_ALLOCATION_ADDRESS = 0x00007ffffffeffffULL;
struct region_info : basic_memory_region
{
uint64_t allocation_base{};

View File

@@ -6,13 +6,13 @@
#include "module/module_manager.hpp"
#include <utils/nt_handle.hpp>
#include <utils/file_handle.hpp>
#include <x64_emulator.hpp>
#include <serialization_helper.hpp>
#include "io_device.hpp"
#include "kusd_mmio.hpp"
#include "windows_objects.hpp"
#include "emulator_thread.hpp"
#define PEB_SEGMENT_SIZE (20 << 20) // 20 MB
#define GS_SEGMENT_SIZE (1 << 20) // 1 MB
@@ -25,486 +25,6 @@
#define GDT_LIMIT 0x1000
#define GDT_ENTRY_SIZE 0x8
class windows_emulator;
struct event : ref_counted_object
{
bool signaled{};
EVENT_TYPE type{};
std::u16string name{};
bool is_signaled()
{
const auto res = this->signaled;
if (this->type == SynchronizationEvent)
{
this->signaled = false;
}
return res;
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->signaled);
buffer.write(this->type);
buffer.write(this->name);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->signaled);
buffer.read(this->type);
buffer.read(this->name);
}
};
struct mutant : ref_counted_object
{
uint32_t locked_count{0};
uint32_t owning_thread_id{};
std::u16string name{};
bool try_lock(const uint32_t thread_id)
{
if (this->locked_count == 0)
{
++this->locked_count;
this->owning_thread_id = thread_id;
return true;
}
if (this->owning_thread_id != thread_id)
{
return false;
}
++this->locked_count;
return true;
}
std::pair<uint32_t, bool> release(const uint32_t thread_id)
{
const auto old_count = this->locked_count;
if (this->locked_count <= 0 || this->owning_thread_id != thread_id)
{
return {old_count, false};
}
--this->locked_count;
return {old_count, true};
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->locked_count);
buffer.write(this->owning_thread_id);
buffer.write(this->name);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->locked_count);
buffer.read(this->owning_thread_id);
buffer.read(this->name);
}
};
struct file_entry
{
std::filesystem::path file_path{};
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->file_path);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(this->file_path);
}
};
struct file_enumeration_state
{
size_t current_index{0};
std::vector<file_entry> files{};
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->current_index);
buffer.write_vector(this->files);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(this->current_index);
buffer.read_vector(this->files);
}
};
struct file : ref_counted_object
{
utils::file_handle handle{};
std::u16string name{};
std::optional<file_enumeration_state> enumeration_state{};
bool is_file() const
{
return this->handle;
}
bool is_directory() const
{
return !this->is_file();
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
// TODO: Serialize handle
buffer.write(this->name);
buffer.write_optional(this->enumeration_state);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read_optional(this->enumeration_state);
this->handle = {};
}
};
struct section : ref_counted_object
{
std::u16string name{};
std::u16string file_name{};
uint64_t maximum_size{};
uint32_t section_page_protection{};
uint32_t allocation_attributes{};
bool is_image() const
{
return this->allocation_attributes & SEC_IMAGE;
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->file_name);
buffer.write(this->maximum_size);
buffer.write(this->section_page_protection);
buffer.write(this->allocation_attributes);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->file_name);
buffer.read(this->maximum_size);
buffer.read(this->section_page_protection);
buffer.read(this->allocation_attributes);
}
};
struct semaphore : ref_counted_object
{
std::u16string name{};
uint32_t current_count{};
uint32_t max_count{};
bool try_lock()
{
if (this->current_count > 0)
{
--this->current_count;
return true;
}
return false;
}
std::pair<uint32_t, bool> release(const uint32_t release_count)
{
const auto old_count = this->current_count;
if (this->current_count + release_count > this->max_count)
{
return {old_count, false};
}
this->current_count += release_count;
return {old_count, true};
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->current_count);
buffer.write(this->max_count);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->current_count);
buffer.read(this->max_count);
}
};
struct port : ref_counted_object
{
std::u16string name{};
uint64_t view_base{};
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->view_base);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->view_base);
}
};
struct process_context;
class moved_marker
{
public:
moved_marker() = default;
moved_marker(const moved_marker& copy) = default;
moved_marker& operator=(const moved_marker&) = default;
moved_marker(moved_marker&& obj) noexcept
: moved_marker()
{
this->operator=(std::move(obj));
}
moved_marker& operator=(moved_marker&& obj) noexcept
{
if (this != &obj)
{
this->was_moved_ = obj.was_moved_;
obj.was_moved_ = true;
}
return *this;
}
~moved_marker() = default;
bool was_moved() const
{
return this->was_moved_;
}
void mark_as_moved()
{
this->was_moved_ = true;
}
private:
bool was_moved_{false};
};
class emulator_thread : public ref_counted_object
{
public:
emulator_thread(memory_manager& memory)
: memory_ptr(&memory)
{
}
emulator_thread(utils::buffer_deserializer& buffer)
: emulator_thread(buffer.read<memory_manager_wrapper>().get())
{
}
emulator_thread(memory_manager& memory, const process_context& context, uint64_t start_address, uint64_t argument,
uint64_t stack_size, uint32_t id);
emulator_thread(const emulator_thread&) = delete;
emulator_thread& operator=(const emulator_thread&) = delete;
emulator_thread(emulator_thread&& obj) noexcept = default;
emulator_thread& operator=(emulator_thread&& obj) noexcept = default;
~emulator_thread() override
{
this->release();
}
moved_marker marker{};
memory_manager* memory_ptr{};
uint64_t stack_base{};
uint64_t stack_size{};
uint64_t start_address{};
uint64_t argument{};
uint64_t executed_instructions{0};
uint32_t id{};
std::u16string name{};
std::optional<NTSTATUS> exit_status{};
std::vector<handle> await_objects{};
bool await_any{false};
bool waiting_for_alert{false};
bool alerted{false};
std::optional<std::chrono::steady_clock::time_point> await_time{};
std::optional<NTSTATUS> pending_status{};
std::optional<emulator_allocator> gs_segment;
std::optional<emulator_object<TEB64>> teb;
std::vector<std::byte> last_registers{};
void mark_as_ready(NTSTATUS status);
bool is_await_time_over() const
{
return this->await_time.has_value() && this->await_time.value() < std::chrono::steady_clock::now();
}
bool is_terminated() const;
bool is_thread_ready(windows_emulator& win_emu);
void save(x64_emulator& emu)
{
this->last_registers = emu.save_registers();
}
void restore(x64_emulator& emu) const
{
emu.restore_registers(this->last_registers);
}
void setup_if_necessary(x64_emulator& emu, const process_context& context)
{
if (!this->executed_instructions)
{
this->setup_registers(emu, context);
}
if (this->pending_status.has_value())
{
const auto status = *this->pending_status;
this->pending_status = {};
emu.reg<uint64_t>(x64_register::rax, static_cast<uint64_t>(status));
}
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
if (this->marker.was_moved())
{
throw std::runtime_error("Object was moved!");
}
buffer.write(this->stack_base);
buffer.write(this->stack_size);
buffer.write(this->start_address);
buffer.write(this->argument);
buffer.write(this->executed_instructions);
buffer.write(this->id);
buffer.write_string(this->name);
buffer.write_optional(this->exit_status);
buffer.write_vector(this->await_objects);
buffer.write(this->await_any);
buffer.write(this->waiting_for_alert);
buffer.write(this->alerted);
buffer.write_optional(this->await_time);
buffer.write_optional(this->pending_status);
buffer.write_optional(this->gs_segment);
buffer.write_optional(this->teb);
buffer.write_vector(this->last_registers);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
if (this->marker.was_moved())
{
throw std::runtime_error("Object was moved!");
}
this->release();
buffer.read(this->stack_base);
buffer.read(this->stack_size);
buffer.read(this->start_address);
buffer.read(this->argument);
buffer.read(this->executed_instructions);
buffer.read(this->id);
buffer.read_string(this->name);
buffer.read_optional(this->exit_status);
buffer.read_vector(this->await_objects);
buffer.read(this->await_any);
buffer.read(this->waiting_for_alert);
buffer.read(this->alerted);
buffer.read_optional(this->await_time);
buffer.read_optional(this->pending_status);
buffer.read_optional(this->gs_segment, [this] { return emulator_allocator(*this->memory_ptr); });
buffer.read_optional(this->teb, [this] { return emulator_object<TEB64>(*this->memory_ptr); });
buffer.read_vector(this->last_registers);
}
void leak_memory()
{
this->marker.mark_as_moved();
}
private:
void setup_registers(x64_emulator& emu, const process_context& context) const;
void release()
{
if (this->marker.was_moved())
{
return;
}
if (this->stack_base)
{
if (!this->memory_ptr)
{
throw std::runtime_error("Emulator was never assigned!");
}
this->memory_ptr->release_memory(this->stack_base, this->stack_size);
this->stack_base = 0;
}
if (this->gs_segment)
{
this->gs_segment->release(*this->memory_ptr);
this->gs_segment = {};
}
}
};
struct process_context
{
process_context(x64_emulator& emu, memory_manager& memory, file_system& file_sys)

View File

@@ -1111,9 +1111,9 @@ namespace
basic_info.PageSize = 0x1000;
basic_info.LowestPhysicalPageNumber = 0x00000001;
basic_info.HighestPhysicalPageNumber = 0x00c9c7ff;
basic_info.AllocationGranularity = 0x10000;
basic_info.MinimumUserModeAddress = 0x0000000000010000;
basic_info.MaximumUserModeAddress = 0x00007ffffffeffff;
basic_info.AllocationGranularity = ALLOCATION_GRANULARITY;
basic_info.MinimumUserModeAddress = MIN_ALLOCATION_ADDRESS;
basic_info.MaximumUserModeAddress = MAX_ALLOCATION_ADDRESS;
basic_info.ActiveProcessorsAffinityMask = 0x0000000000000fff;
basic_info.NumberOfProcessors = 1;
});
@@ -1267,9 +1267,9 @@ namespace
basic_info.PageSize = 0x1000;
basic_info.LowestPhysicalPageNumber = 0x00000001;
basic_info.HighestPhysicalPageNumber = 0x00c9c7ff;
basic_info.AllocationGranularity = 0x10000;
basic_info.MinimumUserModeAddress = 0x0000000000010000;
basic_info.MaximumUserModeAddress = 0x00007ffffffeffff;
basic_info.AllocationGranularity = ALLOCATION_GRANULARITY;
basic_info.MinimumUserModeAddress = MIN_ALLOCATION_ADDRESS;
basic_info.MaximumUserModeAddress = MAX_ALLOCATION_ADDRESS;
basic_info.ActiveProcessorsAffinityMask = 0x0000000000000fff;
basic_info.NumberOfProcessors = 1;
});

View File

@@ -12,46 +12,12 @@
#include <utils/lazy_object.hpp>
#include "apiset.hpp"
#include "exception_dispatch.hpp"
constexpr auto MAX_INSTRUCTIONS_PER_TIME_SLICE = 100000;
namespace
{
template <typename T>
emulator_object<T> allocate_object_on_stack(x64_emulator& emu)
{
const auto old_sp = emu.reg(x64_register::rsp);
const auto new_sp = align_down(old_sp - sizeof(T), std::max(alignof(T), alignof(x64_emulator::pointer_type)));
emu.reg(x64_register::rsp, new_sp);
return {emu, new_sp};
}
void unalign_stack(x64_emulator& emu)
{
auto sp = emu.reg(x64_register::rsp);
sp = align_down(sp - 0x10, 0x10) + 8;
emu.reg(x64_register::rsp, sp);
}
void setup_stack(x64_emulator& emu, const uint64_t stack_base, const size_t stack_size)
{
const uint64_t stack_end = stack_base + stack_size;
emu.reg(x64_register::rsp, stack_end);
}
void setup_gs_segment(x64_emulator& emu, const emulator_allocator& allocator)
{
struct msr_value
{
uint32_t id;
uint64_t value;
};
const msr_value value{IA32_GS_BASE_MSR, allocator.get_base()};
emu.write_register(x64_register::msr, &value, sizeof(value));
}
uint64_t copy_string(x64_emulator& emu, emulator_allocator& allocator, const void* base_ptr, const uint64_t offset,
const size_t length)
{
@@ -318,199 +284,6 @@ namespace
});
}
using exception_record = EMU_EXCEPTION_RECORD<EmulatorTraits<Emu64>>;
using exception_record_map = std::unordered_map<const exception_record*, emulator_object<exception_record>>;
emulator_object<exception_record> save_exception_record(emulator_allocator& allocator,
const exception_record& record,
exception_record_map& record_mapping)
{
const auto record_obj = allocator.reserve<exception_record>();
record_obj.write(record);
if (record.ExceptionRecord)
{
record_mapping.emplace(&record, record_obj);
emulator_object<exception_record> nested_record_obj{allocator.get_memory()};
const auto nested_record = record_mapping.find(reinterpret_cast<exception_record*>(record.ExceptionRecord));
if (nested_record != record_mapping.end())
{
nested_record_obj = nested_record->second;
}
else
{
nested_record_obj = save_exception_record(
allocator, *reinterpret_cast<exception_record*>(record.ExceptionRecord), record_mapping);
}
record_obj.access([&](exception_record& r) {
r.ExceptionRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(nested_record_obj.ptr());
});
}
return record_obj;
}
emulator_object<exception_record> save_exception_record(emulator_allocator& allocator,
const exception_record& record)
{
exception_record_map record_mapping{};
return save_exception_record(allocator, record, record_mapping);
}
uint32_t map_violation_operation_to_parameter(const memory_operation operation)
{
switch (operation)
{
default:
case memory_operation::read:
return 0;
case memory_operation::write:
case memory_operation::exec:
return 1;
}
}
size_t calculate_exception_record_size(const exception_record& record)
{
std::unordered_set<const exception_record*> records{};
size_t total_size = 0;
const exception_record* current_record = &record;
while (current_record)
{
if (!records.insert(current_record).second)
{
break;
}
total_size += sizeof(*current_record);
current_record = reinterpret_cast<exception_record*>(record.ExceptionRecord);
}
return total_size;
}
struct machine_frame
{
uint64_t rip;
uint64_t cs;
uint64_t eflags;
uint64_t rsp;
uint64_t ss;
};
void dispatch_exception_pointers(x64_emulator& emu, const uint64_t dispatcher,
const EMU_EXCEPTION_POINTERS<EmulatorTraits<Emu64>> pointers)
{
constexpr auto mach_frame_size = 0x40;
constexpr auto context_record_size = 0x4F0;
const auto exception_record_size =
calculate_exception_record_size(*reinterpret_cast<exception_record*>(pointers.ExceptionRecord));
const auto combined_size = align_up(exception_record_size + context_record_size, 0x10);
assert(combined_size == 0x590);
const auto allocation_size = combined_size + mach_frame_size;
const auto initial_sp = emu.reg(x64_register::rsp);
const auto new_sp = align_down(initial_sp - allocation_size, 0x100);
const auto total_size = initial_sp - new_sp;
assert(total_size >= allocation_size);
std::vector<uint8_t> zero_memory{};
zero_memory.resize(total_size, 0);
emu.write_memory(new_sp, zero_memory.data(), zero_memory.size());
emu.reg(x64_register::rsp, new_sp);
emu.reg(x64_register::rip, dispatcher);
const emulator_object<CONTEXT64> context_record_obj{emu, new_sp};
context_record_obj.write(*reinterpret_cast<CONTEXT64*>(pointers.ContextRecord));
emulator_allocator allocator{emu, new_sp + context_record_size, exception_record_size};
const auto exception_record_obj =
save_exception_record(allocator, *reinterpret_cast<exception_record*>(pointers.ExceptionRecord));
if (exception_record_obj.value() != allocator.get_base())
{
throw std::runtime_error("Bad exception record position on stack");
}
const emulator_object<machine_frame> machine_frame_obj{emu, new_sp + combined_size};
machine_frame_obj.access([&](machine_frame& frame) {
const auto& record = *reinterpret_cast<CONTEXT64*>(pointers.ContextRecord);
frame.rip = record.Rip;
frame.rsp = record.Rsp;
frame.ss = record.SegSs;
frame.cs = record.SegCs;
frame.eflags = record.EFlags;
});
}
template <typename T>
requires(std::is_integral_v<T>)
void dispatch_exception(x64_emulator& emu, const process_context& proc, const T status,
const std::vector<EmulatorTraits<Emu64>::ULONG_PTR>& parameters)
{
CONTEXT64 ctx{};
ctx.ContextFlags = CONTEXT64_ALL;
context_frame::save(emu, ctx);
exception_record record{};
memset(&record, 0, sizeof(record));
record.ExceptionCode = static_cast<DWORD>(status);
record.ExceptionFlags = 0;
record.ExceptionRecord = 0;
record.ExceptionAddress = emu.read_instruction_pointer();
record.NumberParameters = static_cast<DWORD>(parameters.size());
if (parameters.size() > 15)
{
throw std::runtime_error("Too many exception parameters");
}
for (size_t i = 0; i < parameters.size(); ++i)
{
record.ExceptionInformation[i] = parameters[i];
}
EMU_EXCEPTION_POINTERS<EmulatorTraits<Emu64>> pointers{};
pointers.ContextRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(&ctx);
pointers.ExceptionRecord = reinterpret_cast<EmulatorTraits<Emu64>::PVOID>(&record);
dispatch_exception_pointers(emu, proc.ki_user_exception_dispatcher, pointers);
}
void dispatch_access_violation(x64_emulator& emu, const process_context& proc, const uint64_t address,
const memory_operation operation)
{
dispatch_exception(emu, proc, STATUS_ACCESS_VIOLATION,
{
map_violation_operation_to_parameter(operation),
address,
});
}
void dispatch_illegal_instruction_violation(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_ILLEGAL_INSTRUCTION, {});
}
void dispatch_integer_division_by_zero(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_INTEGER_DIVIDE_BY_ZERO, {});
}
void dispatch_single_step(x64_emulator& emu, const process_context& proc)
{
dispatch_exception(emu, proc, STATUS_SINGLE_STEP, {});
}
void perform_context_switch_work(windows_emulator& win_emu)
{
auto& devices = win_emu.process().devices;
@@ -548,7 +321,7 @@ namespace
auto& emu = win_emu.emu();
auto& context = win_emu.process();
const auto is_ready = thread.is_thread_ready(win_emu);
const auto is_ready = thread.is_thread_ready(context);
if (!is_ready && !force)
{
@@ -623,207 +396,6 @@ namespace
return false;
}
bool is_object_signaled(process_context& c, const handle h, const uint32_t current_thread_id)
{
const auto type = h.value.type;
switch (type)
{
default:
break;
case handle_types::event: {
if (h.value.is_pseudo)
{
return true;
}
auto* e = c.events.get(h);
if (e)
{
return e->is_signaled();
}
break;
}
case handle_types::mutant: {
auto* e = c.mutants.get(h);
return !e || e->try_lock(current_thread_id);
}
case handle_types::semaphore: {
auto* s = c.semaphores.get(h);
if (s)
{
return s->try_lock();
}
break;
}
case handle_types::thread: {
const auto* t = c.threads.get(h);
if (t)
{
return t->is_terminated();
}
break;
}
}
throw std::runtime_error("Bad object: " + std::to_string(h.value.type));
}
}
emulator_thread::emulator_thread(memory_manager& memory, const process_context& context, const uint64_t start_address,
const uint64_t argument, const uint64_t stack_size, const uint32_t id)
: memory_ptr(&memory),
stack_size(page_align_up(std::max(stack_size, static_cast<uint64_t>(STACK_SIZE)))),
start_address(start_address),
argument(argument),
id(id),
last_registers(context.default_register_set)
{
this->stack_base = memory.allocate_memory(this->stack_size, memory_permission::read_write);
this->gs_segment = emulator_allocator{
memory,
memory.allocate_memory(GS_SEGMENT_SIZE, memory_permission::read_write),
GS_SEGMENT_SIZE,
};
this->teb = this->gs_segment->reserve<TEB64>();
this->teb->access([&](TEB64& teb_obj) {
// Skips GetCurrentNlsCache
// This hack can be removed once this is fixed:
// https://github.com/momo5502/emulator/issues/128
reinterpret_cast<uint8_t*>(&teb_obj)[0x179C] = 1;
teb_obj.ClientId.UniqueProcess = 1ul;
teb_obj.ClientId.UniqueThread = static_cast<uint64_t>(this->id);
teb_obj.NtTib.StackLimit = reinterpret_cast<std::uint64_t*>(this->stack_base);
teb_obj.NtTib.StackBase = reinterpret_cast<std::uint64_t*>(this->stack_base + this->stack_size);
teb_obj.NtTib.Self = &this->teb->ptr()->NtTib;
teb_obj.ProcessEnvironmentBlock = context.peb.ptr();
});
}
void emulator_thread::mark_as_ready(const NTSTATUS status)
{
this->pending_status = status;
this->await_time = {};
this->await_objects = {};
// TODO: Find out if this is correct
if (this->waiting_for_alert)
{
this->alerted = false;
}
this->waiting_for_alert = false;
}
bool emulator_thread::is_terminated() const
{
return this->exit_status.has_value();
}
bool emulator_thread::is_thread_ready(windows_emulator& win_emu)
{
if (this->is_terminated())
{
return false;
}
if (this->waiting_for_alert)
{
if (this->alerted)
{
this->mark_as_ready(STATUS_ALERTED);
return true;
}
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_TIMEOUT);
return true;
}
return false;
}
if (!this->await_objects.empty())
{
bool all_signaled = true;
for (uint32_t i = 0; i < this->await_objects.size(); ++i)
{
const auto& obj = this->await_objects[i];
const auto signaled = is_object_signaled(win_emu.process(), obj, this->id);
all_signaled &= signaled;
if (signaled && this->await_any)
{
this->mark_as_ready(STATUS_WAIT_0 + i);
return true;
}
}
if (!this->await_any && all_signaled)
{
this->mark_as_ready(STATUS_SUCCESS);
return true;
}
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_TIMEOUT);
return true;
}
return false;
}
if (this->await_time.has_value())
{
if (this->is_await_time_over())
{
this->mark_as_ready(STATUS_SUCCESS);
return true;
}
return false;
}
return true;
}
void emulator_thread::setup_registers(x64_emulator& emu, const process_context& context) const
{
setup_stack(emu, this->stack_base, this->stack_size);
setup_gs_segment(emu, *this->gs_segment);
CONTEXT64 ctx{};
ctx.ContextFlags = CONTEXT64_ALL;
unalign_stack(emu);
context_frame::save(emu, ctx);
ctx.Rip = context.rtl_user_thread_start;
ctx.Rcx = this->start_address;
ctx.Rdx = this->argument;
const auto ctx_obj = allocate_object_on_stack<CONTEXT64>(emu);
ctx_obj.write(ctx);
unalign_stack(emu);
emu.reg(x64_register::rcx, ctx_obj.value());
emu.reg(x64_register::rdx, context.ntdll->image_base);
emu.reg(x64_register::rip, context.ldr_initialize_thunk);
}
std::unique_ptr<x64_emulator> create_default_x64_emulator()
@@ -1146,7 +718,7 @@ void windows_emulator::start(std::chrono::nanoseconds timeout, size_t count)
while (true)
{
if (this->switch_thread_ || !this->current_thread().is_thread_ready(*this))
if (this->switch_thread_ || !this->current_thread().is_thread_ready(this->process()))
{
this->perform_thread_switch();
}

View File

@@ -0,0 +1,252 @@
#pragma once
#include "handles.hpp"
#include <serialization_helper.hpp>
#include <utils/file_handle.hpp>
#include <platform/synchronisation.hpp>
struct event : ref_counted_object
{
bool signaled{};
EVENT_TYPE type{};
std::u16string name{};
bool is_signaled()
{
const auto res = this->signaled;
if (this->type == SynchronizationEvent)
{
this->signaled = false;
}
return res;
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->signaled);
buffer.write(this->type);
buffer.write(this->name);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->signaled);
buffer.read(this->type);
buffer.read(this->name);
}
};
struct mutant : ref_counted_object
{
uint32_t locked_count{0};
uint32_t owning_thread_id{};
std::u16string name{};
bool try_lock(const uint32_t thread_id)
{
if (this->locked_count == 0)
{
++this->locked_count;
this->owning_thread_id = thread_id;
return true;
}
if (this->owning_thread_id != thread_id)
{
return false;
}
++this->locked_count;
return true;
}
std::pair<uint32_t, bool> release(const uint32_t thread_id)
{
const auto old_count = this->locked_count;
if (this->locked_count <= 0 || this->owning_thread_id != thread_id)
{
return {old_count, false};
}
--this->locked_count;
return {old_count, true};
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->locked_count);
buffer.write(this->owning_thread_id);
buffer.write(this->name);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->locked_count);
buffer.read(this->owning_thread_id);
buffer.read(this->name);
}
};
struct file_entry
{
std::filesystem::path file_path{};
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->file_path);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(this->file_path);
}
};
struct file_enumeration_state
{
size_t current_index{0};
std::vector<file_entry> files{};
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->current_index);
buffer.write_vector(this->files);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(this->current_index);
buffer.read_vector(this->files);
}
};
struct file : ref_counted_object
{
utils::file_handle handle{};
std::u16string name{};
std::optional<file_enumeration_state> enumeration_state{};
bool is_file() const
{
return this->handle;
}
bool is_directory() const
{
return !this->is_file();
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
// TODO: Serialize handle
buffer.write(this->name);
buffer.write_optional(this->enumeration_state);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read_optional(this->enumeration_state);
this->handle = {};
}
};
struct section : ref_counted_object
{
std::u16string name{};
std::u16string file_name{};
uint64_t maximum_size{};
uint32_t section_page_protection{};
uint32_t allocation_attributes{};
bool is_image() const
{
return this->allocation_attributes & SEC_IMAGE;
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->file_name);
buffer.write(this->maximum_size);
buffer.write(this->section_page_protection);
buffer.write(this->allocation_attributes);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->file_name);
buffer.read(this->maximum_size);
buffer.read(this->section_page_protection);
buffer.read(this->allocation_attributes);
}
};
struct semaphore : ref_counted_object
{
std::u16string name{};
uint32_t current_count{};
uint32_t max_count{};
bool try_lock()
{
if (this->current_count > 0)
{
--this->current_count;
return true;
}
return false;
}
std::pair<uint32_t, bool> release(const uint32_t release_count)
{
const auto old_count = this->current_count;
if (this->current_count + release_count > this->max_count)
{
return {old_count, false};
}
this->current_count += release_count;
return {old_count, true};
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->current_count);
buffer.write(this->max_count);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->current_count);
buffer.read(this->max_count);
}
};
struct port : ref_counted_object
{
std::u16string name{};
uint64_t view_base{};
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->name);
buffer.write(this->view_base);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->name);
buffer.read(this->view_base);
}
};