mirror of
https://github.com/open-goal/jak-project
synced 2026-08-06 09:54:10 -04:00
fe2086acfb
This adds a debug server to `goalc` that sends JSON over the socket to communicate with an external debugger using the Debug Adapter Protocol. This lets us debug GOAL code in a proper debugger with breakpoints, step over, step in and step out per line, stack frames and supports watches for global symbols, registers and local variables (local variables only work within the most recent stack frame). Special registers (`r13`, `r14`, `r15`, argument registers, etc.) are tracked separately and the current process register even displays the type of the current `pp` if possible. Watches that track addresses holding a reference type generate a list of field names according to the object's type. All fields will show their name, type and value and, depending on the type, will try to infer extra info like symbol names/values, function names for `function` fields, enum values and more. This also works nested, so any field that is also a reference type can also be accessed and display its fields, etc. Dynamic arrays are also supported where possible, e.g. in `inline-array-class` children and boxed arrays, it will figure out the value of the `length` field and access the memory up to that point so all the elements can be accessed and viewed from the `data` field. Our VS Code extension implements the DAP in open-goal/opengoal-vscode#375. Using it is as simple as connecting a REPL to a running game instance with `(lt)`, compiling with `(mi)` and, with the extension installed, pressing F5 in VS Code to start the debugger. By default, it will try to connect to the game that the active `.gc` file is from, the socket port is different per game (8128 for Jak 1, 8129 for Jak 2, 8130 for Jak 3). The `launch.json` was updated with two entries for this, the second entry lets you pick the game/port manually if desired. ~~Not tested on Windows.~~ Only supports x86 for now.
449 lines
16 KiB
C++
449 lines
16 KiB
C++
/*!
|
|
* @file CodeGenerator.cpp
|
|
* Generate object files from a FileEnv using an emitter::ObjectGenerator.
|
|
* Populates a DebugInfo.
|
|
* Currently owns the logic for emitting the function prologues/epilogues and stack spill ops.
|
|
*/
|
|
|
|
#include "CodeGenerator.h"
|
|
|
|
#include <algorithm>
|
|
#include <stdexcept>
|
|
#include <unordered_set>
|
|
|
|
#include "IR.h"
|
|
|
|
#include "goalc/debugger/DebugInfo.h"
|
|
#include "goalc/emitter/IGen.h"
|
|
|
|
#include "fmt/format.h"
|
|
|
|
using namespace emitter;
|
|
|
|
namespace {
|
|
|
|
void record_local_variables(const FunctionEnv* func, FunctionDebugInfo* debug) {
|
|
const auto& allocations = func->allocations();
|
|
if (!allocations.ok || allocations.ass_as_ranges.empty()) {
|
|
return;
|
|
}
|
|
|
|
// ireg id -> source name, parameters first, then anything bound in a lexical scope
|
|
std::unordered_map<int, std::pair<std::string, bool>> names;
|
|
for (const auto& [symbol, reg_val] : func->params) {
|
|
if (reg_val) {
|
|
names[reg_val->ireg().id] = {symbol.name_ptr, true};
|
|
}
|
|
}
|
|
for (const auto& env : func->child_envs()) {
|
|
auto* lexical = dynamic_cast<LexicalEnv*>(env.get());
|
|
if (!lexical) {
|
|
continue;
|
|
}
|
|
for (const auto& [symbol, reg_val] : lexical->vars) {
|
|
if (reg_val && names.find(reg_val->ireg().id) == names.end()) {
|
|
names[reg_val->ireg().id] = {symbol.name_ptr, false};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (names.empty()) {
|
|
return;
|
|
}
|
|
|
|
const int instruction_count = int(func->code().size());
|
|
|
|
for (const auto& [ireg_id, name_info] : names) {
|
|
if (ireg_id < 0 || ireg_id >= int(allocations.ass_as_ranges.size()) ||
|
|
ireg_id >= int(func->reg_vals().size())) {
|
|
continue;
|
|
}
|
|
const auto& range = allocations.ass_as_ranges.at(ireg_id);
|
|
|
|
LocalVariableDebugInfo local;
|
|
local.name = name_info.first;
|
|
local.is_parameter = name_info.second;
|
|
local.type = func->reg_vals().at(ireg_id)->type();
|
|
|
|
for (int instr = 0; instr < instruction_count; instr++) {
|
|
if (!range.is_live_at_instr(instr)) {
|
|
continue;
|
|
}
|
|
const auto& assignment = range.get(instr);
|
|
if (!assignment.is_assigned()) {
|
|
continue;
|
|
}
|
|
|
|
VariableLocation here;
|
|
here.start_ir = instr;
|
|
here.end_ir = instr;
|
|
if (assignment.kind == Assignment::Kind::REGISTER) {
|
|
here.kind = VariableLocation::Kind::REGISTER;
|
|
here.reg = assignment.reg.id();
|
|
} else if (assignment.kind == Assignment::Kind::STACK) {
|
|
here.kind = VariableLocation::Kind::STACK;
|
|
// spilled variables sit at rsp + slot * 8, matching how the spill ops address them
|
|
here.stack_offset = allocations.get_slot_for_spill(assignment.stack_slot) * GPR_SIZE;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
// extend the previous interval instead of starting a new one where nothing changed
|
|
if (!local.locations.empty()) {
|
|
auto& previous = local.locations.back();
|
|
if (previous.end_ir == instr - 1 && previous.kind == here.kind &&
|
|
previous.reg == here.reg && previous.stack_offset == here.stack_offset) {
|
|
previous.end_ir = instr;
|
|
continue;
|
|
}
|
|
}
|
|
local.locations.push_back(here);
|
|
}
|
|
|
|
if (!local.locations.empty()) {
|
|
debug->locals.push_back(std::move(local));
|
|
}
|
|
}
|
|
|
|
// parameters first, then alphabetically
|
|
std::sort(debug->locals.begin(), debug->locals.end(),
|
|
[](const LocalVariableDebugInfo& a, const LocalVariableDebugInfo& b) {
|
|
if (a.is_parameter != b.is_parameter) {
|
|
return a.is_parameter;
|
|
}
|
|
return a.name < b.name;
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CodeGenerator::CodeGenerator(FileEnv* env,
|
|
DebugInfo* debug_info,
|
|
GameVersion version,
|
|
InstructionSet instruction_set)
|
|
: m_gen(version, instruction_set), m_fe(env), m_debug_info(debug_info) {}
|
|
|
|
/*!
|
|
* Generate an object file.
|
|
*/
|
|
std::vector<u8> CodeGenerator::run(const TypeSystem* ts) {
|
|
std::unordered_set<std::string> function_names;
|
|
|
|
// first, add each function to the ObjectGenerator (but don't add any data)
|
|
for (auto& f : m_fe->functions()) {
|
|
if (function_names.find(f->name()) == function_names.end()) {
|
|
function_names.insert(f->name());
|
|
} else {
|
|
printf("Failed to codegen, there are two functions with internal names [%s]\n",
|
|
f->name().c_str());
|
|
throw std::runtime_error("Failed to codegen.");
|
|
}
|
|
auto rec =
|
|
m_gen.add_function_to_seg(f->segment, &m_debug_info->add_function(f->name(), m_fe->name()));
|
|
for (auto& x : f->code_source()) {
|
|
rec.debug->code_sources.push_back(x.heap_obj);
|
|
}
|
|
for (auto& x : f->code()) {
|
|
rec.debug->ir_strings.push_back(x->print());
|
|
}
|
|
record_local_variables(f.get(), rec.debug);
|
|
}
|
|
|
|
// next, add all static objects.
|
|
for (auto& static_obj : m_fe->statics()) {
|
|
static_obj->generate(&m_gen);
|
|
}
|
|
|
|
// next, add instructions to functions
|
|
for (size_t i = 0; i < m_fe->functions().size(); i++) {
|
|
do_function(m_fe->functions().at(i).get(), i);
|
|
}
|
|
|
|
// generate a v3 object.
|
|
return m_gen.generate_data_v3(ts).to_vector();
|
|
}
|
|
|
|
void CodeGenerator::do_function(FunctionEnv* env, int f_idx) {
|
|
if (env->is_asm_func) {
|
|
if (m_gen.instr_set() == InstructionSet::X86) {
|
|
do_asm_function_x86(env, f_idx, env->asm_func_saved_regs);
|
|
} else if (m_gen.instr_set() == InstructionSet::ARM64) {
|
|
do_asm_function_arm64(env, f_idx, env->asm_func_saved_regs);
|
|
} else {
|
|
throw std::runtime_error("CodeGenerator::do_function, instruction set not supported");
|
|
}
|
|
} else {
|
|
if (m_gen.instr_set() == InstructionSet::X86) {
|
|
do_goal_function_x86(env, f_idx);
|
|
} else if (m_gen.instr_set() == InstructionSet::ARM64) {
|
|
do_goal_function_arm64(env, f_idx);
|
|
} else {
|
|
throw std::runtime_error("CodeGenerator::do_function, instruction set not supported");
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Add instructions to the function, specified by index.
|
|
* Generates prologues / epilogues.
|
|
*/
|
|
void CodeGenerator::do_goal_function_x86(FunctionEnv* env, int f_idx) {
|
|
bool use_new_xmms = true;
|
|
auto* debug = &m_debug_info->function_by_name(env->name());
|
|
|
|
auto f_rec = m_gen.get_existing_function_record(f_idx);
|
|
// todo, extra alignment settings
|
|
|
|
auto& ri = emitter::gRegInfo;
|
|
const auto& allocs = env->alloc_result();
|
|
|
|
// compute how much stack we will use
|
|
int stack_offset = 0;
|
|
|
|
// count how many xmm's we have to backup
|
|
int n_xmm_backups = 0;
|
|
for (auto& saved_reg : allocs.used_saved_regs) {
|
|
if (saved_reg.is_xmm(m_gen.instr_set())) {
|
|
n_xmm_backups++;
|
|
}
|
|
}
|
|
|
|
// only for new xmms. if n == 0, we don't use this at all.
|
|
int xmm_backup_stack_offset = 8 + XMM_SIZE * n_xmm_backups;
|
|
|
|
if (use_new_xmms) {
|
|
if (n_xmm_backups > 0) {
|
|
// offset the stack
|
|
stack_offset += xmm_backup_stack_offset;
|
|
m_gen.add_instr_no_ir(f_rec, IGen::sub_gpr64_imm(m_gen, RSP, xmm_backup_stack_offset),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
// back up xmms
|
|
int i = 0;
|
|
for (auto& saved_reg : allocs.used_saved_regs) {
|
|
if (saved_reg.is_xmm(m_gen.instr_set())) {
|
|
int offset = i * XMM_SIZE;
|
|
m_gen.add_instr_no_ir(f_rec,
|
|
IGen::store128_xmm128_reg_offset(m_gen, RSP, saved_reg, offset),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// back up xmms (currently not aligned)
|
|
for (auto& saved_reg : allocs.used_saved_regs) {
|
|
if (saved_reg.is_xmm(m_gen.instr_set())) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::sub_gpr64_imm8s(m_gen, RSP, XMM_SIZE),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
m_gen.add_instr_no_ir(f_rec, IGen::store128_gpr64_simd128(m_gen, RSP, saved_reg),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
stack_offset += XMM_SIZE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// back up gprs
|
|
for (auto& saved_reg : allocs.used_saved_regs) {
|
|
if (saved_reg.is_gpr(m_gen.instr_set())) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::push_gpr64(m_gen, saved_reg),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
stack_offset += GPR_SIZE;
|
|
}
|
|
}
|
|
|
|
// do we include an extra push to get 8 more bytes to keep the stack aligned?
|
|
bool bonus_push = false;
|
|
|
|
// the offset to add directly to rsp for stack variables or spills (no push/pop)
|
|
int manually_added_stack_offset =
|
|
GPR_SIZE * (allocs.stack_slots_for_spills + allocs.stack_slots_for_vars);
|
|
stack_offset += manually_added_stack_offset;
|
|
|
|
// do we need to align or manually offset?
|
|
if (manually_added_stack_offset || allocs.needs_aligned_stack_for_spills ||
|
|
env->needs_aligned_stack()) {
|
|
if (!(stack_offset & 15)) {
|
|
if (manually_added_stack_offset) {
|
|
// if we're already adding to rsp, just add 8 more.
|
|
manually_added_stack_offset += 8;
|
|
} else {
|
|
// otherwise to an extra push, and remember so we can do an extra pop later on.
|
|
bonus_push = true;
|
|
m_gen.add_instr_no_ir(f_rec, IGen::push_gpr64(m_gen, ri.get_saved_gpr(0)),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
}
|
|
stack_offset += 8;
|
|
}
|
|
|
|
ASSERT(stack_offset & 15);
|
|
|
|
// do manual stack offset.
|
|
if (manually_added_stack_offset) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::sub_gpr64_imm(m_gen, RSP, manually_added_stack_offset),
|
|
InstructionInfo::Kind::PROLOGUE);
|
|
}
|
|
}
|
|
debug->stack_usage = stack_offset;
|
|
|
|
// emit each IR into x86 instructions.
|
|
for (int ir_idx = 0; ir_idx < int(env->code().size()); ir_idx++) {
|
|
auto& ir = env->code().at(ir_idx);
|
|
// start of IR
|
|
auto i_rec = m_gen.add_ir(f_rec);
|
|
|
|
// load anything off the stack that was spilled and is needed.
|
|
auto& bonus = allocs.stack_ops.at(ir_idx);
|
|
for (auto& op : bonus.ops) {
|
|
if (op.load) {
|
|
if (op.reg.is_gpr(m_gen.instr_set()) && op.reg_class == RegClass::GPR_64) {
|
|
// todo, s8 or 0 offset if possible?
|
|
m_gen.add_instr(IGen::load64_gpr64_plus_s32(
|
|
m_gen, op.reg, allocs.get_slot_for_spill(op.slot) * GPR_SIZE, RSP),
|
|
i_rec);
|
|
} else if (op.reg.is_xmm(m_gen.instr_set()) && op.reg_class == RegClass::FLOAT) {
|
|
// load xmm32 off of the stack
|
|
m_gen.add_instr(IGen::load_reg_offset_xmm32(
|
|
m_gen, op.reg, RSP, allocs.get_slot_for_spill(op.slot) * GPR_SIZE),
|
|
i_rec);
|
|
} else if (op.reg.is_xmm(m_gen.instr_set()) &&
|
|
(op.reg_class == RegClass::VECTOR_FLOAT || op.reg_class == RegClass::INT_128)) {
|
|
m_gen.add_instr(IGen::load128_xmm128_reg_offset(
|
|
m_gen, op.reg, RSP, allocs.get_slot_for_spill(op.slot) * GPR_SIZE),
|
|
i_rec);
|
|
} else {
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// do the actual op
|
|
ir->do_codegen_x86(&m_gen, allocs, i_rec);
|
|
|
|
// store things back on the stack if needed.
|
|
for (auto& op : bonus.ops) {
|
|
if (op.store) {
|
|
if (op.reg.is_gpr(m_gen.instr_set()) && op.reg_class == RegClass::GPR_64) {
|
|
// todo, s8 or 0 offset if possible?
|
|
m_gen.add_instr(IGen::store64_gpr64_plus_s32(
|
|
m_gen, RSP, allocs.get_slot_for_spill(op.slot) * GPR_SIZE, op.reg),
|
|
i_rec);
|
|
} else if (op.reg.is_xmm(m_gen.instr_set()) && op.reg_class == RegClass::FLOAT) {
|
|
// store xmm32 on the stack
|
|
m_gen.add_instr(IGen::store_reg_offset_xmm32(
|
|
m_gen, RSP, op.reg, allocs.get_slot_for_spill(op.slot) * GPR_SIZE),
|
|
i_rec);
|
|
} else if (op.reg.is_xmm(m_gen.instr_set()) &&
|
|
(op.reg_class == RegClass::VECTOR_FLOAT || op.reg_class == RegClass::INT_128)) {
|
|
m_gen.add_instr(IGen::store128_xmm128_reg_offset(
|
|
m_gen, RSP, op.reg, allocs.get_slot_for_spill(op.slot) * GPR_SIZE),
|
|
i_rec);
|
|
} else {
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
}
|
|
} // end IR loop
|
|
|
|
// EPILOGUE
|
|
if (manually_added_stack_offset || allocs.needs_aligned_stack_for_spills ||
|
|
env->needs_aligned_stack()) {
|
|
if (manually_added_stack_offset) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::add_gpr64_imm(m_gen, RSP, manually_added_stack_offset),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
|
|
if (bonus_push) {
|
|
ASSERT(!manually_added_stack_offset);
|
|
m_gen.add_instr_no_ir(f_rec, IGen::pop_gpr64(m_gen, ri.get_saved_gpr(0)),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
}
|
|
|
|
for (int i = int(allocs.used_saved_regs.size()); i-- > 0;) {
|
|
auto& saved_reg = allocs.used_saved_regs.at(i);
|
|
if (saved_reg.is_gpr(m_gen.instr_set())) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::pop_gpr64(m_gen, saved_reg),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
}
|
|
|
|
if (use_new_xmms) {
|
|
if (n_xmm_backups > 0) {
|
|
int j = n_xmm_backups;
|
|
for (int i = int(allocs.used_saved_regs.size()); i-- > 0;) {
|
|
auto& saved_reg = allocs.used_saved_regs.at(i);
|
|
if (saved_reg.is_xmm(m_gen.instr_set())) {
|
|
j--;
|
|
int offset = j * XMM_SIZE;
|
|
m_gen.add_instr_no_ir(f_rec,
|
|
IGen::load128_xmm128_reg_offset(m_gen, saved_reg, RSP, offset),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
}
|
|
ASSERT(j == 0);
|
|
m_gen.add_instr_no_ir(f_rec, IGen::add_gpr64_imm(m_gen, RSP, xmm_backup_stack_offset),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
} else {
|
|
for (int i = int(allocs.used_saved_regs.size()); i-- > 0;) {
|
|
auto& saved_reg = allocs.used_saved_regs.at(i);
|
|
if (saved_reg.is_xmm(m_gen.instr_set())) {
|
|
m_gen.add_instr_no_ir(f_rec, IGen::load128_simd128_gpr64(m_gen, saved_reg, RSP),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
m_gen.add_instr_no_ir(f_rec, IGen::add_gpr64_imm8s(m_gen, RSP, XMM_SIZE),
|
|
InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_gen.add_instr_no_ir(f_rec, IGen::ret(m_gen), InstructionInfo::Kind::EPILOGUE);
|
|
}
|
|
|
|
void CodeGenerator::do_goal_function_arm64(FunctionEnv* env, int f_idx) {
|
|
throw std::runtime_error("NYI - CodeGenerator::do_goal_function_arm64");
|
|
}
|
|
|
|
void CodeGenerator::do_asm_function_x86(FunctionEnv* env, int f_idx, bool allow_saved_regs) {
|
|
auto f_rec = m_gen.get_existing_function_record(f_idx);
|
|
const auto& allocs = env->alloc_result();
|
|
|
|
if (!allow_saved_regs && !allocs.used_saved_regs.empty()) {
|
|
std::string err = fmt::format(
|
|
"ASM Function {}'s coloring using the following callee-saved registers: ", env->name());
|
|
for (auto& x : allocs.used_saved_regs) {
|
|
err += x.print();
|
|
err += " ";
|
|
}
|
|
err.pop_back();
|
|
err.push_back('.');
|
|
throw std::runtime_error(err);
|
|
}
|
|
|
|
if (allocs.stack_slots_for_spills) {
|
|
throw std::runtime_error("ASM Function has used the stack for spills.");
|
|
}
|
|
|
|
if (allocs.stack_slots_for_vars) {
|
|
throw std::runtime_error("ASM Function has variables on the stack.");
|
|
}
|
|
|
|
// emit each IR into x86 instructions.
|
|
for (int ir_idx = 0; ir_idx < int(env->code().size()); ir_idx++) {
|
|
auto& ir = env->code().at(ir_idx);
|
|
// start of IR
|
|
auto i_rec = m_gen.add_ir(f_rec);
|
|
|
|
// Make sure we aren't automatically accessing the stack.
|
|
if (!allocs.stack_ops.at(ir_idx).ops.empty()) {
|
|
throw std::runtime_error("ASM Function used a bonus op.");
|
|
}
|
|
|
|
// do the actual op
|
|
ir->do_codegen_x86(&m_gen, allocs, i_rec);
|
|
}
|
|
}
|
|
|
|
void CodeGenerator::do_asm_function_arm64(FunctionEnv* env, int f_idx, bool allow_saved_regs) {
|
|
throw std::runtime_error("NYI - CodeGenerator::do_asm_function");
|
|
}
|