mirror of
https://github.com/lifting-bits/remill
synced 2026-06-21 13:56:07 +00:00
65817f05b8
* stuff * make decoders standalone from arch * compiles * fix lazy initialization of intrinsic table * add back sleigh arch * added switching between arches... need to update contexts * add instructin sizes * thumb triplet * back out of lambda approach * updating context in thumb case * fix differential tester to initialize lifter * add back lambda approach * aarch32 context updates * add tests * fix assetions in tests * fix address alignment for tests * add better decode function * fix tests * add conditional tests * fix uninitialized context in contextupdater * restore old contexts on call... * null out branch taken arch on indirects * fix fallthrough for conditional where neither src evaluates properly * initialize is interproc * fix initial context arm * fix boolop bug where params would be lifted when not bool op * fix comparison * remove debug * make missing context non fatal * bump sleigh * add insn flow variant * stub out computing categories * add control flow structuring * adding unconditoinal abnormal * start refactoring flows to share more structure * rework conditionals to express a combination of a condition with an abnormal flow * finish basic flow analysis * add context updater * implement eq * stop passing back contexts through return value * add tests * rework constructors, make flow usage consistent * implement flows in aarch32 * add implementation of flows for old cateogries and no context * make sleigh lifter backwards compatible by applying flows to instructions * fix bug in generating coarse flows * refactor to allow lifter to bring along decoding state * rework insertion of branch taken vats * fix off by one * fix size * dont reset bytes before use * disable broken float ops * back of ambiguity to non fatal error, also add logging of callother's encountered * update names * remove debug logging * add comment about x86 context * move to headers * move eqs * order of ctors, fields, methods * noop subtype of normal * remove direct constructors * remove more duplication * more duplication in noop * remove unused field * add utility for is thumb * absolute path * remove duplication * absolute paths * review fixes * make const refs * make enums match style guide * add constructor to flow * non ref * make refs * move map out of optional * refer to enum * move curr id out * fix BOOL_NEGATE for non 1 bit * type alias for decoding result of branch taken and flow * more delog * this language makes me incredibly happy * remap flags and treat metavars more like how the instructionlifter does it * more fixes to metavars * fix pc reg definition so consistent with value during isntruction execution * fix alignment of test * add todo * extract condition * add comment
478 lines
14 KiB
C++
478 lines
14 KiB
C++
#include <glog/logging.h>
|
|
#include <lib/Arch/Sleigh/ControlFlowStructuring.h>
|
|
|
|
namespace remill::sleigh {
|
|
|
|
bool isVarnodeInConstantSpace(VarnodeData vnode) {
|
|
auto spc = vnode.getAddr().getSpace();
|
|
return spc->constant_space_index == spc->getIndex();
|
|
}
|
|
|
|
|
|
namespace {
|
|
|
|
// variant casting taken c&ped.
|
|
template <class... Args>
|
|
struct variant_cast_proxy {
|
|
std::variant<Args...> v;
|
|
|
|
template <class... ToArgs>
|
|
operator std::variant<ToArgs...>() const {
|
|
return std::visit([](auto &&arg) -> std::variant<ToArgs...> { return arg; },
|
|
v);
|
|
}
|
|
};
|
|
|
|
template <class... Args>
|
|
auto variant_cast(const std::variant<Args...> &v)
|
|
-> variant_cast_proxy<Args...> {
|
|
return {v};
|
|
}
|
|
|
|
enum CoarseEffect { kAbnormal, kNormal };
|
|
|
|
struct CoarseFlow {
|
|
CoarseEffect eff;
|
|
bool is_conditional;
|
|
};
|
|
|
|
enum CoarseCategory { kCatNormal, kCatAbnormal, kCatConditionalAbnormal };
|
|
|
|
|
|
static CoarseEffect EffectFromDirectControlFlowOp(const RemillPcodeOp &op,
|
|
uint64_t next_pc) {
|
|
CHECK(op.op == CPUI_BRANCH || op.op == CPUI_CBRANCH);
|
|
return op.vars[0].offset == next_pc ? CoarseEffect::kNormal
|
|
: CoarseEffect::kAbnormal;
|
|
}
|
|
|
|
static std::optional<CoarseFlow>
|
|
CoarseFlowFromControlFlowOp(const RemillPcodeOp &op, uint64_t next_pc) {
|
|
if (op.op == CPUI_CALL || op.op == CPUI_CALLIND || op.op == CPUI_BRANCHIND ||
|
|
op.op == CPUI_RETURN) {
|
|
return {{CoarseEffect::kAbnormal, false}};
|
|
}
|
|
|
|
// either a branch or a cbranch
|
|
|
|
// figure out if this is a fallthrough, input 0 is the next target
|
|
|
|
auto is_conditional = op.op == CPUI_CBRANCH;
|
|
if (isVarnodeInConstantSpace(op.vars[0])) {
|
|
// this is an internal branch.. we cant handle that right now
|
|
return std::nullopt;
|
|
}
|
|
|
|
return {{EffectFromDirectControlFlowOp(op, next_pc), is_conditional}};
|
|
}
|
|
|
|
// gets a list of indeces and coarse categories in this pcodeop block
|
|
static std::optional<std::map<size_t, CoarseFlow>>
|
|
CoarseFlows(const std::vector<RemillPcodeOp> &ops, uint64_t next_pc) {
|
|
std::map<size_t, CoarseFlow> res;
|
|
size_t ind = 0;
|
|
for (auto op : ops) {
|
|
if (ControlFlowStructureAnalysis::isControlFlowPcodeOp(op.op)) {
|
|
auto cc = CoarseFlowFromControlFlowOp(op, next_pc);
|
|
if (!cc) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
res.emplace(ind, *cc);
|
|
}
|
|
|
|
ind++;
|
|
}
|
|
|
|
// insert a pseudo control flow op at the end
|
|
// add a fallthrough insn at +1 to represent a last fallthrough if there is a chance we fallthrough at the end
|
|
auto insn_may_fallthrough_at_end =
|
|
ops.empty() ||
|
|
!ControlFlowStructureAnalysis::isControlFlowPcodeOp(
|
|
ops[ops.size() - 1].op) ||
|
|
ops[ops.size() - 1].op == CPUI_CBRANCH;
|
|
if (insn_may_fallthrough_at_end) {
|
|
CoarseFlow cat = {CoarseEffect::kNormal, false};
|
|
res.emplace(ops.size(), cat);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static bool isConditionalAbnormal(CoarseFlow flow) {
|
|
return flow.eff == CoarseEffect::kAbnormal && flow.is_conditional;
|
|
}
|
|
|
|
static bool isUnconditionalAbnormal(CoarseFlow flow) {
|
|
return flow.eff == CoarseEffect::kAbnormal && !flow.is_conditional;
|
|
}
|
|
|
|
static bool isConditionalNormal(CoarseFlow flow) {
|
|
return flow.eff == CoarseEffect::kNormal && flow.is_conditional;
|
|
}
|
|
|
|
static bool isUnconditionalNormal(CoarseFlow flow) {
|
|
return flow.eff == CoarseEffect::kNormal && !flow.is_conditional;
|
|
}
|
|
|
|
static std::optional<CoarseCategory>
|
|
CoarseCategoryFromFlows(const std::map<size_t, CoarseFlow> &ops) {
|
|
|
|
|
|
auto all_normal_effects = std::all_of(
|
|
ops.begin(), ops.end(), [](const std::pair<size_t, CoarseFlow> &op) {
|
|
return op.second.eff == CoarseEffect::kNormal;
|
|
});
|
|
if (all_normal_effects) {
|
|
return CoarseCategory::kCatNormal;
|
|
}
|
|
|
|
auto all_abnormal_effects = std::all_of(
|
|
ops.begin(), ops.end(), [](const std::pair<size_t, CoarseFlow> &op) {
|
|
return op.second.eff == CoarseEffect::kAbnormal;
|
|
});
|
|
if (all_abnormal_effects) {
|
|
return CoarseCategory::kCatAbnormal;
|
|
}
|
|
|
|
if (ops.size() == 2) {
|
|
auto fst = ops.begin()->second;
|
|
auto snd = ops.rbegin()->second;
|
|
if (((isConditionalAbnormal(fst) && isUnconditionalNormal(snd)) ||
|
|
(isConditionalNormal(fst) && isUnconditionalAbnormal(snd)))) {
|
|
return CoarseCategory::kCatConditionalAbnormal;
|
|
}
|
|
}
|
|
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
struct Flow {
|
|
size_t pcode_index;
|
|
CoarseFlow flow;
|
|
std::optional<DecodingContext> context;
|
|
|
|
Flow(size_t pcode_index, CoarseFlow flow,
|
|
std::optional<DecodingContext> context)
|
|
: pcode_index(pcode_index),
|
|
flow(std::move(flow)),
|
|
context(std::move(context)) {}
|
|
};
|
|
|
|
std::vector<Flow>
|
|
GetBoundContextsForFlows(const std::vector<RemillPcodeOp> &ops,
|
|
const std::map<size_t, CoarseFlow> &cc,
|
|
ContextUpdater &updater) {
|
|
|
|
std::vector<Flow> res;
|
|
CHECK(cc.size() >= 1);
|
|
CHECK(cc.crbegin()->first <= ops.size());
|
|
for (size_t curr_ind = 0; curr_ind <= ops.size(); curr_ind++) {
|
|
if (auto curr = cc.find(curr_ind); curr != cc.end()) {
|
|
auto cont = updater.GetContext();
|
|
res.emplace_back(curr_ind, curr->second, cont);
|
|
}
|
|
|
|
if (curr_ind < ops.size()) {
|
|
updater.ApplyPcodeOp(ops[curr_ind]);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
// DirectJump, IndirectJump, FunctionReturn
|
|
static std::optional<Instruction::AbnormalFlow>
|
|
AbnormalCategoryOfFlow(const Flow &flow, const RemillPcodeOp &op) {
|
|
if (op.op == CPUI_RETURN) {
|
|
Instruction::IndirectFlow id_flow(flow.context);
|
|
Instruction::FunctionReturn ret(id_flow);
|
|
return ret;
|
|
}
|
|
|
|
if (op.op == CPUI_BRANCHIND) {
|
|
Instruction::IndirectFlow id_flow(flow.context);
|
|
Instruction::IndirectJump id_jump(id_flow);
|
|
return id_jump;
|
|
}
|
|
|
|
if (op.op == CPUI_BRANCH && !isVarnodeInConstantSpace(op.vars[0]) &&
|
|
flow.context) {
|
|
auto target = op.vars[0].offset;
|
|
Instruction::DirectFlow dflow(target, *flow.context);
|
|
Instruction::DirectJump djump(dflow);
|
|
return djump;
|
|
}
|
|
|
|
if (op.op == CPUI_CALL) {
|
|
auto target = op.vars[0].offset;
|
|
Instruction::DirectFlow dflow(target, *flow.context);
|
|
Instruction::DirectFunctionCall call(dflow);
|
|
return call;
|
|
}
|
|
|
|
if (op.op == CPUI_CALLIND) {
|
|
Instruction::IndirectFlow id_flow(flow.context);
|
|
Instruction::IndirectFunctionCall call(id_flow);
|
|
return call;
|
|
}
|
|
|
|
|
|
// still need to pick up the flow for the actual abnormal transition
|
|
if (op.op == CPUI_CBRANCH) {
|
|
auto target = op.vars[0].offset;
|
|
Instruction::DirectFlow dflow(target, *flow.context);
|
|
Instruction::DirectJump djump(dflow);
|
|
return djump;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
static ControlFlowStructureAnalysis::SleighDecodingResult
|
|
ExtractNonConditionalCategory(
|
|
const std::vector<Flow> &flows, const std::vector<RemillPcodeOp> &ops,
|
|
std::function<std::optional<Instruction::InstructionFlowCategory>(
|
|
const Flow &, const RemillPcodeOp &)>
|
|
compute_single_flow_category) {
|
|
|
|
// So here the requirement to make this cateogry work is that all flows target the same abnormal (or are all returns), and all decoding contexts are equal
|
|
std::vector<Instruction::InstructionFlowCategory> cats;
|
|
for (auto flow : flows) {
|
|
if (auto cat = compute_single_flow_category(flow, ops[flow.pcode_index])) {
|
|
cats.push_back(*cat);
|
|
} else {
|
|
DLOG(ERROR) << "Missing flow cat";
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// if all cats are equal then we have our result
|
|
|
|
if (cats.size() < 1) {
|
|
DLOG(ERROR) << "No extracted cats";
|
|
return std::nullopt;
|
|
}
|
|
|
|
Instruction::InstructionFlowCategory fst = cats[0];
|
|
auto all_flows_equal = [&fst](Instruction::InstructionFlowCategory curr_cat) {
|
|
return fst == curr_cat;
|
|
};
|
|
if (std::all_of(cats.begin(), cats.end(), std::move(all_flows_equal))) {
|
|
return std::make_pair(fst, std::nullopt);
|
|
}
|
|
DLOG(ERROR) << "Not equal flows";
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
static ControlFlowStructureAnalysis::SleighDecodingResult
|
|
ExtractNormal(const std::vector<Flow> &flows,
|
|
const std::vector<RemillPcodeOp> &ops) {
|
|
// So we already know the op fallsthrough
|
|
return ExtractNonConditionalCategory(
|
|
flows, ops,
|
|
[](const Flow &flow, const RemillPcodeOp &op)
|
|
-> std::optional<Instruction::InstructionFlowCategory> {
|
|
if (flow.context) {
|
|
Instruction::NormalInsn norm(
|
|
Instruction::FallthroughFlow(*flow.context));
|
|
return norm;
|
|
}
|
|
DLOG(ERROR) << "Normal does not have context";
|
|
return std::nullopt;
|
|
});
|
|
}
|
|
|
|
|
|
static ControlFlowStructureAnalysis::SleighDecodingResult
|
|
ExtractAbnormal(const std::vector<Flow> &flows,
|
|
const std::vector<RemillPcodeOp> &ops) {
|
|
return ExtractNonConditionalCategory(
|
|
flows, ops,
|
|
[](const Flow &flow, const RemillPcodeOp &op)
|
|
-> std::optional<Instruction::InstructionFlowCategory> {
|
|
auto res = AbnormalCategoryOfFlow(flow, op);
|
|
if (res) {
|
|
return variant_cast(*res);
|
|
}
|
|
return std::nullopt;
|
|
});
|
|
}
|
|
|
|
static ControlFlowStructureAnalysis::SleighDecodingResult
|
|
ExtractConditionalAbnormal(const std::vector<Flow> &flows,
|
|
const std::vector<RemillPcodeOp> &ops) {
|
|
if (flows.size() != 2) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto &first_flow = flows[0];
|
|
const auto &snd_flow = flows[1];
|
|
|
|
// Two case sto handle here either conditional_fallthrough->abnormal
|
|
// Or conditional_abnormal -> fallthrough
|
|
|
|
|
|
if (!isConditionalNormal(first_flow.flow) &&
|
|
!isConditionalAbnormal(first_flow.flow)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto flip_cond = isConditionalNormal(first_flow.flow);
|
|
const auto &abnormal_flow =
|
|
isConditionalNormal(first_flow.flow) ? snd_flow : first_flow;
|
|
const auto &normal_flow =
|
|
isConditionalNormal(first_flow.flow) ? first_flow : snd_flow;
|
|
// so here we know the first flow is conditional of some sort and it should be followed by some unconditonal flow
|
|
CHECK(isUnconditionalAbnormal(snd_flow.flow) ||
|
|
isUnconditionalNormal(snd_flow.flow));
|
|
|
|
const auto &cond_insn = ops[first_flow.pcode_index];
|
|
|
|
CHECK(cond_insn.op == CPUI_CBRANCH);
|
|
|
|
BranchTakenVar taken_var = {
|
|
flip_cond,
|
|
cond_insn.vars[1],
|
|
first_flow.pcode_index,
|
|
};
|
|
|
|
|
|
if (!normal_flow.context) {
|
|
return std::nullopt;
|
|
}
|
|
auto normal_context = *normal_flow.context;
|
|
|
|
auto abnormal_part =
|
|
AbnormalCategoryOfFlow(abnormal_flow, ops[abnormal_flow.pcode_index]);
|
|
|
|
|
|
if (!abnormal_part) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
Instruction::ConditionalInstruction cond(
|
|
*abnormal_part, Instruction::FallthroughFlow(normal_context));
|
|
|
|
return {{cond, taken_var}};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
bool ControlFlowStructureAnalysis::isControlFlowPcodeOp(OpCode opc) {
|
|
return opc == OpCode::CPUI_BRANCH || opc == OpCode::CPUI_CBRANCH ||
|
|
opc == OpCode::CPUI_CALL || opc == OpCode::CPUI_BRANCHIND ||
|
|
opc == OpCode::CPUI_CALLIND || opc == OpCode::CPUI_RETURN;
|
|
}
|
|
|
|
|
|
// Since wre aren't supporting internal control flow right now we can categorize based on the first outgoing flow
|
|
// The only subtlety here really is allowing for conditional normals where we cbranch [fallthrough_addr] and then potentially fallthrough
|
|
|
|
/*
|
|
So in a coarse grained way we can just treat indirect/direct/interprocedural flows as the same thing and losely classify these as "abnormal" or non-fallthrough.
|
|
Either a fallthrough or an abnormal flow can be conditional
|
|
|
|
So the first step is to categorize a coarse grained control flow category which is one of:
|
|
- Normal, in this case there are only fallthroughs, conditional or otherwise
|
|
- Conditional Abnormal: either we have a CONDITIONAL_FALLTHROUGH followed by an kAbnormal
|
|
- or we have a CONDITIONAL_ABNORMAL followed by a FALLTHROUGH
|
|
- Abnormal: we have many ABNORMALs conditional or otherwise
|
|
|
|
We forbid multiple conditionals in a flow because then we'd need to join conditions
|
|
|
|
After we find coarse categories and the flows follow these patterns, we determine if there is a constant context for each relevant flow.
|
|
|
|
Finally we pass these coarse flows to a final categorizer to attempt to print these into a flow type
|
|
*/
|
|
|
|
ControlFlowStructureAnalysis::SleighDecodingResult
|
|
ControlFlowStructureAnalysis::ComputeCategory(
|
|
const std::vector<RemillPcodeOp> &ops, uint64_t fallthrough_addr,
|
|
DecodingContext entry_context) {
|
|
|
|
auto maybe_cc = CoarseFlows(ops, fallthrough_addr);
|
|
if (!maybe_cc) {
|
|
DLOG(ERROR) << "No coarse flow found";
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto cc = std::move(*maybe_cc);
|
|
|
|
auto maybe_ccategory = CoarseCategoryFromFlows(cc);
|
|
if (!maybe_ccategory) {
|
|
DLOG(ERROR) << "No coarse category found";
|
|
return std::nullopt;
|
|
}
|
|
auto context_updater = this->BuildContextUpdater(std::move(entry_context));
|
|
auto flows = GetBoundContextsForFlows(ops, cc, context_updater);
|
|
|
|
switch (*maybe_ccategory) {
|
|
case CoarseCategory::kCatAbnormal: return ExtractAbnormal(flows, ops);
|
|
case CoarseCategory::kCatConditionalAbnormal:
|
|
return ExtractConditionalAbnormal(flows, ops);
|
|
case CoarseCategory::kCatNormal: return ExtractNormal(flows, ops);
|
|
}
|
|
}
|
|
|
|
// Applies a pcode op to the held context, this may produce a complete context
|
|
void ContextUpdater::ApplyPcodeOp(const RemillPcodeOp &op) {
|
|
if (!op.outvar) {
|
|
return;
|
|
}
|
|
|
|
auto out = *op.outvar;
|
|
auto reg_name = this->engine.getRegisterName(out.space, out.offset, out.size);
|
|
auto maybe_remill_reg_name = this->context_reg_mapping.find(reg_name);
|
|
if (maybe_remill_reg_name == this->context_reg_mapping.end()) {
|
|
return;
|
|
}
|
|
|
|
auto remill_reg_name = maybe_remill_reg_name->second;
|
|
|
|
if (op.op == CPUI_COPY && isVarnodeInConstantSpace(op.vars[0])) {
|
|
this->curr_context.UpdateContextReg(remill_reg_name, op.vars[0].offset);
|
|
} else {
|
|
this->curr_context.DropReg(remill_reg_name);
|
|
}
|
|
}
|
|
|
|
// May have a complete context
|
|
std::optional<DecodingContext> ContextUpdater::GetContext() const {
|
|
for (const auto &[_, remill_reg] : this->context_reg_mapping) {
|
|
if (!this->curr_context.HasValueForReg(remill_reg)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
return this->curr_context;
|
|
}
|
|
|
|
ContextUpdater ControlFlowStructureAnalysis::BuildContextUpdater(
|
|
DecodingContext initial_context) {
|
|
return ContextUpdater(this->context_reg_mapping, std::move(initial_context),
|
|
this->engine);
|
|
}
|
|
|
|
ContextUpdater::ContextUpdater(
|
|
const std::unordered_map<std::string, std::string> &context_reg_mapping,
|
|
DecodingContext initial_context, Sleigh &engine_)
|
|
: context_reg_mapping(context_reg_mapping),
|
|
curr_context(std::move(initial_context)),
|
|
engine(engine_) {}
|
|
|
|
|
|
ControlFlowStructureAnalysis::ControlFlowStructureAnalysis(
|
|
const std::unordered_map<std::string, std::string> ®ister_mapping_,
|
|
Sleigh &engine_)
|
|
: context_reg_mapping(register_mapping_),
|
|
engine(engine_) {}
|
|
|
|
|
|
} // namespace remill::sleigh
|