opt/final_inline/FinalInlineV2.cpp (895 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. */ #include "FinalInlineV2.h" #include <boost/variant.hpp> #include <iostream> #include <unordered_set> #include <vector> #include "CFGMutation.h" #include "ConfigFiles.h" #include "Debug.h" #include "DexAccess.h" #include "DexClass.h" #include "DexUtil.h" #include "EditableCfgAdapter.h" #include "IRCode.h" #include "LocalDce.h" #include "PassManager.h" #include "Purity.h" #include "Resolver.h" #include "ScopedCFG.h" #include "Shrinker.h" #include "Timer.h" #include "Trace.h" #include "TypeSystem.h" #include "TypeUtil.h" #include "Walkers.h" /* * dx-generated class initializers often use verbose bytecode sequences to * initialize static fields, instead of relying on the more compact * encoded_value formats. This pass determines the values of the static * fields after the <clinit> has finished running, which it uses to generate * their encoded_value equivalents. This applies to both final statics and * non-final statics. * * Additionally, for static final fields, this pass inlines sgets to them where * possible, replacing them with const / const-wide / const-string opcodes. */ namespace cp = constant_propagation; using namespace sparta; namespace { /* * Foo.<clinit> may read some static fields from class Bar, in which case * Bar.<clinit> will be executed first by the VM to determine the values of * those fields. * * Similarly, to ensure that our analysis of Foo.<clinit> knows as much about * Bar's static fields as possible, we want to analyze Bar.<clinit> before * Foo.<clinit>, since Foo.<clinit> depends on it. As such, we do a topological * sort of the classes here based on these dependencies. * * Note that the class initialization graph is *not* guaranteed to be acyclic. * (JLS SE7 12.4.1 indicates that cycles are indeed allowed.) In that case, * this pass cannot safely optimize the static final constants. */ Scope reverse_tsort_by_clinit_deps(const Scope& scope) { std::unordered_set<const DexClass*> scope_set(scope.begin(), scope.end()); Scope result; std::unordered_set<const DexClass*> visiting; std::unordered_set<const DexClass*> visited; std::function<void(DexClass*)> visit = [&](DexClass* cls) { if (visited.count(cls) != 0 || scope_set.count(cls) == 0) { return; } if (visiting.count(cls)) { throw final_inline::class_initialization_cycle(cls); } visiting.emplace(cls); auto clinit = cls->get_clinit(); if (clinit != nullptr && clinit->get_code() != nullptr) { editable_cfg_adapter::iterate_with_iterator( clinit->get_code(), [&](const IRList::iterator& it) { auto insn = it->insn; if (opcode::is_an_sget(insn->opcode())) { auto dependee_cls = type_class(insn->get_field()->get_class()); if (dependee_cls == nullptr || dependee_cls == cls) { return editable_cfg_adapter::LOOP_CONTINUE; } visit(dependee_cls); } return editable_cfg_adapter::LOOP_CONTINUE; }); } visiting.erase(cls); result.emplace_back(cls); visited.emplace(cls); }; for (DexClass* cls : scope) { visit(cls); } return result; } /** * Similar to reverse_tsort_by_clinit_deps(...), but since we are currently * only dealing with instance field from class that only have one <init> * so stop when we are at a class that don't have exactly one constructor, * we are not dealing with them now so we won't have knowledge about their * instance field. */ Scope reverse_tsort_by_init_deps(const Scope& scope) { std::unordered_set<const DexClass*> scope_set(scope.begin(), scope.end()); Scope result; std::unordered_set<const DexClass*> visiting; std::unordered_set<const DexClass*> visited; std::function<void(DexClass*)> visit = [&](DexClass* cls) { if (visited.count(cls) != 0 || scope_set.count(cls) == 0) { return; } if (visiting.count(cls) != 0) { TRACE(FINALINLINE, 1, "Possible class init cycle (could be benign):"); for (auto visiting_cls : visiting) { TRACE(FINALINLINE, 1, " %s", SHOW(visiting_cls)); } TRACE(FINALINLINE, 1, " %s", SHOW(cls)); fprintf(stderr, "WARNING: Possible class init cycle found in FinalInlineV2. " "To check re-run with TRACE=FINALINLINE:1.\n"); return; } visiting.emplace(cls); const auto& ctors = cls->get_ctors(); if (ctors.size() == 1) { auto ctor = ctors[0]; if (ctor != nullptr && ctor->get_code() != nullptr) { editable_cfg_adapter::iterate_with_iterator( ctor->get_code(), [&](const IRList::iterator& it) { auto insn = it->insn; if (opcode::is_an_iget(insn->opcode())) { auto dependee_cls = type_class(insn->get_field()->get_class()); if (dependee_cls == nullptr || dependee_cls == cls) { return editable_cfg_adapter::LOOP_CONTINUE; } visit(dependee_cls); } return editable_cfg_adapter::LOOP_CONTINUE; }); } } visiting.erase(cls); result.emplace_back(cls); visited.emplace(cls); }; for (DexClass* cls : scope) { visit(cls); } return result; } using CombinedAnalyzer = InstructionAnalyzerCombiner<cp::ClinitFieldAnalyzer, cp::WholeProgramAwareAnalyzer, cp::StringAnalyzer, cp::ConstantClassObjectAnalyzer, cp::PrimitiveAnalyzer>; using CombinedInitAnalyzer = InstructionAnalyzerCombiner<cp::InitFieldAnalyzer, cp::WholeProgramAwareAnalyzer, cp::StringAnalyzer, cp::ConstantClassObjectAnalyzer, cp::PrimitiveAnalyzer>; /* * Converts a ConstantValue into its equivalent encoded_value. Returns null if * no such encoding is known. */ class encoding_visitor : public boost::static_visitor<std::unique_ptr<DexEncodedValue>> { public: explicit encoding_visitor(const DexField* field, const XStoreRefs* xstores, const DexType* declaring_type) : m_field(field), m_xstores(xstores), m_declaring_type(declaring_type) {} std::unique_ptr<DexEncodedValue> operator()( const SignedConstantDomain& dom) const { auto cst = dom.get_constant(); if (!cst) { return nullptr; } auto ev = DexEncodedValue::zero_for_type(m_field->get_type()); ev->value(static_cast<uint64_t>(*cst)); return ev; } std::unique_ptr<DexEncodedValue> operator()(const StringDomain& dom) const { auto cst = dom.get_constant(); // Older DalvikVM handles only two types of classes: // https://android.googlesource.com/platform/dalvik.git/+/android-4.3_r3/vm/oo/Class.cpp#3846 // Without this checking, we may mistakenly accept a "const-string" and // "sput-object Ljava/lang/CharSequence;" pair. Such pair can cause a // libdvm.so abort with "Bogus static initialization". if (cst && m_field->get_type() == type::java_lang_String()) { return std::unique_ptr<DexEncodedValue>(new DexEncodedValueString(*cst)); } else { return nullptr; } } std::unique_ptr<DexEncodedValue> operator()( const ConstantClassObjectDomain& dom) const { auto cst = dom.get_constant(); if (!cst) { return nullptr; } if (m_field->get_type() != type::java_lang_Class()) { // See above: There's a limitation in older DalvikVMs return nullptr; } auto type = const_cast<DexType*>(*cst); if (!m_xstores || m_xstores->illegal_ref(m_declaring_type, type)) { return nullptr; } return std::unique_ptr<DexEncodedValue>(new DexEncodedValueType(type)); } template <typename Domain> std::unique_ptr<DexEncodedValue> operator()(const Domain&) const { return nullptr; } private: const DexField* m_field; const XStoreRefs* m_xstores; const DexType* m_declaring_type; }; class ClassInitStrategy final : public call_graph::SingleCalleeStrategy { public: explicit ClassInitStrategy( const method_override_graph::Graph& method_override_graph, const Scope& scope) : call_graph::SingleCalleeStrategy(method_override_graph, scope) {} call_graph::RootAndDynamic get_roots() const override { call_graph::RootAndDynamic root_and_dynamic; auto& roots = root_and_dynamic.roots; walk::methods(m_scope, [&](DexMethod* method) { if (method::is_clinit(method)) { roots.emplace_back(method); } }); return root_and_dynamic; } }; void encode_values(DexClass* cls, const FieldEnvironment& field_env, const PatriciaTreeSet<const DexFieldRef*>& blocklist, const XStoreRefs* xstores) { for (auto* field : cls->get_sfields()) { if (blocklist.contains(field)) { continue; } auto value = field_env.get(field); auto encoded_value = ConstantValue::apply_visitor( encoding_visitor(field, xstores, cls->get_type()), value); if (encoded_value == nullptr) { continue; } field->set_value(std::move(encoded_value)); TRACE(FINALINLINE, 2, "Found encodable field: %s %s", SHOW(field), SHOW(value)); } } } // namespace namespace final_inline { call_graph::Graph build_class_init_graph(const Scope& scope) { Timer t("Build class init graph"); auto graph = call_graph::Graph( ClassInitStrategy(*method_override_graph::build_graph(scope), scope)); return graph; } StaticFieldReadAnalysis::StaticFieldReadAnalysis( const call_graph::Graph& call_graph, const std::unordered_set<std::string>& allowed_opaque_callee_names) : m_graph(call_graph) { // By default, the analysis gives up when it sees a true virtual callee. // However, we can allow some methods to be treated as if no field is read // from the callees so the analysis gives up less often. for (const auto& name : allowed_opaque_callee_names) { DexMethodRef* callee = DexMethod::get_method(name); if (callee) { m_allowed_opaque_callees.emplace(callee); } } } /* * If a field is both read and written to in its initializer, then we can * update its encoded value with the value at exit only if the reads (sgets) are * are dominated by the writes (sputs) -- otherwise we may change program * semantics. Checking for dominance takes some work, and static fields are * rarely read in their class' <clinit>, so we simply avoid inlining all fields * that are read in their class' <clinit>. * * This analysis is an interprocedural analysis that collects all static field * reads from the current method. Technically there are other opcodes that * triggers more <clinit>s, which can also read from a field. To make this fully * sound, we need to account for potential class loads as well. */ StaticFieldReadAnalysis::Result StaticFieldReadAnalysis::analyze( const DexMethod* method) { std::unordered_set<const DexMethod*> pending; Result last = Result::bottom(); while (true) { Result new_result = analyze(method, pending); if (pending.count(method) == 0 || new_result == last) { pending.erase(method); m_finalized.emplace(method); return new_result; } else { last = new_result; } } } StaticFieldReadAnalysis::Result StaticFieldReadAnalysis::analyze( const DexMethod* method, std::unordered_set<const DexMethod*>& pending_methods) { if (!method) { return {}; } if (m_finalized.count(method)) { return m_summaries.at(method); } auto code = const_cast<IRCode*>(method->get_code()); if (!code) { return {}; } Result ret{}; editable_cfg_adapter::iterate_with_iterator( code, [&](const IRList::iterator& it) { auto insn = it->insn; if (opcode::is_an_sget(insn->opcode())) { ret.add(insn->get_field()); } return editable_cfg_adapter::LOOP_CONTINUE; }); pending_methods.emplace(method); m_summaries[method] = ret; bool callee_pending = false; editable_cfg_adapter::iterate_with_iterator( code, [&](const IRList::iterator& it) { auto insn = it->insn; if (opcode::is_an_invoke(insn->opcode())) { auto callee_method_def = resolve_method( insn->get_method(), opcode_to_search(insn), method); if (!callee_method_def || callee_method_def->is_external() || !callee_method_def->is_concrete() || m_allowed_opaque_callees.count(callee_method_def)) { return editable_cfg_adapter::LOOP_CONTINUE; } auto callees = resolve_callees_in_graph(m_graph, method, insn); if (callees.empty()) { TRACE(FINALINLINE, 2, "%s has opaque callees %s", SHOW(method), SHOW(insn->get_method())); ret = Result::top(); callee_pending = false; return editable_cfg_adapter::LOOP_BREAK; } for (const DexMethod* callee : callees) { Result callee_result; if (pending_methods.count(callee)) { callee_pending = true; callee_result = m_summaries.at(callee); } else { callee_result = analyze(callee, pending_methods); } ret.join_with(callee_result); if (ret.is_top()) { callee_pending = false; return editable_cfg_adapter::LOOP_BREAK; } if (pending_methods.count(callee)) { callee_pending = true; } } } return editable_cfg_adapter::LOOP_CONTINUE; }); if (!callee_pending) { pending_methods.erase(method); } m_summaries[method] = ret; return ret; } /* * This method determines the values of the static fields after the <clinit> * has finished running and generates their encoded_value equivalents. * * Additionally, for static final fields, this method collects and returns them * as part of the WholeProgramState object. */ cp::WholeProgramState analyze_and_simplify_clinits( const Scope& scope, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const XStoreRefs* xstores, const std::unordered_set<const DexType*>& blocklist_types, const std::unordered_set<std::string>& allowed_opaque_callee_names) { const std::unordered_set<DexMethodRef*> pure_methods = get_pure_methods(); cp::WholeProgramState wps(blocklist_types); auto method_override_graph = method_override_graph::build_graph(scope); auto graph = call_graph::Graph(ClassInitStrategy(*method_override_graph, scope)); StaticFieldReadAnalysis analysis(graph, allowed_opaque_callee_names); for (DexClass* cls : reverse_tsort_by_clinit_deps(scope)) { ConstantEnvironment env; cp::set_encoded_values(cls, &env); auto clinit = cls->get_clinit(); if (clinit != nullptr && clinit->get_code() != nullptr) { auto* code = clinit->get_code(); { cfg::ScopedCFG cfg(code); cfg->calculate_exit_block(); cp::intraprocedural::FixpointIterator intra_cp( *cfg, CombinedAnalyzer(cls->get_type(), &wps, nullptr, nullptr, nullptr)); intra_cp.run(env); env = intra_cp.get_exit_state_at(cfg->exit_block()); // Generate the new encoded_values and re-run the analysis. StaticFieldReadAnalysis::Result res = analysis.analyze(clinit); if (res.is_bottom() || res.is_top()) { TRACE(FINALINLINE, 1, "Skipped encoding for class %s.", SHOW(cls)); } else { encode_values(cls, env.get_field_environment(), res.elements(), xstores); } auto fresh_env = ConstantEnvironment(); cp::set_encoded_values(cls, &fresh_env); intra_cp.run(fresh_env); // Detect any field writes made redundant by the new encoded_values and // remove those sputs. cp::Transform::Config transform_config; transform_config.class_under_init = cls->get_type(); cp::Transform(transform_config) .legacy_apply_constants_and_prune_unreachable( intra_cp, wps, *cfg, xstores, cls->get_type()); // Delete the instructions rendered dead by the removal of those sputs. LocalDce(&init_classes_with_side_effects, pure_methods) .dce(*cfg, /* normalize_new_instances */ true, clinit->get_class()); } // If the clinit is empty now, delete it. if (method::is_trivial_clinit(*code)) { cls->remove_method(clinit); } } wps.collect_static_finals(cls, env.get_field_environment()); } return wps; } /* * Similar to analyze_and_simplify_clinits(). * This method determines the values of the instance fields after the <init> * has finished running and generates their encoded_value equivalents. * * Unlike static field, if instance field were changed outside of <init>, the * instance field might have different value for different class instance. And * for class with multiple <init>, the outcome of ifields might be different * based on which constructor was used when initializing the instance. So * we are only considering class with only one <init>. */ cp::WholeProgramState analyze_and_simplify_inits( const Scope& scope, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const XStoreRefs* xstores, const std::unordered_set<const DexType*>& blocklist_types, const cp::EligibleIfields& eligible_ifields) { const std::unordered_set<DexMethodRef*> pure_methods = get_pure_methods(); cp::WholeProgramState wps(blocklist_types); for (DexClass* cls : reverse_tsort_by_init_deps(scope)) { if (cls->is_external()) { continue; } ConstantEnvironment env; auto ctors = cls->get_ctors(); if (ctors.size() > 1) { continue; } if (ctors.size() == 1) { bool has_same_type_arg = false; auto cls_type = cls->get_type(); for (auto arg_type : *(ctors[0]->get_proto()->get_args())) { if (arg_type == cls_type) { has_same_type_arg = true; } } if (has_same_type_arg) { continue; } } cp::set_ifield_values(cls, eligible_ifields, &env); if (ctors.size() == 1) { auto ctor = ctors[0]; if (ctor->get_code() != nullptr) { auto* code = ctor->get_code(); cfg::ScopedCFG cfg(code); cfg->calculate_exit_block(); cp::intraprocedural::FixpointIterator intra_cp( *cfg, CombinedInitAnalyzer(cls->get_type(), &wps, nullptr, nullptr, nullptr)); intra_cp.run(env); env = intra_cp.get_exit_state_at(cfg->exit_block()); // Remove redundant iputs in inits cp::Transform::Config transform_config; transform_config.class_under_init = cls->get_type(); cp::Transform(transform_config) .legacy_apply_constants_and_prune_unreachable( intra_cp, wps, *cfg, xstores, cls->get_type()); // Delete the instructions rendered dead by the removal of those iputs. LocalDce(&init_classes_with_side_effects, pure_methods) .dce(*cfg, /* normalize_new_instances */ true, ctor->get_class()); } } wps.collect_instance_finals(cls, eligible_ifields, env.get_field_environment()); } return wps; } } // namespace final_inline namespace { namespace check_this { using ThisDomain = sparta::ConstantAbstractDomain<bool>; using ThisEnvironment = sparta::PatriciaTreeMapAbstractEnvironment<uint32_t, ThisDomain>; /** * Fixpoint analysis to track register that may hold "this" object, so that we * can use this info to find methods that are invoked on "this" object. * TODO(suree404): Switch to use existed LocalPointerAnalysis. */ class ThisObjectAnalysis final : public sparta::MonotonicFixpointIterator<cfg::GraphInterface, ThisEnvironment> { public: explicit ThisObjectAnalysis(cfg::ControlFlowGraph* cfg, DexMethod* method, size_t this_param_reg) : MonotonicFixpointIterator(*cfg, cfg->blocks().size()), m_method(method), m_this_param_reg(this_param_reg) {} void analyze_node(const NodeId& node, ThisEnvironment* env) const override { for (auto& mie : InstructionIterable(*node)) { analyze_instruction(mie.insn, env); } } ThisEnvironment analyze_edge( cfg::Edge* const&, const ThisEnvironment& exit_state_at_source) const override { return exit_state_at_source; } boost::optional<std::unordered_set<DexMethod*>> collect_method_called_on_this() { std::unordered_set<DexMethod*> return_set; auto* code = m_method->get_code(); auto& cfg = code->cfg(); for (cfg::Block* block : cfg.blocks()) { auto env = get_entry_state_at(block); auto ii = InstructionIterable(block); for (auto it = ii.begin(); it != ii.end(); it++) { IRInstruction* insn = it->insn; auto op = insn->opcode(); if (opcode::is_an_invoke(op)) { bool use_this = false; for (auto src : insn->srcs()) { auto this_info = env.get(src).get_constant(); if (!this_info || *this_info) { use_this = true; break; } } if (use_this) { auto insn_method = insn->get_method(); auto callee = resolve_method(insn_method, opcode_to_search(insn), m_method); if (insn->opcode() == OPCODE_INVOKE_STATIC || insn->opcode() == OPCODE_INVOKE_DIRECT) { if (callee != nullptr && callee->get_code() != nullptr) { return_set.emplace(callee); } } else { return_set.emplace(callee); } } } else if (op == OPCODE_IPUT_OBJECT || op == OPCODE_SPUT_OBJECT || op == OPCODE_APUT_OBJECT) { auto this_info = env.get(insn->src(0)).get_constant(); if (!this_info || *this_info) { return boost::none; } } else if (op == OPCODE_FILLED_NEW_ARRAY) { for (auto src : insn->srcs()) { auto this_info = env.get(src).get_constant(); if (!this_info || *this_info) { return boost::none; } } } analyze_instruction(insn, &env); } } return return_set; } private: void analyze_instruction(IRInstruction* insn, ThisEnvironment* env) const { auto default_case = [&]() { if (insn->has_dest()) { env->set(insn->dest(), ThisDomain(false)); } else if (insn->has_move_result_any()) { env->set(RESULT_REGISTER, ThisDomain(false)); } }; switch (insn->opcode()) { case OPCODE_MOVE_OBJECT: { env->set(insn->dest(), env->get(insn->src(0))); break; } case IOPCODE_LOAD_PARAM_OBJECT: { if (insn->dest() == m_this_param_reg) { env->set(insn->dest(), ThisDomain(true)); } else { env->set(insn->dest(), ThisDomain(false)); } break; } case OPCODE_CHECK_CAST: { env->set(RESULT_REGISTER, env->get(insn->src(0))); break; } case IOPCODE_MOVE_RESULT_PSEUDO_OBJECT: { env->set(insn->dest(), env->get(RESULT_REGISTER)); break; } default: { default_case(); break; } } } const DexMethod* m_method; size_t m_this_param_reg; }; } // namespace check_this /** * This function adds instance fields in cls_to_check that the method * accessed in blocklist_ifields. * Return false if all ifields are excluded - no need to check further. */ bool get_ifields_read( const std::unordered_set<std::string>& allowlist_method_names, const std::unordered_set<const DexType*>& parent_intf_set, const DexClass* ifield_cls, const DexMethod* method, ConcurrentSet<DexField*>* blocklist_ifields, std::unordered_set<const DexMethod*>* visited) { if (visited->count(method)) { return true; } visited->emplace(method); if (method != nullptr) { if (method::is_init(method) && parent_intf_set.count(method->get_class())) { // For call on its parent's ctor, no need to proceed. return true; } for (const auto& name : allowlist_method_names) { // Allowed methods name from config, ignore. // We have this allowlist so that we can ignore some methods that // are safe and won't read instance field. // TODO: Switch to a proper interprocedural fixpoint analysis. if (method->get_name()->str() == name) { return true; } } } if (method == nullptr || method->get_code() == nullptr) { // We can't track down further, don't process any ifields from ifield_cls. for (const auto& field : ifield_cls->get_ifields()) { blocklist_ifields->emplace(field); } return false; } bool res = true; editable_cfg_adapter::iterate_with_iterator( const_cast<IRCode*>(method->get_code()), [&](const IRList::iterator& it) { auto insn = it->insn; if (opcode::is_an_iget(insn->opcode())) { // Meet accessing of a ifield in a method called from <init>, add // to blocklist. auto field = resolve_field(insn->get_field(), FieldSearch::Instance); if (field != nullptr && field->get_class() == ifield_cls->get_type()) { blocklist_ifields->emplace(field); } } else if (opcode::is_an_invoke(insn->opcode())) { auto insn_method = insn->get_method(); auto callee = resolve_method(insn_method, opcode_to_search(insn), method); if (insn->opcode() == OPCODE_INVOKE_DIRECT || insn->opcode() == OPCODE_INVOKE_STATIC) { // For invoke on a direct/static method, if we can't resolve them or // there is no code after resolved, those must be methods not // not implemented by us, so they won't access our instance fields // as well. if (!callee || !callee->get_code()) { return editable_cfg_adapter::LOOP_CONTINUE; } } else { bool no_current_type = true; // No need to check on methods whose class/argumetns are not // superclass or interface of ifield_cls. if (callee != nullptr && !parent_intf_set.count(callee->get_class())) { for (const auto& type : *callee->get_proto()->get_args()) { if (parent_intf_set.count(type)) { no_current_type = false; } } } else if (callee == nullptr && !parent_intf_set.count(insn_method->get_class())) { for (const auto& type : *insn_method->get_proto()->get_args()) { if (parent_intf_set.count(type)) { no_current_type = false; } } } else { no_current_type = false; } if (no_current_type) { return editable_cfg_adapter::LOOP_CONTINUE; } } // Recusive check every methods accessed from <init>. bool keep_going = get_ifields_read(allowlist_method_names, parent_intf_set, ifield_cls, callee, blocklist_ifields, visited); if (!keep_going) { res = false; return editable_cfg_adapter::LOOP_BREAK; } } return editable_cfg_adapter::LOOP_CONTINUE; }); return res; } /** * This function add ifields like x in following example in blocklist to avoid * inlining them. * class Foo { * final int x; * Foo() { * bar(); * x = 1; * } * bar() { * // x is zero here, we don't want FinalInline to make it take value 1. * if (x == 1) { ... } * } * } */ ConcurrentSet<DexField*> get_ifields_read_in_callees( const Scope& scope, const std::unordered_set<std::string>& allowlist_method_names) { ConcurrentSet<DexField*> return_ifields; TypeSystem ts(scope); std::vector<DexClass*> relevant_classes; for (auto cls : scope) { if (cls->is_external()) { continue; } auto ctors = cls->get_ctors(); if (ctors.size() != 1 || cls->get_ifields().empty()) { // We are not inlining ifields in multi-ctors class so can also ignore // them here. // Also no need to proceed if there is no ifields for a class. continue; } auto ctor = ctors.front(); auto code = ctor->get_code(); if (code != nullptr) { relevant_classes.push_back(cls); } } walk::parallel::classes(relevant_classes, [](DexClass* cls) { auto ctor = cls->get_ctors().front(); auto code = ctor->get_code(); code->build_cfg(/* editable */ true); code->cfg().calculate_exit_block(); }); walk::parallel::classes( relevant_classes, [&return_ifields, &ts, &allowlist_method_names](DexClass* cls) { auto ctor = cls->get_ctors().front(); auto code = ctor->get_code(); always_assert(code != nullptr); auto& cfg = code->cfg(); check_this::ThisObjectAnalysis fixpoint( &cfg, ctor, cfg.get_param_instructions().begin()->insn->dest()); fixpoint.run(check_this::ThisEnvironment()); // Only check on methods called with this object as arguments. auto check_methods = fixpoint.collect_method_called_on_this(); if (!check_methods) { // This object escaped to heap, blocklist all. for (const auto& field : cls->get_ifields()) { return_ifields.emplace(field); } return; } if (!check_methods->empty()) { std::unordered_set<const DexMethod*> visited; const auto& parent_chain = ts.parent_chain(cls->get_type()); std::unordered_set<const DexType*> parent_intf_set{ parent_chain.begin(), parent_chain.end()}; const auto& intf_set = ts.get_implemented_interfaces(cls->get_type()); parent_intf_set.insert(intf_set.begin(), intf_set.end()); for (const auto method : *check_methods) { bool keep_going = get_ifields_read(allowlist_method_names, parent_intf_set, cls, method, &return_ifields, &visited); if (!keep_going) { break; } } } }); walk::parallel::classes(relevant_classes, [](DexClass* cls) { auto ctor = cls->get_ctors().front(); auto code = ctor->get_code(); code->clear_cfg(); }); return return_ifields; } cp::EligibleIfields gather_ifield_candidates( const Scope& scope, const std::unordered_set<std::string>& allowlist_method_names) { cp::EligibleIfields eligible_ifields; std::unordered_set<DexField*> ifields_candidates; walk::fields(scope, [&](DexField* field) { // Collect non-final instance field candidates that are non external, // and can be deleted. if (is_static(field) || field->is_external() || !can_delete(field) || is_volatile(field)) { return; } if (is_final(field)) { eligible_ifields.emplace(field); return; } DexClass* field_cls = type_class(field->get_class()); if (field_cls != nullptr && field_cls->get_ctors().size() > 1) { // Class with multiple constructors, ignore it now. return; } ifields_candidates.emplace(field); }); walk::code(scope, [&](DexMethod* method, IRCode& code) { // Remove candidate field if it was written in code other than its class' // init function. editable_cfg_adapter::iterate_with_iterator( &code, [&](const IRList::iterator& it) { auto insn = it->insn; auto op = insn->opcode(); if (opcode::is_an_iput(op)) { auto field = resolve_field(insn->get_field(), FieldSearch::Instance); if (field == nullptr || (method::is_init(method) && method->get_class() == field->get_class())) { // If couldn't resolve the field, or this method is this field's // class's init function, move on. return editable_cfg_adapter::LOOP_CONTINUE; } // We assert that final fields are not modified outside of <init> // methods. javac seems to enforce this, but it's unclear if the JVM // spec actually forbids that. Doing the check here simplifies the // constant propagation analysis later -- we can determine the // values of these fields without analyzing any methods invoked from // the <init> methods. always_assert_log( !is_final(field), "FinalInlinePassV2: encountered one final instance " "field been changed outside of its class's <init> " "file, for temporary solution set " "\"inline_instance_field\" in \"FinalInlinePassV2\" " "to be false."); ifields_candidates.erase(field); } return editable_cfg_adapter::LOOP_CONTINUE; }); }); for (DexField* field : ifields_candidates) { eligible_ifields.emplace(field); } auto blocklist_ifields = get_ifields_read_in_callees(scope, allowlist_method_names); for (DexField* field : blocklist_ifields) { eligible_ifields.erase(field); } return eligible_ifields; } FinalInlinePassV2::Stats inline_final_gets( std::optional<DexStoresVector*> stores, const Scope& scope, int min_sdk, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const XStoreRefs* xstores, const cp::WholeProgramState& wps, const std::unordered_set<const DexType*>& blocklist_types, cp::FieldType field_type) { std::atomic<size_t> inlined_count{0}; std::atomic<size_t> init_classes{0}; using namespace shrinker; ShrinkerConfig shrinker_config; shrinker_config.run_const_prop = true; shrinker_config.run_cse = true; shrinker_config.run_copy_prop = true; shrinker_config.run_local_dce = true; shrinker_config.compute_pure_methods = false; auto maybe_shrinker = stores ? std::make_optional<Shrinker>(**stores, scope, init_classes_with_side_effects, shrinker_config, min_sdk) : std::nullopt; walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) { if (field_type == cp::FieldType::STATIC && method::is_clinit(method)) { return; } code.build_cfg(/* editable */); cfg::CFGMutation mutation(code.cfg()); size_t replacements = 0; for (auto block : code.cfg().blocks()) { auto ii = InstructionIterable(block); for (auto it = ii.begin(); it != ii.end(); it++) { auto insn = it->insn; auto op = insn->opcode(); if (opcode::is_an_iget(op) || opcode::is_an_sget(op)) { auto field = resolve_field(insn->get_field()); if (field == nullptr || blocklist_types.count(field->get_class())) { continue; } if (field_type == cp::FieldType::INSTANCE && method::is_init(method) && method->get_class() == field->get_class()) { // Don't propagate a field's value in ctors of its class with // value after ctor finished. continue; } auto cfg_it = block->to_cfg_instruction_iterator(it); auto replacement = ConstantValue::apply_visitor( cp::value_to_instruction_visitor( code.cfg().move_result_of(cfg_it)->insn, xstores, method->get_class()), wps.get_field_value(field)); if (replacement.empty()) { continue; } auto init_class_insn = opcode::is_an_sget(op) ? init_classes_with_side_effects.create_init_class_insn( field->get_class()) : nullptr; if (init_class_insn) { replacement.insert(replacement.begin(), init_class_insn); init_classes++; } mutation.replace(cfg_it, replacement); replacements++; } } } mutation.flush(); code.clear_cfg(); if (replacements > 0 && maybe_shrinker) { // We need to rebuild the cfg. code.build_cfg(/* editable */); code.clear_cfg(); maybe_shrinker->shrink_method(method); } inlined_count.fetch_add(replacements); }); return {(size_t)inlined_count, (size_t)init_classes}; } } // namespace FinalInlinePassV2::Stats FinalInlinePassV2::run( const Scope& scope, int min_sdk, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const XStoreRefs* xstores, const Config& config, std::optional<DexStoresVector*> stores) { try { auto wps = final_inline::analyze_and_simplify_clinits( scope, init_classes_with_side_effects, xstores, config.blocklist_types); return inline_final_gets(stores, scope, min_sdk, init_classes_with_side_effects, xstores, wps, config.blocklist_types, cp::FieldType::STATIC); } catch (final_inline::class_initialization_cycle& e) { std::cerr << e.what(); return {0, 0}; } } FinalInlinePassV2::Stats FinalInlinePassV2::run_inline_ifields( const Scope& scope, int min_sdk, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const XStoreRefs* xstores, const cp::EligibleIfields& eligible_ifields, const Config& config, std::optional<DexStoresVector*> stores) { auto wps = final_inline::analyze_and_simplify_inits( scope, init_classes_with_side_effects, xstores, config.blocklist_types, eligible_ifields); return inline_final_gets(stores, scope, min_sdk, init_classes_with_side_effects, xstores, wps, config.blocklist_types, cp::FieldType::INSTANCE); } void FinalInlinePassV2::run_pass(DexStoresVector& stores, ConfigFiles& conf, PassManager& mgr) { always_assert_log( !mgr.init_class_lowering_has_run(), "Implementation limitation: FinalInlinePassV2 could introduce new " "init-class instructions."); if (mgr.no_proguard_rules()) { TRACE(FINALINLINE, 1, "FinalInlinePassV2 not run because no ProGuard configuration was " "provided."); return; } auto scope = build_class_scope(stores); auto min_sdk = mgr.get_redex_options().min_sdk; init_classes::InitClassesWithSideEffects init_classes_with_side_effects( scope, conf.create_init_class_insns()); XStoreRefs xstores(stores); auto sfield_stats = run(scope, min_sdk, init_classes_with_side_effects, &xstores, m_config, &stores); FinalInlinePassV2::Stats ifield_stats{0, 0}; if (m_config.inline_instance_field) { cp::EligibleIfields eligible_ifields = gather_ifield_candidates(scope, m_config.allowlist_method_names); ifield_stats = run_inline_ifields(scope, min_sdk, init_classes_with_side_effects, &xstores, eligible_ifields, m_config, &stores); always_assert(ifield_stats.init_classes == 0); } mgr.incr_metric("num_static_finals_inlined", sfield_stats.inlined_count); mgr.incr_metric("num_instance_finals_inlined", ifield_stats.inlined_count); mgr.incr_metric("num_init_classes", sfield_stats.init_classes); } static FinalInlinePassV2 s_pass;