mirror of
https://github.com/open-goal/jak-project
synced 2026-09-09 20:21:28 -04:00
[Decompiler] WIP: Stack Spills (#382)
* set up types * cleaned up type analysis and got things working through atomic ops * expression working, need types * improved types and names * getting close * finish up dma-disasm * fix
This commit is contained in:
@@ -10,6 +10,7 @@ add_library(
|
||||
analysis/inline_asm_rewrite.cpp
|
||||
analysis/insert_lets.cpp
|
||||
analysis/reg_usage.cpp
|
||||
analysis/stack_spill.cpp
|
||||
analysis/type_analysis.cpp
|
||||
analysis/variable_naming.cpp
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
* Can print itself (within the context of a LinkedObjectFile).
|
||||
*/
|
||||
|
||||
#ifndef NEXT_INSTRUCTION_H
|
||||
#define NEXT_INSTRUCTION_H
|
||||
|
||||
#include <vector>
|
||||
#include "OpcodeInfo.h"
|
||||
#include "Register.h"
|
||||
@@ -112,4 +109,3 @@ class Instruction {
|
||||
int cop2_dest_mask_intel() const;
|
||||
};
|
||||
} // namespace decompiler
|
||||
#endif // NEXT_INSTRUCTION_H
|
||||
|
||||
@@ -1639,4 +1639,94 @@ void FunctionEndOp::collect_vars(RegAccessSet& vars) const {
|
||||
vars.insert(m_return_reg);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// StackSpillStoreOp
|
||||
/////////////////////////////
|
||||
|
||||
StackSpillStoreOp::StackSpillStoreOp(RegisterAccess value, int size, int offset, int my_idx)
|
||||
: AtomicOp(my_idx), m_value(value), m_size(size), m_offset(offset) {
|
||||
assert(m_value.mode() == AccessMode::READ);
|
||||
}
|
||||
|
||||
goos::Object StackSpillStoreOp::to_form(const std::vector<DecompilerLabel>&, const Env& env) const {
|
||||
return pretty_print::build_list(
|
||||
fmt::format("stack-store {} :offset {} :sz {}", m_value.to_string(env), m_offset, m_size));
|
||||
}
|
||||
|
||||
bool StackSpillStoreOp::operator==(const AtomicOp& other) const {
|
||||
if (typeid(StackSpillStoreOp) != typeid(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto po = dynamic_cast<const StackSpillStoreOp*>(&other);
|
||||
assert(po);
|
||||
return m_size == po->m_size && m_value == po->m_value && m_offset == po->m_offset;
|
||||
}
|
||||
|
||||
bool StackSpillStoreOp::is_sequence_point() const {
|
||||
return true; // this might not be totally true, but it seems kind of scary to allow it to
|
||||
// reorder.
|
||||
}
|
||||
|
||||
void StackSpillStoreOp::update_register_info() {
|
||||
m_read_regs.push_back(m_value.reg());
|
||||
}
|
||||
|
||||
void StackSpillStoreOp::collect_vars(RegAccessSet& vars) const {
|
||||
vars.insert(m_value);
|
||||
}
|
||||
|
||||
RegisterAccess StackSpillStoreOp::get_set_destination() const {
|
||||
throw std::runtime_error("StackSpillStoreOp cannot be treated as a set! operation");
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// StackSpillLoadOp
|
||||
/////////////////////////////
|
||||
|
||||
StackSpillLoadOp::StackSpillLoadOp(RegisterAccess dst,
|
||||
int size,
|
||||
int offset,
|
||||
bool is_signed,
|
||||
int my_idx)
|
||||
: AtomicOp(my_idx), m_dst(dst), m_size(size), m_offset(offset), m_is_signed(is_signed) {
|
||||
assert(m_dst.mode() == AccessMode::WRITE);
|
||||
}
|
||||
|
||||
goos::Object StackSpillLoadOp::to_form(const std::vector<DecompilerLabel>&, const Env& env) const {
|
||||
return pretty_print::build_list(fmt::format("stack-load {} :offset {} :sz {} :sext #{}",
|
||||
m_dst.to_string(env), m_offset, m_size,
|
||||
m_is_signed ? 't' : 'f'));
|
||||
}
|
||||
|
||||
bool StackSpillLoadOp::operator==(const AtomicOp& other) const {
|
||||
if (typeid(StackSpillStoreOp) != typeid(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto po = dynamic_cast<const StackSpillLoadOp*>(&other);
|
||||
assert(po);
|
||||
return m_size == po->m_size && m_dst == po->m_dst && m_offset == po->m_offset &&
|
||||
m_is_signed == po->m_is_signed;
|
||||
}
|
||||
|
||||
bool StackSpillLoadOp::is_sequence_point() const {
|
||||
return true; // this might not be totally true, but it seems kind of scary to allow it to
|
||||
// reorder.
|
||||
}
|
||||
|
||||
void StackSpillLoadOp::update_register_info() {
|
||||
m_write_regs.push_back(m_dst.reg());
|
||||
}
|
||||
|
||||
void StackSpillLoadOp::collect_vars(RegAccessSet& vars) const {
|
||||
vars.insert(m_dst);
|
||||
}
|
||||
|
||||
RegisterAccess StackSpillLoadOp::get_set_destination() const {
|
||||
// todo!
|
||||
throw std::runtime_error("StackSpillLoadOp cannot be treated as a set! operation");
|
||||
}
|
||||
|
||||
} // namespace decompiler
|
||||
|
||||
@@ -697,8 +697,7 @@ struct IR2_RegOffset {
|
||||
class FunctionEndOp : public AtomicOp {
|
||||
public:
|
||||
explicit FunctionEndOp(int my_idx);
|
||||
virtual goos::Object to_form(const std::vector<DecompilerLabel>& labels,
|
||||
const Env& env) const override;
|
||||
goos::Object to_form(const std::vector<DecompilerLabel>& labels, const Env& env) const override;
|
||||
bool operator==(const AtomicOp& other) const override;
|
||||
bool is_sequence_point() const override;
|
||||
RegisterAccess get_set_destination() const override;
|
||||
@@ -719,5 +718,52 @@ class FunctionEndOp : public AtomicOp {
|
||||
RegisterAccess m_return_reg;
|
||||
};
|
||||
|
||||
/*!
|
||||
* An operation to store a variable from the stack.
|
||||
*/
|
||||
class StackSpillStoreOp : public AtomicOp {
|
||||
public:
|
||||
StackSpillStoreOp(RegisterAccess value, int size, int offset, int my_idx);
|
||||
goos::Object to_form(const std::vector<DecompilerLabel>& labels, const Env& env) const override;
|
||||
bool operator==(const AtomicOp& other) const override;
|
||||
bool is_sequence_point() const override;
|
||||
FormElement* get_as_form(FormPool& pool, const Env& env) const override;
|
||||
RegisterAccess get_set_destination() const override;
|
||||
void update_register_info() override;
|
||||
TypeState propagate_types_internal(const TypeState& input,
|
||||
const Env& env,
|
||||
DecompilerTypeSystem& dts) override;
|
||||
void collect_vars(RegAccessSet& vars) const override;
|
||||
|
||||
private:
|
||||
RegisterAccess m_value;
|
||||
int m_size;
|
||||
int m_offset;
|
||||
};
|
||||
|
||||
/*!
|
||||
* An operation to load a variable from the stack.
|
||||
*/
|
||||
class StackSpillLoadOp : public AtomicOp {
|
||||
public:
|
||||
StackSpillLoadOp(RegisterAccess dst, int size, int offset, bool is_signed, int my_idx);
|
||||
goos::Object to_form(const std::vector<DecompilerLabel>& labels, const Env& env) const override;
|
||||
bool operator==(const AtomicOp& other) const override;
|
||||
bool is_sequence_point() const override;
|
||||
FormElement* get_as_form(FormPool& pool, const Env& env) const override;
|
||||
RegisterAccess get_set_destination() const override;
|
||||
void update_register_info() override;
|
||||
TypeState propagate_types_internal(const TypeState& input,
|
||||
const Env& env,
|
||||
DecompilerTypeSystem& dts) override;
|
||||
void collect_vars(RegAccessSet& vars) const override;
|
||||
|
||||
private:
|
||||
RegisterAccess m_dst;
|
||||
int m_size;
|
||||
int m_offset;
|
||||
bool m_is_signed;
|
||||
};
|
||||
|
||||
bool get_as_reg_offset(const SimpleExpression& expr, IR2_RegOffset* out);
|
||||
} // namespace decompiler
|
||||
|
||||
@@ -723,4 +723,29 @@ FormElement* FunctionEndOp::get_as_form(FormPool& pool, const Env&) const {
|
||||
FormElement* AsmBranchOp::get_as_form(FormPool& pool, const Env&) const {
|
||||
return pool.alloc_element<AtomicOpElement>(this);
|
||||
}
|
||||
|
||||
FormElement* StackSpillLoadOp::get_as_form(FormPool& pool, const Env& env) const {
|
||||
TypeSpec type("object");
|
||||
auto kv = env.stack_slot_entries.find(m_offset);
|
||||
if (kv != env.stack_slot_entries.end()) {
|
||||
type = kv->second.typespec;
|
||||
}
|
||||
return pool.alloc_element<SetVarElement>(m_dst,
|
||||
pool.alloc_single_element_form<StackSpillValueElement>(
|
||||
nullptr, m_size, m_offset, m_is_signed),
|
||||
true, type);
|
||||
}
|
||||
|
||||
FormElement* StackSpillStoreOp::get_as_form(FormPool& pool, const Env& env) const {
|
||||
auto& slot_type = env.stack_slot_entries.at(m_offset).typespec;
|
||||
auto src_type = env.get_types_before_op(m_my_idx).get(m_value.reg()).typespec();
|
||||
std::optional<TypeSpec> cast_type;
|
||||
|
||||
if (!env.dts->ts.tc(slot_type, src_type)) {
|
||||
// we fail the typecheck for a normal set!, so add a cast.
|
||||
cast_type = slot_type;
|
||||
}
|
||||
|
||||
return pool.alloc_element<StackSpillStoreElement>(m_value, m_size, m_offset, cast_type);
|
||||
}
|
||||
} // namespace decompiler
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "common/log/log.h"
|
||||
#include "AtomicOp.h"
|
||||
#include "decompiler/util/DecompilerTypeSystem.h"
|
||||
#include "decompiler/IR2/bitfields.h"
|
||||
|
||||
namespace decompiler {
|
||||
|
||||
@@ -284,7 +285,13 @@ TP_Type SimpleExpression::get_type_int2(const TypeState& input,
|
||||
if (m_args[1].is_int() && is_int_or_uint(dts, arg0_type)) {
|
||||
assert(m_args[1].get_int() >= 0);
|
||||
assert(m_args[1].get_int() < 64);
|
||||
return TP_Type::make_from_product(1ull << m_args[1].get_int(), is_signed(dts, arg0_type));
|
||||
// this could be a bitfield access or a multiply.
|
||||
// we pick bitfield access if the parent is a bitfield.
|
||||
if (dynamic_cast<BitFieldType*>(dts.ts.lookup_type(arg0_type.typespec()))) {
|
||||
return TP_Type::make_from_left_shift_bitfield(arg0_type.typespec(), m_args[1].get_int());
|
||||
} else {
|
||||
return TP_Type::make_from_product(1ull << m_args[1].get_int(), is_signed(dts, arg0_type));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_args[1].is_int() && dts.ts.tc(TypeSpec("pointer"), arg0_type.typespec())) {
|
||||
@@ -293,6 +300,27 @@ TP_Type SimpleExpression::get_type_int2(const TypeState& input,
|
||||
}
|
||||
break;
|
||||
|
||||
case Kind::RIGHT_SHIFT_ARITH:
|
||||
case Kind::RIGHT_SHIFT_LOGIC: {
|
||||
bool is_unsigned = m_kind == Kind::RIGHT_SHIFT_LOGIC;
|
||||
if (arg0_type.kind == TP_Type::Kind::LEFT_SHIFTED_BITFIELD && m_args[1].is_int()) {
|
||||
// second op in left/right shift combo
|
||||
int end_bit = 64 - arg0_type.get_left_shift();
|
||||
|
||||
int size = 64 - m_args[1].get_int();
|
||||
int start_bit = end_bit - size;
|
||||
if (start_bit < 0) {
|
||||
throw std::runtime_error("Bad bitfield start bit");
|
||||
}
|
||||
|
||||
auto type = dts.ts.lookup_type(arg0_type.get_bitfield_type());
|
||||
auto as_bitfield = dynamic_cast<BitFieldType*>(type);
|
||||
assert(as_bitfield);
|
||||
auto field = find_field(dts.ts, as_bitfield, start_bit, size, is_unsigned);
|
||||
return TP_Type::make_from_ts(field.type());
|
||||
}
|
||||
} break;
|
||||
|
||||
case Kind::MUL_SIGNED: {
|
||||
if (arg0_type.is_integer_constant() && is_int_or_uint(dts, arg1_type)) {
|
||||
return TP_Type::make_from_product(arg0_type.get_integer_constant(),
|
||||
@@ -1023,4 +1051,39 @@ TypeState AsmBranchOp::propagate_types_internal(const TypeState& input,
|
||||
return output;
|
||||
}
|
||||
|
||||
TypeState StackSpillLoadOp::propagate_types_internal(const TypeState& input,
|
||||
const Env& env,
|
||||
DecompilerTypeSystem&) {
|
||||
// stack slot load
|
||||
auto info = env.stack_spills().lookup(m_offset);
|
||||
if (info.size != m_size) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Stack slot load mismatch: defined as size {}, got size {}\n", info.size, m_size));
|
||||
}
|
||||
|
||||
if (info.is_signed != m_is_signed) {
|
||||
throw std::runtime_error("Stack slot signed mismatch");
|
||||
}
|
||||
|
||||
auto& loaded_type = input.get_slot(m_offset);
|
||||
auto result = input;
|
||||
result.get(m_dst.reg()) = loaded_type;
|
||||
return result;
|
||||
}
|
||||
|
||||
TypeState StackSpillStoreOp::propagate_types_internal(const TypeState& input,
|
||||
const Env& env,
|
||||
DecompilerTypeSystem&) {
|
||||
auto info = env.stack_spills().lookup(m_offset);
|
||||
if (info.size != m_size) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Stack slot load mismatch: defined as size {}, got size {}\n", info.size, m_size));
|
||||
}
|
||||
|
||||
auto& stored_type = input.get(m_value.reg());
|
||||
auto result = input;
|
||||
result.spill_slots[m_offset] = stored_type;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace decompiler
|
||||
|
||||
+53
-17
@@ -91,12 +91,17 @@ const std::string& Env::remapped_name(const std::string& name) const {
|
||||
}
|
||||
|
||||
goos::Object Env::get_variable_name_with_cast(const RegisterAccess& access) const {
|
||||
return get_variable_name_with_cast(access.reg(), access.idx(), access.mode());
|
||||
auto result = get_variable_and_cast(access);
|
||||
if (result.cast) {
|
||||
return pretty_print::build_list("the-as", result.cast->print(), result.name);
|
||||
} else {
|
||||
return pretty_print::to_symbol(result.name);
|
||||
}
|
||||
}
|
||||
|
||||
goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, AccessMode mode) const {
|
||||
if (reg.get_kind() == Reg::FPR || reg.get_kind() == Reg::GPR) {
|
||||
auto& var_info = m_var_names.lookup(reg, atomic_idx, mode);
|
||||
VariableWithCast Env::get_variable_and_cast(const RegisterAccess& access) const {
|
||||
if (access.reg().get_kind() == Reg::FPR || access.reg().get_kind() == Reg::GPR) {
|
||||
auto& var_info = m_var_names.lookup(access.reg(), access.idx(), access.mode());
|
||||
// this is a bit of a confusing process. The first step is to grab the auto-generated name:
|
||||
std::string original_name = var_info.name();
|
||||
auto lookup_name = original_name;
|
||||
@@ -118,27 +123,32 @@ goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, Acce
|
||||
}
|
||||
|
||||
// next, we insert type casts that make enforce the user override.
|
||||
auto type_kv = m_typecasts.find(atomic_idx);
|
||||
auto type_kv = m_typecasts.find(access.idx());
|
||||
if (type_kv != m_typecasts.end()) {
|
||||
for (auto& x : type_kv->second) {
|
||||
if (x.reg == reg) {
|
||||
if (x.reg == access.reg()) {
|
||||
// let's make sure the above claim is true
|
||||
TypeSpec type_in_reg;
|
||||
if (has_type_analysis() && mode == AccessMode::READ) {
|
||||
type_in_reg = get_types_for_op_mode(atomic_idx, AccessMode::READ).get(reg).typespec();
|
||||
if (has_type_analysis() && access.mode() == AccessMode::READ) {
|
||||
type_in_reg = get_types_for_op_mode(access.idx(), AccessMode::READ)
|
||||
.get(access.reg())
|
||||
.typespec();
|
||||
if (type_in_reg.print() != x.type_name) {
|
||||
lg::error(
|
||||
"Decompiler type consistency error. There was a typecast for reg {} at idx {} "
|
||||
"(var {}) to type {}, but the actual type is {} ({})",
|
||||
reg.to_charp(), atomic_idx, lookup_name, x.type_name, type_in_reg.print(),
|
||||
type_in_reg.print());
|
||||
access.reg().to_charp(), access.idx(), lookup_name, x.type_name,
|
||||
type_in_reg.print(), type_in_reg.print());
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (type_of_var != type_in_reg) {
|
||||
// TODO - use the when possible?
|
||||
return pretty_print::build_list("the-as", x.type_name, lookup_name);
|
||||
VariableWithCast result;
|
||||
result.cast = TypeSpec(x.type_name);
|
||||
result.name = lookup_name;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +156,9 @@ goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, Acce
|
||||
|
||||
// type analysis stuff runs before variable types, so we insert casts that account
|
||||
// for the changing types due to the lca(uses) that is used to generate variable types.
|
||||
auto type_of_reg = get_types_for_op_mode(atomic_idx, mode).get(reg).typespec();
|
||||
if (mode == AccessMode::READ) {
|
||||
auto type_of_reg =
|
||||
get_types_for_op_mode(access.idx(), access.mode()).get(access.reg()).typespec();
|
||||
if (access.mode() == AccessMode::READ) {
|
||||
// note - this may be stricter than needed. but that's ok.
|
||||
|
||||
if (type_of_var != type_of_reg) {
|
||||
@@ -156,7 +167,10 @@ goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, Acce
|
||||
// {}\n ",
|
||||
// lookup_name, reg.to_charp(), atomic_idx, type_of_reg.print(),
|
||||
// var_info.type.typespec().print(), type_of_var.print());
|
||||
return pretty_print::build_list("the-as", type_of_reg.print(), lookup_name);
|
||||
VariableWithCast result;
|
||||
result.cast = type_of_reg;
|
||||
result.name = lookup_name;
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// if we're setting a variable, we are a little less strict.
|
||||
@@ -170,12 +184,21 @@ goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, Acce
|
||||
}
|
||||
}
|
||||
|
||||
return pretty_print::to_symbol(lookup_name);
|
||||
VariableWithCast result;
|
||||
result.name = lookup_name;
|
||||
return result;
|
||||
|
||||
} else {
|
||||
return pretty_print::to_symbol(reg.to_charp());
|
||||
VariableWithCast result;
|
||||
result.name = access.reg().to_charp();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
goos::Object Env::get_variable_name_with_cast(Register reg, int atomic_idx, AccessMode mode) const {
|
||||
return get_variable_name_with_cast(RegisterAccess(mode, reg, atomic_idx, true));
|
||||
}
|
||||
|
||||
std::optional<TypeSpec> Env::get_user_cast_for_access(const RegisterAccess& access) const {
|
||||
if (access.reg().get_kind() == Reg::FPR || access.reg().get_kind() == Reg::GPR) {
|
||||
auto& var_info = m_var_names.lookup(access.reg(), access.idx(), access.mode());
|
||||
@@ -420,9 +443,22 @@ goos::Object Env::local_var_type_list(const Form* top_level_form,
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
elts.push_back(pretty_print::build_list(lookup_name, x.type.typespec().print()));
|
||||
}
|
||||
|
||||
// sort in increasing offset.
|
||||
// it looks like this is the order the GOAL compiler itself used.
|
||||
std::vector<StackSpillEntry> spills;
|
||||
for (auto& x : stack_slot_entries) {
|
||||
spills.push_back(x.second);
|
||||
}
|
||||
std::sort(spills.begin(), spills.end(),
|
||||
[](const StackSpillEntry& a, const StackSpillEntry& b) { return a.offset < b.offset; });
|
||||
for (auto& x : spills) {
|
||||
elts.push_back(pretty_print::build_list(x.name(), x.typespec.print()));
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count_out) {
|
||||
*count_out = count;
|
||||
}
|
||||
|
||||
+42
-5
@@ -5,6 +5,7 @@
|
||||
#include <cassert>
|
||||
#include <common/goos/Object.h>
|
||||
#include "decompiler/util/TP_Type.h"
|
||||
#include "decompiler/util/StackSpillMap.h"
|
||||
#include "decompiler/Disasm/Register.h"
|
||||
#include "decompiler/IR2/IR2_common.h"
|
||||
#include "decompiler/analysis/reg_usage.h"
|
||||
@@ -16,12 +17,31 @@ class Form;
|
||||
class DecompilerTypeSystem;
|
||||
struct FunctionAtomicOps;
|
||||
|
||||
struct VariableWithCast {
|
||||
std::string name;
|
||||
std::optional<TypeSpec> cast;
|
||||
};
|
||||
|
||||
struct StackVarEntry {
|
||||
StackVariableHint hint;
|
||||
TypeSpec ref_type; // the actual type of the address.
|
||||
int size = -1;
|
||||
};
|
||||
|
||||
struct StackSpillEntry {
|
||||
TP_Type tp_type;
|
||||
TypeSpec typespec;
|
||||
int offset;
|
||||
std::optional<std::string> name_override;
|
||||
std::string name() const {
|
||||
if (name_override) {
|
||||
return *name_override;
|
||||
} else {
|
||||
return fmt::format("sv-{}", offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* An "environment" for a single function.
|
||||
* This contains data for an entire function, like which registers are live when, the types of
|
||||
@@ -34,17 +54,16 @@ class Env {
|
||||
bool has_local_vars() const { return m_has_local_vars; }
|
||||
bool has_type_analysis() const { return m_has_types; }
|
||||
bool has_reg_use() const { return m_has_reg_use; }
|
||||
const RegUsageInfo& reg_use() const {
|
||||
assert(m_has_reg_use);
|
||||
return m_reg_use;
|
||||
}
|
||||
|
||||
void set_reg_use(const RegUsageInfo& info) {
|
||||
m_reg_use = info;
|
||||
m_has_reg_use = true;
|
||||
}
|
||||
|
||||
const RegUsageInfo& reg_use() const {
|
||||
assert(m_has_reg_use);
|
||||
return m_reg_use;
|
||||
}
|
||||
|
||||
RegUsageInfo& reg_use() {
|
||||
assert(m_has_reg_use);
|
||||
return m_reg_use;
|
||||
@@ -54,6 +73,7 @@ class Env {
|
||||
goos::Object get_variable_name_with_cast(Register reg, int atomic_idx, AccessMode mode) const;
|
||||
goos::Object get_variable_name_with_cast(const RegisterAccess& access) const;
|
||||
std::string get_variable_name(const RegisterAccess& access) const;
|
||||
VariableWithCast get_variable_and_cast(const RegisterAccess& access) const;
|
||||
std::optional<TypeSpec> get_user_cast_for_access(const RegisterAccess& access) const;
|
||||
TypeSpec get_variable_type(const RegisterAccess& access, bool using_user_var_types) const;
|
||||
|
||||
@@ -158,9 +178,24 @@ class Env {
|
||||
|
||||
void set_retype_map(const std::unordered_map<std::string, TypeSpec>& map) { m_var_retype = map; }
|
||||
|
||||
void set_stack_spills(const StackSpillMap& map) { m_stack_spill_map = map; }
|
||||
const StackSpillMap& stack_spills() const { return m_stack_spill_map; }
|
||||
|
||||
// todo - remove these hacks at some point.
|
||||
LinkedObjectFile* file = nullptr;
|
||||
DecompilerTypeSystem* dts = nullptr;
|
||||
std::unordered_map<int, StackSpillEntry> stack_slot_entries;
|
||||
|
||||
std::string get_spill_slot_var_name(int offset) const {
|
||||
auto kv = stack_slot_entries.find(offset);
|
||||
if (kv == stack_slot_entries.end()) {
|
||||
return fmt::format("sv-{}", offset);
|
||||
} else {
|
||||
return kv->second.name();
|
||||
}
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::string>& var_remap_map() const { return m_var_remap; }
|
||||
|
||||
private:
|
||||
RegisterAccess m_end_var;
|
||||
@@ -186,5 +221,7 @@ class Env {
|
||||
|
||||
std::unordered_set<std::string> m_vars_defined_in_let;
|
||||
std::optional<TypeSpec> m_type_analysis_return_type;
|
||||
|
||||
StackSpillMap m_stack_spill_map;
|
||||
};
|
||||
} // namespace decompiler
|
||||
@@ -2291,6 +2291,52 @@ void VectorFloatLoadStoreElement::collect_vf_regs(RegSet& regs) const {
|
||||
regs.insert(m_vf_reg);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// StackSpillStoreElement
|
||||
////////////////////////////////
|
||||
|
||||
StackSpillStoreElement::StackSpillStoreElement(RegisterAccess value,
|
||||
int size,
|
||||
int stack_offset,
|
||||
const std::optional<TypeSpec>& cast_type)
|
||||
: m_value(value), m_size(size), m_stack_offset(stack_offset), m_cast_type(cast_type) {}
|
||||
|
||||
goos::Object StackSpillStoreElement::to_form_internal(const Env& env) const {
|
||||
return pretty_print::build_list(
|
||||
fmt::format("set! {}", env.get_spill_slot_var_name(m_stack_offset)), m_value.to_form(env));
|
||||
}
|
||||
|
||||
void StackSpillStoreElement::apply(const std::function<void(FormElement*)>& f) {
|
||||
f(this);
|
||||
}
|
||||
|
||||
void StackSpillStoreElement::apply_form(const std::function<void(Form*)>&) {}
|
||||
|
||||
void StackSpillStoreElement::collect_vars(RegAccessSet& vars, bool) const {
|
||||
vars.insert(m_value);
|
||||
}
|
||||
|
||||
void StackSpillStoreElement::get_modified_regs(RegSet&) const {}
|
||||
|
||||
////////////////////////////////
|
||||
// StackSpillValueElement
|
||||
////////////////////////////////
|
||||
|
||||
StackSpillValueElement::StackSpillValueElement(int size, int stack_offset, bool is_signed)
|
||||
: m_size(size), m_stack_offset(stack_offset), m_is_signed(is_signed) {}
|
||||
|
||||
goos::Object StackSpillValueElement::to_form_internal(const Env& env) const {
|
||||
return pretty_print::to_symbol(env.get_spill_slot_var_name(m_stack_offset));
|
||||
}
|
||||
|
||||
void StackSpillValueElement::apply(const std::function<void(FormElement*)>& f) {
|
||||
f(this);
|
||||
}
|
||||
|
||||
void StackSpillValueElement::apply_form(const std::function<void(Form*)>&) {}
|
||||
void StackSpillValueElement::collect_vars(RegAccessSet&, bool) const {}
|
||||
void StackSpillValueElement::get_modified_regs(RegSet&) const {}
|
||||
|
||||
////////////////////////////////
|
||||
// Utilities
|
||||
////////////////////////////////
|
||||
|
||||
@@ -1316,6 +1316,48 @@ class VectorFloatLoadStoreElement : public FormElement {
|
||||
bool m_is_load = false;
|
||||
};
|
||||
|
||||
class StackSpillStoreElement : public FormElement {
|
||||
public:
|
||||
StackSpillStoreElement(RegisterAccess value,
|
||||
int size,
|
||||
int stack_offset,
|
||||
const std::optional<TypeSpec>& cast_type);
|
||||
goos::Object to_form_internal(const Env& env) const override;
|
||||
void apply(const std::function<void(FormElement*)>& f) override;
|
||||
void apply_form(const std::function<void(Form*)>& f) override;
|
||||
void collect_vars(RegAccessSet& vars, bool recursive) const override;
|
||||
void get_modified_regs(RegSet& regs) const override;
|
||||
void push_to_stack(const Env& env, FormPool& pool, FormStack& stack) override;
|
||||
const std::optional<TypeSpec>& cast_type() const { return m_cast_type; }
|
||||
|
||||
private:
|
||||
RegisterAccess m_value;
|
||||
int m_size = -1;
|
||||
int m_stack_offset = -1;
|
||||
std::optional<TypeSpec> m_cast_type;
|
||||
};
|
||||
|
||||
// the value from a stack load.
|
||||
class StackSpillValueElement : public FormElement {
|
||||
public:
|
||||
StackSpillValueElement(int size, int stack_offset, bool is_signed);
|
||||
goos::Object to_form_internal(const Env& env) const override;
|
||||
void apply(const std::function<void(FormElement*)>& f) override;
|
||||
void apply_form(const std::function<void(Form*)>& f) override;
|
||||
void collect_vars(RegAccessSet& vars, bool recursive) const override;
|
||||
void get_modified_regs(RegSet& regs) const override;
|
||||
void update_from_stack(const Env& env,
|
||||
FormPool& pool,
|
||||
FormStack& stack,
|
||||
std::vector<FormElement*>* result,
|
||||
bool allow_side_effects) override;
|
||||
|
||||
private:
|
||||
int m_size = -1;
|
||||
int m_stack_offset = -1;
|
||||
bool m_is_signed = false;
|
||||
};
|
||||
|
||||
/*!
|
||||
* A Form is a wrapper around one or more FormElements.
|
||||
* This is done for two reasons:
|
||||
|
||||
@@ -308,8 +308,8 @@ bool is_uint_type(const Env& env, int my_idx, RegisterAccess var) {
|
||||
|
||||
bool is_ptr_or_child(const Env& env, int my_idx, RegisterAccess var, bool) {
|
||||
// Now that decompiler types are synced up properly, we don't want this.
|
||||
// auto type = as_var ? env.get_variable_type(var, true).base_type()
|
||||
// : env.get_types_before_op(my_idx).get(var.reg()).typespec().base_type();
|
||||
// auto type = as_var ? env.get_variable_type(var, true).base_type()
|
||||
// : env.get_types_before_op(my_idx).get(var.reg()).typespec().base_type();
|
||||
auto type = env.get_types_before_op(my_idx).get(var.reg()).typespec().base_type();
|
||||
return type == "pointer";
|
||||
}
|
||||
@@ -603,7 +603,7 @@ void SimpleExpressionElement::update_from_stack_add_i(const Env& env,
|
||||
args.push_back(pool.alloc_single_element_form<SimpleAtomElement>(nullptr, m_expr.get_arg(1)));
|
||||
}
|
||||
|
||||
bool arg0_ptr = is_ptr_or_child(env, m_my_idx, m_expr.get_arg(0).var(), is_var(args.at(0)));
|
||||
bool arg0_ptr = is_ptr_or_child(env, m_my_idx, m_expr.get_arg(0).var(), true);
|
||||
|
||||
// Look for getting an address inside of an object.
|
||||
// (+ <integer 108 + int> process). array style access with a stride of 1.
|
||||
@@ -943,7 +943,7 @@ void SimpleExpressionElement::update_from_stack_logor_or_logand(const Env& env,
|
||||
}
|
||||
|
||||
BitfieldManip step(manip_kind, m_expr.get_arg(1).get_int());
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
if (other) {
|
||||
result->push_back(other);
|
||||
} else {
|
||||
@@ -983,7 +983,7 @@ void SimpleExpressionElement::update_from_stack_logor_or_logand(const Env& env,
|
||||
assert(false);
|
||||
}
|
||||
BitfieldManip step(manip_kind, arg1_atom->get_int());
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
assert(!other); // shouldn't be complete.
|
||||
result->push_back(read_elt);
|
||||
return;
|
||||
@@ -997,7 +997,7 @@ void SimpleExpressionElement::update_from_stack_logor_or_logand(const Env& env,
|
||||
assert(false);
|
||||
}
|
||||
auto step = BitfieldManip::from_form(manip_kind, stripped_arg1);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
if (other) {
|
||||
result->push_back(other);
|
||||
} else {
|
||||
@@ -1066,7 +1066,7 @@ void SimpleExpressionElement::update_from_stack_left_shift(const Env& env,
|
||||
auto base = pop_to_forms({m_expr.get_arg(0).var()}, env, pool, stack, allow_side_effects).at(0);
|
||||
auto read_elt = pool.alloc_element<BitfieldAccessElement>(base, arg0_type);
|
||||
BitfieldManip step(BitfieldManip::Kind::LEFT_SHIFT, m_expr.get_arg(1).get_int());
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
assert(!other); // shouldn't be complete.
|
||||
result->push_back(read_elt);
|
||||
} else {
|
||||
@@ -1087,7 +1087,7 @@ void SimpleExpressionElement::update_from_stack_right_shift_logic(const Env& env
|
||||
auto base = pop_to_forms({m_expr.get_arg(0).var()}, env, pool, stack, allow_side_effects).at(0);
|
||||
auto read_elt = pool.alloc_element<BitfieldAccessElement>(base, arg0_type);
|
||||
BitfieldManip step(BitfieldManip::Kind::RIGHT_SHIFT_LOGICAL, m_expr.get_arg(1).get_int());
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
assert(other); // should be a high field.
|
||||
result->push_back(other);
|
||||
} else {
|
||||
@@ -1100,7 +1100,7 @@ void SimpleExpressionElement::update_from_stack_right_shift_logic(const Env& env
|
||||
|
||||
if (as_bitfield_access) {
|
||||
BitfieldManip step(BitfieldManip::Kind::RIGHT_SHIFT_LOGICAL, m_expr.get_arg(1).get_int());
|
||||
auto next = as_bitfield_access->push_step(step, env.dts->ts, pool);
|
||||
auto next = as_bitfield_access->push_step(step, env.dts->ts, pool, env);
|
||||
if (next) {
|
||||
result->push_back(next);
|
||||
} else {
|
||||
@@ -1139,7 +1139,7 @@ void SimpleExpressionElement::update_from_stack_right_shift_arith(const Env& env
|
||||
auto base = pop_to_forms({m_expr.get_arg(0).var()}, env, pool, stack, allow_side_effects).at(0);
|
||||
auto read_elt = pool.alloc_element<BitfieldAccessElement>(base, arg0_type);
|
||||
BitfieldManip step(BitfieldManip::Kind::RIGHT_SHIFT_ARITH, m_expr.get_arg(1).get_int());
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
assert(other); // should be a high field.
|
||||
result->push_back(other);
|
||||
} else {
|
||||
@@ -2068,6 +2068,12 @@ void CondWithElseElement::push_to_stack(const Env& env, FormPool& pool, FormStac
|
||||
form->push_back(stack.pop_back(pool));
|
||||
} else {
|
||||
FormStack temp_stack(false);
|
||||
if (form == entry.body) {
|
||||
auto as_setvar = dynamic_cast<SetVarElement*>(form->elts().back());
|
||||
if (as_setvar && as_setvar->is_dead_set() && as_setvar->src_type() != TypeSpec("float")) {
|
||||
rewrite_as_set = false;
|
||||
}
|
||||
}
|
||||
for (auto& elt : form->elts()) {
|
||||
elt->push_to_stack(env, pool, temp_stack);
|
||||
}
|
||||
@@ -2119,8 +2125,9 @@ void CondWithElseElement::push_to_stack(const Env& env, FormPool& pool, FormStac
|
||||
source_types.push_back(last_in_body->src_type());
|
||||
}
|
||||
last_var = last_in_body->dst();
|
||||
} // For now, I am fine with letting this fail. For example, if the set is eliminated by a
|
||||
// coloring move. If this makes really ugly code later on, we could use this to disable
|
||||
}
|
||||
// For now, I am fine with letting this fail. For example, if the set is eliminated by a
|
||||
// coloring move. If this makes really ugly code later on, we could use this to disable
|
||||
// write as set.
|
||||
}
|
||||
|
||||
@@ -2320,7 +2327,7 @@ FormElement* ConditionElement::make_nonzero_check_generic(const Env& env,
|
||||
dynamic_cast<BitfieldAccessElement*>(source_forms.at(0)->try_as_single_element());
|
||||
if (as_bitfield_op) {
|
||||
bitfield_compare = as_bitfield_op->push_step(
|
||||
BitfieldManip(BitfieldManip::Kind::NONZERO_COMPARE, 0), env.dts->ts, pool);
|
||||
BitfieldManip(BitfieldManip::Kind::NONZERO_COMPARE, 0), env.dts->ts, pool, env);
|
||||
}
|
||||
|
||||
if (bitfield_compare) {
|
||||
@@ -2698,7 +2705,7 @@ void push_asm_srl_to_stack(const AsmOp* op,
|
||||
auto base = pop_to_forms({*var}, env, pool, stack, true).at(0);
|
||||
auto read_elt = pool.alloc_element<BitfieldAccessElement>(base, arg0_type);
|
||||
BitfieldManip step(BitfieldManip::Kind::RIGHT_SHIFT_LOGICAL_32BIT, integer);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool);
|
||||
auto other = read_elt->push_step(step, env.dts->ts, pool, env);
|
||||
assert(other); // should be a high field.
|
||||
stack.push_value_to_reg(*dst, pool.alloc_single_form(nullptr, other), true,
|
||||
env.get_variable_type(*dst, true));
|
||||
@@ -2740,6 +2747,7 @@ void AtomicOpElement::push_to_stack(const Env& env, FormPool& pool, FormStack& s
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Can't push atomic op to stack: " + m_op->to_string(env));
|
||||
}
|
||||
|
||||
@@ -3079,6 +3087,20 @@ void ConditionalMoveFalseElement::push_to_stack(const Env& env, FormPool& pool,
|
||||
true, TypeSpec("symbol"));
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
// StackSpillStoreElement
|
||||
///////////////////////////
|
||||
void StackSpillStoreElement::push_to_stack(const Env& env, FormPool& pool, FormStack& stack) {
|
||||
mark_popped();
|
||||
auto src = pop_to_forms({m_value}, env, pool, stack, true).at(0);
|
||||
auto dst = pool.alloc_single_element_form<ConstantTokenElement>(
|
||||
nullptr, env.get_spill_slot_var_name(m_stack_offset));
|
||||
if (m_cast_type) {
|
||||
src = cast_form(src, *m_cast_type, pool, env);
|
||||
}
|
||||
stack.push_form_element(pool.alloc_element<SetFormFormElement>(dst, src), true);
|
||||
}
|
||||
|
||||
void VectorFloatLoadStoreElement::push_to_stack(const Env&, FormPool&, FormStack& stack) {
|
||||
mark_popped();
|
||||
stack.push_form_element(this, true);
|
||||
@@ -3147,4 +3169,13 @@ void StackVarDefElement::update_from_stack(const Env&,
|
||||
result->push_back(this);
|
||||
}
|
||||
|
||||
void StackSpillValueElement::update_from_stack(const Env&,
|
||||
FormPool&,
|
||||
FormStack&,
|
||||
std::vector<FormElement*>* result,
|
||||
bool) {
|
||||
mark_popped();
|
||||
result->push_back(this);
|
||||
}
|
||||
|
||||
} // namespace decompiler
|
||||
|
||||
@@ -143,7 +143,6 @@ void BitfieldAccessElement::get_modified_regs(RegSet& regs) const {
|
||||
m_base->get_modified_regs(regs);
|
||||
}
|
||||
|
||||
namespace {
|
||||
const BitField& find_field(const TypeSystem& ts,
|
||||
const BitFieldType* type,
|
||||
int start_bit,
|
||||
@@ -172,6 +171,7 @@ const BitField& find_field(const TypeSystem& ts,
|
||||
!looking_for_unsigned, type->get_name()));
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::optional<BitField> find_field_from_mask(const TypeSystem& ts,
|
||||
const BitFieldType* type,
|
||||
uint64_t mask) {
|
||||
@@ -227,7 +227,8 @@ std::optional<BitFieldDef> get_bitfield_initial_set(Form* form,
|
||||
*/
|
||||
FormElement* BitfieldAccessElement::push_step(const BitfieldManip step,
|
||||
const TypeSystem& ts,
|
||||
FormPool& pool) {
|
||||
FormPool& pool,
|
||||
const Env& env) {
|
||||
if (m_steps.empty() && step.kind == BitfieldManip::Kind::LEFT_SHIFT) {
|
||||
// for left/right shift combo to get a field.
|
||||
m_steps.push_back(step);
|
||||
@@ -351,8 +352,6 @@ FormElement* BitfieldAccessElement::push_step(const BitfieldManip step,
|
||||
|
||||
return pool.alloc_element<ModifiedCopyBitfieldElement>(m_type, m_base,
|
||||
std::vector<BitFieldDef>{*val});
|
||||
|
||||
// todo check that the mask and the set are compatible with eachother
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown state in BitfieldReadElement");
|
||||
|
||||
@@ -66,7 +66,10 @@ class BitfieldAccessElement : public FormElement {
|
||||
void apply_form(const std::function<void(Form*)>& f) override;
|
||||
void collect_vars(RegAccessSet& vars, bool recursive) const override;
|
||||
void get_modified_regs(RegSet& regs) const override;
|
||||
FormElement* push_step(const BitfieldManip step, const TypeSystem& ts, FormPool& pool);
|
||||
FormElement* push_step(const BitfieldManip step,
|
||||
const TypeSystem& ts,
|
||||
FormPool& pool,
|
||||
const Env& env);
|
||||
|
||||
private:
|
||||
Form* m_base = nullptr;
|
||||
@@ -151,4 +154,10 @@ Form* cast_to_int_enum(const EnumType* type_info,
|
||||
Form* in);
|
||||
|
||||
std::optional<u64> get_goal_integer_constant(Form* in, const Env&);
|
||||
|
||||
const BitField& find_field(const TypeSystem& ts,
|
||||
const BitFieldType* type,
|
||||
int start_bit,
|
||||
int size,
|
||||
std::optional<bool> looking_for_unsigned);
|
||||
} // namespace decompiler
|
||||
|
||||
@@ -67,6 +67,7 @@ class ObjectFileDB {
|
||||
void analyze_functions_ir1();
|
||||
void analyze_functions_ir2(const std::string& output_dir);
|
||||
void ir2_top_level_pass();
|
||||
void ir2_stack_spill_slot_pass();
|
||||
void ir2_basic_block_pass();
|
||||
void ir2_atomic_op_pass();
|
||||
void ir2_type_analysis_pass();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "decompiler/analysis/final_output.h"
|
||||
#include "decompiler/analysis/expression_build.h"
|
||||
#include "decompiler/analysis/inline_asm_rewrite.h"
|
||||
#include "decompiler/analysis/stack_spill.h"
|
||||
#include "decompiler/analysis/anonymous_function_def.h"
|
||||
#include "common/goos/PrettyPrinter.h"
|
||||
#include "decompiler/IR2/Form.h"
|
||||
@@ -34,6 +35,8 @@ void ObjectFileDB::analyze_functions_ir2(const std::string& output_dir) {
|
||||
ir2_top_level_pass();
|
||||
lg::info("Processing basic blocks and control flow graph...");
|
||||
ir2_basic_block_pass();
|
||||
lg::info("Finding stack spills...");
|
||||
ir2_stack_spill_slot_pass();
|
||||
lg::info("Converting to atomic ops...");
|
||||
ir2_atomic_op_pass();
|
||||
lg::info("Running type analysis...");
|
||||
@@ -239,6 +242,23 @@ void ObjectFileDB::ir2_basic_block_pass() {
|
||||
100.f * inspect_methods / total_functions);
|
||||
}
|
||||
|
||||
void ObjectFileDB::ir2_stack_spill_slot_pass() {
|
||||
Timer timer;
|
||||
int functions_with_spills = 0;
|
||||
int total_slots = 0;
|
||||
for_each_function_def_order([&](Function& func, int, ObjectFileData&) {
|
||||
auto spill_map = build_spill_map(func.instructions, {func.prologue_end, func.epilogue_start});
|
||||
auto map_size = spill_map.size();
|
||||
if (map_size) {
|
||||
functions_with_spills++;
|
||||
total_slots += map_size;
|
||||
}
|
||||
func.ir2.env.set_stack_spills(spill_map);
|
||||
});
|
||||
lg::info("Analyzed stack spills: found {} functions will spills (total {} vars), took {:.2f} ms",
|
||||
functions_with_spills, total_slots, timer.getMs());
|
||||
}
|
||||
|
||||
/*!
|
||||
* Conversion of MIPS instructions into AtomicOps. The AtomicOps represent what we
|
||||
* think are IR of the original GOAL compiler.
|
||||
|
||||
@@ -41,6 +41,10 @@ Register rv0() {
|
||||
return make_gpr(Reg::V0);
|
||||
}
|
||||
|
||||
Register rsp() {
|
||||
return make_gpr(Reg::SP);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Variable Helpers
|
||||
/////////////////////////
|
||||
@@ -149,6 +153,12 @@ std::unique_ptr<AtomicOp> make_standard_load(const Instruction& i0,
|
||||
if (i0.get_src(0).is_label() && i0.get_src(1).is_reg(rfp())) {
|
||||
// it's an FP relative load.
|
||||
src = SimpleAtom::make_static_address(i0.get_src(0).get_label()).as_expr();
|
||||
} else if (i0.get_src(0).is_imm() && i0.get_src(1).is_reg(rsp()) &&
|
||||
(kind == LoadVarOp::Kind::SIGNED || kind == LoadVarOp::Kind::UNSIGNED)) {
|
||||
// it's a stack spill.
|
||||
return std::make_unique<StackSpillLoadOp>(make_dst_var(i0, idx), load_size,
|
||||
i0.get_src(0).get_imm(),
|
||||
kind == LoadVarOp::Kind::SIGNED, idx);
|
||||
} else if (i0.get_src(0).is_imm() && i0.get_src(0).get_imm() == 0) {
|
||||
// the offset is 0
|
||||
src = make_src_atom(i0.get_src(1).get_reg(), idx).as_expr();
|
||||
@@ -165,8 +175,17 @@ std::unique_ptr<AtomicOp> make_standard_store(const Instruction& i0,
|
||||
int idx,
|
||||
int store_size,
|
||||
StoreOp::Kind kind) {
|
||||
if (i0.get_src(2).is_reg(Register(Reg::GPR, Reg::SP))) {
|
||||
return std::make_unique<AsmOp>(i0, idx);
|
||||
if (i0.get_src(2).is_reg(Register(Reg::GPR, Reg::SP)) && kind == StoreOp::Kind::INTEGER) {
|
||||
if (kind == StoreOp::Kind::INTEGER && store_size == 4 && i0.get_src(1).get_imm() == 0) {
|
||||
// this is a bit of a hack. enter-state does a sw onto the stack that's not a spill, but
|
||||
// instead manipulates the stores "ra" register that will later be restored.
|
||||
// I believe sw is never used for stack spills, and no stack variable is ever located at
|
||||
// sp + 0, so this should be safe.
|
||||
return std::make_unique<AsmOp>(i0, idx);
|
||||
}
|
||||
// it's a stack spill.
|
||||
return std::make_unique<StackSpillStoreOp>(make_src_var(i0.get_src(0).get_reg(), idx),
|
||||
store_size, i0.get_src(1).get_imm(), idx);
|
||||
}
|
||||
SimpleAtom val;
|
||||
SimpleExpression dst;
|
||||
|
||||
@@ -34,8 +34,18 @@ bool convert_to_expressions(
|
||||
// get variable names from the user.
|
||||
f.ir2.env.map_args_from_config(arg_names, var_override_map);
|
||||
|
||||
// override variable types from the user.
|
||||
// convert to typespec
|
||||
for (auto& info : f.ir2.env.stack_slot_entries) {
|
||||
auto rename = f.ir2.env.var_remap_map().find(info.second.name());
|
||||
if (rename != f.ir2.env.var_remap_map().end()) {
|
||||
info.second.name_override = rename->second;
|
||||
}
|
||||
// debug
|
||||
// fmt::print("STACK {} : {} ({})\n", info.first, info.second.typespec.print(),
|
||||
// info.second.tp_type.print());
|
||||
}
|
||||
|
||||
// override variable types from the user.
|
||||
std::unordered_map<std::string, TypeSpec> retype;
|
||||
for (auto& remap : var_override_map) {
|
||||
if (remap.second.type) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <stdexcept>
|
||||
#include "third-party/fmt/core.h"
|
||||
#include "stack_spill.h"
|
||||
#include "decompiler/Disasm/DecompilerLabel.h"
|
||||
|
||||
namespace decompiler {
|
||||
|
||||
std::string StackSpillSlot::print() const {
|
||||
return fmt::format("[{:3d}] {}{}", offset, is_signed ? 's' : 'u', size * 8);
|
||||
}
|
||||
|
||||
void StackSpillMap::add_access(const StackSpillSlot& access) {
|
||||
auto existing = m_slot_map.find(access.offset);
|
||||
if (existing != m_slot_map.end()) {
|
||||
if (access != existing->second) {
|
||||
throw std::runtime_error(fmt::format("Inconsistent stack access:\n{}\n{}\n",
|
||||
existing->second.print(), access.print()));
|
||||
}
|
||||
} else {
|
||||
m_slot_map.insert({access.offset, access});
|
||||
}
|
||||
}
|
||||
|
||||
const StackSpillSlot& StackSpillMap::lookup(int offset) const {
|
||||
auto result = m_slot_map.find(offset);
|
||||
if (result == m_slot_map.end()) {
|
||||
throw std::runtime_error(fmt::format("unknown stack spill slot at offset {}", offset));
|
||||
}
|
||||
return result->second;
|
||||
}
|
||||
|
||||
void StackSpillMap::finalize() {
|
||||
// how many variables exist at each byte. should be 1 or 0.
|
||||
int max_offset = 0;
|
||||
for (auto& slot : m_slot_map) {
|
||||
max_offset = std::max(max_offset, slot.second.offset + slot.second.size);
|
||||
}
|
||||
|
||||
assert(max_offset < 4096); // just a sanity check here
|
||||
std::vector<int> var_count(max_offset, 0);
|
||||
|
||||
for (auto& slot : m_slot_map) {
|
||||
for (int i = 0; i < slot.second.size; i++) {
|
||||
var_count.at(slot.second.offset + i)++;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < var_count.size(); i++) {
|
||||
if (var_count[i] > 1) {
|
||||
throw std::runtime_error(
|
||||
fmt::format("There are {} variables at stack offset {}", var_count[i], i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int StackSpillMap::size() const {
|
||||
return m_slot_map.size();
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct StackInstrInfo {
|
||||
InstructionKind kind;
|
||||
bool is_load;
|
||||
int size;
|
||||
bool is_signed;
|
||||
};
|
||||
|
||||
constexpr StackInstrInfo stack_instrs[] = {{InstructionKind::SQ, false, 16, false},
|
||||
{InstructionKind::LQ, true, 16, false}};
|
||||
} // namespace
|
||||
|
||||
StackSpillMap build_spill_map(const std::vector<Instruction>& instructions, Range<int> range) {
|
||||
StackSpillMap map;
|
||||
|
||||
for (auto idx : range) {
|
||||
auto& instr = instructions.at(idx);
|
||||
|
||||
for (auto& instr_template : stack_instrs) {
|
||||
if (instr.kind == instr_template.kind) {
|
||||
// we are the right kind.
|
||||
auto src_reg = instr.get_src(instr_template.is_load ? 1 : 2).get_reg();
|
||||
if (src_reg == Register(Reg::GPR, Reg::SP)) {
|
||||
StackSpillSlot slot;
|
||||
slot.offset = instr.get_src(instr_template.is_load ? 0 : 1).get_imm();
|
||||
slot.size = instr_template.size;
|
||||
slot.is_signed = instr_template.is_signed;
|
||||
map.add_access(slot);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.finalize();
|
||||
return map;
|
||||
}
|
||||
} // namespace decompiler
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "decompiler/Disasm/Instruction.h"
|
||||
#include "common/util/Range.h"
|
||||
#include "decompiler/util/StackSpillMap.h"
|
||||
|
||||
namespace decompiler {
|
||||
|
||||
/*!
|
||||
* Given the instructions for a function, build a StackSpillMap containing all memory used to
|
||||
* spill register variables. The range should be the non-prologue/non-epilogue instruction range.
|
||||
*/
|
||||
StackSpillMap build_spill_map(const std::vector<Instruction>& instructions, Range<int> range);
|
||||
|
||||
} // namespace decompiler
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace decompiler {
|
||||
namespace {
|
||||
TypeState construct_initial_typestate(const TypeSpec& f_ts) {
|
||||
TypeState construct_initial_typestate(const TypeSpec& f_ts, const Env& env) {
|
||||
TypeState result;
|
||||
int goal_args[] = {Reg::A0, Reg::A1, Reg::A2, Reg::A3, Reg::T0, Reg::T1, Reg::T2, Reg::T3};
|
||||
assert(f_ts.base_type() == "function");
|
||||
@@ -11,11 +11,16 @@ TypeState construct_initial_typestate(const TypeSpec& f_ts) {
|
||||
for (int i = 0; i < int(f_ts.arg_count()) - 1; i++) {
|
||||
auto reg_id = goal_args[i];
|
||||
auto reg_type = f_ts.get_arg(i);
|
||||
result.gpr_types[reg_id] = TP_Type::make_from_ts(reg_type);
|
||||
result.get(Register(Reg::GPR, reg_id)) = TP_Type::make_from_ts(reg_type);
|
||||
}
|
||||
|
||||
// todo, more specific process types for behaviors.
|
||||
result.gpr_types[Reg::S6] = TP_Type::make_from_ts(TypeSpec("process"));
|
||||
result.get(Register(Reg::GPR, Reg::S6)) = TP_Type::make_from_ts(TypeSpec("process"));
|
||||
|
||||
// initialize stack slots as uninitialized
|
||||
for (auto slot_info : env.stack_spills().map()) {
|
||||
result.spill_slots.insert({slot_info.first, {}});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -80,7 +85,7 @@ bool run_type_analysis_ir2(const TypeSpec& my_type, DecompilerTypeSystem& dts, F
|
||||
op_types.resize(func.ir2.atomic_ops->ops.size());
|
||||
auto& aop = func.ir2.atomic_ops;
|
||||
|
||||
// STEP 1 - topologocial sort the blocks. This gives us an order where we:
|
||||
// STEP 1 - topological sort the blocks. This gives us an order where we:
|
||||
// - never visit unreachable blocks (we can't type propagate these)
|
||||
// - always visit at least one predecessor of a block before that block
|
||||
auto order = func.bb_topo_sort();
|
||||
@@ -88,7 +93,7 @@ bool run_type_analysis_ir2(const TypeSpec& my_type, DecompilerTypeSystem& dts, F
|
||||
assert(order.vist_order.front() == 0);
|
||||
|
||||
// STEP 2 - initialize type state for the first block to the function argument types.
|
||||
block_init_types.at(0) = construct_initial_typestate(my_type);
|
||||
block_init_types.at(0) = construct_initial_typestate(my_type, func.ir2.env);
|
||||
|
||||
// STEP 3 - propagate types until the result stops changing
|
||||
bool run_again = true;
|
||||
@@ -109,7 +114,8 @@ bool run_type_analysis_ir2(const TypeSpec& my_type, DecompilerTypeSystem& dts, F
|
||||
try {
|
||||
op_types.at(op_id) = op->propagate_types(*init_types, func.ir2.env, dts);
|
||||
} catch (std::runtime_error& e) {
|
||||
lg::warn("Function {} failed type prop: {}", func.guessed_name.to_string(), e.what());
|
||||
lg::warn("Function {} failed type prop at op {}: {}", func.guessed_name.to_string(),
|
||||
op_id, e.what());
|
||||
func.warnings.type_prop_warning("{}", e.what());
|
||||
func.ir2.env.set_types(block_init_types, op_types, *func.ir2.atomic_ops, my_type);
|
||||
return false;
|
||||
@@ -162,6 +168,35 @@ bool run_type_analysis_ir2(const TypeSpec& my_type, DecompilerTypeSystem& dts, F
|
||||
}
|
||||
}
|
||||
|
||||
// figure out the types of stack spill variables:
|
||||
auto& env = func.ir2.env;
|
||||
bool changed;
|
||||
for (auto& type_info : op_types) {
|
||||
for (auto& spill : type_info.spill_slots) {
|
||||
auto& slot_info = env.stack_slot_entries[spill.first];
|
||||
slot_info.tp_type =
|
||||
dts.tp_lca(env.stack_slot_entries[spill.first].tp_type, spill.second, &changed);
|
||||
slot_info.offset = spill.first;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& type_info : block_init_types) {
|
||||
for (auto& spill : type_info.spill_slots) {
|
||||
auto& slot_info = env.stack_slot_entries[spill.first];
|
||||
slot_info.tp_type =
|
||||
dts.tp_lca(env.stack_slot_entries[spill.first].tp_type, spill.second, &changed);
|
||||
slot_info.offset = spill.first;
|
||||
}
|
||||
}
|
||||
|
||||
// convert to typespec
|
||||
for (auto& info : env.stack_slot_entries) {
|
||||
info.second.typespec = info.second.tp_type.typespec();
|
||||
// debug
|
||||
// fmt::print("STACK {} : {} ({})\n", info.first, info.second.typespec.print(),
|
||||
// info.second.tp_type.print());
|
||||
}
|
||||
|
||||
func.ir2.env.set_types(block_init_types, op_types, *func.ir2.atomic_ops, my_type);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -2444,6 +2444,7 @@
|
||||
(unpack-v4-16 109)
|
||||
(unpack-v4-8 110)
|
||||
(unpack-v4-5 111)
|
||||
(cmd-mask 239) ;; not sure what this is
|
||||
)
|
||||
|
||||
(defenum vif-cmd-32
|
||||
@@ -2772,12 +2773,12 @@
|
||||
)
|
||||
|
||||
(define-extern *vif-disasm-table* (array vif-disasm-element)) ;; unknown type
|
||||
;;(define-extern disasm-vif-tag (function (pointer uint32) int symbol int symbol))
|
||||
(define-extern disasm-dma-tag (function dma-tag symbol int))
|
||||
(define-extern disasm-vif-tag (function (pointer vif-tag) int symbol symbol int))
|
||||
(define-extern disasm-dma-tag (function dma-tag symbol none))
|
||||
(define-extern disasm-vif-details (function symbol (pointer uint8) vif-cmd int symbol))
|
||||
(define-extern vif-disasm-element type)
|
||||
(define-extern *dma-disasm* symbol)
|
||||
(define-extern disasm-dma-list function)
|
||||
(define-extern disasm-dma-list (function dma-packet symbol symbol symbol int symbol))
|
||||
|
||||
;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -181,6 +181,36 @@
|
||||
[[202, 225], "s3", "(pointer uint16)"]
|
||||
],
|
||||
|
||||
"disasm-vif-tag": [
|
||||
[[81, 85], "t1", "vif-stcycl-imm"],
|
||||
[242, "a0", "vif-unpack-imm"]
|
||||
],
|
||||
|
||||
"disasm-dma-list": [
|
||||
[25, "v1", "dma-tag"],
|
||||
|
||||
[153, "v1", "dma-packet"],
|
||||
[189, "v1", "dma-packet"],
|
||||
[229, "v1", "dma-packet"],
|
||||
[258, "v1", "dma-packet"],
|
||||
[302, "v1", "dma-packet"],
|
||||
[308, "v1", "dma-packet"],
|
||||
|
||||
//[133, "v1", "(pointer uint64)"],
|
||||
[152, "v1", "(pointer uint64)"],
|
||||
|
||||
[167, "v1", "(pointer uint64)"],
|
||||
[176, "v1", "(pointer uint64)"],
|
||||
[198, "v1", "(pointer uint64)"],
|
||||
[207, "v1", "(pointer uint64)"],
|
||||
[238, "v1", "(pointer uint64)"],
|
||||
[247, "v1", "(pointer uint64)"],
|
||||
[282, "v1", "(pointer uint64)"],
|
||||
[291, "v1", "(pointer uint64)"],
|
||||
[324, "v1", "(pointer uint64)"],
|
||||
[334, "v1", "(pointer uint64)"]
|
||||
],
|
||||
|
||||
// LEVEL
|
||||
"lookup-level-info": [
|
||||
[3, "a1", "symbol"],
|
||||
|
||||
@@ -687,6 +687,42 @@
|
||||
"vars": { "s4-0": "count2", "s3-0": "data-ptr", "s2-0": "i" }
|
||||
},
|
||||
|
||||
"disasm-vif-tag": {
|
||||
"args": ["data", "words", "stream", "details"],
|
||||
"vars": {
|
||||
"gp-0": "byte-idx",
|
||||
"v1-0": "cmd-template-idx",
|
||||
"a0-12": "print-kind",
|
||||
"s1-0": "first-tag",
|
||||
"s0-0": "packet-size",
|
||||
"t1-1": ["stcycl-imm", "vif-stcycl-imm"],
|
||||
"sv-16": "cmd",
|
||||
"sv-32": "data-ptr",
|
||||
"sv-48": "data-idx",
|
||||
"sv-64": "unpack-imm"
|
||||
}
|
||||
},
|
||||
|
||||
"disasm-dma-list": {
|
||||
"args": ["data", "mode", "verbose", "stream", "expected-size"],
|
||||
"vars": {
|
||||
"sv-16": "addr",
|
||||
"sv-32": "data-2",
|
||||
"sv-48": "qwc",
|
||||
"sv-64": "ra-1",
|
||||
"sv-80": "ra-2",
|
||||
"sv-96": "call-depth",
|
||||
"sv-112": "current-tag",
|
||||
"s2-0": "mode-2",
|
||||
"s3-0": "verbose-2",
|
||||
"gp-0": "stream-2",
|
||||
"s1-0": "expected-size-2",
|
||||
"s0-0": "end-condition",
|
||||
"s4-0": "total-qwc",
|
||||
"s5-0": "total-tags"
|
||||
}
|
||||
},
|
||||
|
||||
"cpad-invalid!": {
|
||||
"args": ["pad"]
|
||||
},
|
||||
|
||||
@@ -367,10 +367,28 @@ bool DecompilerTypeSystem::tp_lca(TypeState* combined, const TypeState& add) {
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& x : add.spill_slots) {
|
||||
// auto existing = combined->spill_slots.find(x.first);
|
||||
// if (existing == combined->spill_slots.end()) {
|
||||
// result = true;
|
||||
// combined->spill_slots.insert({existing->first, existing->second});
|
||||
// }
|
||||
bool diff = false;
|
||||
auto new_type = tp_lca(combined->spill_slots[x.first], x.second, &diff);
|
||||
if (diff) {
|
||||
result = true;
|
||||
combined->spill_slots[x.first] = new_type;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int DecompilerTypeSystem::get_format_arg_count(const std::string& str) const {
|
||||
// temporary hack, remove this.
|
||||
if (str == "ERROR: dma tag has data in reserved bits ~X~%") {
|
||||
return 0;
|
||||
}
|
||||
int arg_count = 0;
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
if (str.at(i) == '~') {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace decompiler {
|
||||
/*!
|
||||
* Memory on the stack used to spill a register.
|
||||
* This just has a size/is_signed? and doesn't know anything about types.
|
||||
*/
|
||||
struct StackSpillSlot {
|
||||
int offset = -1; // relative to sp
|
||||
int size = -1; // bytes
|
||||
bool is_signed = false; // set to false for quadwords/doublewords
|
||||
|
||||
bool operator==(const StackSpillSlot& other) const {
|
||||
return offset == other.offset && size == other.size && is_signed == other.is_signed;
|
||||
}
|
||||
|
||||
bool operator!=(const StackSpillSlot& other) const { return !((*this) == other); }
|
||||
|
||||
std::string print() const;
|
||||
};
|
||||
|
||||
/*!
|
||||
* Map of StackSpillSlots for a function.
|
||||
*/
|
||||
class StackSpillMap {
|
||||
public:
|
||||
void add_access(const StackSpillSlot& access);
|
||||
void finalize();
|
||||
const StackSpillSlot& lookup(int offset) const;
|
||||
int size() const;
|
||||
const std::unordered_map<int, StackSpillSlot>& map() const { return m_slot_map; }
|
||||
|
||||
private:
|
||||
std::unordered_map<int, StackSpillSlot> m_slot_map;
|
||||
};
|
||||
} // namespace decompiler
|
||||
@@ -59,6 +59,8 @@ std::string TP_Type::print() const {
|
||||
return fmt::format("<vmethod {}>", m_ts.print());
|
||||
case Kind::NON_VIRTUAL_METHOD:
|
||||
return fmt::format("<method {}>", m_ts.print());
|
||||
case Kind::LEFT_SHIFTED_BITFIELD:
|
||||
return fmt::format("(<{}> << {})", m_ts.print(), m_int);
|
||||
case Kind::INVALID:
|
||||
default:
|
||||
assert(false);
|
||||
@@ -105,6 +107,8 @@ bool TP_Type::operator==(const TP_Type& other) const {
|
||||
case Kind::INTEGER_CONSTANT_PLUS_VAR_MULT:
|
||||
return m_int == other.m_int && m_ts == other.m_ts &&
|
||||
m_extra_multiplier == other.m_extra_multiplier;
|
||||
case Kind::LEFT_SHIFTED_BITFIELD:
|
||||
return m_int == other.m_int && m_ts == other.m_ts;
|
||||
case Kind::INVALID:
|
||||
default:
|
||||
assert(false);
|
||||
@@ -157,6 +161,8 @@ TypeSpec TP_Type::typespec() const {
|
||||
return m_ts;
|
||||
case Kind::NON_VIRTUAL_METHOD:
|
||||
return m_ts;
|
||||
case Kind::LEFT_SHIFTED_BITFIELD:
|
||||
return TypeSpec("int"); // ideally this is never used.
|
||||
case Kind::INVALID:
|
||||
default:
|
||||
assert(false);
|
||||
|
||||
@@ -32,6 +32,7 @@ class TP_Type {
|
||||
DYNAMIC_METHOD_ACCESS, // partial access into a
|
||||
VIRTUAL_METHOD,
|
||||
NON_VIRTUAL_METHOD,
|
||||
LEFT_SHIFTED_BITFIELD, // (bitfield << some-constant)
|
||||
INVALID
|
||||
} kind = Kind::UNINITIALIZED;
|
||||
TP_Type() = default;
|
||||
@@ -57,6 +58,7 @@ class TP_Type {
|
||||
case Kind::INTEGER_CONSTANT_PLUS_VAR_MULT:
|
||||
case Kind::VIRTUAL_METHOD:
|
||||
case Kind::NON_VIRTUAL_METHOD:
|
||||
case Kind::LEFT_SHIFTED_BITFIELD:
|
||||
return false;
|
||||
case Kind::UNINITIALIZED:
|
||||
case Kind::OBJECT_NEW_METHOD:
|
||||
@@ -209,6 +211,14 @@ class TP_Type {
|
||||
return result;
|
||||
}
|
||||
|
||||
static TP_Type make_from_left_shift_bitfield(const TypeSpec& ts, int amount) {
|
||||
TP_Type result;
|
||||
result.kind = Kind::LEFT_SHIFTED_BITFIELD;
|
||||
result.m_ts = ts;
|
||||
result.m_int = amount;
|
||||
return result;
|
||||
}
|
||||
|
||||
const TypeSpec& get_objects_typespec() const {
|
||||
assert(kind == Kind::TYPESPEC || kind == Kind::INTEGER_CONSTANT_PLUS_VAR);
|
||||
return m_ts;
|
||||
@@ -249,6 +259,16 @@ class TP_Type {
|
||||
return m_extra_multiplier;
|
||||
}
|
||||
|
||||
int get_left_shift() const {
|
||||
assert(kind == Kind::LEFT_SHIFTED_BITFIELD);
|
||||
return m_int;
|
||||
}
|
||||
|
||||
const TypeSpec& get_bitfield_type() const {
|
||||
assert(kind == Kind::LEFT_SHIFTED_BITFIELD);
|
||||
return m_ts;
|
||||
}
|
||||
|
||||
private:
|
||||
TypeSpec m_ts;
|
||||
std::string m_str;
|
||||
@@ -260,6 +280,7 @@ class TP_Type {
|
||||
struct TypeState {
|
||||
TP_Type gpr_types[32];
|
||||
TP_Type fpr_types[32];
|
||||
std::unordered_map<int, TP_Type> spill_slots;
|
||||
|
||||
std::string print_gpr_masked(u32 mask) const;
|
||||
TP_Type& get(const Register& r) {
|
||||
@@ -285,6 +306,16 @@ struct TypeState {
|
||||
throw std::runtime_error("TP_Type::get failed");
|
||||
}
|
||||
}
|
||||
|
||||
const TP_Type& get_slot(int offset) const {
|
||||
auto result = spill_slots.find(offset);
|
||||
if (result == spill_slots.end()) {
|
||||
throw std::runtime_error("TP_Type::get_slot failed: " + std::to_string(offset));
|
||||
}
|
||||
return result->second;
|
||||
}
|
||||
|
||||
TP_Type& get_slot(int offset) { return spill_slots[offset]; }
|
||||
};
|
||||
|
||||
u32 regs_to_gpr_mask(const std::vector<Register>& regs);
|
||||
|
||||
Reference in New Issue
Block a user