// // Dis VM // File: thread.cpp // Author: arr // #include #include #include #include #include #include #include #include #include "execution_table.h" #include "tool_dispatch.h" using namespace disvm; using namespace disvm::runtime; namespace { // Update VM registers based on the instruction. void decode_address(const vm_exec_op_t &inst, vm_registers_t ®) { switch (inst.source.mode) { case address_mode_t::offset_indirect_fp: reg.src = reinterpret_cast(reinterpret_cast(reg.stack.peek_frame()->base()) + inst.source.register1); break; case address_mode_t::offset_indirect_mp: reg.src = reinterpret_cast(reinterpret_cast(reg.mp_base->get_allocation()) + inst.source.register1); break; case address_mode_t::offset_double_indirect_fp: { const auto frame_offset = *reinterpret_cast(reinterpret_cast(reg.stack.peek_frame()->base()) + inst.source.register1); reg.src = reinterpret_cast(frame_offset + inst.source.register2); break; } case address_mode_t::offset_double_indirect_mp: { const auto mp_offset = *reinterpret_cast(reinterpret_cast(reg.mp_base->get_allocation()) + inst.source.register1); reg.src = reinterpret_cast(mp_offset + inst.source.register2); break; } case address_mode_t::immediate: reg.src = reinterpret_cast(const_cast(&inst.source.register1)); break; case address_mode_t::none: reg.src = nullptr; break; default: assert(false && "Unknown source addressing"); } switch (inst.destination.mode) { case address_mode_t::offset_indirect_fp: reg.dest = reinterpret_cast(reinterpret_cast(reg.stack.peek_frame()->base()) + inst.destination.register1); break; case address_mode_t::offset_indirect_mp: reg.dest = reinterpret_cast(reinterpret_cast(reg.mp_base->get_allocation()) + inst.destination.register1); break; case address_mode_t::offset_double_indirect_fp: { const auto frame_offset = *reinterpret_cast(reinterpret_cast(reg.stack.peek_frame()->base()) + inst.destination.register1); reg.dest = reinterpret_cast(frame_offset + inst.destination.register2); break; } case address_mode_t::offset_double_indirect_mp: { const auto mp_offset = *reinterpret_cast(reinterpret_cast(reg.mp_base->get_allocation()) + inst.destination.register1); reg.dest = reinterpret_cast(mp_offset + inst.destination.register2); break; } case address_mode_t::immediate: reg.dest = reinterpret_cast(const_cast(&inst.destination.register1)); break; case address_mode_t::none: reg.dest = nullptr; break; default: assert(false && "Unknown destination addressing"); } switch (inst.middle.mode) { case address_mode_middle_t::small_offset_indirect_fp: reg.mid = reinterpret_cast(reinterpret_cast(reg.stack.peek_frame()->base()) + inst.middle.register1); break; case address_mode_middle_t::small_offset_indirect_mp: reg.mid = reinterpret_cast(reinterpret_cast(reg.mp_base->get_allocation()) + inst.middle.register1); break; case address_mode_middle_t::small_immediate: reg.mid = reinterpret_cast(const_cast(&inst.middle.register1)); break; case address_mode_middle_t::none: // [SPEC] This is an undocumented expectation but required in many operations (e.g. i++). reg.mid = reg.dest; break; default: assert(false && "Unknown middle addressing"); } #ifndef NDEBUG // This is a perf critical function so logging is only available in debug builds if (debug::is_component_tracing_enabled()) { debug::log_msg( debug::component_trace_t::addressing, debug::log_level_t::debug, "decode: registers: %d (%#" PRIxPTR " %#" PRIxPTR " %#" PRIxPTR ")\n", inst.opcode, reg.src, reg.mid, reg.dest); } #endif } uint32_t get_unique_thread_id() { static std::atomic thread_id_counter{ 1 }; return thread_id_counter.fetch_add(1); } } vm_registers_t::vm_registers_t( const vm_thread_t &thread, vm_module_ref_t &entry) : current_thread_state{ vm_thread_state_t::ready } , module_ref{ &entry } , mp_base{ entry.mp_base } , thread{ thread } , pc{ entry.module->header.entry_pc } , stack{ entry.module->header.stack_extent } , tool_dispatch{ nullptr } , src{ nullptr } , mid{ nullptr } , dest{ nullptr } { // Add a reference to the module ref and MP register if (mp_base != nullptr) mp_base->add_ref(); module_ref->add_ref(); } vm_registers_t::~vm_registers_t() { dec_ref_count_and_free(mp_base); debug::assign_debug_pointer(&mp_base); dec_ref_count_and_free(module_ref); debug::assign_debug_pointer(&module_ref); } void disvm::runtime::walk_stack(const vm_registers_t &r, vm_stack_walk_callback_t callback) { assert(callback != nullptr); auto current_pc = r.pc; auto current_module = r.module_ref; auto current_frame = r.stack.peek_frame(); while (current_frame != nullptr) { if (!callback(current_frame->base(), current_pc, *current_module)) break; current_pc = current_frame->prev_pc(); auto prev_module_maybe = current_frame->prev_module_ref(); if (prev_module_maybe != nullptr) current_module = prev_module_maybe; current_frame = current_frame->prev_frame(); } } std::shared_ptr vm_thread_t::type_desc() { return intrinsic_type_desc::type(); } vm_thread_t::vm_thread_t( vm_module_ref_t &entry, uint32_t parent_thread_id) : vm_alloc_t(vm_thread_t::type_desc()) , _parent_thread_id{ parent_thread_id } , _registers{ *this, entry } , _thread_id{ get_unique_thread_id() } { assert(_parent_thread_id != _thread_id); assert(static_cast(_registers.pc) < entry.code_section.size()); // Set up the stack const auto entry_type = entry.module->header.entry_type; auto frame_type = entry.type_section[entry_type]; _registers.stack.alloc_frame(frame_type); // Pushing the initial frame sets the FP register _registers.stack.push_frame(); debug::log_msg(debug::component_trace_t::thread, debug::log_level_t::debug, "init: vm thread: %d %d\n", _thread_id, _parent_thread_id); } vm_thread_t::vm_thread_t( vm_module_ref_t &entry, uint32_t parent_thread_id, const vm_frame_t &initial_frame, vm_pc_t start_pc) : vm_alloc_t(vm_thread_t::type_desc()) , _parent_thread_id{ parent_thread_id } , _registers{ *this, entry } , _thread_id{ get_unique_thread_id() } { _registers.pc = start_pc; assert(_parent_thread_id != _thread_id); assert(static_cast(_registers.pc) < entry.code_section.size()); // Set the stack auto frame_type = initial_frame.frame_type; _registers.stack.alloc_frame(frame_type); // Pushing the initial frame sets the FP register auto current_frame = _registers.stack.push_frame(); // Copy over the frame into this thread - pass arguments to the thread current_frame->copy_frame_contents(initial_frame); if (debug::is_component_tracing_enabled()) debug::log_msg(debug::component_trace_t::thread, debug::log_level_t::debug, "init: vm thread: %d %d\n", _thread_id, _parent_thread_id); } vm_thread_t::~vm_thread_t() { if (debug::is_component_tracing_enabled()) debug::log_msg(debug::component_trace_t::thread, debug::log_level_t::debug, "destroy: vm thread: %d %d\n", _thread_id, _parent_thread_id); } vm_thread_state_t vm_thread_t::execute(vm_t &vm, const uint32_t quanta) { _registers.current_thread_state = vm_thread_state_t::running; assert(_registers.pc != runtime_constants::invalid_program_counter); auto execution_duration = uint32_t{ 0 }; try { for (; execution_duration < quanta; ++execution_duration) { if (_registers.stack.peek_frame() == nullptr) { if (debug::is_component_tracing_enabled()) debug::log_msg(debug::component_trace_t::thread, debug::log_level_t::debug, "exit: vm thread: stack exhausted: %d\n", _thread_id); _registers.current_thread_state = vm_thread_state_t::empty_stack; auto tool_dispatch = _registers.tool_dispatch.load(); if (tool_dispatch != nullptr) tool_dispatch->on_thread_end(*this); break; } const auto &code_section = _registers.module_ref->code_section; assert(static_cast(_registers.pc) < code_section.size()); assert(!_registers.module_ref->is_builtin_module() && "Interpreter thread is unable to execute native instructions"); const auto &inst = code_section[_registers.pc].op; const auto opcode = static_cast(inst.opcode); if (static_cast(opcode_t::last_opcode) < opcode) throw vm_system_exception{ "Unknown op-code" }; // Instruction is valid, decode and update registers for operation decode_address(inst, _registers); _registers.pc++; vm_exec_table[opcode](_registers, vm); if (_registers.current_thread_state != vm_thread_state_t::running) break; } } catch (const unhandled_user_exception &uue) { // [TODO] Store the exception std::fprintf(stderr, "%s - '%s' in %s at instruction %d\n", uue.what(), uue.exception_id, uue.module_name, uue.program_counter); _registers.current_thread_state = vm_thread_state_t::broken; } catch (const index_out_of_range_memory &ioor) { std::fprintf(stderr, "%s - %d [%d,%d]\n", ioor.what(), ioor.invalid_value, ioor.valid_min, ioor.valid_max); _registers.current_thread_state = vm_thread_state_t::broken; } catch (const vm_user_exception &ue) { std::fprintf(stderr, "%s\n", ue.what()); _registers.current_thread_state = vm_thread_state_t::broken; } // Log the execution result if (debug::is_component_tracing_enabled()) { debug::log_msg( debug::component_trace_t::thread, debug::log_level_t::debug, "status: vm thread: execute: %d %d %d %d\n", _thread_id, execution_duration, quanta, _registers.current_thread_state); } // If the thread is still in the running state transition it back to ready. if (_registers.current_thread_state == vm_thread_state_t::running) _registers.current_thread_state = vm_thread_state_t::ready; return _registers.current_thread_state; } void vm_thread_t::set_tool_dispatch(vm_tool_dispatch_t *dispatch) { assert((_registers.tool_dispatch.load() == nullptr) != (dispatch == nullptr) && "Dispatch value should toggle between null and non-null"); _registers.tool_dispatch = dispatch; } vm_thread_state_t vm_thread_t::get_state() const { return _registers.current_thread_state; } uint32_t vm_thread_t::get_thread_id() const { return _thread_id; } uint32_t vm_thread_t::get_parent_thread_id() const { return _parent_thread_id; } const vm_registers_t& vm_thread_t::get_registers() const { return _registers; }