opt/object-escape-analysis/ObjectEscapeAnalysis.cpp (1,368 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* * This pass identifies tracable object allocations that don't escape, and then * attempts to inline all code interacting with the local object, turning all * instance fields into registers. The changes are only applied when the * estimated savings are not negative. This helps reduce... * - object allocations at runtime, and * - code size by eliminating a many of the involved classes, fields and * methods. * * At the core is an interprocedural escape analysis with method-level summaries * that... * - may include results of method invocations as allocations, and * - follows arguments to non-true-virtual method invocations. * * This pass is conservative: Any use of an object allocation that isn't fully * understood, e.g. an external method invocation, causes that allocation to * become ineligable for optimization. In any case, this pass will not transform * a root method with the no_optimizations annotation. * * The pass computes... * - method summaries, indicating whether a method allocates and returns an * object that doesn't otherwise escape, and which method arguments don't * escape * - "inline anchors", which are particular instructions (in particular methods) * which produce a new unescaped object, either by directly allocating it or * invoking a method that directly or indirect allocates and returns an object * that doesn't otherwise escape, and then possibly use that object in ways * where it doesn't escape * - "root methods", which are all the methods which contain "inline anchors" of * types whose allocation instructions are all ultimately inlinably anchored. * - "reduced methods", which are root methods where all inlinable anchors got * fully inlined, and the fields of allocated objects got turned into * registers (and the transformation does not produce estimated negative net * savings) * * Notes: * - The transformation doesn't directly eliminate the object allocation, as the * object might be involved in some identity comparisons, e.g. for * null-checks. Instead, the object allocation gets rewritten to create an * object of type java.lang.Object, and other optimizations such as * constant-propagation and local-dead-code-elimination should be able to * remove that remaining code in most cases. * * Ideas for future work: * - Support check-cast instructions for singleton-allocations * - Support conditional branches over either zero or single allocations * - Refine the tracing of object allocations in root methods to ignore * unanchored object allocations * - Instead of inlining all invoked methods, consider transforming those which * do not mutate or compare the allocated object as follows: instead of * passing in the allocated object via an argument, pass in all read fields * are passed in as separate arguments. This could reduce the size increase * due to multiple inlined method body copies. We already do something similar * for constructors. */ #include "ObjectEscapeAnalysis.h" #include <algorithm> #include <optional> #include "ApiLevelChecker.h" #include "BaseIRAnalyzer.h" #include "CFGMutation.h" #include "ConfigFiles.h" #include "ConstantAbstractDomain.h" #include "ControlFlow.h" #include "DexClass.h" #include "IRCode.h" #include "IRInstruction.h" #include "IROpcode.h" #include "Inliner.h" #include "LiveRange.h" #include "MethodOverrideGraph.h" #include "PassManager.h" #include "PatriciaTreeMap.h" #include "PatriciaTreeMapAbstractEnvironment.h" #include "PatriciaTreeSet.h" #include "PatriciaTreeSetAbstractDomain.h" #include "Resolver.h" #include "Show.h" #include "StringBuilder.h" #include "Walkers.h" using namespace sparta; namespace mog = method_override_graph; namespace { const int MAX_INLINE_INVOKES_ITERATIONS = 8; using Locations = std::vector<std::pair<DexMethod*, const IRInstruction*>>; // Collect all allocation and invoke instructions, as well as non-virtual // invocation dependencies. void analyze_scope( const Scope& scope, const std::unordered_set<DexMethod*>& non_true_virtual, std::unordered_map<DexType*, Locations>* new_instances, std::unordered_map<DexMethod*, Locations>* invokes, std::unordered_map<DexMethod*, std::unordered_set<DexMethod*>>* dependencies) { Timer t("analyze_scope"); ConcurrentMap<DexType*, Locations> concurrent_new_instances; ConcurrentMap<DexMethod*, Locations> concurrent_invokes; ConcurrentMap<DexMethod*, std::unordered_set<DexMethod*>> concurrent_dependencies; walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) { code.build_cfg(/* editable */ true); for (auto& mie : InstructionIterable(code.cfg())) { auto insn = mie.insn; if (insn->opcode() == OPCODE_NEW_INSTANCE) { auto cls = type_class(insn->get_type()); if (cls && !cls->is_external()) { concurrent_new_instances.update( insn->get_type(), [&](auto*, auto& vec, bool) { vec.emplace_back(method, insn); }); } } else if (opcode::is_an_invoke(insn->opcode())) { auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); if (callee && (!callee->is_virtual() || non_true_virtual.count(callee))) { concurrent_invokes.update(callee, [&](auto*, auto& vec, bool) { vec.emplace_back(method, insn); }); if (!method->is_virtual() || non_true_virtual.count(method)) { concurrent_dependencies.update( callee, [method](auto, auto& set, auto) { set.insert(method); }); } } } } }); for (auto& p : concurrent_new_instances) { new_instances->insert(std::move(p)); } for (auto& p : concurrent_invokes) { invokes->insert(std::move(p)); } for (auto& p : concurrent_dependencies) { dependencies->insert(std::move(p)); } } // A benign method invocation can be ignored during the escape analysis. bool is_benign(const DexMethodRef* method_ref) { static const std::unordered_set<std::string> methods = { // clang-format off "Ljava/lang/Object;.<init>:()V", // clang-format on }; return method_ref->is_def() && methods.count(method_ref->as_def()->get_deobfuscated_name_or_empty()); } constexpr const IRInstruction* NO_ALLOCATION = nullptr; using namespace ir_analyzer; // For each object, we track which instruction might have allocated it: // - new-instance and invoke- instruction might represent allocation points // - NO_ALLOCATION is a value for which the allocation instruction is not known, // or it is not an object using Domain = sparta::PatriciaTreeSetAbstractDomain<const IRInstruction*>; // For each register that holds a relevant value, keep track of it. using Environment = sparta::PatriciaTreeMapAbstractEnvironment<reg_t, Domain>; struct MethodSummary { // A parameter is "benign" if a provided argument does not escape std::unordered_set<src_index_t> benign_params; // A method might contain a unique instruction which allocates an object that // is eventually unconditionally returned. const IRInstruction* allocation_insn{nullptr}; }; // The analyzer computes... // - which instructions allocate (new-instance, invoke-) // - which allocations escape (and how) // - which allocations return class Analyzer final : public BaseIRAnalyzer<Environment> { public: explicit Analyzer( const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, cfg::ControlFlowGraph& cfg) : BaseIRAnalyzer(cfg), m_method_summaries(method_summaries) { MonotonicFixpointIterator::run(Environment::top()); } static const IRInstruction* get_singleton_allocation(const Domain& domain) { always_assert(domain.kind() == AbstractValueKind::Value); auto& elements = domain.elements(); if (elements.size() != 1) { return nullptr; } return *elements.begin(); } void analyze_instruction(const IRInstruction* insn, Environment* current_state) const override { const auto escape = [&](src_index_t src_idx) { auto reg = insn->src(src_idx); const auto& domain = current_state->get(reg); always_assert(domain.kind() == AbstractValueKind::Value); for (auto allocation_insn : domain.elements()) { if (allocation_insn != NO_ALLOCATION) { m_escapes[allocation_insn].insert( {const_cast<IRInstruction*>(insn), src_idx}); } } }; if (insn->opcode() == OPCODE_NEW_INSTANCE) { auto type = insn->get_type(); auto cls = type_class(type); if (cls && !cls->is_external()) { m_escapes[insn]; current_state->set(RESULT_REGISTER, Domain(insn)); return; } } else if (insn->opcode() == IOPCODE_LOAD_PARAM_OBJECT) { m_escapes[insn]; current_state->set(insn->dest(), Domain(insn)); return; } else if (insn->opcode() == OPCODE_RETURN_OBJECT) { const auto& domain = current_state->get(insn->src(0)); always_assert(domain.kind() == AbstractValueKind::Value); m_returns.insert(domain.elements().begin(), domain.elements().end()); return; } else if (insn->opcode() == OPCODE_MOVE_RESULT_OBJECT || insn->opcode() == IOPCODE_MOVE_RESULT_PSEUDO_OBJECT) { const auto& domain = current_state->get(RESULT_REGISTER); current_state->set(insn->dest(), domain); return; } else if (insn->opcode() == OPCODE_MOVE_OBJECT) { const auto& domain = current_state->get(insn->src(0)); current_state->set(insn->dest(), domain); return; } else if (insn->opcode() == OPCODE_INSTANCE_OF || opcode::is_an_iget(insn->opcode())) { if (get_singleton_allocation(current_state->get(insn->src(0)))) { current_state->set(RESULT_REGISTER, Domain(NO_ALLOCATION)); return; } } else if (opcode::is_a_monitor(insn->opcode()) || insn->opcode() == OPCODE_IF_EQZ || insn->opcode() == OPCODE_IF_NEZ) { if (get_singleton_allocation(current_state->get(insn->src(0)))) { return; } } else if (opcode::is_an_iput(insn->opcode())) { if (get_singleton_allocation(current_state->get(insn->src(1)))) { escape(0); return; } } else if (opcode::is_an_invoke(insn->opcode())) { if (is_benign(insn->get_method())) { current_state->set(RESULT_REGISTER, Domain(NO_ALLOCATION)); return; } auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); auto it = m_method_summaries.find(callee); auto benign_params = it == m_method_summaries.end() ? nullptr : &it->second.benign_params; for (src_index_t i = 0; i < insn->srcs_size(); i++) { if (!benign_params || !benign_params->count(i) || !get_singleton_allocation(current_state->get(insn->src(i)))) { escape(i); } } Domain domain(NO_ALLOCATION); if (it != m_method_summaries.end() && it->second.allocation_insn) { m_escapes[insn]; domain = Domain(insn); } current_state->set(RESULT_REGISTER, domain); return; } for (src_index_t i = 0; i < insn->srcs_size(); i++) { escape(i); } if (insn->has_dest()) { current_state->set(insn->dest(), Domain(NO_ALLOCATION)); if (insn->dest_is_wide()) { current_state->set(insn->dest() + 1, Domain::top()); } } else if (insn->has_move_result_any()) { current_state->set(RESULT_REGISTER, Domain(NO_ALLOCATION)); } } const std::unordered_map<const IRInstruction*, std::unordered_set<live_range::Use>>& get_escapes() { return m_escapes; } const std::unordered_set<const IRInstruction*>& get_returns() { return m_returns; } std::unordered_set<IRInstruction*> get_inlinables() { std::unordered_set<IRInstruction*> inlinables; for (auto& p : m_escapes) { if (p.second.empty() && p.first->opcode() != IOPCODE_LOAD_PARAM_OBJECT && !m_returns.count(p.first)) { inlinables.insert(const_cast<IRInstruction*>(p.first)); } } return inlinables; } private: const std::unordered_map<DexMethod*, MethodSummary>& m_method_summaries; mutable std::unordered_map<const IRInstruction*, std::unordered_set<live_range::Use>> m_escapes; mutable std::unordered_set<const IRInstruction*> m_returns; }; std::unordered_map<DexMethod*, MethodSummary> compute_method_summaries( PassManager& mgr, const Scope& scope, const std::unordered_map<DexMethod*, std::unordered_set<DexMethod*>>& dependencies, const std::unordered_set<DexMethod*>& non_true_virtual) { Timer t("compute_method_summaries"); std::unordered_set<DexMethod*> impacted_methods; walk::code(scope, [&](DexMethod* method, IRCode&) { if (!method->is_virtual() || non_true_virtual.count(method)) { impacted_methods.insert(method); } }); std::unordered_map<DexMethod*, MethodSummary> method_summaries; size_t analysis_iterations = 0; while (!impacted_methods.empty()) { Timer t2("analysis iteration"); analysis_iterations++; TRACE(OEA, 1, "[object escape analysis] analysis_iteration %zu", analysis_iterations); ConcurrentMap<DexMethod*, MethodSummary> recomputed_method_summaries; workqueue_run<DexMethod*>( [&](DexMethod* method) { auto& cfg = method->get_code()->cfg(); Analyzer analyzer(method_summaries, cfg); const auto& escapes = analyzer.get_escapes(); const auto& returns = analyzer.get_returns(); src_index_t src_index = 0; for (auto& mie : InstructionIterable(cfg.get_param_instructions())) { if (mie.insn->opcode() == IOPCODE_LOAD_PARAM_OBJECT && escapes.at(mie.insn).empty() && !returns.count(mie.insn)) { recomputed_method_summaries.update( method, [src_index](DexMethod*, auto& ms, bool) { ms.benign_params.insert(src_index); }); } src_index++; } const IRInstruction* allocation_insn; if (returns.size() == 1 && (allocation_insn = *returns.begin()) != NO_ALLOCATION && escapes.at(allocation_insn).empty() && allocation_insn->opcode() != IOPCODE_LOAD_PARAM_OBJECT) { recomputed_method_summaries.update( method, [allocation_insn](DexMethod*, auto& ms, bool) { ms.allocation_insn = allocation_insn; }); } }, impacted_methods); std::unordered_set<DexMethod*> changed_methods; for (auto& p : recomputed_method_summaries) { auto& ms = method_summaries[p.first]; for (auto src_index : p.second.benign_params) { if (ms.benign_params.insert(src_index).second) { changed_methods.insert(p.first); } } if (p.second.allocation_insn && !ms.allocation_insn) { ms.allocation_insn = p.second.allocation_insn; changed_methods.insert(p.first); } } impacted_methods.clear(); for (auto method : changed_methods) { auto it = dependencies.find(method); if (it != dependencies.end()) { impacted_methods.insert(it->second.begin(), it->second.end()); } } } mgr.incr_metric("analysis_iterations", analysis_iterations); return method_summaries; } DexType* get_allocated_type( const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, DexMethod* method) { auto insn = method_summaries.at(method).allocation_insn; while (insn->opcode() != OPCODE_NEW_INSTANCE) { always_assert(opcode::is_an_invoke(insn->opcode())); auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); insn = method_summaries.at(callee).allocation_insn; } return insn->get_type(); } using InlineAnchorsOfType = std::unordered_map<DexMethod*, std::unordered_set<IRInstruction*>>; std::unordered_map<DexType*, InlineAnchorsOfType> compute_inline_anchors( const Scope& scope, const std::unordered_map<DexMethod*, MethodSummary>& method_summaries) { Timer t("compute_inline_anchors"); ConcurrentMap<DexType*, InlineAnchorsOfType> concurrent_inline_anchors; walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) { Analyzer analyzer(method_summaries, code.cfg()); auto inlinables = analyzer.get_inlinables(); for (auto insn : inlinables) { if (insn->opcode() == OPCODE_NEW_INSTANCE) { TRACE(OEA, 3, "[object escape analysis] inline anchor [%s] %s", SHOW(method), SHOW(insn)); concurrent_inline_anchors.update( insn->get_type(), [&](auto*, auto& map, bool) { map[method].insert(insn); }); continue; } always_assert(opcode::is_an_invoke(insn->opcode())); auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); always_assert(method_summaries.at(callee).allocation_insn); auto type = get_allocated_type(method_summaries, callee); TRACE(OEA, 3, "[object escape analysis] inline anchor [%s] %s", SHOW(method), SHOW(insn)); concurrent_inline_anchors.update( type, [&](auto*, auto& map, bool) { map[method].insert(insn); }); } }); std::unordered_map<DexType*, InlineAnchorsOfType> inline_anchors; for (auto& p : concurrent_inline_anchors) { inline_anchors.insert(std::move(p)); } return inline_anchors; } std::unordered_map<DexMethod*, std::unordered_map<DexType*, bool>> compute_root_methods( PassManager& mgr, const std::unordered_map<DexType*, Locations>& new_instances, const std::unordered_map<DexMethod*, Locations>& invokes, const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, const std::unordered_map<DexType*, InlineAnchorsOfType>& inline_anchors) { Timer t("compute_root_methods"); std::unordered_set<DexType*> candidate_types; std::unordered_map<DexMethod*, std::unordered_map<DexType*, bool>> root_methods; for (auto& [type, method_insn_pairs] : new_instances) { auto it = inline_anchors.find(type); if (it == inline_anchors.end()) { continue; } auto& inline_anchors_of_type = it->second; bool multiples = inline_anchors_of_type.size() > 1; std::function<bool(const std::pair<DexMethod*, const IRInstruction*>&)> is_anchored; is_anchored = [&](const auto& p) { auto [method, insn] = p; auto it2 = inline_anchors_of_type.find(method); if (it2 != inline_anchors_of_type.end() && it2->second.count(const_cast<IRInstruction*>(insn))) { return true; } auto it3 = method_summaries.find(method); if (it3 == method_summaries.end() || it3->second.allocation_insn != insn) { return false; } auto it4 = invokes.find(method); if (it4 == invokes.end()) { return false; } if (it4->second.size() > 1) { multiples = true; } for (auto q : it4->second) { if (!is_anchored(q)) { return false; } } return true; }; if (std::all_of(method_insn_pairs.begin(), method_insn_pairs.end(), is_anchored)) { candidate_types.insert(type); for (auto& [method, insns] : inline_anchors_of_type) { if (!method->rstate.no_optimizations()) { TRACE(OEA, 3, "[object escape analysis] root method %s with %s%s", SHOW(method), SHOW(type), multiples ? " multiples" : ""); root_methods[method].emplace(type, multiples); } } } } TRACE(OEA, 1, "[object escape analysis] candidate types: %zu", candidate_types.size()); mgr.incr_metric("candidate types", candidate_types.size()); return root_methods; } size_t get_code_size(DexMethod* method) { auto& cfg = method->get_code()->cfg(); size_t code_size{0}; for (auto& mie : InstructionIterable(cfg)) { auto insn = mie.insn; if (opcode::is_a_move(insn->opcode()) || opcode::is_a_return(insn->opcode())) { continue; } code_size += mie.insn->size(); } return code_size; } struct Stats { std::atomic<size_t> total_savings{0}; std::atomic<size_t> reduced_methods{0}; std::atomic<size_t> selected_reduced_methods{0}; std::atomic<size_t> invokes_not_inlinable_callee_is_init_unexpandable{0}; std::atomic<size_t> invokes_not_inlinable_callee_inconcrete{0}; std::atomic<size_t> invokes_not_inlinable_callee_is_init_too_many_params_to_expand{0}; std::atomic<size_t> invokes_not_inlinable_inlining{0}; std::atomic<size_t> invokes_not_inlinable_too_many_iterations{0}; std::atomic<size_t> anchors_not_inlinable_inlining{0}; std::atomic<size_t> stackify_returns_objects{0}; std::atomic<size_t> too_costly_multiple_conditional_classes{0}; std::atomic<size_t> too_costly_irreducible_classes{0}; std::atomic<size_t> too_costly_globally{0}; std::atomic<size_t> expanded_ctors{0}; }; // Predict what a method's deobfuscated name would be. std::string show_deobfuscated(const DexType* type, const DexString* name, const DexProto* proto) { string_builders::StaticStringBuilder<5> b; b << show_deobfuscated(type) << "." << show(name) << ":" << show_deobfuscated(proto); return b.str(); } // Helper class to deal with (otherwise uninlinable) constructors that take a // (newly created) object, and only use it to read ifields. For those // constructors, we identify when we can replace the (newly created) object // parameter with a sequence of field value parameters. class ExpandableConstructorParams { private: // For each class, and each constructor, and each parameter, we record the // (ordered) list of ifields that are read from the parameter, if the // parameter doesn't otherwise escape, and the implied expanded constructor // arg list is not in conflict with any other constructor arg list. using ClassInfo = std::unordered_map< DexMethod*, std::unordered_map<param_index_t, std::vector<DexField*>>>; mutable ConcurrentMap<DexType*, std::shared_ptr<ClassInfo>> m_class_infos; // For each requested expanded constructor method ref, we remember the // original and ctor, and which parameter was expanded. using MethodParam = std::pair<DexMethod*, param_index_t>; mutable std::unordered_map<DexMethodRef*, MethodParam> m_candidates; mutable std::mutex m_candidates_mutex; // We keep track of deobfuscated ctor names already in use before the pass, to // avoid reusing them. std::unordered_set<const DexString*> m_deobfuscated_ctor_names; static std::vector<DexType*> get_expanded_args_vector( DexMethod* ctor, param_index_t param_index, const std::vector<DexField*>& fields) { always_assert(param_index > 0); auto args = ctor->get_proto()->get_args(); always_assert(param_index <= args->size()); std::vector<DexType*> args_vector; args_vector.reserve(args->size() - 1 + fields.size()); for (param_index_t i = 0; i < args->size(); i++) { if (i != param_index - 1) { args_vector.push_back(args->at(i)); continue; } for (auto f : fields) { args_vector.push_back(f->get_type()); } } return args_vector; } // Get or create the class-info for a given type. ClassInfo* get_class_info(DexType* type) const { auto res = m_class_infos.get(type, nullptr); if (res) { return res.get(); } res = std::make_shared<ClassInfo>(); std::set<std::vector<DexType*>> args_vectors; auto cls = type_class(type); if (cls) { // First, collect all of the (guaranteed to be distinct) args of the // existing constructors. for (auto* ctor : cls->get_ctors()) { auto args = ctor->get_proto()->get_args(); std::vector<DexType*> args_vector(args->begin(), args->end()); auto inserted = args_vectors.insert(std::move(args_vector)).second; always_assert(inserted); } // Second, for each ctor, and each (non-first) parameter that is only used // in igets, compute the expanded constructor args and record them if they // don't create a conflict. for (auto* ctor : cls->get_ctors()) { auto code = ctor->get_code(); if (!code || ctor->rstate.no_optimizations()) { continue; } live_range::MoveAwareChains chains(code->cfg()); auto du_chains = chains.get_def_use_chains(); param_index_t param_index{1}; auto ii = code->cfg().get_param_instructions(); for (auto it = std::next(ii.begin()); it != ii.end(); it++, param_index++) { bool expandable{true}; std::vector<DexField*> fields; for (auto& use : du_chains[it->insn]) { if (opcode::is_an_iget(use.insn->opcode())) { auto* field = resolve_field(use.insn->get_field(), FieldSearch::Instance); if (field) { fields.push_back(field); continue; } } expandable = false; break; } if (!expandable) { continue; } std::sort(fields.begin(), fields.end(), compare_dexfields); // remove duplicates fields.erase(std::unique(fields.begin(), fields.end()), fields.end()); auto expanded_args_vector = get_expanded_args_vector(ctor, param_index, fields); // We need to check if we don't have too many args that won't fit into // an invoke/range instruction. uint32_t range_size = 1; for (auto arg_type : expanded_args_vector) { range_size += type::is_wide_type(arg_type) ? 2 : 1; } if (range_size <= 0xff) { auto inserted = args_vectors.insert(std::move(expanded_args_vector)).second; if (inserted) { (*res)[ctor].emplace(param_index, std::move(fields)); } } } } } m_class_infos.update(type, [&](auto*, auto& value, bool exists) { if (exists) { // Oh well, we wasted some racing with another thread. res = value; return; } value = res; }); return res.get(); } // Given an earlier created expanded constructor method ref, fill in the code. DexMethod* make_expanded_ctor_concrete(DexMethodRef* expanded_ctor_ref) { auto [ctor, param_index] = m_candidates.at(expanded_ctor_ref); // We start from the original ctor method body, and mutate a copy. std::unique_ptr<IRCode> cloned_code = std::make_unique<IRCode>(std::make_unique<cfg::ControlFlowGraph>()); ctor->get_code()->cfg().deep_copy(&cloned_code->cfg()); auto& cfg = cloned_code->cfg(); cfg::CFGMutation mutation(cfg); // Replace load-param of (newly created) object with a sequence of // load-params for the field values used by the ctor; initialize the (newly // created) object register with a const-0, so that any remaining // move-object instructions are still valid. auto block = cfg.entry_block(); auto load_param_it = block->to_cfg_instruction_iterator(block->get_first_insn()); always_assert(!load_param_it.is_end()); for (param_index_t i = 0; i < param_index; i++) { load_param_it++; always_assert(!load_param_it.is_end()); } auto last_load_params_it = block->to_cfg_instruction_iterator( block->get_last_param_loading_insn()); auto null_insn = (new IRInstruction(OPCODE_CONST)) ->set_dest(load_param_it->insn->dest()) ->set_literal(0); mutation.insert_after(last_load_params_it, {null_insn}); std::vector<IRInstruction*> new_load_param_insns; std::unordered_map<DexField*, reg_t> field_regs; auto& fields = m_class_infos.at_unsafe(ctor->get_class())->at(ctor).at(param_index); for (auto field : fields) { auto reg = type::is_wide_type(field->get_type()) ? cfg.allocate_wide_temp() : cfg.allocate_temp(); auto inserted = field_regs.emplace(field, reg).second; always_assert(inserted); auto load_param_insn = (new IRInstruction(opcode::load_opcode(field->get_type()))) ->set_dest(reg); new_load_param_insns.push_back(load_param_insn); } mutation.replace(load_param_it, new_load_param_insns); // Replace all igets on the (newly created) object with moves from the new // field value load-params. No other (non-move) uses of the (newly created) // object can exist. live_range::MoveAwareChains chains(cfg); auto du_chains = chains.get_def_use_chains(); std::unordered_set<IRInstruction*> use_insns; for (auto& use : du_chains[load_param_it->insn]) { use_insns.insert(use.insn); } auto ii = InstructionIterable(cfg); for (auto it = ii.begin(); it != ii.end(); it++) { if (!use_insns.count(it->insn)) { continue; } auto insn = it->insn; always_assert(opcode::is_an_iget(insn->opcode())); auto* field = resolve_field(insn->get_field(), FieldSearch::Instance); always_assert(field); auto move_result_pseudo_it = cfg.move_result_of(it); always_assert(!move_result_pseudo_it.is_end()); auto reg = field_regs.at(field); auto dest = move_result_pseudo_it->insn->dest(); auto move_insn = (new IRInstruction(opcode::move_opcode(field->get_type()))) ->set_src(0, reg) ->set_dest(dest); mutation.replace(it, {move_insn}); } // Use the mutated copied ctor code to conretize the expanded ctor. mutation.flush(); expanded_ctor_ref->make_concrete(ACC_CONSTRUCTOR | ACC_PUBLIC, std::move(cloned_code), false); auto expanded_ctor = expanded_ctor_ref->as_def(); always_assert(expanded_ctor); expanded_ctor->rstate.set_generated(); int api_level = api::LevelChecker::get_method_level(ctor); expanded_ctor->rstate.set_api_level(api_level); expanded_ctor->set_deobfuscated_name(show_deobfuscated(expanded_ctor)); return expanded_ctor; } public: explicit ExpandableConstructorParams(const Scope& scope) { walk::classes(scope, [&](DexClass* cls) { for (auto ctor : cls->get_ctors()) { auto deob = ctor->get_deobfuscated_name_or_null(); if (deob) { m_deobfuscated_ctor_names.insert(deob); } } }); } // Try to create a method-ref that represents an expanded ctor, where a // particular parameter representing a (newly created) object gets replaced by // a sequence of field values used by the ctor. DexMethodRef* get_expanded_ctor_ref(DexMethod* ctor, param_index_t param_index, std::vector<DexField*>** fields) const { auto type = ctor->get_class(); auto class_info = get_class_info(type); auto it = class_info->find(ctor); if (it == class_info->end()) { return nullptr; } auto it2 = it->second.find(param_index); if (it2 == it->second.end()) { return nullptr; } auto name = ctor->get_name(); auto args_vector = get_expanded_args_vector(ctor, param_index, it2->second); auto type_list = DexTypeList::make_type_list(std::move(args_vector)); auto proto = DexProto::make_proto(type::_void(), type_list); auto deob = show_deobfuscated(type, name, proto); if (m_deobfuscated_ctor_names.count(DexString::make_string(deob))) { // Some other method ref already has the synthetic deobfuscated name that // we'd later want to give to the new generated ctor. return nullptr; } std::lock_guard<std::mutex> lock_guard(m_candidates_mutex); auto expanded_ctor_ref = DexMethod::get_method(type, name, proto); if (expanded_ctor_ref) { if (!m_candidates.count(expanded_ctor_ref)) { // There's already a pre-existing method registered, maybe a method that // became unreachable. As other Redex optimizations might have persisted // this method-ref, we don't want to interact with it. return nullptr; } } else { expanded_ctor_ref = DexMethod::make_method(type, name, proto); always_assert(show_deobfuscated(expanded_ctor_ref) == deob); auto emplaced = m_candidates .emplace(expanded_ctor_ref, std::make_pair(ctor, param_index)) .second; always_assert(emplaced); } *fields = &it2->second; return expanded_ctor_ref; } // Make sure that all newly used expanded ctors actually exist as concrete // methods. void flush(const Scope& scope, Stats* stats) { // First, find all expanded_ctor_ref that made it into the updated code. ConcurrentSet<DexMethodRef*> used_expanded_ctor_refs; walk::parallel::opcodes(scope, [&](DexMethod*, IRInstruction* insn) { if (opcode::is_invoke_direct(insn->opcode()) && m_candidates.count(insn->get_method())) { used_expanded_ctor_refs.insert(insn->get_method()); } }); // Second, make them all concrete. ConcurrentSet<DexMethod*> expanded_ctors; workqueue_run<DexMethodRef*>( [&](DexMethodRef* expanded_ctor_ref) { expanded_ctors.insert(make_expanded_ctor_concrete(expanded_ctor_ref)); }, used_expanded_ctor_refs); // Add the newly concretized ctors to their classes. std::vector<DexMethod*> ordered(expanded_ctors.begin(), expanded_ctors.end()); std::sort(ordered.begin(), ordered.end(), compare_dexmethods); for (auto expanded_ctor : ordered) { type_class(expanded_ctor->get_class())->add_method(expanded_ctor); } // Finally, erase the unused ctor method refs. for (auto [ctor, param_index] : m_candidates) { if (!used_expanded_ctor_refs.count(ctor)) { DexMethod::erase_method(ctor); DexMethod::delete_method_DO_NOT_USE(static_cast<DexMethod*>(ctor)); } } stats->expanded_ctors += expanded_ctors.size(); } }; // Data structure to derive local or accumulated global net savings struct NetSavings { int local{0}; std::unordered_set<DexType*> classes; std::unordered_set<DexMethod*> methods; NetSavings& operator+=(const NetSavings& other) { local += other.local; classes.insert(other.classes.begin(), other.classes.end()); methods.insert(other.methods.begin(), other.methods.end()); return *this; } int get_value() const { int net_savings{local}; for (auto method : methods) { auto code_size = get_code_size(method); net_savings += 20 + code_size; } for (auto type : classes) { auto cls = type_class(type); always_assert(cls); if (can_delete(cls) && !cls->get_clinit()) { net_savings += 48; } for (auto field : cls->get_ifields()) { if (can_delete(field)) { net_savings += 8; } } } return net_savings; } }; // Data structure that represents a (cloned) reduced method, together with some // auxiliary information that allows to derive net savings. struct ReducedMethod { DexMethod* method; size_t initial_code_size; std::unordered_map<DexMethod*, std::unordered_set<DexType*>> inlined_methods; NetSavings get_net_savings( const std::unordered_map<DexType*, bool>& types, const std::unordered_set<DexType*>& irreducible_types, NetSavings* conditional_net_savings) const { auto final_code_size = get_code_size(method); NetSavings net_savings; net_savings.local = (int)initial_code_size - (int)final_code_size; std::unordered_set<DexType*> remaining; for (auto& [inlined_method, inlined_types] : inlined_methods) { if (!can_delete(inlined_method)) { remaining.insert(inlined_types.begin(), inlined_types.end()); continue; } bool any_remaining = false; for (auto type : inlined_types) { if (types.at(type) || irreducible_types.count(type)) { remaining.insert(type); any_remaining = true; } } if (any_remaining) { conditional_net_savings->methods.insert(inlined_method); continue; } net_savings.methods.insert(inlined_method); } for (auto [type, multiples] : types) { if (remaining.count(type) || multiples || irreducible_types.count(type)) { conditional_net_savings->classes.insert(type); continue; } net_savings.classes.insert(type); } return net_savings; } }; class RootMethodReducer { private: const ExpandableConstructorParams& m_expandable_constructor_params; MultiMethodInliner& m_inliner; const std::unordered_map<DexMethod*, MethodSummary>& m_method_summaries; Stats* m_stats; bool m_is_init_or_clinit; DexMethod* m_method; const std::unordered_map<DexType*, bool>& m_types; public: RootMethodReducer( const ExpandableConstructorParams& expandable_constructor_params, MultiMethodInliner& inliner, const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, Stats* stats, bool is_init_or_clinit, DexMethod* method, const std::unordered_map<DexType*, bool>& types) : m_expandable_constructor_params(expandable_constructor_params), m_inliner(inliner), m_method_summaries(method_summaries), m_stats(stats), m_is_init_or_clinit(is_init_or_clinit), m_method(method), m_types(types) {} std::optional<ReducedMethod> reduce() { shrink(); auto initial_code_size{get_code_size(m_method)}; if (!inline_anchors() || !expand_or_inline_invokes()) { return std::nullopt; } while (auto* insn = find_inlinable_new_instance()) { if (!stackify(insn)) { return std::nullopt; } } shrink(); return (ReducedMethod){m_method, initial_code_size, std::move(m_inlined_methods)}; } private: void shrink() { m_inliner.get_shrinker().shrink_code(m_method->get_code(), is_static(m_method), m_is_init_or_clinit, m_method->get_class(), m_method->get_proto(), [this]() { return show(m_method); }); } bool inline_insns(const std::unordered_set<IRInstruction*>& insns) { auto inlined = m_inliner.inline_callees(m_method, insns); return inlined == insns.size(); } // Given a constructor invocation, replace a particular argument with the // sequence of the argument's field values to flow into an expanded // constructor. DexMethodRef* expand_invoke(cfg::CFGMutation& mutation, const cfg::InstructionIterator& it, param_index_t param_index) { auto insn = it->insn; auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); always_assert(callee); always_assert(callee->is_concrete()); std::vector<DexField*>* fields; auto expanded_ctor_ref = m_expandable_constructor_params.get_expanded_ctor_ref( callee, param_index, &fields); if (!expanded_ctor_ref) { return nullptr; } insn->set_method(expanded_ctor_ref); auto obj_reg = insn->src(param_index); auto srcs_range = insn->srcs(); std::vector<reg_t> srcs_copy(srcs_range.begin(), srcs_range.end()); insn->set_srcs_size(srcs_copy.size() - 1 + fields->size()); for (param_index_t i = param_index; i < srcs_copy.size() - 1; i++) { insn->set_src(i + fields->size(), srcs_copy.at(i + 1)); } std::vector<IRInstruction*> instructions_to_insert; auto& cfg = m_method->get_code()->cfg(); for (auto field : *fields) { auto reg = type::is_wide_type(field->get_type()) ? cfg.allocate_wide_temp() : cfg.allocate_temp(); insn->set_src(param_index++, reg); auto iget_opcode = opcode::iget_opcode_for_field(field); instructions_to_insert.push_back((new IRInstruction(iget_opcode)) ->set_src(0, obj_reg) ->set_field(field)); auto move_result_pseudo_opcode = opcode::move_result_pseudo_for_iget(iget_opcode); instructions_to_insert.push_back( (new IRInstruction(move_result_pseudo_opcode))->set_dest(reg)); } mutation.insert_before(it, std::move(instructions_to_insert)); return expanded_ctor_ref; } bool expand_invokes(const std::unordered_map<IRInstruction*, param_index_t>& invokes_to_expand, std::unordered_set<DexMethodRef*>* expanded_ctor_refs) { if (invokes_to_expand.empty()) { return true; } auto& cfg = m_method->get_code()->cfg(); cfg::CFGMutation mutation(cfg); auto ii = InstructionIterable(cfg); for (auto it = ii.begin(); it != ii.end(); it++) { auto insn = it->insn; auto it2 = invokes_to_expand.find(insn); if (it2 == invokes_to_expand.end()) { continue; } auto param_index = it2->second; auto expanded_ctor_ref = expand_invoke(mutation, it, param_index); if (!expanded_ctor_ref) { return false; } expanded_ctor_refs->insert(expanded_ctor_ref); } mutation.flush(); return true; } // Inline all "anchors" until all relevant allocations are new-instance // instructions in the (root) method. bool inline_anchors() { auto& cfg = m_method->get_code()->cfg(); while (true) { Analyzer analyzer(m_method_summaries, cfg); std::unordered_set<IRInstruction*> invokes_to_inline; auto inlinables = analyzer.get_inlinables(); for (auto insn : inlinables) { if (insn->opcode() == OPCODE_NEW_INSTANCE) { continue; } always_assert(opcode::is_an_invoke(insn->opcode())); auto callee = resolve_method(insn->get_method(), opcode_to_search(insn)); auto type = get_allocated_type(m_method_summaries, callee); if (m_types.count(type)) { invokes_to_inline.insert(insn); m_inlined_methods[callee].insert(type); } } if (invokes_to_inline.empty()) { return true; } if (!inline_insns(invokes_to_inline)) { m_stats->anchors_not_inlinable_inlining++; return false; } // simplify to prune now unreachable code, e.g. from removed exception // handlers cfg.simplify(); } } bool is_inlinable_new_instance(IRInstruction* insn) const { return insn->opcode() == OPCODE_NEW_INSTANCE && m_types.count(insn->get_type()); } IRInstruction* find_inlinable_new_instance() const { auto& cfg = m_method->get_code()->cfg(); for (auto& mie : InstructionIterable(cfg)) { auto insn = mie.insn; if (is_inlinable_new_instance(insn)) { return insn; } } return nullptr; } // Expand or inline all uses of all relevant new-instance instructions that // involve invoke- instructions, until there are no more such uses. bool expand_or_inline_invokes() { auto& cfg = m_method->get_code()->cfg(); std::unordered_set<DexMethodRef*> expanded_ctor_refs; for (int iteration = 0; iteration < MAX_INLINE_INVOKES_ITERATIONS; iteration++) { std::unordered_set<IRInstruction*> invokes_to_inline; std::unordered_map<IRInstruction*, param_index_t> invokes_to_expand; live_range::MoveAwareChains chains(cfg); auto du_chains = chains.get_def_use_chains(); std::unordered_map<IRInstruction*, std::unordered_map<src_index_t, DexType*>> aggregated_uses; for (auto& [insn, uses] : du_chains) { if (!is_inlinable_new_instance(insn)) { continue; } auto type = insn->get_type(); for (auto& use : uses) { auto emplaced = aggregated_uses[use.insn].emplace(use.src_index, type).second; always_assert(emplaced); } } for (auto& [uses_insn, src_indices] : aggregated_uses) { if (!opcode::is_an_invoke(uses_insn->opcode()) || is_benign(uses_insn->get_method())) { continue; } if (expanded_ctor_refs.count(uses_insn->get_method())) { m_stats->invokes_not_inlinable_callee_inconcrete++; return false; } if (method::is_init(uses_insn->get_method()) && !src_indices.empty() && !src_indices.count(0)) { if (src_indices.size() > 1) { m_stats ->invokes_not_inlinable_callee_is_init_too_many_params_to_expand++; return false; } always_assert(src_indices.size() == 1); auto src_index = src_indices.begin()->first; invokes_to_expand.emplace(uses_insn, src_index); continue; } auto callee = resolve_method(uses_insn->get_method(), opcode_to_search(uses_insn)); always_assert(callee); always_assert(callee->is_concrete()); invokes_to_inline.insert(uses_insn); for (auto [src_index, type] : src_indices) { m_inlined_methods[callee].insert(type); } } if (!expand_invokes(invokes_to_expand, &expanded_ctor_refs)) { m_stats->invokes_not_inlinable_callee_is_init_unexpandable++; return false; } if (invokes_to_inline.empty()) { // Nothing else to do return true; } if (!inline_insns(invokes_to_inline)) { m_stats->invokes_not_inlinable_inlining++; return false; } // simplify to prune now unreachable code, e.g. from removed exception // handlers cfg.simplify(); } m_stats->invokes_not_inlinable_too_many_iterations++; return false; } // Given a new-instance instruction whose (main) uses are as the receiver in // iget- and iput- instruction, transform all such field accesses into // accesses to registers, one per field. bool stackify(IRInstruction* new_instance_insn) { auto& cfg = m_method->get_code()->cfg(); std::unordered_map<DexField*, reg_t> field_regs; std::vector<DexField*> ordered_fields; auto get_field_reg = [&](DexFieldRef* ref) { always_assert(ref->is_def()); auto field = ref->as_def(); auto it = field_regs.find(field); if (it == field_regs.end()) { auto wide = type::is_wide_type(field->get_type()); auto reg = wide ? cfg.allocate_wide_temp() : cfg.allocate_temp(); it = field_regs.emplace(field, reg).first; ordered_fields.push_back(field); } return it->second; }; live_range::MoveAwareChains chains(cfg); auto du_chains = chains.get_def_use_chains(); auto uses = du_chains[new_instance_insn]; std::unordered_set<IRInstruction*> instructions_to_replace; bool identity_matters{false}; for (auto& use : uses) { auto opcode = use.insn->opcode(); if (opcode::is_an_iput(opcode)) { always_assert(use.src_index == 1); } else if (opcode::is_an_invoke(opcode) || opcode::is_a_monitor(opcode)) { always_assert(use.src_index == 0); } else if (opcode == OPCODE_IF_EQZ || opcode == OPCODE_IF_NEZ) { identity_matters = true; continue; } else if (opcode::is_move_object(opcode)) { continue; } else if (opcode::is_return_object(opcode)) { // Can happen if the root method is also an allocator m_stats->stackify_returns_objects++; return false; } else { always_assert_log( opcode::is_an_iget(opcode) || opcode::is_instance_of(opcode), "Unexpected use: %s at %u", SHOW(use.insn), use.src_index); } instructions_to_replace.insert(use.insn); } cfg::CFGMutation mutation(cfg); auto ii = InstructionIterable(cfg); auto new_instance_insn_it = ii.end(); for (auto it = ii.begin(); it != ii.end(); it++) { auto insn = it->insn; if (!instructions_to_replace.count(insn)) { if (insn == new_instance_insn) { new_instance_insn_it = it; } continue; } auto opcode = insn->opcode(); if (opcode::is_an_iget(opcode)) { auto move_result_it = cfg.move_result_of(it); auto new_insn = (new IRInstruction(opcode::iget_to_move(opcode))) ->set_src(0, get_field_reg(insn->get_field())) ->set_dest(move_result_it->insn->dest()); mutation.replace(it, {new_insn}); } else if (opcode::is_an_iput(opcode)) { auto new_insn = (new IRInstruction(opcode::iput_to_move(opcode))) ->set_src(0, insn->src(0)) ->set_dest(get_field_reg(insn->get_field())); mutation.replace(it, {new_insn}); } else if (opcode::is_an_invoke(opcode)) { always_assert(is_benign(insn->get_method())); if (!identity_matters) { mutation.remove(it); } } else if (opcode::is_instance_of(opcode)) { auto move_result_it = cfg.move_result_of(it); auto new_insn = (new IRInstruction(OPCODE_CONST)) ->set_literal(type::is_subclass(insn->get_type(), new_instance_insn->get_type())) ->set_dest(move_result_it->insn->dest()); mutation.replace(it, {new_insn}); } else if (opcode::is_a_monitor(opcode)) { mutation.remove(it); } else { not_reached(); } } always_assert(!new_instance_insn_it.is_end()); auto init_class_insn = m_inliner.get_shrinker() .get_init_classes_with_side_effects() .create_init_class_insn(new_instance_insn->get_type()); if (init_class_insn) { mutation.insert_before(new_instance_insn_it, {init_class_insn}); } if (identity_matters) { new_instance_insn_it->insn->set_type(type::java_lang_Object()); } else { auto move_result_it = cfg.move_result_of(new_instance_insn_it); auto new_insn = (new IRInstruction(OPCODE_CONST)) ->set_literal(0) ->set_dest(move_result_it->insn->dest()); mutation.replace(new_instance_insn_it, {new_insn}); } // Insert zero-initialization code for field registers. std::sort(ordered_fields.begin(), ordered_fields.end(), compare_dexfields); std::vector<IRInstruction*> field_inits; field_inits.reserve(ordered_fields.size()); for (auto field : ordered_fields) { auto wide = type::is_wide_type(field->get_type()); auto opcode = wide ? OPCODE_CONST_WIDE : OPCODE_CONST; auto reg = field_regs.at(field); auto new_insn = (new IRInstruction(opcode))->set_literal(0)->set_dest(reg); field_inits.push_back(new_insn); } mutation.insert_before(new_instance_insn_it, field_inits); mutation.flush(); // simplify to prune now unreachable code, e.g. from removed exception // handlers cfg.simplify(); return true; } std::unordered_map<DexMethod*, std::unordered_set<DexType*>> m_inlined_methods; }; // Reduce all root methods std::unordered_map<DexMethod*, ReducedMethod> compute_reduced_methods( ExpandableConstructorParams& expandable_constructor_params, MultiMethodInliner& inliner, const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, const std::unordered_map<DexMethod*, std::unordered_map<DexType*, bool>>& root_methods, std::unordered_set<DexType*>* irreducible_types, Stats* stats) { // We make a copy before we start reducing a root method, in case we run // into issues, or negative net savings. ConcurrentMap<DexMethod*, ReducedMethod> concurrent_reduced_methods; ConcurrentSet<DexType*> concurrent_irreducible_types; workqueue_run<std::pair<DexMethod*, std::unordered_map<DexType*, bool>>>( [&](const std::pair<DexMethod*, std::unordered_map<DexType*, bool>>& p) { auto& [method, types] = p; const std::string& name_str = method->get_name()->str(); DexMethod* copy = DexMethod::make_method_from( method, method->get_class(), DexString::make_string(name_str + "$redex_stack_allocated")); RootMethodReducer root_method_reducer{expandable_constructor_params, inliner, method_summaries, stats, method::is_init(method) || method::is_clinit(method), copy, types}; auto reduced_method = root_method_reducer.reduce(); if (!reduced_method) { DexMethod::erase_method(copy); DexMethod::delete_method_DO_NOT_USE(copy); for (auto [type, multiples] : types) { concurrent_irreducible_types.insert(type); } return; } concurrent_reduced_methods.emplace(method, std::move(*reduced_method)); }, root_methods); irreducible_types->insert(concurrent_irreducible_types.begin(), concurrent_irreducible_types.end()); std::unordered_map<DexMethod*, ReducedMethod> res; for (auto& [method, reduced_method] : concurrent_reduced_methods) { res.emplace(method, std::move(reduced_method)); } return res; } // Select all those reduced methods which will result in overall size savings, // either by looking at local net savings for just a single reduced method, or // by considering families of reduced methods that affect the same classes. std::unordered_set<DexMethod*> select_reduced_methods( const std::unordered_map<DexMethod*, std::unordered_map<DexType*, bool>>& root_methods, const std::unordered_map<DexMethod*, ReducedMethod>& reduced_methods, std::unordered_set<DexType*>* irreducible_types, Stats* stats) { // First, we are going to identify all reduced methods which will result in // local net savings, considering just a single reduced method at a time. // We'll also build up families of reduced methods for which can later do a // global net savings analysis. ConcurrentSet<DexType*> concurrent_irreducible_types; ConcurrentSet<DexMethod*> concurrent_selected_reduced_methods; // A family of reduced methods for which we'll look at the combined global net // savings struct Family { std::unordered_map<DexMethod*, ReducedMethod> reduced_methods; NetSavings global_net_savings; }; ConcurrentMap<DexType*, Family> concurrent_families; workqueue_run<std::pair<DexMethod*, ReducedMethod>>( [&](const std::pair<DexMethod*, ReducedMethod>& p) { auto method = p.first; auto& reduced_method = p.second; auto& types = root_methods.at(method); NetSavings conditional_net_savings; auto local_net_savings = reduced_method.get_net_savings( types, *irreducible_types, &conditional_net_savings); if (local_net_savings.get_value() >= 0) { stats->total_savings += local_net_savings.get_value(); concurrent_selected_reduced_methods.insert(method); } else { auto& classes = conditional_net_savings.classes; if (std::any_of(classes.begin(), classes.end(), [&](DexType* type) { return irreducible_types->count(type); })) { stats->too_costly_irreducible_classes++; } else if (classes.size() > 1) { stats->too_costly_multiple_conditional_classes++; } else if (classes.empty()) { stats->too_costly_globally++; } else { always_assert(classes.size() == 1); auto conditional_type = *classes.begin(); concurrent_families.update( conditional_type, [&](auto*, Family& family, bool) { family.global_net_savings += local_net_savings; family.global_net_savings += conditional_net_savings; family.reduced_methods.emplace(method, reduced_method); }); return; } for (auto [type, multiples] : types) { concurrent_irreducible_types.insert(type); } } }, reduced_methods); irreducible_types->insert(concurrent_irreducible_types.begin(), concurrent_irreducible_types.end()); // Second, perform global net savings analysis workqueue_run<std::pair<DexType*, Family>>( [&](const std::pair<DexType*, Family>& p) { auto& [type, family] = p; if (irreducible_types->count(type) || family.global_net_savings.get_value() < 0) { stats->too_costly_globally += family.reduced_methods.size(); return; } stats->total_savings += family.global_net_savings.get_value(); for (auto& [method, reduced_method] : family.reduced_methods) { concurrent_selected_reduced_methods.insert(method); } }, concurrent_families); return std::unordered_set<DexMethod*>( concurrent_selected_reduced_methods.begin(), concurrent_selected_reduced_methods.end()); } void reduce( DexStoresVector& stores, const Scope& scope, ConfigFiles& conf, const std::unordered_map<DexMethod*, MethodSummary>& method_summaries, const std::unordered_map<DexMethod*, std::unordered_map<DexType*, bool>>& root_methods, Stats* stats) { Timer t("reduce"); init_classes::InitClassesWithSideEffects init_classes_with_side_effects( scope, conf.create_init_class_insns()); ConcurrentMethodRefCache concurrent_resolved_refs; auto concurrent_resolver = [&](DexMethodRef* method, MethodSearch search) { return resolve_method(method, search, concurrent_resolved_refs); }; std::unordered_set<DexMethod*> no_default_inlinables; // customize shrinking options auto inliner_config = conf.get_inliner_config(); inliner_config.shrinker = shrinker::ShrinkerConfig(); inliner_config.shrinker.run_const_prop = true; inliner_config.shrinker.run_cse = true; inliner_config.shrinker.run_copy_prop = true; inliner_config.shrinker.run_local_dce = true; inliner_config.shrinker.compute_pure_methods = false; int min_sdk = 0; MultiMethodInliner inliner(scope, init_classes_with_side_effects, stores, no_default_inlinables, concurrent_resolver, inliner_config, min_sdk, MultiMethodInlinerMode::None); // First, we compute all reduced methods ExpandableConstructorParams expandable_constructor_params(scope); std::unordered_set<DexType*> irreducible_types; auto reduced_methods = compute_reduced_methods( expandable_constructor_params, inliner, method_summaries, root_methods, &irreducible_types, stats); stats->reduced_methods = reduced_methods.size(); // Second, we select reduced methods that will result in net savings auto selected_reduced_methods = select_reduced_methods( root_methods, reduced_methods, &irreducible_types, stats); stats->selected_reduced_methods = selected_reduced_methods.size(); // Finally, we are going to apply those selected methods, and clean up all // reduced method clones workqueue_run<std::pair<DexMethod*, ReducedMethod>>( [&](const std::pair<DexMethod*, ReducedMethod>& p) { auto& [method, reduced_method] = p; if (selected_reduced_methods.count(method)) { method->set_code(reduced_method.method->release_code()); } DexMethod::erase_method(reduced_method.method); DexMethod::delete_method_DO_NOT_USE(reduced_method.method); }, reduced_methods); expandable_constructor_params.flush(scope, stats); } } // namespace void ObjectEscapeAnalysisPass::run_pass(DexStoresVector& stores, ConfigFiles& conf, PassManager& mgr) { const auto scope = build_class_scope(stores); auto method_override_graph = mog::build_graph(scope); init_classes::InitClassesWithSideEffects init_classes_with_side_effects( scope, conf.create_init_class_insns(), method_override_graph.get()); auto non_true_virtual = mog::get_non_true_virtuals(*method_override_graph, scope); std::unordered_map<DexType*, Locations> new_instances; std::unordered_map<DexMethod*, Locations> invokes; std::unordered_map<DexMethod*, std::unordered_set<DexMethod*>> dependencies; analyze_scope(scope, non_true_virtual, &new_instances, &invokes, &dependencies); auto method_summaries = compute_method_summaries(mgr, scope, dependencies, non_true_virtual); auto inline_anchors = compute_inline_anchors(scope, method_summaries); auto root_methods = compute_root_methods(mgr, new_instances, invokes, method_summaries, inline_anchors); Stats stats; reduce(stores, scope, conf, method_summaries, root_methods, &stats); walk::parallel::code(scope, [&](DexMethod*, IRCode& code) { code.clear_cfg(); }); TRACE(OEA, 1, "[object escape analysis] total savings: %zu", (size_t)stats.total_savings); TRACE(OEA, 1, "[object escape analysis] %zu root methods lead to %zu reduced methods " "of which %zu were selected and %zu anchors not inlinable because " "inlining failed, %zu/%zu invokes not inlinable because callee is " "init, %zu invokes not inlinable because callee is not concrete," "%zu invokes not inlinable because inlining failed, %zu invokes not " "inlinable after too many iterations, %zu stackify returned objects, " "%zu too costly with irreducible classes, %zu too costly with multiple " "conditional classes, %zu too costly globally; %zu expanded ctors", root_methods.size(), (size_t)stats.reduced_methods, (size_t)stats.selected_reduced_methods, (size_t)stats.anchors_not_inlinable_inlining, (size_t)stats.invokes_not_inlinable_callee_is_init_unexpandable, (size_t)stats .invokes_not_inlinable_callee_is_init_too_many_params_to_expand, (size_t)stats.invokes_not_inlinable_callee_inconcrete, (size_t)stats.invokes_not_inlinable_inlining, (size_t)stats.invokes_not_inlinable_too_many_iterations, (size_t)stats.stackify_returns_objects, (size_t)stats.too_costly_irreducible_classes, (size_t)stats.too_costly_multiple_conditional_classes, (size_t)stats.too_costly_globally, (size_t)stats.expanded_ctors); mgr.incr_metric("total_savings", stats.total_savings); mgr.incr_metric("root_methods", root_methods.size()); mgr.incr_metric("reduced_methods", (size_t)stats.reduced_methods); mgr.incr_metric("selected_reduced_methods", (size_t)stats.selected_reduced_methods); mgr.incr_metric("root_method_anchors_not_inlinable_inlining", (size_t)stats.anchors_not_inlinable_inlining); mgr.incr_metric( "root_method_invokes_not_inlinable_callee_is_init_unexpandable", (size_t)stats.invokes_not_inlinable_callee_is_init_unexpandable); mgr.incr_metric( "root_method_invokes_not_inlinable_callee_is_init_too_many_params_to_" "expand", (size_t) stats.invokes_not_inlinable_callee_is_init_too_many_params_to_expand); mgr.incr_metric("root_method_invokes_not_inlinable_callee_inconcrete", (size_t)stats.invokes_not_inlinable_callee_inconcrete); mgr.incr_metric("root_method_invokes_not_inlinable_inlining", (size_t)stats.invokes_not_inlinable_inlining); mgr.incr_metric("root_method_invokes_not_inlinable_too_many_iterations", (size_t)stats.invokes_not_inlinable_too_many_iterations); mgr.incr_metric("root_method_stackify_returns_objects", (size_t)stats.stackify_returns_objects); mgr.incr_metric("root_method_too_costly_globally", (size_t)stats.too_costly_globally); mgr.incr_metric("root_method_too_costly_multiple_conditional_classes", (size_t)stats.too_costly_multiple_conditional_classes); mgr.incr_metric("root_method_too_costly_irreducible_classes", (size_t)stats.too_costly_irreducible_classes); mgr.incr_metric("expanded_ctors", (size_t)stats.expanded_ctors); } static ObjectEscapeAnalysisPass s_pass;